1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * https://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18 package org.apache.commons.exec.util;
19
20 import java.io.File;
21 import java.util.ArrayList;
22 import java.util.List;
23 import java.util.Map;
24 import java.util.Objects;
25 import java.util.StringTokenizer;
26
27 /**
28 * Supplement of commons-lang, the stringSubstitution() was in a simpler implementation available in an older commons-lang implementation.
29 *
30 * This class is not part of the public API and could change without warning.
31 */
32 public class StringUtils {
33
34 private static final String[] EMPTY_STRING_ARRAY = {};
35 private static final String SINGLE_QUOTE = "\'";
36 private static final String DOUBLE_QUOTE = "\"";
37 private static final char SLASH_CHAR = '/';
38 private static final char BACKSLASH_CHAR = '\\';
39
40 /**
41 * Fixes the file separator char for the target platform using the following replacement.
42 * <ul>
43 * <li>'/' → File.separatorChar</li>
44 * <li>'\\' → File.separatorChar</li>
45 * </ul>
46 *
47 * @param arg the argument to fix.
48 * @return the transformed argument.
49 */
50 public static String fixFileSeparatorChar(final String arg) {
51 return arg.replace(SLASH_CHAR, File.separatorChar).replace(BACKSLASH_CHAR, File.separatorChar);
52 }
53
54 /**
55 * Determines if this is a quoted argument - either single or double quoted.
56 *
57 * @param argument the argument to check.
58 * @return true when the argument is quoted.
59 */
60 public static boolean isQuoted(final String argument) {
61 return argument.startsWith(SINGLE_QUOTE) && argument.endsWith(SINGLE_QUOTE) || argument.startsWith(DOUBLE_QUOTE) && argument.endsWith(DOUBLE_QUOTE);
62 }
63
64 /**
65 * Put quotes around the given String if necessary.
66 * <p>
67 * If the argument doesn't include spaces or quotes, return it as is. If it contains double quotes, use single quotes - else surround the argument by double
68 * quotes.
69 * </p>
70 *
71 * @param argument the argument to be quoted.
72 * @return the quoted argument.
73 * @throws IllegalArgumentException If argument contains both types of quotes.
74 */
75 public static String quoteArgument(final String argument) {
76
77 String cleanedArgument = argument.trim();
78
79 // strip the quotes from both ends
80 while (cleanedArgument.startsWith(SINGLE_QUOTE) || cleanedArgument.startsWith(DOUBLE_QUOTE)) {
81 cleanedArgument = cleanedArgument.substring(1);
82 }
83
84 while (cleanedArgument.endsWith(SINGLE_QUOTE) || cleanedArgument.endsWith(DOUBLE_QUOTE)) {
85 cleanedArgument = cleanedArgument.substring(0, cleanedArgument.length() - 1);
86 }
87
88 final StringBuilder buf = new StringBuilder();
89 if (cleanedArgument.contains(DOUBLE_QUOTE)) {
90 if (cleanedArgument.contains(SINGLE_QUOTE)) {
91 throw new IllegalArgumentException("Can't handle single and double quotes in same argument");
92 }
93 return buf.append(SINGLE_QUOTE).append(cleanedArgument).append(SINGLE_QUOTE).toString();
94 }
95 if (cleanedArgument.contains(SINGLE_QUOTE) || cleanedArgument.contains(" ")) {
96 return buf.append(DOUBLE_QUOTE).append(cleanedArgument).append(DOUBLE_QUOTE).toString();
97 }
98 return cleanedArgument;
99 }
100
101 /**
102 * Split a string into an array of strings based on a separator.
103 *
104 * @param input what to split.
105 * @param splitChar what to split on.
106 * @return the array of strings.
107 */
108 public static String[] split(final String input, final String splitChar) {
109 final StringTokenizer tokens = new StringTokenizer(input, splitChar);
110 final List<String> strList = new ArrayList<>();
111 while (tokens.hasMoreTokens()) {
112 strList.add(tokens.nextToken());
113 }
114 return strList.toArray(EMPTY_STRING_ARRAY);
115 }
116
117 /**
118 * Perform a series of substitutions.
119 * <p>
120 * The substitutions are performed by replacing ${variable} in the target string with the value of provided by the key "variable" in the provided hash
121 * table.
122 * </p>
123 * <p>
124 * A key consists of the following characters:
125 * </p>
126 * <ul>
127 * <li>letter
128 * <li>digit
129 * <li>dot character
130 * <li>hyphen character
131 * <li>plus character
132 * <li>underscore character
133 * </ul>
134 *
135 * @param argStr the argument string to be processed.
136 * @param vars name/value pairs used for substitution.
137 * @param isLenient ignore a key not found in vars or throw a RuntimeException?
138 * @return String target string with replacements.
139 */
140 public static StringBuffer stringSubstitution(final String argStr, final Map<? super String, ?> vars, final boolean isLenient) {
141
142 final StringBuffer argBuf = new StringBuffer();
143
144 if (argStr == null || argStr.isEmpty()) {
145 return argBuf;
146 }
147
148 if (vars == null || vars.isEmpty()) {
149 return argBuf.append(argStr);
150 }
151
152 final int argStrLength = argStr.length();
153
154 for (int cIdx = 0; cIdx < argStrLength;) {
155
156 char ch = argStr.charAt(cIdx);
157 char del = ' ';
158
159 switch (ch) {
160
161 case '$':
162 final StringBuilder nameBuf = new StringBuilder();
163 del = argStr.charAt(cIdx + 1);
164 if (del == '{') {
165 cIdx++;
166
167 for (++cIdx; cIdx < argStr.length(); ++cIdx) {
168 ch = argStr.charAt(cIdx);
169 if (ch != '_' && ch != '.' && ch != '-' && ch != '+' && !Character.isLetterOrDigit(ch)) {
170 break;
171 }
172 nameBuf.append(ch);
173 }
174
175 if (nameBuf.length() >= 0) {
176
177 String value;
178 final Object temp = vars.get(nameBuf.toString());
179
180 if (temp instanceof File) {
181 // for a file we have to fix the separator chars to allow
182 // cross-platform compatibility
183 value = fixFileSeparatorChar(((File) temp).getAbsolutePath());
184 } else {
185 value = Objects.toString(temp, null);
186 }
187
188 if (value != null) {
189 argBuf.append(value);
190 } else {
191 if (!isLenient) {
192 // complain that no variable was found
193 throw new IllegalArgumentException("No value found for : " + nameBuf);
194 }
195 // just append the unresolved variable declaration
196 argBuf.append("${").append(nameBuf.toString()).append("}");
197 }
198
199 del = argStr.charAt(cIdx);
200
201 if (del != '}') {
202 throw new IllegalArgumentException("Delimiter not found for : " + nameBuf);
203 }
204 }
205 } else {
206 argBuf.append(ch);
207 }
208 cIdx++;
209
210 break;
211
212 default:
213 argBuf.append(ch);
214 ++cIdx;
215 break;
216 }
217 }
218
219 return argBuf;
220 }
221
222 /**
223 * Concatenates an array of string using a separator.
224 *
225 * @param strings the strings to concatenate.
226 * @param separator the separator between two strings.
227 * @return the concatenated strings.
228 * @deprecated Use {@link String#join(CharSequence, CharSequence...)}.
229 */
230 @Deprecated
231 public static String toString(final String[] strings, final String separator) {
232 return String.join(separator, strings);
233 }
234
235 /**
236 * Constructs a new instance.
237 *
238 * @deprecated Will be private in the next major version.
239 */
240 @Deprecated
241 public StringUtils() {
242 // empty
243 }
244 }