http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/cf2f7a93/utils/common/src/main/java/org/apache/brooklyn/util/text/ByteSizeStrings.java ---------------------------------------------------------------------- diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/text/ByteSizeStrings.java b/utils/common/src/main/java/org/apache/brooklyn/util/text/ByteSizeStrings.java new file mode 100644 index 0000000..cc28efc --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/text/ByteSizeStrings.java @@ -0,0 +1,416 @@ +/* + * 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.util.Formattable; +import java.util.FormattableFlags; +import java.util.Formatter; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.annotation.Nullable; + +import com.google.common.base.Function; + +/** + * A formatter to pretty-print numeric values representing sizes in byes. + * <p> + * The {@link ByteSizeStrings#builder()} presents a fluent interface to create + * various configurations of formatting. The defaults produce metric units in + * multiples of 1000 bytes at a precision of three significant figures. This is + * the way disk space is normally measured, for example {@literal 128.1GB}. + * <p> + * Alternatively the {@link ByteSizeStrings#iso()} convenience method produces + * ISO standard units in multiples of 1024 bytes, with the same precision as the + * metric output. This is how RAM is normally measured, for example {@literal 12.4MiB} + * or {@literal 1.04GiB}. + * <p> + * Finally, the {@link ByteSizeStrings#java()} convenience method will produce + * strings suitable for use with a Java command line, as part of the {@code -Xms} + * or {@code -Xmx} options. These output integer values only, so values up to + * 10GB will be reported in MB to preserve accuracy. For size values over 1000GB, + * the output will still be formatted as GB but rounded to a mutiple of 1000. + * <p> + * The class is immutable and thread safe once built and a single instance of + * the three pre-defined configurations is created and returned buy the methods + * described above. + * + * @see Strings#makeSizeString(long) + * @see Strings#makeISOSizeString(long) + * @see Strings#makeJavaSizeString(long) + */ +public class ByteSizeStrings implements Function<Long, String> { + + /** + * Configures and builds a {@link ByteSizeStrings} formatter. + */ + public static class Builder { + + private String suffixBytes = "B"; + private String suffixKilo = "kB"; + private String suffixMega = "MB"; + private String suffixGiga = "GB"; + private String suffixTera = "TB"; + private boolean addSpace = true; + private int bytesPerMetricUnit = 1000; + private int maxLen = 4; + private int precision = 3; + private int lowerLimit = 1; + + /** + * The suffix to use when printing bytes. + */ + public Builder suffixBytes(String suffixBytes) { this.suffixBytes = suffixBytes; return this; } + + /** + * The suffix to use when printing Kilobytes. + */ + public Builder suffixKilo(String suffixKilo) { this.suffixKilo = suffixKilo; return this; } + + /** + * The suffix to use when printing Megabytes. + */ + public Builder suffixMega(String suffixMega) { this.suffixMega = suffixMega; return this; } + + /** + * The suffix to use when printing Gigabytes. + */ + public Builder suffixGiga(String suffixGiga) { this.suffixGiga = suffixGiga; return this; } + + /** + * The suffix to use when printing Terabytes. + */ + public Builder suffixTera(String suffixTera) { this.suffixTera = suffixTera; return this; } + + /** + * Whether to add a space between the value and the unit suffix. + * <p> + * Defaults is {@literal true} for '5 MiB' output. + */ + public Builder addSpace(boolean addSpace) { this.addSpace = addSpace; return this; } + public Builder addSpace() { this.addSpace = true; return this; } + public Builder noSpace() { this.addSpace = false; return this; } + + /** + * The number of bytes per metric usnit, usually either 1000 or 1024. + * <p> + * Used to determine when to use the next suffix string. + */ + public Builder bytesPerMetricUnit(int bytesPerMetricUnit) { this.bytesPerMetricUnit = bytesPerMetricUnit; return this; } + + /** + * The maximum length of the printed number. + * + * @see Strings#makeRealString(double, int, int, int, double, boolean) + */ + public Builder maxLen(int maxLen) { this.maxLen = maxLen; return this; } + + /** + * The number of digits accuracy desired in the printed number. + * + * @see Strings#makeRealString(double, int, int, int, double, boolean) + */ + public Builder precision(int precision) { this.precision = precision; return this; } + + /** + * Prints using a lower suffix until the size is greater than this limit multiplied + * by bytes per metric unit, when the next highest suffix will be used.§ + * <p> + * If this has the value 5 then sizes up to 5000 will be printed as bytes, and over 5000 + * as Kilobytes. + */ + public Builder lowerLimit(int lowerLimit) { this.lowerLimit = lowerLimit; return this; } + + /** + * Returns an immutable {@link ByteSizeStrings} formatter using the builder configuration. + */ + public ByteSizeStrings build() { + String space = addSpace ? " " : ""; + return new ByteSizeStrings(space + suffixBytes, space + suffixKilo, space + suffixMega, space + suffixGiga, + space + suffixTera, bytesPerMetricUnit, maxLen, precision, lowerLimit); + } + + } + + /** + * Returns a builder for a {@link ByteSizeStrings} formatter. + */ + public static Builder builder() { return new Builder(); } + + /** + * Format byte sizes suitable for Java {@code -Xms} arguments. + */ + public static final ByteSizeStrings java() { return JAVA; } + + private static final ByteSizeStrings JAVA = ByteSizeStrings.builder() + .suffixBytes("") + .suffixKilo("k") + .suffixMega("m") + .suffixGiga("g") + .suffixTera("000g") // Java has no Tera suffix + .noSpace() + .bytesPerMetricUnit(1024) + .maxLen(6) + .precision(0) + .lowerLimit(10) + .build(); + + /** + * Formats byte sizes using ISO standard suffixes and binary multiples of 1024 + */ + public static ByteSizeStrings iso() { return ISO; } + + private static ByteSizeStrings ISO = ByteSizeStrings.builder() + .suffixBytes("B") + .suffixKilo("KiB") + .suffixMega("MiB") + .suffixGiga("GiB") + .suffixTera("TiB") + .bytesPerMetricUnit(1024) + .build(); + + /** + * Default byte size formatter using metric multiples of 1000. + */ + public static ByteSizeStrings metric() { return METRIC; } + + private static ByteSizeStrings METRIC = ByteSizeStrings.builder().build(); + + private String suffixBytes; + private String suffixKilo; + private String suffixMega; + private String suffixGiga; + private String suffixTera; + private int bytesPerMetricUnit; + private int maxLen; + private int precision; + private int lowerLimit; + + /** + * For use by the {@link Builder} only. + */ + private ByteSizeStrings(String suffixBytes, String suffixKilo, String suffixMega, String suffixGiga, + String suffixTera, int bytesPerMetricUnit, int maxLen, int precision, int lowerLimit) { + this.suffixBytes = suffixBytes; + this.suffixKilo = suffixKilo; + this.suffixMega = suffixMega; + this.suffixGiga = suffixGiga; + this.suffixTera = suffixTera; + this.bytesPerMetricUnit = bytesPerMetricUnit; + this.maxLen = maxLen; + this.precision = precision; + this.lowerLimit = lowerLimit; + } + + /** @deprecated Use {@link ByteSizeStrings#builder()} */ + @Deprecated + public ByteSizeStrings() { } + + /** @deprecated Use {@link ByteSizeStrings.Builder#suffixBytes(String)} */ + @Deprecated + public void setSuffixBytes(String suffixBytes) { + this.suffixBytes = suffixBytes; + } + + /** @deprecated Use {@link ByteSizeStrings.Builder#suffixKilo(String)} */ + @Deprecated + public void setSuffixKilo(String suffixKilo) { + this.suffixKilo = suffixKilo; + } + + /** @deprecated Use {@link ByteSizeStrings.Builder#suffixMega(String)} */ + @Deprecated + public void setSuffixMega(String suffixMega) { + this.suffixMega = suffixMega; + } + + /** @deprecated Use {@link ByteSizeStrings.Builder#suffixGiga(String)} */ + @Deprecated + public void setSuffixGiga(String suffixGiga) { + this.suffixGiga = suffixGiga; + } + + /** @deprecated Use {@link ByteSizeStrings.Builder#suffixTera(String)} */ + @Deprecated + public void setSuffixTera(String suffixTera) { + this.suffixTera = suffixTera; + } + + /** @deprecated Use {@link ByteSizeStrings.Builder#bytesPerMetricUnit(int)} */ + @Deprecated + public void setBytesPerMetricUnit(int bytesPerMetricUnit) { + this.bytesPerMetricUnit = bytesPerMetricUnit; + } + + /** @deprecated Use {@link ByteSizeStrings.Builder#maxLen(int)} */ + @Deprecated + public void setMaxLen(int maxLen) { + this.maxLen = maxLen; + } + + /** @deprecated Use {@link ByteSizeStrings.Builder#precision(int)} */ + @Deprecated + public void setPrecision(int precision) { + this.precision = precision; + } + + /** @deprecated Use {@link ByteSizeStrings.Builder#lowerLimit(int)} */ + @Deprecated + public void setLowerLimit(int lowerLimit) { + this.lowerLimit = lowerLimit; + } + + /** + * Format the {@literal size} bytes as a String. + */ + public String makeSizeString(long size) { + return makeSizeString(size, precision); + } + + /** + * Format the {@literal size} bytes as a String with the given precision. + */ + public String makeSizeString(long size, int precision) { + long t = size; + if (t==0) return "0"+suffixBytes; + if (t<0) return "-"+makeSizeString(-t); + long b = t%bytesPerMetricUnit; + t = t/bytesPerMetricUnit; + long kb = t%bytesPerMetricUnit; + t = t/bytesPerMetricUnit; + long mb = t%bytesPerMetricUnit; + t = t/bytesPerMetricUnit; + long gb = t%bytesPerMetricUnit; + t = t/bytesPerMetricUnit; + long tb = t; + + if (tb>lowerLimit) + return Strings.makeRealString(tb + (1.0*gb/bytesPerMetricUnit), -1, precision, 0) + suffixTera; + if (gb>lowerLimit) + return Strings.makeRealString((tb*bytesPerMetricUnit) + gb + (1.0*mb/bytesPerMetricUnit), maxLen, precision, 0) + suffixGiga; + if (mb>lowerLimit) + return Strings.makeRealString((gb*bytesPerMetricUnit) + mb + (1.0*kb/bytesPerMetricUnit), maxLen, precision, 0) + suffixMega; + if (kb>lowerLimit) + return Strings.makeRealString((mb*bytesPerMetricUnit) + kb + (1.0*b/bytesPerMetricUnit), maxLen, precision, 0) + suffixKilo; + return (kb*bytesPerMetricUnit) + b + suffixBytes; + } + + /** + * Returns a {@link Formattable} object that can be used with {@link String#format(String, Object...)}. + * <p> + * When used as the argument for a {@literal %s} format string element, the {@literal bytes} value + * will be formatted using the current {@link ByteSizeStrings} values, or if the alternative + * flag is set (using the {@literal %#s} format string) it will use the {@link ByteSizeStrings#metric()} + * formatter. Finally, the precision of the formatted value can be adjusted using format string + * argumenbts like {@literal %.6s}. + * + * @see http://docs.oracle.com/javase/7/docs/api/java/util/Formatter.html#syntax + */ + public Formattable formatted(final long bytes) { + return new Formattable() { + @Override + public void formatTo(Formatter formatter, int flags, int width, int precision) { + boolean alternate = (flags & FormattableFlags.ALTERNATE) == FormattableFlags.ALTERNATE; + ByteSizeStrings strings = alternate ? ByteSizeStrings.metric() : ByteSizeStrings.this; + if (precision != -1) { + formatter.format("%s", strings.makeSizeString(bytes, precision)); + } else { + formatter.format("%s", strings.makeSizeString(bytes)); + } + } + }; + } + + /** + * A {@link Function} implementation that formats its input using the current {@link ByteSizeStrings} values. + */ + @Override + @Nullable + public String apply(@Nullable Long input) { + if (input == null) return null; + return makeSizeString(input); + } + + public static long parse(String sizeString) { + return parse(sizeString, null); + } + public static long parse(String sizeString, String defaultUnits) { + return parse(sizeString, defaultUnits, null); + } + /** parses the given string as a byte size string, e.g. "4gb" + * @param sizeString string to parse + * @param defaultUnit optional units to append if a number (no units) are supplied + * @param bytesMode optional evaluation mode to force 1024 or 1000 as the interpretation of the unit prefix; + * if omitted, it will depend on the units supplied, + * 1000 for {@link #metric()} (e.g. "1kB"), and + * 1024 for {@link #java()} (e.g. "1k") and {@link #iso()} (e.g. "1KiB") + * @return number of bytes represented by this string + */ + public static long parse(String sizeStringOriginal, String defaultUnit, ByteSizeStrings bytesMode) { + String sizeString = sizeStringOriginal.trim(); + String units; + Matcher matcher = Pattern.compile("[A-Za-z]+").matcher(sizeString); + if (!matcher.find()) { + if (defaultUnit==null) { + throw new IllegalArgumentException("Cannot parse '"+sizeStringOriginal+"' as a size string"); + } + units = defaultUnit; + } else { + units = matcher.group(); + int unitsIndex = sizeString.indexOf(units); + if (sizeString.length() > unitsIndex+units.length()) { + throw new IllegalArgumentException("Cannot parse '"+sizeStringOriginal+"' as a size string"); + } + sizeString = sizeString.substring(0, unitsIndex).trim(); + } + + int exponent = -1; + ByteSizeStrings matchedMode = null; + for (ByteSizeStrings mode: new ByteSizeStrings[] { ISO, JAVA, METRIC } ) { + matchedMode = mode; + if (units.equalsIgnoreCase(mode.suffixBytes.trim())) { exponent = 0; break; } + if (units.equalsIgnoreCase(mode.suffixKilo.trim())) { exponent = 1; break; } + if (units.equalsIgnoreCase(mode.suffixMega.trim())) { exponent = 2; break; } + if (units.equalsIgnoreCase(mode.suffixGiga.trim())) { exponent = 3; break; } + if (units.equalsIgnoreCase(mode.suffixTera.trim())) { exponent = 4; break; } + } + + if (exponent==-1) { + // did not match; try other standard ones + if (units.equalsIgnoreCase("t")) { + exponent = 4; + matchedMode = java(); + } else { + throw new IllegalArgumentException("Cannot parse '"+sizeStringOriginal+"' as a size string (as '"+sizeString+"' "+units+")"); + } + } + + double base = Double.parseDouble(sizeString.trim()); + + if (bytesMode==null) bytesMode=matchedMode; + + while (exponent>0) { + base *= bytesMode.bytesPerMetricUnit; + exponent--; + } + + return (long)base; + } + +}
http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/cf2f7a93/utils/common/src/main/java/org/apache/brooklyn/util/text/ComparableVersion.java ---------------------------------------------------------------------- diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/text/ComparableVersion.java b/utils/common/src/main/java/org/apache/brooklyn/util/text/ComparableVersion.java new file mode 100644 index 0000000..9065603 --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/text/ComparableVersion.java @@ -0,0 +1,89 @@ +/* + * 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; + + +/** takes a version string, and compares to other versions, using {@link NaturalOrderComparator} */ +public class ComparableVersion implements Comparable<String> { + + public final String version; + + public ComparableVersion(String version) { + this.version = version; + } + + public int compareTo(String target) { + return new NaturalOrderComparator().compare(version, target); + } + + public boolean isGreaterThanOrEqualTo(String target) { + return compareTo(target) >= 0; + } + public boolean isGreaterThanAndNotEqualTo(String target) { + return compareTo(target) > 0; + } + public boolean isLessThanOrEqualTo(String target) { + return compareTo(target) <= 0; + } + public boolean isLessThanAndNotEqualTo(String target) { + return compareTo(target) < 0; + } + + /** inclusive at endpoints */ + public boolean isInRange(String lowerBound, String upperBound) { + return isGreaterThanAndNotEqualTo(lowerBound) && isLessThanAndNotEqualTo(upperBound); + } + + /** parses a string expressed with common mathematical sematics, + * as either square brackets (inclusive), round brackets (exclusive), or one of each, + * surrounding a pair of version strings separated by a comma, where a version string + * consists of any non-whitespace non-bracket characters + * (ie numbers, letters, dots, hyphens, underscores) or is empty (to indicate no bound); + * e.g. "[10.6,10.7)" to mean >= 10.6 and < 10.7; + * "[10.6,)" to mean >= 10.6. + */ + public boolean isInRange(String range) { + String r = range.trim(); + boolean strictLeft, strictRight; + + if (r.startsWith("(")) strictLeft = true; + else if (r.startsWith("[")) strictLeft = false; + else throw new IllegalArgumentException("Range must start with ( or ["); + if (r.endsWith(")")) strictRight = true; + else if (r.endsWith("]")) strictRight = false; + else throw new IllegalArgumentException("Range must end with ) or ]"); + + int i = r.indexOf(","); + if (i==-1) throw new IllegalArgumentException("Range must contain , following the open bracket and version"); + String left = r.substring(1, i).trim(); + String right = r.substring(i+1, r.length()-1).trim(); + + if (left.length()>0) { + if (strictLeft && compareTo(left)<=0) return false; + if (!strictLeft && compareTo(left)<0) return false; + } + if (right.length()>0) { + if (strictRight && compareTo(right)>=0) return false; + if (!strictRight && compareTo(right)>0) return false; + } + + return true; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/cf2f7a93/utils/common/src/main/java/org/apache/brooklyn/util/text/FormattedString.java ---------------------------------------------------------------------- diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/text/FormattedString.java b/utils/common/src/main/java/org/apache/brooklyn/util/text/FormattedString.java new file mode 100644 index 0000000..cdb36f7 --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/text/FormattedString.java @@ -0,0 +1,47 @@ +/* + * 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 com.google.common.base.Preconditions; +import com.google.common.base.Supplier; + +/** wraps a call to {@link String#format(String, Object...)} in a toString, i.e. using %s syntax, + * useful for places where we want deferred evaluation + * (e.g. as message to {@link Preconditions} to skip concatenation when not needed) */ +public class FormattedString { + private final String pattern; + private final Object[] args; + public FormattedString(String pattern, Object[] args) { + this.pattern = pattern; + this.args = args; + } + @Override + public String toString() { + return String.format(pattern, args); + } + public String getPattern() { + return pattern; + } + public Object[] getArgs() { + return args; + } + public Supplier<String> supplier() { + return Strings.toStringSupplier(this); + } +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/cf2f7a93/utils/common/src/main/java/org/apache/brooklyn/util/text/Identifiers.java ---------------------------------------------------------------------- diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/text/Identifiers.java b/utils/common/src/main/java/org/apache/brooklyn/util/text/Identifiers.java new file mode 100644 index 0000000..8d3e035 --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/text/Identifiers.java @@ -0,0 +1,210 @@ +/* + * 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.util.Random; + +public class Identifiers { + + private static Random random = new Random(); + + public static final String JAVA_GOOD_START_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_"; + public static final String JAVA_GOOD_NONSTART_CHARS = JAVA_GOOD_START_CHARS+"1234567890"; + + public static final String JAVA_GENERATED_IDENTIFIER_START_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + public static final String JAVA_GENERATED_IDENTIFIERNONSTART_CHARS = JAVA_GENERATED_IDENTIFIER_START_CHARS+"1234567890"; + + public static final String BASE64_VALID_CHARS = JAVA_GENERATED_IDENTIFIERNONSTART_CHARS+"+="; + + public static final String ID_VALID_START_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + public static final String ID_VALID_NONSTART_CHARS = ID_VALID_START_CHARS+"1234567890"; + + /** makes a random id string (letters and numbers) of the given length; + * starts with letter (upper or lower) so can be used as java-id; + * tests ensure random distribution, so random ID of length 5 + * is about 2^29 possibilities + * <p> + * With ID of length 4 it is not unlikely (15% chance) to get + * duplicates in the first 2000 attempts. + * With ID of length 8 there is 1% chance to get duplicates + * in the first 1M attempts and 50% for the first 16M. + * <p> + * implementation is efficient, uses char array, and + * makes one call to random per 5 chars; makeRandomId(5) + * takes about 4 times as long as a simple Math.random call, + * or about 50 times more than a simple x++ instruction; + * in other words, it's appropriate for contexts where random id's are needed, + * but use efficiently (ie cache it per object), and + * prefer to use a counter where feasible + * <p> + * in general this is preferable to base64 as is more portable, + * can be used throughout javascript (as ID's which don't allow +) + * or as java identifiers (which don't allow numbers in the first char) + **/ + public static String makeRandomId(int l) { + //this version is 30-50% faster than the old double-based one, + //which computed a random every 3 turns -- + //takes about 600 ns to do id of len 10, compared to 10000 ns for old version [on 1.6ghz machine] + if (l<=0) return ""; + char[] id = new char[l]; + int d = random.nextInt( (26+26) * (26+26+10) * (26+26+10) * (26+26+10) * (26+26+10)); + int i = 0; + id[i] = ID_VALID_START_CHARS.charAt(d % (26+26)); + d /= (26+26); + if (++i<l) do { + id[i] = ID_VALID_NONSTART_CHARS.charAt(d%(26+26+10)); + if (++i>=l) break; + if (i%5==0) { + d = random.nextInt( (26+26+10) * (26+26+10) * (26+26+10) * (26+26+10) * (26+26+10)); + } else { + d /= (26+26+10); + } + } while (true); + //Message.message("random id is " + id); + return new String(id); + } + + /** creates a short identifier comfortable in java and OS's, given an input hash code + * <p> + * result is always at least of length 1, shorter if the hash is smaller */ + public static String makeIdFromHash(long d) { + StringBuffer result = new StringBuffer(); + if (d<0) d=-d; + // correction for Long.MIN_VALUE + if (d<0) d=-(d+1000); + + result.append(ID_VALID_START_CHARS.charAt((int)(d % (26+26)))); + d /= (26+26); + while (d!=0) { + result.append(ID_VALID_NONSTART_CHARS.charAt((int)(d%(26+26+10)))); + d /= (26+26+10); + } + return result.toString(); + } + + /** makes a random id string (letters and numbers) of the given length; + * starts with letter (upper or lower) so can be used as java-id; + * tests ensure random distribution, so random ID of length 5 + * is about 2^29 possibilities + * <p> + * implementation is efficient, uses char array, and + * makes one call to random per 5 chars; makeRandomId(5) + * takes about 4 times as long as a simple Math.random call, + * or about 50 times more than a simple x++ instruction; + * in other words, it's appropriate for contexts where random id's are needed, + * but use efficiently (ie cache it per object), and + * prefer to use a counter where feasible + **/ + public static String makeRandomJavaId(int l) { + // copied from Monterey util's com.cloudsoftcorp.util.StringUtils. + // TODO should share code with makeRandomId, just supplying different char sets (though the char sets in fact are the same..) + + //this version is 30-50% faster than the old double-based one, + //which computed a random every 3 turns -- + //takes about 600 ns to do id of len 10, compared to 10000 ns for old version [on 1.6ghz machine] + if (l<=0) return ""; + char[] id = new char[l]; + int d = random.nextInt( (26+26) * (26+26+10) * (26+26+10) * (26+26+10) * (26+26+10)); + int i = 0; + id[i] = JAVA_GENERATED_IDENTIFIER_START_CHARS.charAt(d % (26+26)); + d /= (26+26); + if (++i<l) do { + id[i] = JAVA_GENERATED_IDENTIFIERNONSTART_CHARS.charAt(d%(26+26+10)); + if (++i>=l) break; + if (i%5==0) { + d = random.nextInt( (26+26+10) * (26+26+10) * (26+26+10) * (26+26+10) * (26+26+10)); + } else { + d /= (26+26+10); + } + } while (true); + //Message.message("random id is " + id); + return new String(id); + } + + public static double randomDouble() { + return random.nextDouble(); + } + public static long randomLong() { + return random.nextLong(); + } + public static boolean randomBoolean() { + return random.nextBoolean(); + } + public static int randomInt() { + return random.nextInt(); + } + /** returns in [0,upbound) */ + public static int randomInt(int upbound) { + return random.nextInt(upbound); + } + /** returns the array passed in */ + public static byte[] randomBytes(byte[] buf) { + random.nextBytes(buf); + return buf; + } + public static byte[] randomBytes(int length) { + byte[] buf = new byte[length]; + return randomBytes(buf); + } + + public static String makeRandomBase64Id(int length) { + StringBuilder s = new StringBuilder(); + while (length>0) { + appendBase64IdFromValueOfLength(randomLong(), length>10 ? 10 : length, s); + length -= 10; + } + return s.toString(); + } + public static String getBase64IdFromValue(long value) { + return getBase64IdFromValue(value, 10); + } + public static String getBase64IdFromValue(long value, int length) { + StringBuilder s = new StringBuilder(); + appendBase64IdFromValueOfLength(value, length, s); + return s.toString(); + } + public static void appendBase64IdFromValueOfLength(long value, int length, StringBuffer sb) { + if (length>11) + throw new IllegalArgumentException("can't get a Base64 string longer than 11 chars from a long"); + long idx = value; + for (int i=0; i<length; i++) { + byte x = (byte)(idx & 63); + sb.append(BASE64_VALID_CHARS.charAt(x)); + idx = idx >> 6; + } + } + public static void appendBase64IdFromValueOfLength(long value, int length, StringBuilder sb) { + if (length>11) + throw new IllegalArgumentException("can't get a Base64 string longer than 11 chars from a long"); + long idx = value; + for (int i=0; i<length; i++) { + byte x = (byte)(idx & 63); + sb.append(BASE64_VALID_CHARS.charAt(x)); + idx = idx >> 6; + } + } + + public static boolean isValidToken(String token, String validStartChars, String validSubsequentChars) { + if (token==null || token.length()==0) return false; + if (validStartChars.indexOf(token.charAt(0))==-1) return false; + for (int i=1; i<token.length(); i++) + if (validSubsequentChars.indexOf(token.charAt(i))==-1) return false; + return true; + } +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/cf2f7a93/utils/common/src/main/java/org/apache/brooklyn/util/text/KeyValueParser.java ---------------------------------------------------------------------- diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/text/KeyValueParser.java b/utils/common/src/main/java/org/apache/brooklyn/util/text/KeyValueParser.java new file mode 100644 index 0000000..1e42b67 --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/text/KeyValueParser.java @@ -0,0 +1,124 @@ +/* + * 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.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import com.google.common.base.Splitter; + + +/** + * Parses a String that consists of multiple arguments, which are either single or key-value pairs. + * The value may be in quotes. + * + * For example: + * a=x, b="x x", c, "d d" + * + * Would return the ordered map: + * "a" = "x" + * "b" = "x x" + * "c" = null + * "d d" = null + * + * Consider instead using {@link Splitter#withKeyValueSeparator(char)}, but that doesn't give the + * same behaviour for values, see {@link QuotedStringTokenizer}. For example: + * <pre> + * {@code + * String val = "a=x,b=y"; + * Map<String,String> map = Splitter.on(",").withKeyValueSeparator("=").split(val); + * } + * </pre> + * + * @author aled + **/ +public class KeyValueParser { + + public static String toLine(Map<String, String> parts) { + QuotedStringTokenizer tokenizer = new QuotedStringTokenizer("", true); + + StringBuilder result = new StringBuilder(); + for (Map.Entry<String, String> entry : parts.entrySet()) { + if (result.length()>0) result.append(", "); + result.append(tokenizer.quoteToken(entry.getKey())); + if (entry.getValue() != null) result.append("="+tokenizer.quoteToken(entry.getValue())); + } + return result.toString(); + } + + public static String toLine(Collection<String> parts) { + QuotedStringTokenizer tokenizer = new QuotedStringTokenizer("", false); + + StringBuilder result = new StringBuilder(); + for (String part : parts) { + result.append(tokenizer.quoteToken(part)+", "); + } + if (result.length() > 0) result.deleteCharAt(result.length()-1); + return result.toString(); + } + + public static List<String> parseList(String line) { + List<String> result = new ArrayList<String>(); + QuotedStringTokenizer tokenizer = new QuotedStringTokenizer(line, null, true, ",", false); + + while (tokenizer.hasMoreTokens()) { + result.add(tokenizer.unquoteToken(tokenizer.nextToken().trim())); + } + return result; + } + + @Deprecated // use parseMap + public static Map<String,String> parse(String line) { + return parseMap(line); + } + + /** takes a string of the form "key=value,key2=value2" and returns a map; + * values can be quoted (but not keys) */ + public static Map<String,String> parseMap(String line) { + Map<String,String> result = new LinkedHashMap<String,String>(); + + QuotedStringTokenizer tokenizer = new QuotedStringTokenizer(line, null, true, ",", false); + + while (tokenizer.hasMoreTokens()) { + //String token = tokenizer.unquoteToken(tokenizer.nextToken().trim()); + String token = tokenizer.nextToken().trim(); + + int index = token.indexOf("="); + + if (index < 0) { + String unquotedKey = tokenizer.unquoteToken(token); + result.put(unquotedKey, null); + + } else if (index < (token.length()-1)) { + String unquotedKey = tokenizer.unquoteToken(token.substring(0, index).trim()); + String unquotedVal = tokenizer.unquoteToken(token.substring(index+1).trim()); + result.put(unquotedKey, unquotedVal); + + } else { // ends with = + assert index == token.length() -1; + String unquotedKey = tokenizer.unquoteToken(token.substring(0, index).trim()); + result.put(unquotedKey, ""); + } + } + return result; + } +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/cf2f7a93/utils/common/src/main/java/org/apache/brooklyn/util/text/NaturalOrderComparator.java ---------------------------------------------------------------------- diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/text/NaturalOrderComparator.java b/utils/common/src/main/java/org/apache/brooklyn/util/text/NaturalOrderComparator.java new file mode 100644 index 0000000..be53acc --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/text/NaturalOrderComparator.java @@ -0,0 +1,165 @@ +/* + * 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. + * + +BROOKLYN NOTE: This is based on code from Pierre-Luc Paour, +adapted for the Brooklyn project in accordance with the original terms below. +Main changes are the package and edits for more recent Java compatibility. + +-- + +NaturalOrderComparator.java -- Perform 'natural order' comparisons of strings in Java. +Copyright (C) 2003 by Pierre-Luc Paour <[email protected]> + +Based on the C version by Martin Pool, of which this is more or less a straight conversion. +Copyright (C) 2000 by Martin Pool <[email protected]> + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not +claim that you wrote the original software. If you use this software +in a product, an acknowledgment in the product documentation would be +appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be +misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. + + */ +package org.apache.brooklyn.util.text; + +import java.util.Comparator; + +/** comparator which takes two strings and puts them in an order with special rules for numbers to be placed in numeric order; + * e.g. "10">"9", including when those numbers occur in the midst of equal text; e.g. "a10" > "a9"; + * but not if the text differs; e.g. "a10" < "b9" + * <p> + * class is thread-safe. nulls not supported. (to support nulls, wrap in guava: + * <code>Ordering.from(NaturalOrderComparator.INSTANCE).nullsFirst()</code>) + */ +public class NaturalOrderComparator implements Comparator<String> { + + public static final NaturalOrderComparator INSTANCE = new NaturalOrderComparator(); + + int compareRight(String a, String b) + { + int bias = 0; + int ia = 0; + int ib = 0; + + // The longest run of digits wins. That aside, the greatest + // value wins, but we can't know that it will until we've scanned + // both numbers to know that they have the same magnitude, so we + // remember it in BIAS. + for (;; ia++, ib++) { + char ca = charAt(a, ia); + char cb = charAt(b, ib); + + if (!Character.isDigit(ca) + && !Character.isDigit(cb)) { + return bias; + } else if (!Character.isDigit(ca)) { + return -1; + } else if (!Character.isDigit(cb)) { + return +1; + } else if (ca < cb) { + if (bias == 0) { + bias = -1; + } + } else if (ca > cb) { + if (bias == 0) + bias = +1; + } else if (ca == 0 && cb == 0) { + return bias; + } + } + } + + public int compare(String a, String b) { + + int ia = 0, ib = 0; + int nza = 0, nzb = 0; + char ca, cb; + int result; + + while (true) { + // only count the number of zeroes leading the last number compared + nza = nzb = 0; + + ca = charAt(a, ia); cb = charAt(b, ib); + + // skip over leading spaces or zeros + while (Character.isSpaceChar(ca) || ca == '0') { + if (ca == '0') { + nza++; + } else { + // only count consecutive zeroes + nza = 0; + } + + ca = charAt(a, ++ia); + } + + while (Character.isSpaceChar(cb) || cb == '0') { + if (cb == '0') { + nzb++; + } else { + // only count consecutive zeroes + nzb = 0; + } + + cb = charAt(b, ++ib); + } + + // process run of digits + if (Character.isDigit(ca) && Character.isDigit(cb)) { + if ((result = compareRight(a.substring(ia), b.substring(ib))) != 0) { + return result; + } + } + + if (ca == 0 && cb == 0) { + // The strings compare the same. Perhaps the caller + // will want to call strcmp to break the tie. + return nza - nzb; + } + + if (ca < cb) { + return -1; + } else if (ca > cb) { + return +1; + } + + ++ia; ++ib; + } + } + + static char charAt(String s, int i) { + if (i >= s.length()) { + return 0; + } else { + return s.charAt(i); + } + } + +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/cf2f7a93/utils/common/src/main/java/org/apache/brooklyn/util/text/QuotedStringTokenizer.java ---------------------------------------------------------------------- diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/text/QuotedStringTokenizer.java b/utils/common/src/main/java/org/apache/brooklyn/util/text/QuotedStringTokenizer.java new file mode 100644 index 0000000..0a0ade9 --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/text/QuotedStringTokenizer.java @@ -0,0 +1,196 @@ +/* + * 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.util.ArrayList; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.StringTokenizer; + +/** As 'StringTokenizer' but items in quotes (single or double) are treated as single tokens + * (cf mortbay's QuotedStringTokenizer) + */ +public class QuotedStringTokenizer { + + final StringTokenizer delegate; + final String quoteChars; + final boolean includeQuotes; + final String delimiters; + final boolean includeDelimiters; + + public static String DEFAULT_QUOTE_CHARS = "\"\'"; + + + protected String DEFAULT_QUOTE_CHARS() { + return DEFAULT_QUOTE_CHARS; + } + + public final static String DEFAULT_DELIMITERS = " \t\n\r\f"; + + /** default quoted tokenizer, using single and double quotes as quote chars and returning quoted results + * (use unquoteToken to unquote), and using whitespace chars as delimeters (not included as tokens); + * string may be null if the nothing will be tokenized and the class is used only for + * quoteToken(String) and unquote(String). + */ + public QuotedStringTokenizer(String stringToTokenize) { + this(stringToTokenize, true); + } + public QuotedStringTokenizer(String stringToTokenize, boolean includeQuotes) { + this(stringToTokenize, null, includeQuotes); + } + public QuotedStringTokenizer(String stringToTokenize, String quoteChars, boolean includeQuotes) { + this(stringToTokenize, quoteChars, includeQuotes, null, false); + } + + public QuotedStringTokenizer(String stringToTokenize, String quoteChars, boolean includeQuotes, String delimiters, boolean includeDelimiters) { + delegate = new StringTokenizer(stringToTokenize==null ? "" : stringToTokenize, (delimiters==null ? DEFAULT_DELIMITERS : delimiters), true); + this.quoteChars = quoteChars==null ? DEFAULT_QUOTE_CHARS() : quoteChars; + this.includeQuotes = includeQuotes; + this.delimiters = delimiters==null ? DEFAULT_DELIMITERS : delimiters; + this.includeDelimiters = includeDelimiters; + updateNextToken(); + } + + public static class Builder { + private String quoteChars = DEFAULT_QUOTE_CHARS; + private boolean includeQuotes=true; + private String delimiterChars=DEFAULT_DELIMITERS; + private boolean includeDelimiters=false; + + public QuotedStringTokenizer build(String stringToTokenize) { + return new QuotedStringTokenizer(stringToTokenize, quoteChars, includeQuotes, delimiterChars, includeDelimiters); + } + public List<String> buildList(String stringToTokenize) { + return new QuotedStringTokenizer(stringToTokenize, quoteChars, includeQuotes, delimiterChars, includeDelimiters).remainderAsList(); + } + + public Builder quoteChars(String quoteChars) { this.quoteChars = quoteChars; return this; } + public Builder addQuoteChars(String quoteChars) { this.quoteChars = this.quoteChars + quoteChars; return this; } + public Builder includeQuotes(boolean includeQuotes) { this.includeQuotes = includeQuotes; return this; } + public Builder delimiterChars(String delimiterChars) { this.delimiterChars = delimiterChars; return this; } + public Builder addDelimiterChars(String delimiterChars) { this.delimiterChars = this.delimiterChars + delimiterChars; return this; } + public Builder includeDelimiters(boolean includeDelimiters) { this.includeDelimiters = includeDelimiters; return this; } + } + public static Builder builder() { + return new Builder(); + } + + String peekedNextToken = null; + + public synchronized boolean hasMoreTokens() { + return peekedNextToken!=null; + } + + public synchronized String nextToken() { + if (peekedNextToken==null) throw new NoSuchElementException(); + String lastToken = peekedNextToken; + updateNextToken(); + return includeQuotes ? lastToken : unquoteToken(lastToken); + } + + /** this method removes all unescaped quote chars, i.e. quote chars preceded by no backslashes (or a larger even number of them); + * it also unescapes '\\' as '\'. it does no other unescaping. */ + public String unquoteToken(String word) { + // ( (\\A|[^\\\\]) (\\\\\\\\)* ) [ Pattern.quote(quoteChars) ] $1 + word = word.replaceAll( + "((\\A|[^\\\\])(\\\\\\\\)*)["+ + //Pattern.quote( + quoteChars + //) + +"]+", + "$1"); + //above pattern removes any quote preceded by even number of backslashes + //now it is safe to replace any \c by c + word = word.replaceAll("\\\\"+"([\\\\"+ + //Pattern.quote( + quoteChars + //) + +"])", "$1"); + + return word; + } + + /** returns the input text escaped for use with unquoteTokens, and wrapped in the quoteChar[0] (usu a double quote) */ + public String quoteToken(String unescapedText) { + String result = unescapedText; + //replace every backslash by two backslashes + result = result.replaceAll("\\\\", "\\\\\\\\"); + //now replace every quote char by backslash quote char + result = result.replaceAll("(["+quoteChars+"])", "\\\\$1"); + //then wrap in quote + result = quoteChars.charAt(0) + result + quoteChars.charAt(0); + return result; + } + + protected synchronized void updateNextToken() { + peekedNextToken = null; + String token; + do { + if (!delegate.hasMoreTokens()) return; + token = delegate.nextToken(); + //skip delimeters + } while (!includeDelimiters && token.matches("["+delimiters+"]+")); + + StringBuffer nextToken = new StringBuffer(token); + pullUntilValid(nextToken); + peekedNextToken = nextToken.toString(); + } + + private void pullUntilValid(StringBuffer nextToken) { + while (hasOpenQuote(nextToken.toString(), quoteChars) && delegate.hasMoreTokens()) { + //keep appending until the quote is ended or there are no more quotes + nextToken.append(delegate.nextToken()); + } + } + + public static boolean hasOpenQuote(String stringToCheck) { + return hasOpenQuote(stringToCheck, DEFAULT_QUOTE_CHARS); + } + + public static boolean hasOpenQuote(String stringToCheck, String quoteChars) { + String x = stringToCheck; + if (x==null) return false; + + StringBuffer xi = new StringBuffer(); + for (int i=0; i<x.length(); i++) { + char c = x.charAt(i); + if (c=='\\') i++; + else if (quoteChars.indexOf(c)>=0) { + xi.append(c); + } + } + x = xi.toString(); + + while (x.length()>0) { + char c = x.charAt(0); + int match = x.indexOf(c, 1); + if (match==-1) return true; + x = x.substring(match+1); + } + return false; + } + + public List<String> remainderAsList() { + List<String> l = new ArrayList<String>(); + while (hasMoreTokens()) + l.add(nextToken()); + return l; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/cf2f7a93/utils/common/src/main/java/org/apache/brooklyn/util/text/StringEscapes.java ---------------------------------------------------------------------- diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/text/StringEscapes.java b/utils/common/src/main/java/org/apache/brooklyn/util/text/StringEscapes.java new file mode 100644 index 0000000..276d0fb --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/text/StringEscapes.java @@ -0,0 +1,412 @@ +/* + * 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; + +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") + + /** 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()-2).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(); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/cf2f7a93/utils/common/src/main/java/org/apache/brooklyn/util/text/StringFunctions.java ---------------------------------------------------------------------- diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/text/StringFunctions.java b/utils/common/src/main/java/org/apache/brooklyn/util/text/StringFunctions.java new file mode 100644 index 0000000..3eec640 --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/text/StringFunctions.java @@ -0,0 +1,157 @@ +/* + * 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 javax.annotation.Nullable; + +import com.google.common.base.CaseFormat; +import com.google.common.base.CharMatcher; +import com.google.common.base.Function; +import com.google.common.base.Functions; +import com.google.common.base.Preconditions; + +public class StringFunctions { + + public static Function<String,String> append(final String suffix) { + return new Function<String, String>() { + @Override + @Nullable + public String apply(@Nullable String input) { + if (input==null) return null; + return input + suffix; + } + }; + } + + public static Function<String,String> prepend(final String prefix) { + return new Function<String, String>() { + @Override + @Nullable + public String apply(@Nullable String input) { + if (input==null) return null; + return prefix + input; + } + }; + } + + /** given e.g. "hello %s" returns a function which will insert a string into that pattern */ + public static Function<Object, String> formatter(final String pattern) { + return new Function<Object, String>() { + public String apply(@Nullable Object input) { + return String.format(pattern, input); + } + }; + } + + /** given e.g. "hello %s %s" returns a function which will insert an array of two strings into that pattern */ + public static Function<Object[], String> formatterForArray(final String pattern) { + return new Function<Object[], String>() { + public String apply(@Nullable Object[] input) { + return String.format(pattern, input); + } + }; + } + + /** joins the given objects in a collection as a toString with the given separator */ + public static Function<Iterable<?>, String> joiner(final String separator) { + return new Function<Iterable<?>, String>() { + public String apply(@Nullable Iterable<?> input) { + return Strings.join(input, separator); + } + }; + } + + /** joins the given objects as a toString with the given separator, but expecting an array of objects, not a collection */ + public static Function<Object[], String> joinerForArray(final String separator) { + return new Function<Object[], String>() { + public String apply(@Nullable Object[] input) { + if (input == null) return Strings.EMPTY; + return Strings.join(input, separator); + } + }; + } + + /** provided here as a convenience; prefer {@link Functions#toStringFunction()} */ + public static Function<Object,String> toStringFunction() { + return Functions.toStringFunction(); + } + + /** returns function which gives length of input, with -1 for nulls */ + public static Function<String,Integer> length() { + return new Function<String,Integer>() { + @Override + public Integer apply(@Nullable String input) { + if (input == null) return -1; + return input.length(); + } + }; + } + + /** Surrounds an input string with the given prefix and suffix */ + public static Function<String,String> surround(final String prefix, final String suffix) { + Preconditions.checkNotNull(prefix); + Preconditions.checkNotNull(suffix); + return new Function<String,String>() { + @Override + public String apply(@Nullable String input) { + if (input == null) return null; + return prefix+input+suffix; + } + }; + } + + public static Function<String, String> trim() { + return new Function<String, String>() { + @Override + public String apply(@Nullable String input) { + if (input == null) return null; + if (Strings.isBlank(input)) return Strings.EMPTY; + return CharMatcher.BREAKING_WHITESPACE.trimFrom(input); + } + }; + } + + public static Function<String, String> toLowerCase() { + return new Function<String, String>() { + @Override + public String apply(String input) { + return input.toLowerCase(); + } + }; + } + + public static Function<String, String> toUpperCase() { + return new Function<String, String>() { + @Override + public String apply(String input) { + return input.toUpperCase(); + } + }; + } + + public static Function<String, String> convertCase(final CaseFormat src, final CaseFormat target) { + return new Function<String, String>() { + @Override + public String apply(String input) { + return src.to(target, input); + } + }; + } + +}
