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 */
018
019package org.apache.commons.exec.util;
020
021
022import java.io.File;
023import java.util.ArrayList;
024import java.util.List;
025import java.util.Map;
026import java.util.StringTokenizer;
027
028/**
029 * Supplement of commons-lang, the stringSubstitution() was in a simpler
030 * implementation available in an older commons-lang implementation.
031 *
032 * This class is not part of the public API and could change without
033 * warning.
034 *
035 * @version $Id: StringUtils.java 1636204 2014-11-02 22:30:31Z ggregory $
036 */
037public class StringUtils {
038
039    private static final String SINGLE_QUOTE = "\'";
040    private static final String DOUBLE_QUOTE = "\"";
041    private static final char SLASH_CHAR = '/';
042    private static final char BACKSLASH_CHAR = '\\';
043
044    /**
045     * Perform a series of substitutions.
046     * <p>
047     * The substitutions are performed by replacing ${variable} in the target string with the value of provided by the
048     * key "variable" in the provided hash table.
049     * </p>
050     * <p>
051     * A key consists of the following characters:
052     * </p>
053     * <ul>
054     * <li>letter
055     * <li>digit
056     * <li>dot character
057     * <li>hyphen character
058     * <li>plus character
059     * <li>underscore character
060     * </ul>
061     *
062     * @param argStr
063     *            the argument string to be processed
064     * @param vars
065     *            name/value pairs used for substitution
066     * @param isLenient
067     *            ignore a key not found in vars or throw a RuntimeException?
068     * @return String target string with replacements.
069     */
070    public static StringBuffer stringSubstitution(final String argStr, final Map<? super String, ?> vars, final boolean isLenient) {
071
072        final StringBuffer argBuf = new StringBuffer();
073
074        if (argStr == null || argStr.length() == 0) {
075            return argBuf;
076        }
077
078        if (vars == null || vars.size() == 0) {
079            return argBuf.append(argStr);
080        }
081
082        final int argStrLength = argStr.length();
083
084        for (int cIdx = 0; cIdx < argStrLength;) {
085
086            char ch = argStr.charAt(cIdx);
087            char del = ' ';
088
089            switch (ch) {
090
091                case '$':
092                    final StringBuilder nameBuf = new StringBuilder();
093                    del = argStr.charAt(cIdx + 1);
094                    if (del == '{') {
095                        cIdx++;
096
097                        for (++cIdx; cIdx < argStr.length(); ++cIdx) {
098                            ch = argStr.charAt(cIdx);
099                            if (ch == '_' || ch == '.' || ch == '-' || ch == '+' || Character.isLetterOrDigit(ch)) {
100                                nameBuf.append(ch);
101                            } else {
102                                break;
103                            }
104                        }
105
106                        if (nameBuf.length() >= 0) {
107
108                            String value;
109                            final Object temp = vars.get(nameBuf.toString());
110
111                            if (temp instanceof File) {
112                                // for a file we have to fix the separator chars to allow
113                                // cross-platform compatibility
114                                value = fixFileSeparatorChar(((File) temp).getAbsolutePath());
115                            }
116                            else {
117                                value = temp != null ? temp.toString() : null;    
118                            }
119
120                            if (value != null) {
121                                argBuf.append(value);
122                            } else {
123                                if (isLenient) {
124                                    // just append the unresolved variable declaration
125                                    argBuf.append("${").append(nameBuf.toString()).append("}");
126                                } else {
127                                    // complain that no variable was found
128                                    throw new RuntimeException("No value found for : " + nameBuf);
129                                }
130                            }
131
132                            del = argStr.charAt(cIdx);
133
134                            if (del != '}') {
135                                throw new RuntimeException("Delimiter not found for : " + nameBuf);
136                            }
137                        }
138
139                        cIdx++;
140                    }
141                    else {
142                        argBuf.append(ch);
143                        ++cIdx;
144                    }
145
146                    break;
147
148                default:
149                    argBuf.append(ch);
150                    ++cIdx;
151                    break;
152            }
153        }
154
155        return argBuf;
156    }
157
158    /**
159     * Split a string into an array of strings based
160     * on a separator.
161     *
162     * @param input     what to split
163     * @param splitChar what to split on
164     * @return the array of strings
165     */
166    public static String[] split(final String input, final String splitChar) {
167        final StringTokenizer tokens = new StringTokenizer(input, splitChar);
168        final List<String> strList = new ArrayList<String>();
169        while (tokens.hasMoreTokens()) {
170            strList.add(tokens.nextToken());
171        }
172        return strList.toArray(new String[strList.size()]);
173    }
174
175    /**
176     * Fixes the file separator char for the target platform
177     * using the following replacement.
178     * 
179     * <ul>
180     *  <li>'/' &#x2192; File.separatorChar</li>
181     *  <li>'\\' &#x2192; File.separatorChar</li>
182     * </ul>
183     *
184     * @param arg the argument to fix
185     * @return the transformed argument 
186     */
187    public static String fixFileSeparatorChar(final String arg) {
188        return arg.replace(SLASH_CHAR, File.separatorChar).replace(
189                BACKSLASH_CHAR, File.separatorChar);
190    }
191
192    /**
193     * Concatenates an array of string using a separator.
194     *
195     * @param strings the strings to concatenate
196     * @param separator the separator between two strings
197     * @return the concatenated strings
198     */
199    public static String toString(final String[] strings, final String separator) {
200        final StringBuilder sb = new StringBuilder();
201        for (int i = 0; i < strings.length; i++) {
202            if (i > 0) {
203                sb.append(separator);
204            }
205            sb.append(strings[i]);
206        }
207        return sb.toString();
208    }
209
210    /**
211     * Put quotes around the given String if necessary.
212     * <p>
213     * If the argument doesn't include spaces or quotes, return it as is. If it
214     * contains double quotes, use single quotes - else surround the argument by
215     * double quotes.
216     * </p>
217     *
218     * @param argument the argument to be quoted
219     * @return the quoted argument
220     * @throws IllegalArgumentException If argument contains both types of quotes
221     */
222    public static String quoteArgument(final String argument) {
223
224        String cleanedArgument = argument.trim();
225
226        // strip the quotes from both ends
227        while (cleanedArgument.startsWith(SINGLE_QUOTE) || cleanedArgument.startsWith(DOUBLE_QUOTE)) {
228            cleanedArgument = cleanedArgument.substring(1);
229        }
230        
231        while (cleanedArgument.endsWith(SINGLE_QUOTE) || cleanedArgument.endsWith(DOUBLE_QUOTE)) {
232            cleanedArgument = cleanedArgument.substring(0, cleanedArgument.length() - 1);
233        }
234
235        final StringBuilder buf = new StringBuilder();
236        if (cleanedArgument.indexOf(DOUBLE_QUOTE) > -1) {
237            if (cleanedArgument.indexOf(SINGLE_QUOTE) > -1) {
238                throw new IllegalArgumentException(
239                        "Can't handle single and double quotes in same argument");
240            }
241            return buf.append(SINGLE_QUOTE).append(cleanedArgument).append(
242                    SINGLE_QUOTE).toString();
243        } else if (cleanedArgument.indexOf(SINGLE_QUOTE) > -1
244                || cleanedArgument.indexOf(" ") > -1) {
245            return buf.append(DOUBLE_QUOTE).append(cleanedArgument).append(
246                    DOUBLE_QUOTE).toString();
247        } else {
248            return cleanedArgument;
249        }
250    }
251
252    /**
253     * Determines if this is a quoted argument - either single or
254     * double quoted.
255     *
256     * @param argument the argument to check
257     * @return true when the argument is quoted
258     */
259    public static boolean isQuoted(final String argument) {
260        return argument.startsWith(SINGLE_QUOTE) && argument.endsWith(SINGLE_QUOTE)
261                || argument.startsWith(DOUBLE_QUOTE) && argument.endsWith(DOUBLE_QUOTE);
262    }
263}