1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License
15  */
16 
17 package com.android.tools.appbundle.bundletool.utils;
18 
19 import java.nio.file.Path;
20 import java.nio.file.Paths;
21 import java.util.ArrayList;
22 import java.util.Arrays;
23 import java.util.Collections;
24 import java.util.HashMap;
25 import java.util.List;
26 import java.util.Map;
27 import java.util.Optional;
28 
29 /**
30  * Utility for flag parsing, specific to the Bundle Tool.
31  *
32  * <p>The flags follow the below convention:
33  *
34  * <p>[bundle-tool] [command1] [command2] .. [command-n] [--flag1] [--flag2=v2].. [--flagn] where:
35  *
36  * <ul>
37  *   <li>commands: cannot start with "-".
38  *   <li>flags: have to start with "--". If they have "=" anything after the first occurrence is
39  *       considered a flag value. By default the flag value is an empty string.
40  * </ul>
41  */
42 public class FlagParser {
43 
44   private List<String> commands = new ArrayList<>();
45   private Map<String, String> flags = new HashMap<>();
46 
47   /**
48    * Parses the given arguments populating the structures.
49    *
50    * <p>Calling this function removes any previous parsing results.
51    */
parse(String[] args)52   public FlagParser parse(String[] args) throws ParseException {
53     this.commands.clear();
54     // Need to wrap it into a proper list implementation to be able to remove elements.
55     List<String> argsToProcess = new ArrayList<>(Arrays.asList(args));
56     while (argsToProcess.size() > 0 && !argsToProcess.get(0).startsWith("-")) {
57       commands.add(argsToProcess.get(0));
58       argsToProcess.remove(0);
59     }
60     this.flags = parseFlags(argsToProcess);
61     return this;
62   }
63 
parseFlags(List<String> args)64   private Map<String, String> parseFlags(List<String> args) throws ParseException {
65     Map<String, String> flagMap = new HashMap<>();
66     for (String arg : args) {
67       if (!arg.startsWith("--")) {
68         throw new ParseException(
69             String.format("Syntax error: flags should start with -- (%s)", arg));
70       }
71       String[] segments = arg.substring(2).split("=", 2);
72       String value = "";
73       if (segments.length == 2) {
74         value = segments[1];
75       }
76       if (flagMap.putIfAbsent(segments[0], value) != null) {
77         throw new ParseException(
78             String.format("Flag %s has been set more than once.", segments[0]));
79       }
80     }
81     return flagMap;
82   }
83 
84   /** Returns true if a given flag has been set. */
isFlagSet(String flagName)85   public boolean isFlagSet(String flagName) {
86     return flags.containsKey(flagName);
87   }
88 
89   /** Returns the flag value wrapped in the Optional class. */
getFlagValue(String flagName)90   public Optional<String> getFlagValue(String flagName) {
91     return Optional.ofNullable(flags.get(flagName));
92   }
93 
94   /**
95    * Returns a flag value. If absent throws IllegalStateException.
96    *
97    * @param flagName name of the flag to fetch
98    * @return string, the value of the flag
99    * @throws IllegalStateException if the flag was not set.
100    */
getRequiredFlagValue(String flagName)101   public String getRequiredFlagValue(String flagName) {
102     return getFlagValue(flagName)
103         .orElseThrow(
104             () ->
105                 new IllegalArgumentException(
106                     String.format("Missing the required --%s flag.", flagName)));
107   }
108 
109   /** Returns the string value of the flag or the default if has not been set. */
getFlagValueOrDefault(String flagName, String defaultValue)110   public String getFlagValueOrDefault(String flagName, String defaultValue) {
111     return flags.getOrDefault(flagName, defaultValue);
112   }
113 
getFlagValueAsPath(String flagName)114   public Optional<Path> getFlagValueAsPath(String flagName) {
115     return Optional.ofNullable(flags.get(flagName)).map(Paths::get);
116   }
117 
118   /**
119    * Returns the value of the flag as list of strings.
120    *
121    * <p>It converts the string flag value to the list assuming it's delimited by a comma. The list
122    * is empty if the flag has not been set.
123    */
getFlagListValue(String flagName)124   public List<String> getFlagListValue(String flagName) {
125     if (!isFlagSet(flagName)) {
126       return Collections.emptyList();
127     }
128     return Arrays.asList(flags.get(flagName).split(","));
129   }
130 
131   /**
132    * Returns the list of commands that were parsed.
133    *
134    * @return the immutable list of commands.
135    */
getCommands()136   public List<String> getCommands() {
137     return Collections.unmodifiableList(commands);
138   }
139 
140   /** Exception encapsulating any flag parsing errors. */
141   public static class ParseException extends RuntimeException {
142 
ParseException(String message)143     public ParseException(String message) {
144       super(message);
145     }
146   }
147 }
148