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