001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 *  contributor license agreements.  See the NOTICE file distributed with
004 *  this work for additional information regarding copyright ownership.
005 *  The ASF licenses this file to You under the Apache License, Version 2.0
006 *  (the "License"); you may not use this file except in compliance with
007 *  the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 *  Unless required by applicable law or agreed to in writing, software
012 *  distributed under the License is distributed on an "AS IS" BASIS,
013 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 *  See the License for the specific language governing permissions and
015 *  limitations under the License.
016 */
017
018package org.apache.commons.exec.util;
019
020import java.io.File;
021import java.util.ArrayList;
022import java.util.List;
023import java.util.Map;
024import java.util.Objects;
025import java.util.StringTokenizer;
026
027/**
028 * Supplement of commons-lang, the stringSubstitution() was in a simpler implementation available in an older commons-lang implementation.
029 *
030 * This class is not part of the public API and could change without warning.
031 */
032public class StringUtils {
033
034    private static final String[] EMPTY_STRING_ARRAY = {};
035    private static final String SINGLE_QUOTE = "\'";
036    private static final String DOUBLE_QUOTE = "\"";
037    private static final char SLASH_CHAR = '/';
038    private static final char BACKSLASH_CHAR = '\\';
039
040    /**
041     * Fixes the file separator char for the target platform using the following replacement.
042     * <ul>
043     * <li>'/' &#x2192; File.separatorChar</li>
044     * <li>'\\' &#x2192; File.separatorChar</li>
045     * </ul>
046     *
047     * @param arg the argument to fix.
048     * @return the transformed argument.
049     */
050    public static String fixFileSeparatorChar(final String arg) {
051        return arg.replace(SLASH_CHAR, File.separatorChar).replace(BACKSLASH_CHAR, File.separatorChar);
052    }
053
054    /**
055     * Determines if this is a quoted argument - either single or double quoted.
056     *
057     * @param argument the argument to check.
058     * @return true when the argument is quoted.
059     */
060    public static boolean isQuoted(final String argument) {
061        return argument.startsWith(SINGLE_QUOTE) && argument.endsWith(SINGLE_QUOTE) || argument.startsWith(DOUBLE_QUOTE) && argument.endsWith(DOUBLE_QUOTE);
062    }
063
064    /**
065     * Put quotes around the given String if necessary.
066     * <p>
067     * 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
068     * quotes.
069     * </p>
070     *
071     * @param argument the argument to be quoted.
072     * @return the quoted argument.
073     * @throws IllegalArgumentException If argument contains both types of quotes.
074     */
075    public static String quoteArgument(final String argument) {
076
077        String cleanedArgument = argument.trim();
078
079        // strip the quotes from both ends
080        while (cleanedArgument.startsWith(SINGLE_QUOTE) || cleanedArgument.startsWith(DOUBLE_QUOTE)) {
081            cleanedArgument = cleanedArgument.substring(1);
082        }
083
084        while (cleanedArgument.endsWith(SINGLE_QUOTE) || cleanedArgument.endsWith(DOUBLE_QUOTE)) {
085            cleanedArgument = cleanedArgument.substring(0, cleanedArgument.length() - 1);
086        }
087
088        final StringBuilder buf = new StringBuilder();
089        if (cleanedArgument.indexOf(DOUBLE_QUOTE) > -1) {
090            if (cleanedArgument.indexOf(SINGLE_QUOTE) > -1) {
091                throw new IllegalArgumentException("Can't handle single and double quotes in same argument");
092            }
093            return buf.append(SINGLE_QUOTE).append(cleanedArgument).append(SINGLE_QUOTE).toString();
094        }
095        if (cleanedArgument.indexOf(SINGLE_QUOTE) > -1 || cleanedArgument.indexOf(" ") > -1) {
096            return buf.append(DOUBLE_QUOTE).append(cleanedArgument).append(DOUBLE_QUOTE).toString();
097        }
098        return cleanedArgument;
099    }
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}