/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.brooklyn.util.text;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Nonnull;
import org.apache.brooklyn.util.collections.MutableList;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.net.URLParamEncoder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Function;
public class StringEscapes {
private static final Logger log = LoggerFactory.getLogger(StringEscapes.class);
/** if s is wrapped in double quotes containing no unescaped double quotes */
public static boolean isWrappedInDoubleQuotes(String s) {
if (Strings.isEmpty(s)) return false;
if (!s.startsWith("\"") || !s.endsWith("\"")) return false;
return (s.substring(1, s.length()-1).replace("\\\\", "").replace("\\\"", "").indexOf("\"")==-1);
}
/** if s is wrapped in single quotes containing no unescaped single quotes */
public static boolean isWrappedInSingleQuotes(String s) {
if (Strings.isEmpty(s)) return false;
if (!s.startsWith("\'") || !s.endsWith("\'")) return false;
return (s.substring(1, s.length()-1).replace("\\\\", "").replace("\\\'", "").indexOf("\'")==-1);
}
/** if s is wrapped in single or double quotes containing no unescaped quotes of that type */
public static boolean isWrappedInMatchingQuotes(String s) {
return isWrappedInDoubleQuotes(s) || isWrappedInSingleQuotes(s);
}
/**
* Encodes a string suitable for use as a parameter in a URL.
*/
public static String escapeUrlParam(String input) {
return URLParamEncoder.encode(input);
}
/**
* Encodes a string suitable for use as a URL in an HTML form: space to +, and high-numbered chars assuming UTF-8.
* However, it will also convert the first "http://" to "http%3A%2F%2F" so is not suitable for converting an
* entire URL.
*
* Also note that parameter-conversion doesn't work in way you'd expect when trying to create a "normal" url.
* See http://stackoverflow.com/questions/724043/http-url-address-encoding-in-java
*
* @see escapeUrlParam(String), and consider using that instead.
*/
public static String escapeHtmlFormUrl(String url) {
try {
return URLEncoder.encode(url, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw Exceptions.propagate(e);
}
}
/** encodes a string to SQL, that is ' becomes '' */
public static String escapeSql(String x) {
//identical to apache commons StringEscapeUtils.escapeSql
if (x==null) return null;
return x.replaceAll("'", "''");
}
public static class BashStringEscapes {
// single quotes don't permit escapes! e.g. echo 'hello \' world' doesn't work;
// you must do 'hello '\'' world' (to get "hello ' world")
public static class WrapBashFunction implements Function<String, String> {
@Override
public String apply(String input) {
return wrapBash(input);
}
}
public static Function<String, String> wrapBash() {
return new WrapBashFunction();
}
/** wraps plain text in double quotes escaped for use in bash double-quoting */
public static String wrapBash(String value) {
StringBuilder out = new StringBuilder();
try {
wrapBash(value, out);
} catch (IOException e) {
//shouldn't happen for string buffer
throw Exceptions.propagate(e);
}
return out.toString();
}
/** @see #wrapBash(String) */
public static void wrapBash(String value, Appendable out) throws IOException {
out.append('"');
escapeLiteralForDoubleQuotedBash(value, out);
out.append('"');
}
private static void escapeLiteralForDoubleQuotedBash(String value, Appendable out) throws IOException {
for (int i=0; i<value.length(); i++) {
char c = value.charAt(i);
if (c=='\\' || c=='\"' || c=='$' || c=='`') {
appendEscaped(out, c);
} else if (c == '!') {
out.append("\"'!'\"");
} else {
out.append(c);
}
}
}
/** performs replacements on a string so that it can be legally inserted into a double-quoted bash context
* (without the surrounding double quotes; see also {@link #wrapBash(String)}) */
public static String escapeLiteralForDoubleQuotedBash(String unquotedInputToBeEscaped) {
StringBuilder out = new StringBuilder();
try {
escapeLiteralForDoubleQuotedBash(unquotedInputToBeEscaped, out);
} catch (IOException e) {
// shouldn't happen for StringBuilder
throw Exceptions.propagate(e);
}
return out.toString();
}
/** transforms e.g. [ "-Dname=Bob Johnson", "-Dnet.worth=$100" ] to
* a java string "\"-Dname=Bob Johnson\" \"-Dnet.worth=\$100\"" --
* which can be inserted into a bash command where it will be picked up as 2 params
*/
public static String doubleQuoteLiteralsForBash(String... args) {
StringBuilder result = new StringBuilder();
for (String arg: args) {
if (!Strings.isEmpty(result)) result.append(" ");
result.append("\"");
result.append(escapeLiteralForDoubleQuotedBash(arg));
result.append("\"");
}
return result.toString();
}
//between java and regex parsing, this gives a single backslash and double quote
private static final String BACKSLASH = "\\\\";
private static final String DOUBLE_QUOTE = "\\\"";
public static boolean isValidForDoubleQuotingInBash(String x) {
return (checkValidForDoubleQuotingInBash(x)==null);
}
public static void assertValidForDoubleQuotingInBash(String x) {
String problem = checkValidForDoubleQuotingInBash(x);
if (problem==null) return;
throw new IllegalArgumentException("String \""+x+"\" not acceptable for bash argument (including double quotes): "+problem);
}
private static String checkValidForDoubleQuotingInBash(String x) {
//double quotes must be preceded by a backslash (preceded by 0 or more bash-escaped backslashes)
if (x.matches( "[^"+BACKSLASH+DOUBLE_QUOTE+"]*"+
"("+BACKSLASH+BACKSLASH+")*"+
DOUBLE_QUOTE+".*")) return "unescaped double quote";
return null;
}
/** given a string in bash notation, e.g. with quoted portions needing unescaped, returns the unescaped and unquoted version */
public static String unwrapBashQuotesAndEscapes(String s) {
return applyUnquoteAndUnescape(s, "Bash", true);
}
}
public static class JavaStringEscapes {
/** converts normal string to java escaped for double-quotes (but not wrapped in double quotes) */
public static String escapeJavaString(String value) {
StringBuilder out = new StringBuilder();
try {
escapeJavaString(value, out);
} catch (IOException e) {
//shouldn't happen for string builder
throw Exceptions.propagate(e);
}
return out.toString();
}
/** converts normal string to java escaped for double-quotes and wrapped in those double quotes */
public static String wrapJavaString(String value) {
StringBuilder out = new StringBuilder();
try {
wrapJavaString(value, out);
} catch (IOException e) {
//shouldn't happen for string builder
throw Exceptions.propagate(e);
}
return out.toString();
}
public static List<String> wrapJavaStrings(Iterable<String> values) {
if (values==null) return null;
List<String> result = MutableList.of();
for (String v: values) result.add(wrapJavaString(v));
return result;
}
/** as {@link #unwrapJavaString(String)} if the given string is wrapped in double quotes;
* otherwise just returns the given string */
public static String unwrapJavaStringIfWrapped(String s) {
if (!StringEscapes.isWrappedInDoubleQuotes(s)) return s;
return unwrapJavaString(s);
}
/** converts normal string to java escaped for double-quotes and wrapped in those double quotes */
public static void wrapJavaString(String value, Appendable out) throws IOException {
if (value==null) {
out.append("null");
} else {
out.append('"');
escapeJavaString(value, out);
out.append('"');
}
}
/** converts normal string to java escaped for double-quotes (but not wrapped in double quotes) */
public static void escapeJavaString(@Nonnull String value, Appendable out) throws IOException {
for (int i=0; i<value.length(); i++) {
char c = value.charAt(i);
if (c=='\\' || c=='"') {
// NB do NOT escape single quotes; while valid for java, it is not in JSON (breaks jQuery.parseJSON)
appendEscaped(out, c);
} else if (c=='\n') {
appendEscaped(out, 'n');
} else if (c=='\t') {
appendEscaped(out, 't');
} else if (c=='\r') {
appendEscaped(out, 'r');
} else {
out.append(c);
}
}
}
/** given a string in java syntax, e.g. wrapped in quotes and with backslash escapes, returns the literal value,
* without the surrounding quotes and unescaped; throws IllegalArgumentException if not a valid java string */
public static String unwrapJavaString(String s) {
return applyUnquoteAndUnescape(s, "Java", false);
}
/**
* Unwraps a sequence of quoted java strings, that are each separated by the given separator.
* @param trimmedArg
* @return
*/
public static List<String> unwrapQuotedJavaStringList(String s, String separator) {
List<String> result = new ArrayList<String>();
String remaining = s.trim();
while (remaining.length() > 0) {
int endIndex = findNextClosingQuoteOf(remaining);
result.add(unwrapJavaString(remaining.substring(0, endIndex+1)));
remaining = remaining.substring(endIndex+1).trim();
if (remaining.startsWith(separator)) {
remaining = remaining.substring(separator.length()).trim();
} else if (remaining.length() > 0) {
throw new IllegalArgumentException("String '"+s+"' has invalid separators, should be '"+separator+"'");
}
}
return result;
}
private static int findNextClosingQuoteOf(String s) {
boolean escaped = false;
boolean quoted = false;
for (int i=0; i<s.length(); i++) {
char c = s.charAt(i);
if (!quoted) {
assert (i==0);
assert !escaped;
if (c=='"') quoted = true;
else throw new IllegalArgumentException("String '"+s+"' is not a valid Java string (must start with double quote)");
} else {
if (escaped) {
escaped = false;
} else {
if (c=='\\') escaped = true;
else if (c=='\"') {
quoted = false;
return i;
}
}
}
}
assert quoted;
throw new IllegalArgumentException("String '"+s+"' is not a valid Java string (unterminated string)");
}
/** converts a comma separated list in a single string to a list of strings,
* doing what would be expected if given java or json style string as input,
* and falling back to returning the input.
* <p>
* this method does <b>not</b> throw exceptions on invalid input,
* but just returns that input
* <p>
* specifically, uses the following rules (executed once in sequence:
* <li> 1) if of form <code>[ X ]</code> (in brackets after trim),
* then removes brackets and applies following rules to X (for any X including quoted or with commas)
* <li> 2) if of form <code>"X"</code>
* (in double quotes after trim,
* where X contains no internal double quotes unless escaped with backslash)
* then returns list containing X unescaped (\x replaced by x)
* <li> 3) if of form <code>X</code> or <code>X, Y, ...</code>
* (where X, Y, ... each satisfy the constraint given in 2, or have no double quotes or commas in them)
* then returns the concatenation of rule 2 applied to non-empty X, Y, ...
* (if you want an empty string in a list, you must double quote it)
* <li> 4) for any other form X returns [ X ], including empty list for empty string
* <p>
* @see #unwrapOptionallyQuotedJavaStringList(String)
**/
public static List<String> unwrapJsonishListIfPossible(String input) {
try {
return unwrapOptionallyQuotedJavaStringList(input);
} catch (Exception e) {
Exceptions.propagateIfFatal(e);
if (e instanceof IllegalArgumentException) {
if (log.isDebugEnabled())
log.debug("Unable to parse JSON list '"+input+"' ("+e+"); treating as single-element string list");
} else {
log.warn("Unable to parse JSON list '"+input+"' ("+e+"); treating as single-element string list", e);
}
return MutableList.of(input);
}
}
/** as {@link #unwrapJsonishListIfPossible(String)} but throwing errors
* if something which looks like a string or set of brackets is not well-formed
* (this does the work for that method)
* @throws IllegalArgumentException if looks to have quoted list or surrounding brackets but they are not syntactically valid */
public static List<String> unwrapOptionallyQuotedJavaStringList(String input) {
if (input==null) return null;
MutableList<String> result = MutableList.of();
String i1 = input.trim();
boolean inBrackets = (i1.startsWith("[") && i1.endsWith("]"));
if (inBrackets) i1 = i1.substring(1, i1.length()-1).trim();
QuotedStringTokenizer qst = new QuotedStringTokenizer(i1, "\"", true, ",", false);
while (qst.hasMoreTokens()) {
String t = qst.nextToken().trim();
if (isWrappedInDoubleQuotes(t))
result.add(unwrapJavaString(t));
else {
if (inBrackets && (t.indexOf('[')>=0 || t.indexOf(']')>=0))
throw new IllegalArgumentException("Literal square brackets must be quoted, in element '"+t+"'");
result.add(t.trim());
}
}
return result;
}
}
private static void appendEscaped(Appendable out, char c) throws IOException {
out.append('\\');
out.append(c);
}
private static String applyUnquoteAndUnescape(String s, String mode, boolean allowMultipleQuotes) {
StringBuilder result = new StringBuilder();
boolean escaped = false;
boolean quoted = false;
for (int i=0; i<s.length(); i++) {
char c = s.charAt(i);
if (!quoted) {
assert (i==0 || allowMultipleQuotes);
assert !escaped;
if (c=='"') quoted = true;
else if (!allowMultipleQuotes)
throw new IllegalArgumentException("String '"+s+"' is not a valid "+mode+" string (must start with double quote)");
else result.append(c);
} else {
if (escaped) {
if (c=='\\' || c=='"' || c=='\'') result.append(c);
else if (c=='n') result.append('\n');
else if (c=='t') result.append('\t');
else if (c=='r') result.append('\r');
else throw new IllegalArgumentException("String '"+s+"' is not a valid "+mode+" string (unsupported escape char '"+c+"' at position "+i+")");
escaped = false;
} else {
if (c=='\\') escaped = true;
else if (c=='\"') {
quoted = false;
if (!allowMultipleQuotes && i<s.length()-1)
throw new IllegalArgumentException("String '"+s+"' is not a valid "+mode+" string (unescaped interior double quote at position "+i+")");
} else result.append(c);
}
}
}
if (quoted)
throw new IllegalArgumentException("String '"+s+"' is not a valid "+mode+" string (unterminated string)");
assert !escaped;
return result.toString();
}
}