You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@brooklyn.apache.org by ha...@apache.org on 2015/08/18 17:03:18 UTC

[09/64] [abbrv] incubator-brooklyn git commit: BROOKLYN-162 - apply org.apache package prefix to utils-common

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/cf2f7a93/utils/common/src/main/java/org/apache/brooklyn/util/text/StringPredicates.java
----------------------------------------------------------------------
diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/text/StringPredicates.java b/utils/common/src/main/java/org/apache/brooklyn/util/text/StringPredicates.java
new file mode 100644
index 0000000..2006e65
--- /dev/null
+++ b/utils/common/src/main/java/org/apache/brooklyn/util/text/StringPredicates.java
@@ -0,0 +1,310 @@
+/*
+ * 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.Serializable;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+import org.apache.brooklyn.util.collections.MutableSet;
+
+import com.google.common.base.Function;
+import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+
+public class StringPredicates {
+
+    /** predicate form of {@link Strings#isBlank(CharSequence)} */
+    public static <T extends CharSequence> Predicate<T> isBlank() {
+        return new IsBlank<T>();
+    }
+
+    private static final class IsBlank<T extends CharSequence> implements Predicate<T> {
+        @Override
+        public boolean apply(@Nullable CharSequence input) {
+            return Strings.isBlank(input);
+        }
+
+        @Override
+        public String toString() {
+            return "isBlank()";
+        }
+    }
+
+    /** @deprecated since 0.7.0 kept only to allow conversion of anonymous inner classes */
+    @SuppressWarnings("unused") @Deprecated 
+    private static Predicate<CharSequence> isBlankOld() {
+        return new Predicate<CharSequence>() {
+            @Override
+            public boolean apply(@Nullable CharSequence input) {
+                return Strings.isBlank(input);
+            }
+            @Override
+            public String toString() {
+                return "isBlank";
+            }
+        };
+    }
+
+
+    /** Tests if object is non-null and not a blank string.
+     * <p>
+     * Predicate form of {@link Strings#isNonBlank(CharSequence)} also accepting objects non-null, for convenience */
+    public static <T> Predicate<T> isNonBlank() {
+        return new IsNonBlank<T>();
+    }
+
+    private static final class IsNonBlank<T> implements Predicate<T> {
+        @Override
+        public boolean apply(@Nullable Object input) {
+            if (input==null) return false;
+            if (!(input instanceof CharSequence)) return true;
+            return Strings.isNonBlank((CharSequence)input);
+        }
+
+        @Override
+        public String toString() {
+            return "isNonBlank()";
+        }
+    }
+    
+    // -----------------
+    
+    public static <T extends CharSequence> Predicate<T> containsLiteralIgnoreCase(final String fragment) {
+        return new ContainsLiteralIgnoreCase<T>(fragment);
+    }
+
+    private static final class ContainsLiteralIgnoreCase<T extends CharSequence> implements Predicate<T> {
+        private final String fragment;
+
+        private ContainsLiteralIgnoreCase(String fragment) {
+            this.fragment = fragment;
+        }
+
+        @Override
+        public boolean apply(@Nullable CharSequence input) {
+            return Strings.containsLiteralIgnoreCase(input, fragment);
+        }
+
+        @Override
+        public String toString() {
+            return "containsLiteralCaseInsensitive("+fragment+")";
+        }
+    }
+
+    public static <T extends CharSequence> Predicate<T> containsLiteral(final String fragment) {
+        return new ContainsLiteral<T>(fragment);
+    }
+    
+    private static final class ContainsLiteral<T extends CharSequence> implements Predicate<T> {
+        private final String fragment;
+
+        private ContainsLiteral(String fragment) {
+            this.fragment = fragment;
+        }
+
+        @Override
+        public boolean apply(@Nullable CharSequence input) {
+            return Strings.containsLiteral(input, fragment);
+        }
+
+        @Override
+        public String toString() {
+            return "containsLiteral("+fragment+")";
+        }
+    }
+
+    /** @deprecated since 0.7.0 kept only to allow conversion of anonymous inner classes */
+    @SuppressWarnings("unused") @Deprecated 
+    private static Predicate<CharSequence> containsLiteralCaseInsensitiveOld(final String fragment) {
+        return new Predicate<CharSequence>() {
+            @Override
+            public boolean apply(@Nullable CharSequence input) {
+                return Strings.containsLiteralIgnoreCase(input, fragment);
+            }
+            @Override
+            public String toString() {
+                return "containsLiteralCaseInsensitive("+fragment+")";
+            }
+        };
+    }
+
+    /** @deprecated since 0.7.0 kept only to allow conversion of anonymous inner classes */
+    @SuppressWarnings("unused") @Deprecated 
+    private static Predicate<CharSequence> containsLiteralOld(final String fragment) {
+        return new Predicate<CharSequence>() {
+            @Override
+            public boolean apply(@Nullable CharSequence input) {
+                return Strings.containsLiteral(input, fragment);
+            }
+            @Override
+            public String toString() {
+                return "containsLiteral("+fragment+")";
+            }
+        };
+    }
+    
+    // -----------------
+    
+    public static <T extends CharSequence> Predicate<T> containsAllLiterals(final String... fragments) {
+        List<Predicate<CharSequence>> fragmentPredicates = Lists.newArrayList();
+        for (String fragment : fragments) {
+            fragmentPredicates.add(containsLiteral(fragment));
+        }
+        return Predicates.and(fragmentPredicates);
+    }
+
+    /** @deprecated since 0.7.0 kept only to allow conversion of anonymous inner classes */
+    @SuppressWarnings("unused") @Deprecated 
+    private static Predicate<CharSequence> containsAllLiteralsOld(final String... fragments) {
+        return Predicates.and(Iterables.transform(Arrays.asList(fragments), new Function<String,Predicate<CharSequence>>() {
+            @Override
+            public Predicate<CharSequence> apply(String input) {
+                return containsLiteral(input);
+            }
+        }));
+    }
+    
+    // -----------------
+
+    public static Predicate<CharSequence> containsRegex(final String regex) {
+        // "Pattern" ... what a bad name :)
+        return Predicates.containsPattern(regex);
+    }
+
+    // -----------------
+    
+    public static <T extends CharSequence> Predicate<T> startsWith(final String prefix) {
+        return new StartsWith<T>(prefix);
+    }
+
+    private static final class StartsWith<T extends CharSequence> implements Predicate<T> {
+        private final String prefix;
+        private StartsWith(String prefix) {
+            this.prefix = prefix;
+        }
+        @Override
+        public boolean apply(CharSequence input) {
+            return (input != null) && input.toString().startsWith(prefix);
+        }
+        @Override
+        public String toString() {
+            return "startsWith("+prefix+")";
+        }
+    }
+
+    /** @deprecated since 0.7.0 kept only to allow conversion of anonymous inner classes */
+    @SuppressWarnings("unused") @Deprecated 
+    private static Predicate<CharSequence> startsWithOld(final String prefix) {
+        return new Predicate<CharSequence>() {
+            @Override
+            public boolean apply(CharSequence input) {
+                return (input != null) && input.toString().startsWith(prefix);
+            }
+        };
+    }
+
+    // -----------------
+    
+    /** true if the object *is* a {@link CharSequence} starting with the given prefix */
+    public static Predicate<Object> isStringStartingWith(final String prefix) {
+        return Predicates.<Object>and(Predicates.instanceOf(CharSequence.class),
+            Predicates.compose(startsWith(prefix), StringFunctions.toStringFunction()));
+    }
+
+    /** @deprecated since 0.7.0 kept only to allow conversion of anonymous inner classes */
+    @SuppressWarnings("unused") @Deprecated 
+    private static Predicate<Object> isStringStartingWithOld(final String prefix) {
+        return new Predicate<Object>() {
+            @Override
+            public boolean apply(Object input) {
+                return (input instanceof CharSequence) && input.toString().startsWith(prefix);
+            }
+        };
+    }
+
+    // ---------------
+    
+    public static <T> Predicate<T> equalToAny(Iterable<T> vals) {
+        return new EqualToAny<T>(vals);
+    }
+
+    private static class EqualToAny<T> implements Predicate<T>, Serializable {
+        private static final long serialVersionUID = 6209304291945204422L;
+        private final Set<T> vals;
+        
+        public EqualToAny(Iterable<? extends T> vals) {
+            this.vals = MutableSet.copyOf(vals); // so allows nulls
+        }
+        @Override
+        public boolean apply(T input) {
+            return vals.contains(input);
+        }
+        @Override
+        public String toString() {
+            return "equalToAny("+vals+")";
+        }
+    }
+
+    // -----------
+    
+    public static <T extends CharSequence> Predicate<T> matchesRegex(final String regex) {
+        return new MatchesRegex<T>(regex);
+    }
+
+    protected static class MatchesRegex<T extends CharSequence> implements Predicate<T> {
+        protected final String regex;
+        protected MatchesRegex(String regex) {
+            this.regex = regex;
+        }
+        @Override
+        public boolean apply(CharSequence input) {
+            return (input != null) && input.toString().matches(regex);
+        }
+        @Override
+        public String toString() {
+            return "matchesRegex("+regex+")";
+        }
+    }
+    
+    public static <T extends CharSequence> Predicate<T> matchesGlob(final String glob) {
+        return new MatchesGlob<T>(glob);
+    }
+
+    protected static class MatchesGlob<T extends CharSequence> implements Predicate<T> {
+        protected final String glob;
+        protected MatchesGlob(String glob) {
+            this.glob = glob;
+        }
+        @Override
+        public boolean apply(CharSequence input) {
+            return (input != null) && WildcardGlobs.isGlobMatched(glob, input.toString());
+        }
+        @Override
+        public String toString() {
+            return "matchesGlob("+glob+")";
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/cf2f7a93/utils/common/src/main/java/org/apache/brooklyn/util/text/StringShortener.java
----------------------------------------------------------------------
diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/text/StringShortener.java b/utils/common/src/main/java/org/apache/brooklyn/util/text/StringShortener.java
new file mode 100644
index 0000000..bcf88a3
--- /dev/null
+++ b/utils/common/src/main/java/org/apache/brooklyn/util/text/StringShortener.java
@@ -0,0 +1,150 @@
+/*
+ * 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.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/** utility which takes a bunch of segments and applies shortening rules to them */
+public class StringShortener {
+
+    protected Map<String,String> wordsByIdInOrder = new LinkedHashMap<String,String>();
+    protected String separator = null;
+    
+    protected interface ShorteningRule {
+        /** returns the new list, with the relevant items in the list replaced */
+        public int apply(LinkedHashMap<String, String> words, int maxlen, int length);
+    }
+    
+    protected class TruncationRule implements ShorteningRule {
+        public TruncationRule(String id, int len) {
+            this.id = id;
+            this.len = len;
+        }
+        String id;
+        int len;
+        
+        public int apply(LinkedHashMap<String, String> words, int maxlen, int length) {
+            String v = words.get(id);
+            if (v!=null && v.length()>len) {
+                int charsToRemove = v.length() - len;
+                if (length-charsToRemove < maxlen) charsToRemove = length-maxlen;
+                words.put(id, v.substring(0, v.length() - charsToRemove));
+                length -= charsToRemove;
+                if (charsToRemove==v.length() && separator!=null && length>0)
+                    length -= separator.length();
+            }
+            return length;
+        }
+    }
+    
+    protected class RemovalRule implements ShorteningRule {
+        public RemovalRule(String id) {
+            this.id = id;
+        }
+        String id;
+        
+        public int apply(LinkedHashMap<String, String> words, int maxlen, int length) {
+            String v = words.get(id);
+            if (v!=null) {
+                words.remove(id);
+                length -= v.length();
+                if (separator!=null && length>0)
+                    length -= separator.length();
+            }
+            return length;
+        }
+    }
+    
+    private List<ShorteningRule> rules = new ArrayList<StringShortener.ShorteningRule>();
+    
+
+    public StringShortener separator(String separator) {
+        this.separator = separator;
+        return this;
+    }
+
+    public StringShortener append(String id, String text) {
+        String old = wordsByIdInOrder.put(id, text);
+        if (old!=null) {
+            throw new IllegalStateException("Cannot append with id '"+id+"' when id already present");
+        }
+        // TODO expose a replace or update
+        return this;
+    }
+
+    public StringShortener truncate(String id, int len) {
+        String v = wordsByIdInOrder.get(id);
+        if (v!=null && v.length()>len) {
+            wordsByIdInOrder.put(id, v.substring(0, len));
+        }
+        return this;
+    }
+
+    public StringShortener canTruncate(String id, int len) {
+        rules.add(new TruncationRule(id, len));
+        return this;
+    }
+
+    public StringShortener canRemove(String id) {
+        rules.add(new RemovalRule(id));
+        return this;
+    }
+
+    public String getStringOfMaxLength(int maxlen) {
+        LinkedHashMap<String, String> words = new LinkedHashMap<String,String>();
+        words.putAll(wordsByIdInOrder);
+        int length = 0;
+        for (String w: words.values()) {
+            if (!Strings.isBlank(w)) {
+                length += w.length();
+                if (separator!=null)
+                    length += separator.length();
+            }
+        }
+        if (separator!=null && length>0)
+            // remove trailing separator if one had been added
+            length -= separator.length();
+        
+        List<ShorteningRule> rulesLeft = new ArrayList<ShorteningRule>();
+        rulesLeft.addAll(rules);
+        
+        while (length > maxlen && !rulesLeft.isEmpty()) {
+            ShorteningRule r = rulesLeft.remove(0);
+            length = r.apply(words, maxlen, length);
+        }
+        
+        StringBuilder sb = new StringBuilder();
+        for (String w: words.values()) {
+            if (!Strings.isBlank(w)) {
+                if (separator!=null && sb.length()>0)
+                    sb.append(separator);
+                sb.append(w);
+            }
+        }
+        
+        String result = sb.toString();
+        if (result.length() > maxlen) result = result.substring(0, maxlen);
+        
+        return result;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/cf2f7a93/utils/common/src/main/java/org/apache/brooklyn/util/text/Strings.java
----------------------------------------------------------------------
diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/text/Strings.java b/utils/common/src/main/java/org/apache/brooklyn/util/text/Strings.java
new file mode 100644
index 0000000..0ab9a3a
--- /dev/null
+++ b/utils/common/src/main/java/org/apache/brooklyn/util/text/Strings.java
@@ -0,0 +1,945 @@
+/*
+ * 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.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.text.NumberFormat;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.StringTokenizer;
+
+import javax.annotation.Nullable;
+
+import org.apache.brooklyn.util.collections.MutableList;
+import org.apache.brooklyn.util.collections.MutableMap;
+import org.apache.brooklyn.util.guava.Maybe;
+import org.apache.brooklyn.util.time.Time;
+
+import com.google.common.base.CharMatcher;
+import com.google.common.base.Functions;
+import com.google.common.base.Joiner;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.base.Supplier;
+import com.google.common.base.Suppliers;
+import com.google.common.collect.Ordering;
+
+public class Strings {
+
+    /** The empty {@link String}. */
+    public static final String EMPTY = "";
+
+    /**
+     * Checks if the given string is null or is an empty string.
+     * Useful for pre-String.isEmpty.  And useful for StringBuilder etc.
+     *
+     * @param s the String to check
+     * @return true if empty or null, false otherwise.
+     *
+     * @see #isNonEmpty(CharSequence)
+     * @see #isBlank(CharSequence)
+     * @see #isNonBlank(CharSequence)
+     */
+    public static boolean isEmpty(CharSequence s) {
+        // Note guava has com.google.common.base.Strings.isNullOrEmpty(String),
+        // but that is just for String rather than CharSequence
+        return s == null || s.length()==0;
+    }
+
+    /**
+     * Checks if the given string is empty or only consists of whitespace.
+     *
+     * @param s the String to check
+     * @return true if blank, empty or null, false otherwise.
+     *
+     * @see #isEmpty(CharSequence)
+     * @see #isNonEmpty(CharSequence)
+     * @see #isNonBlank(CharSequence)
+     */
+    public static boolean isBlank(CharSequence s) {
+        return isEmpty(s) || CharMatcher.WHITESPACE.matchesAllOf(s);
+    }
+
+    /**
+     * The inverse of {@link #isEmpty(CharSequence)}.
+     *
+     * @param s the String to check
+     * @return true if non empty, false otherwise.
+     *
+     * @see #isEmpty(CharSequence)
+     * @see #isBlank(CharSequence)
+     * @see #isNonBlank(CharSequence)
+     */
+    public static boolean isNonEmpty(CharSequence s) {
+        return !isEmpty(s);
+    }
+
+    /**
+     * The inverse of {@link #isBlank(CharSequence)}.
+     *
+     * @param s the String to check
+     * @return true if non blank, false otherwise.
+     *
+     * @see #isEmpty(CharSequence)
+     * @see #isNonEmpty(CharSequence)
+     * @see #isBlank(CharSequence)
+     */
+    public static boolean isNonBlank(CharSequence s) {
+        return !isBlank(s);
+    }
+
+    /** @return a {@link Maybe} object which is absent if the argument {@link #isBlank(CharSequence)} */
+    public static <T extends CharSequence> Maybe<T> maybeNonBlank(T s) {
+        if (isNonBlank(s)) return Maybe.of(s);
+        return Maybe.absent();
+    }
+
+    /** throws IllegalArgument if string not empty; cf. guava Preconditions.checkXxxx */
+    public static void checkNonEmpty(CharSequence s) {
+        if (s==null) throw new IllegalArgumentException("String must not be null");
+        if (s.length()==0) throw new IllegalArgumentException("String must not be empty");
+    }
+    /** throws IllegalArgument if string not empty; cf. guava Preconditions.checkXxxx */
+    public static void checkNonEmpty(CharSequence s, String message) {
+        if (isEmpty(s)) throw new IllegalArgumentException(message);
+    }
+
+    /**
+     * Removes suffix from the end of the string. Returns string if it does not end with suffix.
+     */
+    public static String removeFromEnd(String string, String suffix) {
+        if (isEmpty(string)) {
+            return string;
+        } else if (!isEmpty(suffix) && string.endsWith(suffix)) {
+            return string.substring(0, string.length() - suffix.length());
+        } else {
+            return string;
+        }
+    }
+
+    /** removes the first suffix in the list which is present at the end of string
+     * and returns that string; ignores subsequent suffixes if a matching one is found;
+     * returns the original string if no suffixes are at the end
+     * @deprecated since 0.7.0 use {@link #removeFromEnd(String, String)} or {@link #removeAllFromEnd(String, String...)}
+     */
+    @Deprecated
+    public static String removeFromEnd(String string, String ...suffixes) {
+        if (isEmpty(string)) return string;
+        for (String suffix : suffixes)
+            if (suffix!=null && string.endsWith(suffix)) return string.substring(0, string.length() - suffix.length());
+        return string;
+    }
+
+    /**
+     * As removeFromEnd, but repeats until all such suffixes are gone
+     */
+    public static String removeAllFromEnd(String string, String... suffixes) {
+        if (isEmpty(string)) return string;
+        int index = string.length();
+        boolean anotherLoopNeeded = true;
+        while (anotherLoopNeeded) {
+            if (isEmpty(string)) return string;
+            anotherLoopNeeded = false;
+            for (String suffix : suffixes)
+                if (!isEmpty(suffix) && string.startsWith(suffix, index - suffix.length())) {
+                    index -= suffix.length();
+                    anotherLoopNeeded = true;
+                    break;
+                }
+        }
+        return string.substring(0, index);
+    }
+
+    /**
+     * Removes prefix from the beginning of string. Returns string if it does not begin with prefix.
+     */
+    public static String removeFromStart(String string, String prefix) {
+        if (isEmpty(string)) {
+            return string;
+        } else if (!isEmpty(prefix) && string.startsWith(prefix)) {
+            return string.substring(prefix.length());
+        } else {
+            return string;
+        }
+    }
+
+    /** removes the first prefix in the list which is present at the start of string
+     * and returns that string; ignores subsequent prefixes if a matching one is found;
+     * returns the original string if no prefixes match
+     * @deprecated since 0.7.0 use {@link #removeFromStart(String, String)}
+     */
+    @Deprecated
+    public static String removeFromStart(String string, String ...prefixes) {
+        if (isEmpty(string)) return string;
+        for (String prefix : prefixes)
+            if (prefix!=null && string.startsWith(prefix)) return string.substring(prefix.length());
+        return string;
+    }
+
+    /**
+     * As {@link #removeFromStart(String, String)}, repeating until all such prefixes are gone.
+     */
+    public static String removeAllFromStart(String string, String... prefixes) {
+        int index = 0;
+        boolean anotherLoopNeeded = true;
+        while (anotherLoopNeeded) {
+            if (isEmpty(string)) return string;
+            anotherLoopNeeded = false;
+            for (String prefix : prefixes) {
+                if (!isEmpty(prefix) && string.startsWith(prefix, index)) {
+                    index += prefix.length();
+                    anotherLoopNeeded = true;
+                    break;
+                }
+            }
+        }
+        return string.substring(index);
+    }
+
+    /** convenience for {@link com.google.common.base.Joiner} */
+    public static String join(Iterable<? extends Object> list, String separator) {
+        if (list==null) return null;
+        boolean app = false;
+        StringBuilder out = new StringBuilder();
+        for (Object s: list) {
+            if (app) out.append(separator);
+            out.append(s);
+            app = true;
+        }
+        return out.toString();
+    }
+    /** convenience for {@link com.google.common.base.Joiner} */
+    public static String join(Object[] list, String separator) {
+        boolean app = false;
+        StringBuilder out = new StringBuilder();
+        for (Object s: list) {
+            if (app) out.append(separator);
+            out.append(s);
+            app = true;
+        }
+        return out.toString();
+    }
+
+    /** convenience for joining lines together */
+    public static String lines(String ...lines) {
+        return Joiner.on("\n").join(Arrays.asList(lines));
+    }
+
+    /** NON-REGEX - replaces all key->value entries from the replacement map in source (non-regex) */
+    @SuppressWarnings("rawtypes")
+    public static String replaceAll(String source, Map replacements) {
+        for (Object rr: replacements.entrySet()) {
+            Map.Entry r = (Map.Entry)rr;
+            source = replaceAllNonRegex(source, ""+r.getKey(), ""+r.getValue());
+        }
+        return source;
+    }
+
+    /** NON-REGEX replaceAll - see the better, explicitly named {@link #replaceAllNonRegex(String, String, String)}. */
+    public static String replaceAll(String source, String pattern, String replacement) {
+        return replaceAllNonRegex(source, pattern, replacement);
+    }
+
+    /** 
+     * Replaces all instances in source, of the given pattern, with the given replacement
+     * (not interpreting any arguments as regular expressions).
+     * <p>
+     * This is actually the same as the very ambiguous {@link String#replace(CharSequence, CharSequence)},
+     * which does replace all, but not using regex like the similarly ambiguous {@link String#replaceAll(String, String)} as.
+     * Alternatively see {@link #replaceAllRegex(String, String, String)}.
+     */
+    public static String replaceAllNonRegex(String source, String pattern, String replacement) {
+        if (source==null) return source;
+        StringBuilder result = new StringBuilder(source.length());
+        for (int i=0; i<source.length(); ) {
+            if (source.substring(i).startsWith(pattern)) {
+                result.append(replacement);
+                i += pattern.length();
+            } else {
+                result.append(source.charAt(i));
+                i++;
+            }
+        }
+        return result.toString();
+    }
+
+    /** REGEX replacement -- explicit method name for reabaility, doing same as {@link String#replaceAll(String, String)}. */
+    public static String replaceAllRegex(String source, String pattern, String replacement) {
+        return source.replaceAll(pattern, replacement);
+    }
+
+    /** Valid non alphanumeric characters for filenames. */
+    public static final String VALID_NON_ALPHANUM_FILE_CHARS = "-_.";
+
+    /**
+     * Returns a valid filename based on the input.
+     *
+     * A valid filename starts with the first alphanumeric character, then include
+     * all alphanumeric characters plus those in {@link #VALID_NON_ALPHANUM_FILE_CHARS},
+     * with any runs of invalid characters being replaced by {@literal _}.
+     *
+     * @throws NullPointerException if the input string is null.
+     * @throws IllegalArgumentException if the input string is blank.
+     */
+    public static String makeValidFilename(String s) {
+        Preconditions.checkNotNull(s, "Cannot make valid filename from null string");
+        Preconditions.checkArgument(isNonBlank(s), "Cannot make valid filename from blank string");
+        return CharMatcher.anyOf(VALID_NON_ALPHANUM_FILE_CHARS).or(CharMatcher.JAVA_LETTER_OR_DIGIT)
+                .negate()
+                .trimAndCollapseFrom(s, '_');
+    }
+
+    /**
+     * A {@link CharMatcher} that matches valid Java identifier characters.
+     *
+     * @see Character#isJavaIdentifierPart(char)
+     */
+    public static final CharMatcher IS_JAVA_IDENTIFIER_PART = CharMatcher.forPredicate(new Predicate<Character>() {
+        @Override
+        public boolean apply(@Nullable Character input) {
+            return input != null && Character.isJavaIdentifierPart(input);
+        }
+    });
+
+    /**
+     * Returns a valid Java identifier name based on the input.
+     *
+     * Removes certain characterss (like apostrophe), replaces one or more invalid
+     * characterss with {@literal _}, and prepends {@literal _} if the first character
+     * is only valid as an identifier part (not start).
+     * <p>
+     * The result is usually unique to s, though this isn't guaranteed, for example if
+     * all characters are invalid. For a unique identifier use {@link #makeValidUniqueJavaName(String)}.
+     *
+     * @see #makeValidUniqueJavaName(String)
+     */
+    public static String makeValidJavaName(String s) {
+        if (s==null) return "__null";
+        if (s.length()==0) return "__empty";
+        String name = IS_JAVA_IDENTIFIER_PART.negate().collapseFrom(CharMatcher.is('\'').removeFrom(s), '_');
+        if (!Character.isJavaIdentifierStart(s.charAt(0))) return "_" + name;
+        return name;
+    }
+
+    /**
+     * Returns a unique valid java identifier name based on the input.
+     *
+     * Translated as per {@link #makeValidJavaName(String)} but with {@link String#hashCode()}
+     * appended where necessary to guarantee uniqueness.
+     *
+     * @see #makeValidJavaName(String)
+     */
+    public static String makeValidUniqueJavaName(String s) {
+        String name = makeValidJavaName(s);
+        if (isEmpty(s) || IS_JAVA_IDENTIFIER_PART.matchesAllOf(s) || CharMatcher.is('\'').matchesNoneOf(s)) {
+            return name;
+        } else {
+            return name + "_" + s.hashCode();
+        }
+    }
+
+    /** @see {@link Identifiers#makeRandomId(int)} */
+    public static String makeRandomId(int l) {
+        return Identifiers.makeRandomId(l);
+    }
+
+    /** pads the string with 0's at the left up to len; no padding if i longer than len */
+    public static String makeZeroPaddedString(int i, int len) {
+        return makePaddedString(""+i, len, "0", "");
+    }
+
+    /** pads the string with "pad" at the left up to len; no padding if base longer than len */
+    public static String makePaddedString(String base, int len, String left_pad, String right_pad) {
+        String s = ""+(base==null ? "" : base);
+        while (s.length()<len) s=left_pad+s+right_pad;
+        return s;
+    }
+
+    public static void trimAll(String[] s) {
+        for (int i=0; i<s.length; i++)
+            s[i] = (s[i]==null ? "" : s[i].trim());
+    }
+
+    /** creates a string from a real number, with specified accuracy (more iff it comes for free, ie integer-part);
+     * switches to E notation if needed to fit within maxlen; can be padded left up too (not useful)
+     * @param x number to use
+     * @param maxlen maximum length for the numeric string, if possible (-1 to suppress)
+     * @param prec number of digits accuracy desired (more kept for integers)
+     * @param leftPadLen will add spaces at left if necessary to make string this long (-1 to suppress) [probably not usef]
+     * @return such a string
+     */
+    public static String makeRealString(double x, int maxlen, int prec, int leftPadLen) {
+        return makeRealString(x, maxlen, prec, leftPadLen, 0.00000000001, true);
+    }
+    /** creates a string from a real number, with specified accuracy (more iff it comes for free, ie integer-part);
+     * switches to E notation if needed to fit within maxlen; can be padded left up too (not useful)
+     * @param x number to use
+     * @param maxlen maximum length for the numeric string, if possible (-1 to suppress)
+     * @param prec number of digits accuracy desired (more kept for integers)
+     * @param leftPadLen will add spaces at left if necessary to make string this long (-1 to suppress) [probably not usef]
+     * @param skipDecimalThreshhold if positive it will not add a decimal part if the fractional part is less than this threshhold
+     *    (but for a value 3.00001 it would show zeroes, e.g. with 3 precision and positive threshhold <= 0.00001 it would show 3.00);
+     *    if zero or negative then decimal digits are always shown
+     * @param useEForSmallNumbers whether to use E notation for numbers near zero (e.g. 0.001)
+     * @return such a string
+     */
+    public static String makeRealString(double x, int maxlen, int prec, int leftPadLen, double skipDecimalThreshhold, boolean useEForSmallNumbers) {
+        if (x<0) return "-"+makeRealString(-x, maxlen, prec, leftPadLen);
+        NumberFormat df = DecimalFormat.getInstance();
+        //df.setMaximumFractionDigits(maxlen);
+        df.setMinimumFractionDigits(0);
+        //df.setMaximumIntegerDigits(prec);
+        df.setMinimumIntegerDigits(1);
+        df.setGroupingUsed(false);
+        String s;
+        if (x==0) {
+            if (skipDecimalThreshhold>0 || prec<=1) s="0";
+            else {
+                s="0.0";
+                while (s.length()<prec+1) s+="0";
+            }
+        } else {
+//            long bits= Double.doubleToLongBits(x);
+//            int s = ((bits >> 63) == 0) ? 1 : -1;
+//            int e = (int)((bits >> 52) & 0x7ffL);
+//            long m = (e == 0) ?
+//            (bits & 0xfffffffffffffL) << 1 :
+//            (bits & 0xfffffffffffffL) | 0x10000000000000L;
+//            //s*m*2^(e-1075);
+            int log = (int)Math.floor(Math.log10(x));
+            int numFractionDigits = (log>=prec ? 0 : prec-log-1);
+            if (numFractionDigits>0) { //need decimal digits
+                if (skipDecimalThreshhold>0) {
+                    int checkFractionDigits = 0;
+                    double multiplier = 1;
+                    while (checkFractionDigits < numFractionDigits) {
+                        if (Math.abs(x - Math.rint(x*multiplier)/multiplier)<skipDecimalThreshhold)
+                            break;
+                        checkFractionDigits++;
+                        multiplier*=10;
+                    }
+                    numFractionDigits = checkFractionDigits;
+                }
+                df.setMinimumFractionDigits(numFractionDigits);
+                df.setMaximumFractionDigits(numFractionDigits);
+            } else {
+                //x = Math.rint(x);
+                df.setMaximumFractionDigits(0);
+            }
+            s = df.format(x);
+            if (maxlen>0 && s.length()>maxlen) {
+                //too long:
+                double signif = x/Math.pow(10,log);
+                if (s.indexOf(getDefaultDecimalSeparator())>=0) {
+                    //have a decimal point; either we are very small 0.000001
+                    //or prec is larger than maxlen
+                    if (Math.abs(x)<1 && useEForSmallNumbers) {
+                        //very small-- use alternate notation
+                        s = makeRealString(signif, -1, prec, -1) + "E"+log;
+                    } else {
+                        //leave it alone, user error or E not wanted
+                    }
+                } else {
+                    //no decimal point, integer part is too large, use alt notation
+                    s = makeRealString(signif, -1, prec, -1) + "E"+log;
+                }
+            }
+        }
+        if (leftPadLen>s.length())
+            return makePaddedString(s, leftPadLen, " ", "");
+        else
+            return s;
+    }
+
+    /** creates a string from a real number, with specified accuracy (more iff it comes for free, ie integer-part);
+     * switches to E notation if needed to fit within maxlen; can be padded left up too (not useful)
+     * @param x number to use
+     * @param maxlen maximum length for the numeric string, if possible (-1 to suppress)
+     * @param prec number of digits accuracy desired (more kept for integers)
+     * @param leftPadLen will add spaces at left if necessary to make string this long (-1 to suppress) [probably not usef]
+     * @return such a string
+     */
+    public static String makeRealStringNearZero(double x, int maxlen, int prec, int leftPadLen) {
+        if (Math.abs(x)<0.0000000001) x=0;
+        NumberFormat df = DecimalFormat.getInstance();
+        //df.setMaximumFractionDigits(maxlen);
+        df.setMinimumFractionDigits(0);
+        //df.setMaximumIntegerDigits(prec);
+        df.setMinimumIntegerDigits(1);
+        df.setGroupingUsed(false);
+        String s;
+        if (x==0) {
+            if (prec<=1) s="0";
+            else {
+                s="0.0";
+                while (s.length()<prec+1) s+="0";
+            }
+        } else {
+//            long bits= Double.doubleToLongBits(x);
+//            int s = ((bits >> 63) == 0) ? 1 : -1;
+//            int e = (int)((bits >> 52) & 0x7ffL);
+//            long m = (e == 0) ?
+//            (bits & 0xfffffffffffffL) << 1 :
+//            (bits & 0xfffffffffffffL) | 0x10000000000000L;
+//            //s*m*2^(e-1075);
+            int log = (int)Math.floor(Math.log10(x));
+            int scale = (log>=prec ? 0 : prec-log-1);
+            if (scale>0) { //need decimal digits
+                double scale10 = Math.pow(10, scale);
+                x = Math.rint(x*scale10)/scale10;
+                df.setMinimumFractionDigits(scale);
+                df.setMaximumFractionDigits(scale);
+            } else {
+                //x = Math.rint(x);
+                df.setMaximumFractionDigits(0);
+            }
+            s = df.format(x);
+            if (maxlen>0 && s.length()>maxlen) {
+                //too long:
+                double signif = x/Math.pow(10,log);
+                if (s.indexOf('.')>=0) {
+                    //have a decimal point; either we are very small 0.000001
+                    //or prec is larger than maxlen
+                    if (Math.abs(x)<1) {
+                        //very small-- use alternate notation
+                        s = makeRealString(signif, -1, prec, -1) + "E"+log;
+                    } else {
+                        //leave it alone, user error
+                    }
+                } else {
+                    //no decimal point, integer part is too large, use alt notation
+                    s = makeRealString(signif, -1, prec, -1) + "E"+log;
+                }
+            }
+        }
+        if (leftPadLen>s.length())
+            return makePaddedString(s, leftPadLen, " ", "");
+        else
+            return s;
+    }
+
+    /** returns the first word (whitespace delimited text), or null if there is none (input null or all whitespace) */
+    public static String getFirstWord(String s) {
+        if (s==null) return null;
+        int start = 0;
+        while (start<s.length()) {
+            if (!Character.isWhitespace(s.charAt(start)))
+                break;
+            start++;
+        }
+        int end = start;
+        if (end >= s.length())
+            return null;
+        while (end<s.length()) {
+            if (Character.isWhitespace(s.charAt(end)))
+                break;
+            end++;
+        }
+        return s.substring(start, end);
+    }
+
+    /** returns the last word (whitespace delimited text), or null if there is none (input null or all whitespace) */
+    public static String getLastWord(String s) {
+        if (s==null) return null;
+        int end = s.length()-1;
+        while (end >= 0) {
+            if (!Character.isWhitespace(s.charAt(end)))
+                break;
+            end--;
+        }
+        int start = end;
+        if (start < 0)
+            return null;
+        while (start >= 0) {
+            if (Character.isWhitespace(s.charAt(start)))
+                break;
+            start--;
+        }
+        return s.substring(start+1, end+1);
+    }
+
+    /** returns the first word after the given phrase, or null if no such phrase;
+     * if the character immediately after the phrase is not whitespace, the non-whitespace
+     * sequence starting with that character will be returned */
+    public static String getFirstWordAfter(String context, String phrase) {
+        if (context==null || phrase==null) return null;
+        int index = context.indexOf(phrase);
+        if (index<0) return null;
+        return getFirstWord(context.substring(index + phrase.length()));
+    }
+
+   /**
+    * searches in context for the given phrase, and returns the <b>untrimmed</b> remainder of the first line
+    * on which the phrase is found
+    */
+    public static String getRemainderOfLineAfter(String context, String phrase) {
+        if (context == null || phrase == null) return null;
+        int index = context.indexOf(phrase);
+        if (index < 0) return null;
+        int lineEndIndex = context.indexOf("\n", index);
+        if (lineEndIndex <= 0) {
+            return context.substring(index + phrase.length());
+        } else {
+            return context.substring(index + phrase.length(), lineEndIndex);
+        }
+    }
+
+    /** @deprecated use {@link Time#makeTimeStringRounded(long)} */
+    @Deprecated
+    public static String makeTimeString(long utcMillis) {
+        return Time.makeTimeStringRounded(utcMillis);
+    }
+
+    /** returns e.g. { "prefix01", ..., "prefix96" };
+     * see more functional NumericRangeGlobExpander for "prefix{01-96}"
+     */
+    public static String[] makeArray(String prefix, int count) {
+        String[] result = new String[count];
+        int len = (""+count).length();
+        for (int i=1; i<=count; i++)
+            result[i-1] = prefix + makePaddedString("", len, "0", ""+i);
+        return result;
+    }
+
+    public static String[] combineArrays(String[] ...arrays) {
+        int totalLen = 0;
+        for (String[] array : arrays) {
+            if (array!=null) totalLen += array.length;
+        }
+        String[] result = new String[totalLen];
+        int i=0;
+        for (String[] array : arrays) {
+            if (array!=null) for (String s : array) {
+                result[i++] = s;
+            }
+        }
+        return result;
+    }
+
+    public static String toInitialCapOnly(String value) {
+        if (value==null || value.length()==0) return value;
+        return value.substring(0, 1).toUpperCase(Locale.ENGLISH) + value.substring(1).toLowerCase(Locale.ENGLISH);
+    }
+
+    public static String reverse(String name) {
+        return new StringBuffer(name).reverse().toString();
+    }
+
+    public static boolean isLowerCase(String s) {
+        return s.toLowerCase().equals(s);
+    }
+
+    public static String makeRepeated(char c, int length) {
+        StringBuilder result = new StringBuilder(length);
+        for (int i = 0; i < length; i++) {
+            result.append(c);
+        }
+        return result.toString();
+    }
+
+    public static String trim(String s) {
+        if (s==null) return null;
+        return s.trim();
+    }
+
+    public static String trimEnd(String s) {
+        if (s==null) return null;
+        return ("a"+s).trim().substring(1);
+    }
+
+    /** returns up to maxlen characters from the start of s */
+    public static String maxlen(String s, int maxlen) {
+        return maxlenWithEllipsis(s, maxlen, "");
+    }
+
+    /** as {@link #maxlenWithEllipsis(String, int, String) with "..." as the ellipsis */
+    public static String maxlenWithEllipsis(String s, int maxlen) {
+        return maxlenWithEllipsis(s, maxlen, "...");
+    }
+    /** as {@link #maxlenWithEllipsis(String, int) but replacing the last few chars with the given ellipsis */
+    public static String maxlenWithEllipsis(String s, int maxlen, String ellipsis) {
+        if (s==null) return null;
+        if (ellipsis==null) ellipsis="";
+        if (s.length()<=maxlen) return s;
+        return s.substring(0, Math.max(maxlen-ellipsis.length(), 0))+ellipsis;
+    }
+
+    /** returns toString of the object if it is not null, otherwise null */
+    public static String toString(Object o) {
+        return toStringWithValueForNull(o, null);
+    }
+
+    /** returns toString of the object if it is not null, otherwise the given value */
+    public static String toStringWithValueForNull(Object o, String valueIfNull) {
+        if (o==null) return valueIfNull;
+        return o.toString();
+    }
+
+    public static boolean containsLiteralIgnoreCase(CharSequence input, CharSequence fragment) {
+        if (input==null) return false;
+        if (isEmpty(fragment)) return true;
+        int lastValidStartPos = input.length()-fragment.length();
+        char f0u = Character.toUpperCase(fragment.charAt(0));
+        char f0l = Character.toLowerCase(fragment.charAt(0));
+        i: for (int i=0; i<=lastValidStartPos; i++) {
+            char ii = input.charAt(i);
+            if (ii==f0l || ii==f0u) {
+                for (int j=1; j<fragment.length(); j++) {
+                    if (Character.toLowerCase(input.charAt(i+j))!=Character.toLowerCase(fragment.charAt(j)))
+                        continue i;
+                }
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public static boolean containsLiteral(CharSequence input, CharSequence fragment) {
+        if (input==null) return false;
+        if (isEmpty(fragment)) return true;
+        int lastValidStartPos = input.length()-fragment.length();
+        char f0 = fragment.charAt(0);
+        i: for (int i=0; i<=lastValidStartPos; i++) {
+            char ii = input.charAt(i);
+            if (ii==f0) {
+                for (int j=1; j<fragment.length(); j++) {
+                    if (input.charAt(i+j)!=fragment.charAt(j))
+                        continue i;
+                }
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /** Returns a size string using metric suffixes from {@link ByteSizeStrings#metric()}, e.g. 23.5MB */
+    public static String makeSizeString(long sizeInBytes) {
+        return ByteSizeStrings.metric().makeSizeString(sizeInBytes);
+    }
+
+    /** Returns a size string using ISO suffixes from {@link ByteSizeStrings#iso()}, e.g. 23.5MiB */
+    public static String makeISOSizeString(long sizeInBytes) {
+        return ByteSizeStrings.iso().makeSizeString(sizeInBytes);
+    }
+
+    /** Returns a size string using Java suffixes from {@link ByteSizeStrings#java()}, e.g. 23m */
+    public static String makeJavaSizeString(long sizeInBytes) {
+        return ByteSizeStrings.java().makeSizeString(sizeInBytes);
+    }
+
+    /** returns a configurable shortener */
+    public static StringShortener shortener() {
+        return new StringShortener();
+    }
+
+    public static Supplier<String> toStringSupplier(Object src) {
+        return Suppliers.compose(Functions.toStringFunction(), Suppliers.ofInstance(src));
+    }
+
+    /** 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 static FormattedString format(String pattern, Object... args) {
+        return new FormattedString(pattern, args);
+    }
+
+    /** returns "s" if the argument is not 1, empty string otherwise; useful when constructing plurals */
+    public static String s(int count) {
+        return count==1 ? "" : "s";
+    }
+    /** as {@link #s(int)} based on size of argument */
+    public static String s(@Nullable Map<?,?> x) {
+        return s(x==null ? 0 : x.size());
+    }
+    /** as {@link #s(int)} based on size of argument */
+    public static String s(Iterable<?> x) {
+        if (x==null) return s(0);
+        return s(x.iterator());
+    }
+    /** as {@link #s(int)} based on size of argument */
+    public static String s(Iterator<?> x) {
+        int count = 0;
+        if (x==null || !x.hasNext()) {}
+        else {
+            x.next(); count++;
+            if (x.hasNext()) count++;
+        }
+        return s(count);
+    }
+
+    /** returns "ies" if the argument is not 1, "y" otherwise; useful when constructing plurals */
+    public static String ies(int count) {
+        return count==1 ? "y" : "ies";
+    }
+    /** as {@link #ies(int)} based on size of argument */
+    public static String ies(@Nullable Map<?,?> x) {
+        return ies(x==null ? 0 : x.size());
+    }
+    /** as {@link #ies(int)} based on size of argument */
+    public static String ies(Iterable<?> x) {
+        if (x==null) return ies(0);
+        return ies(x.iterator());
+    }
+    /** as {@link #ies(int)} based on size of argument */
+    public static String ies(Iterator<?> x) {
+        int count = 0;
+        if (x==null || !x.hasNext()) {}
+        else {
+            x.next(); count++;
+            if (x.hasNext()) count++;
+        }
+        return ies(count);
+    }
+
+    /** converts a map of any objects to a map of strings, using the tostring, and returning "null" for nulls 
+     * @deprecated since 0.7.0 use {@link #toStringMap(Map, String)} to remove ambiguity about how to handle null */
+    // NB previously the javadoc here was wrong, said it returned null not "null"
+    @Deprecated
+    public static Map<String, String> toStringMap(Map<?,?> map) {
+        return toStringMap(map, "null");
+    }
+    /** converts a map of any objects to a map of strings, using {@link Object#toString()},
+     * with the second argument used where a value (or key) is null */
+    public static Map<String, String> toStringMap(Map<?,?> map, String valueIfNull) {
+        if (map==null) return null;
+        Map<String,String> result = MutableMap.<String, String>of();
+        for (Map.Entry<?,?> e: map.entrySet()) {
+            result.put(toStringWithValueForNull(e.getKey(), valueIfNull), toStringWithValueForNull(e.getValue(), valueIfNull));
+        }
+        return result;
+    }
+    
+    /** converts a list of any objects to a list of strings, using {@link Object#toString()},
+     * with the second argument used where an entry is null */
+    public static List<String> toStringList(List<?> list, String valueIfNull) {
+        if (list==null) return null;
+        List<String> result = MutableList.of();
+        for (Object v: list) result.add(toStringWithValueForNull(v, valueIfNull));
+        return result;
+    }
+
+    /** returns base repeated count times */
+    public static String repeat(String base, int count) {
+        if (base==null) return null;
+        StringBuilder result = new StringBuilder();
+        for (int i=0; i<count; i++)
+            result.append(base);
+        return result.toString();
+    }
+
+    /** returns comparator which compares based on length, with shorter ones first (and null before that);
+     * in event of a tie, it uses the toString order */
+    public static Ordering<String> lengthComparator() {
+        return Ordering.<Integer>natural().onResultOf(StringFunctions.length()).compound(Ordering.<String>natural()).nullsFirst();
+    }
+
+    public static boolean isMultiLine(String s) {
+        if (s==null) return false;
+        if (s.indexOf('\n')>=0 || s.indexOf('\r')>=0) return true;
+        return false;
+    }
+    public static String getFirstLine(String s) {
+        int idx = s.indexOf('\n');
+        if (idx==-1) return s;
+        return s.substring(0, idx);
+    }
+
+    /** looks for first section of text in following the prefix and, if present, before the suffix;
+     * null if the prefix is not present in the string, and everything after the prefix if suffix is not present in the string;
+     * if either prefix or suffix is null, it is treated as the start/end of the string */
+    public static String getFragmentBetween(String input, String prefix, String suffix) {
+        if (input==null) return null;
+        int index;
+        if (prefix!=null) {
+            index = input.indexOf(prefix);
+            if (index==-1) return null;
+            input = input.substring(index + prefix.length());
+        }
+        if (suffix!=null) {
+            index = input.indexOf(suffix);
+            if (index>=0) input = input.substring(0, index);
+        }
+        return input;
+    }
+
+    public static int getWordCount(String phrase, boolean respectQuotes) {
+        if (phrase==null) return 0;
+        phrase = phrase.trim();
+        if (respectQuotes)
+            return new QuotedStringTokenizer(phrase).remainderAsList().size();
+        else
+            return Collections.list(new StringTokenizer(phrase)).size();
+    }
+
+    public static char getDecimalSeparator(Locale locale) {
+        DecimalFormatSymbols dfs = new DecimalFormatSymbols(locale);
+        return dfs.getDecimalSeparator();
+    }
+
+    public static char getDefaultDecimalSeparator() {
+        return getDecimalSeparator(Locale.getDefault());
+    }
+
+    /** replaces each sequence of whitespace in the first string with the replacement in the second string */
+    public static String collapseWhitespace(String x, String whitespaceReplacement) {
+        if (x==null) return null;
+        return replaceAllRegex(x, "\\s+", whitespaceReplacement);
+    }
+
+    public static String toLowerCase(String value) {
+        if (value==null || value.length()==0) return value;
+        return value.toLowerCase(Locale.ENGLISH);
+    }
+
+    /**
+     * @return null if var is null or empty string, otherwise return var
+     */
+    public static String emptyToNull(String var) {
+        if (isNonEmpty(var)) {
+            return var;
+        } else {
+            return null;
+        }
+    }
+    
+    /** Returns canonicalized string from the given object, made "unique" by:
+     * <li> putting sets into the toString order
+     * <li> appending a hash code if it's longer than the max (and the max is bigger than 0) */
+    public static String toUniqueString(Object x, int optionalMax) {
+        if (x instanceof Iterable && !(x instanceof List)) {
+            // unsorted collections should have a canonical order imposed
+            MutableList<String> result = MutableList.of();
+            for (Object xi: (Iterable<?>)x) {
+                result.add(toUniqueString(xi, optionalMax));
+            }
+            Collections.sort(result);
+            x = result.toString();
+        }
+        if (x==null) return "{null}";
+        String xs = x.toString();
+        if (xs.length()<=optionalMax || optionalMax<=0) return xs;
+        return maxlenWithEllipsis(xs, optionalMax-8)+"/"+Integer.toHexString(xs.hashCode());
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/cf2f7a93/utils/common/src/main/java/org/apache/brooklyn/util/text/WildcardGlobs.java
----------------------------------------------------------------------
diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/text/WildcardGlobs.java b/utils/common/src/main/java/org/apache/brooklyn/util/text/WildcardGlobs.java
new file mode 100644
index 0000000..d30b58f
--- /dev/null
+++ b/utils/common/src/main/java/org/apache/brooklyn/util/text/WildcardGlobs.java
@@ -0,0 +1,382 @@
+/*
+ * 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 com.google.common.base.Throwables;
+
+public class WildcardGlobs {
+
+    /** returns true iff the target matches the given pattern,
+     * under simplified bash rules -- viz permitting * and ? and comma delimited patterns inside curly braces 
+     * @throws InvalidPatternException */
+    public static boolean isGlobMatched(String globPattern, String targetText) throws InvalidPatternException {
+        List<String> patterns = getGlobsAfterBraceExpansion(globPattern);       
+        for (String p : patterns) {
+            if (isNoBraceGlobMatched(p, targetText))
+                return true;
+        }
+        return false;
+    }
+
+    /** whether a glob-ish string without braces (e.g. containing just ? and * chars) matches;
+     * can be used directly, also used implicitly by isGlobMatched after glob expansion */
+    public static boolean isNoBraceGlobMatched(String globPattern, String target) {
+        int pi=0, ti=0;
+        while (pi<globPattern.length() && ti<target.length()) {
+            char pc = globPattern.charAt(pi);
+            char tc = target.charAt(pi);
+            if (pc=='?') {
+                pi++; ti++;
+                continue;
+            }
+            if (pc!='*') {
+                if (pc!=tc) return false;
+                pi++; ti++;
+                continue;
+            }
+            //match 0 or more chars
+            String prest = globPattern.substring(pi+1);
+            while (ti<=target.length()) {
+                if (isNoBraceGlobMatched(prest, target.substring(ti)))
+                    return true;
+                ti++;
+            }
+            return false;
+        }
+        while (pi<globPattern.length() && globPattern.charAt(pi)=='*')
+            pi++;
+        return (pi==globPattern.length() && ti==target.length());
+    }   
+
+    /** returns a list with no curly braces in any entries,
+     * and guaranteeing order such that any {..,X,..,Y,..} will result in X being before Y in the resulting list;
+     * e.g. given a{,b,c} gives a ab and ac; no special treatment of numeric ranges, quotes, or parentheses 
+     * (see SpecialistGlobExpander for that) */
+    public static List<String> getGlobsAfterBraceExpansion(String pattern) throws InvalidPatternException {
+        return getGlobsAfterBraceExpansion(pattern, false, PhraseTreatment.NOT_A_SPECIAL_CHAR, PhraseTreatment.NOT_A_SPECIAL_CHAR);
+    }
+
+    /** if a string contains a demarcated phrase, e.g. between open and close parentheses, or inside unescaped quotes
+     * this argument determines how that phrase is treated with regards to brace expansion */
+    public enum PhraseTreatment {
+        /** the region is treated like any other region */
+        NOT_A_SPECIAL_CHAR, 
+        /** the interior will be expanded if there is a {x,y} expression _entirely_ inside the phrase, but otherwise commas inside it will be ignored;
+         * it will be an error if there is a { char inside the phrase, or if the phrase is not internally well-formed with regards to the phrase characters,
+         * (e.g. if quotes are interior expandable and parens are anything but not_a_special_char (e.g. interior expandable or interior not expandable) 
+         * then any expression inside a quoted phrase must have matching parentheses) */
+        INTERIOR_EXPANDABLE, 
+        /** the interior will not be expanded at all, not if there's a comma inside, and not even if there is a {x,y} expression entirely inside; the braces will be left;
+         * interior of parenthetical phrases must have matching parentheses (to determine the right end parenthesis),
+         * apart from parentheses inside any quoted phrases when quotes are interior_not_expandable which will be ignored;
+         * quotes inside not_expandable paren phrases will be ignored */
+        INTERIOR_NOT_EXPANDABLE 
+    };
+
+    protected static class ExpressionToExpand {
+        String resultSoFar;
+        String todo;
+        String operatorStack;
+        public ExpressionToExpand(String resultSoFar, String todo, String operatorStack) {
+            super();
+            this.resultSoFar = resultSoFar;
+            this.todo = todo;
+            this.operatorStack = operatorStack;
+        }
+        @Override
+        public String toString() {
+            return "ExpressionToExpand["+todo+":"+resultSoFar+"/"+operatorStack+"]";
+        }
+    }
+    /** returns a list with no curly braces in any entries; e.g. given a{,b} gives a and ab; 
+     * quotes and parentheses are kept, but their contents may be excluded from expansion or otherwise treated specially as per the flag.
+     * with allowNumericRanges, "{1-3}" is permitted for {1,2,3}. */
+    public static List<String> getGlobsAfterBraceExpansion(String pattern, boolean allowNumericRanges, PhraseTreatment quoteTreatment, PhraseTreatment parenthesesTreatment) throws InvalidPatternException {
+        List<ExpressionToExpand> patterns = new ArrayList<ExpressionToExpand>();
+        List<String> result = new ArrayList<String>();
+        patterns.add(new ExpressionToExpand("", pattern, ""));
+        while (!patterns.isEmpty()) {
+            ExpressionToExpand cs = patterns.remove(0);
+            StringBuffer resultSoFar = new StringBuffer(cs.resultSoFar);
+            String operatorStack = cs.operatorStack;
+            boolean inQuote = operatorStack.contains("\"");
+            boolean expanded = false;
+            for (int i=0; i<cs.todo.length(); i++) {
+                assert !expanded;
+                char c = cs.todo.charAt(i);
+                boolean inParen = operatorStack.contains("(") &&
+                    (!inQuote || operatorStack.lastIndexOf('\"')<operatorStack.lastIndexOf('('));
+                if (inQuote && !(inParen && parenthesesTreatment.equals(PhraseTreatment.INTERIOR_NOT_EXPANDABLE))) {
+                    if (c=='"') {
+                        if (i>0 && cs.todo.charAt(i-1)=='\\') {
+                            //this escaped quote, keep
+                            resultSoFar.append(c);
+                            continue;
+                        }
+                        //unquote
+                        resultSoFar.append(c);
+                        inQuote = false;
+                        if (operatorStack.charAt(operatorStack.length()-1)!='\"')
+                            throw new InvalidPatternException("Quoted string contents not valid, after parsing "+resultSoFar);
+                        operatorStack = operatorStack.substring(0, operatorStack.length()-1);
+                        continue;
+                    }
+                    if (quoteTreatment.equals(PhraseTreatment.INTERIOR_NOT_EXPANDABLE)) {
+                        resultSoFar.append(c);
+                        continue;
+                    }
+                    //interior is expandable, continue parsing as usual below
+                }
+                if (inParen) {
+                    if (c==')') {
+                        //unparen
+                        resultSoFar.append(c);
+                        if (operatorStack.charAt(operatorStack.length()-1)!='(')
+                            throw new InvalidPatternException("Parenthetical contents not valid, after parsing "+resultSoFar);
+                        operatorStack = operatorStack.substring(0, operatorStack.length()-1);
+                        continue;
+                    }
+                    if (parenthesesTreatment.equals(PhraseTreatment.INTERIOR_NOT_EXPANDABLE)) {
+                        resultSoFar.append(c);
+                        if (c=='(')
+                            operatorStack+="(";
+                        continue;
+                    }
+                    //interior is expandable, continue parsing as usual below
+                }
+
+                if (c=='"' && !quoteTreatment.equals(PhraseTreatment.NOT_A_SPECIAL_CHAR)) {
+                    resultSoFar.append(c);
+                    inQuote = true;
+                    operatorStack += "\"";
+                    continue;
+                }
+                if (c=='(' && !parenthesesTreatment.equals(PhraseTreatment.NOT_A_SPECIAL_CHAR)) {
+                    resultSoFar.append(c);
+                    operatorStack += "(";
+                    continue;
+                }
+
+                if (c!='{') {
+                    resultSoFar.append(c);
+                    continue;
+                }
+
+                //brace.. we will need to expand
+                expanded = true;
+                String operatorStackBeforeExpansion = operatorStack;
+                int braceStartIndex = i;
+                int tokenStartIndex = i+1;
+
+                //find matching close brace
+                List<String> tokens = new ArrayList<String>();
+                operatorStack += "{";
+                while (true) {
+                    if (++i>=cs.todo.length()) {
+                        throw new InvalidPatternException("Curly brace not closed, parsing '"+cs.todo.substring(braceStartIndex)+"' after "+resultSoFar);
+                    }
+                    c = cs.todo.charAt(i);
+                    inParen = operatorStack.contains("(") &&
+                        (!inQuote || operatorStack.lastIndexOf('\"')<operatorStack.lastIndexOf('('));
+                    if (inQuote && !(inParen && parenthesesTreatment.equals(PhraseTreatment.INTERIOR_NOT_EXPANDABLE))) {
+                        if (c=='"') {
+                            if (i>0 && cs.todo.charAt(i-1)=='\\') {
+                                //this is escaped quote, doesn't affect status
+                                continue;
+                            }
+                            //unquote
+                            inQuote = false;
+                            if (operatorStack.charAt(operatorStack.length()-1)!='\"')
+                                throw new InvalidPatternException("Quoted string contents not valid, after parsing "+resultSoFar+cs.todo.substring(braceStartIndex, i));
+                            operatorStack = operatorStack.substring(0, operatorStack.length()-1);
+                            continue;
+                        }
+                        if (quoteTreatment.equals(PhraseTreatment.INTERIOR_NOT_EXPANDABLE)) {
+                            continue;
+                        }
+                        //interior is expandable, continue parsing as usual below
+                    }
+                    if (inParen) {
+                        if (c==')') {
+                            //unparen
+                            if (operatorStack.charAt(operatorStack.length()-1)!='(')
+                                throw new InvalidPatternException("Parenthetical contents not valid, after parsing "+resultSoFar+cs.todo.substring(braceStartIndex, i));
+                            operatorStack = operatorStack.substring(0, operatorStack.length()-1);
+                            continue;
+                        }
+                        if (parenthesesTreatment.equals(PhraseTreatment.INTERIOR_NOT_EXPANDABLE)) {
+                            if (c=='(')
+                                operatorStack+="(";
+                            continue;
+                        }
+                        //interior is expandable, continue parsing as usual below
+                    }
+
+                    if (c=='"' && !quoteTreatment.equals(PhraseTreatment.NOT_A_SPECIAL_CHAR)) {
+                        inQuote = true;
+                        operatorStack += "\"";
+                        continue;
+                    }
+                    if (c=='(' && !parenthesesTreatment.equals(PhraseTreatment.NOT_A_SPECIAL_CHAR)) {
+                        operatorStack += "(";
+                        continue;
+                    }
+
+                    if (c=='}') {
+                        if (operatorStack.charAt(operatorStack.length()-1)!='{')
+                            throw new InvalidPatternException("Brace contents not valid, mismatched operators "+operatorStack+" after parsing "+resultSoFar+cs.todo.substring(braceStartIndex, i));
+                        operatorStack = operatorStack.substring(0, operatorStack.length()-1);
+                        if (operatorStack.equals(operatorStackBeforeExpansion)) {
+                            tokens.add(cs.todo.substring(tokenStartIndex, i));
+                            break;
+                        }
+                        continue;
+                    }
+
+                    if (c==',') {
+                        if (operatorStack.length()==operatorStackBeforeExpansion.length()+1) {
+                            tokens.add(cs.todo.substring(tokenStartIndex, i));
+                            tokenStartIndex = i+1;
+                            continue;
+                        }
+                        continue;
+                    }
+
+                    if (c=='{') {
+                        operatorStack += c;
+                        continue;
+                    }
+
+                    //any other char is irrelevant
+                    continue;
+                }
+
+                assert operatorStack.equals(operatorStackBeforeExpansion);
+                assert cs.todo.charAt(i)=='}';
+                assert !tokens.isEmpty();
+
+                String suffix = cs.todo.substring(i+1);
+
+                List<ExpressionToExpand> newPatterns = new ArrayList<ExpressionToExpand>();
+                for (String token : tokens) {
+                    //System.out.println("adding: "+pre+token+post);
+                    if (allowNumericRanges && token.matches("\\s*[0-9]+\\s*-\\s*[0-9]+\\s*")) {
+                        int dashIndex = token.indexOf('-');
+                        String startS = token.substring(0, dashIndex).trim();
+                        String endS = token.substring(dashIndex+1).trim();
+
+                        int start = Integer.parseInt(startS);
+                        int end = Integer.parseInt(endS);
+
+                        if (startS.startsWith("-")) startS=startS.substring(1).trim();
+                        if (endS.startsWith("-")) endS=endS.substring(1).trim();
+                        int minLen = Math.min(startS.length(), endS.length());
+
+                        for (int ti=start; ti<=end; ti++) {
+                            //partial support for negative numbers, but of course they cannot (yet) be specified in the regex above so it is moot
+                            String tokenI = ""+Math.abs(ti);
+                            while (tokenI.length()<minLen) tokenI = "0"+tokenI;
+                            if (ti<0) tokenI = "-"+tokenI;
+                            newPatterns.add(new ExpressionToExpand(resultSoFar.toString(), tokenI+suffix, operatorStackBeforeExpansion));                              
+                        }
+                    } else {
+                        newPatterns.add(new ExpressionToExpand(resultSoFar.toString(), token+suffix, operatorStackBeforeExpansion));
+                    }
+                }
+                // insert new patterns at the start, so we continue to expand them next
+                patterns.addAll(0, newPatterns);
+                
+                break;
+            }
+            if (!expanded) {
+                if (operatorStack.length()>0) {
+                    throw new InvalidPatternException("Unclosed operators "+operatorStack+" parsing "+resultSoFar);
+                }
+                result.add(resultSoFar.toString());
+            }
+        }
+        assert !result.isEmpty();
+        return result;
+    }
+
+    public static class InvalidPatternException extends RuntimeException {
+        private static final long serialVersionUID = -1969068264338310749L;
+        public InvalidPatternException(String msg) {
+            super(msg);
+        }
+    }
+
+    
+    /** expands globs as per #getGlobsAfterBraceExpansion, 
+     * but also handles numeric ranges, 
+     * and optionally allows customized treatment of quoted regions and/or parentheses.
+     * <p>
+     * simple example:  machine-{0-3}-{a,b} returns 8 values,
+     * machine-0-a machine-0-b machine-1-a ... machine-3-b; 
+     * NB leading zeroes are meaningful, so {00-03} expands as 00, 01, 02, 03
+     * <p>
+     * quote INTERIOR_NOT_EXPANDABLE example: a{b,"c,d"} return ab ac,d
+     * <p>
+     * for more detail on special treatment of quote and parentheses see PhraseTreatment and WildcardGlobsTest 
+     */
+    public static class SpecialistGlobExpander {
+
+        private boolean expandNumericRanges;
+        private PhraseTreatment quoteTreatment;
+        private PhraseTreatment parenthesesTreatment;
+
+        public SpecialistGlobExpander(boolean expandNumericRanges, PhraseTreatment quoteTreatment, PhraseTreatment parenthesesTreatment) {
+            this.expandNumericRanges = expandNumericRanges;
+            this.quoteTreatment = quoteTreatment;
+            this.parenthesesTreatment = parenthesesTreatment;
+        }
+        /** expands glob, including custom syntax for numeric part */ 
+        public List<String> expand(String glob) throws InvalidPatternException {
+            return getGlobsAfterBraceExpansion(glob, expandNumericRanges, quoteTreatment, parenthesesTreatment);
+        }
+        
+        /** returns true iff the target matches the given pattern,
+         * under simplified bash rules -- viz permitting * and ? and comma delimited patterns inside curly braces,
+         * as well as things like {1,2,5-10} (and also {01,02,05-10} to keep leading 0)
+         * @throws InvalidPatternException */
+        public boolean isGlobMatchedNumeric(String globPattern, String targetText) throws InvalidPatternException {
+            List<String> patterns = expand(globPattern);        
+            for (String p : patterns) {
+                if (isNoBraceGlobMatched(p, targetText))
+                    return true;
+            }
+            return false;
+        }
+
+        /** expands glob, including custom syntax for numeric part, but to an array, and re-throwing the checked exception as a runtime exception */
+        public String[] expandToArrayUnchecked(String glob) {
+            try {
+                return expand(glob).toArray(new String[0]);
+            } catch (InvalidPatternException e) {
+                throw Throwables.propagate(e);
+            }
+        }
+
+        
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/cf2f7a93/utils/common/src/main/java/org/apache/brooklyn/util/time/CountdownTimer.java
----------------------------------------------------------------------
diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/time/CountdownTimer.java b/utils/common/src/main/java/org/apache/brooklyn/util/time/CountdownTimer.java
new file mode 100644
index 0000000..508657d
--- /dev/null
+++ b/utils/common/src/main/java/org/apache/brooklyn/util/time/CountdownTimer.java
@@ -0,0 +1,119 @@
+/*
+ * 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.time;
+
+import java.util.concurrent.TimeUnit;
+
+import org.apache.brooklyn.util.exceptions.Exceptions;
+
+import com.google.common.base.Stopwatch;
+
+public class CountdownTimer {
+
+    Stopwatch stopwatch = Stopwatch.createUnstarted();
+    Duration limit;
+    
+    private CountdownTimer(Duration limit) {
+        this.limit = limit;
+    }
+    
+    /** starts the timer, either initially or if {@link #pause()}d; no-op if already running */
+    public synchronized CountdownTimer start() {
+        if (!stopwatch.isRunning()) stopwatch.start();
+        return this;
+    }
+
+    /** pauses the timer, if running; no-op if not running */
+    public synchronized CountdownTimer pause() {
+        if (stopwatch.isRunning()) stopwatch.stop();
+        return this;
+    }
+
+    /** returns underlying stopwatch, which caller can inspect for more details or modify */
+    public Stopwatch getStopwatch() {
+        return stopwatch;
+    }
+    
+    /** how much total time this timer should run for */
+    public Duration getLimit() {
+        return limit;
+    }
+
+    /** return how long the timer has been running (longer than limit if {@link #isExpired()}) */
+    public Duration getDurationElapsed() {
+        return Duration.nanos(stopwatch.elapsed(TimeUnit.NANOSECONDS));
+    }
+    
+    /** returns how much time is left (negative if {@link #isExpired()}) */
+    public Duration getDurationRemaining() {
+        return Duration.millis(limit.toMilliseconds() - stopwatch.elapsed(TimeUnit.MILLISECONDS));
+    }
+
+    /** true iff the timer has been running for the duration specified at creation time */
+    public boolean isExpired() {
+        return stopwatch.elapsed(TimeUnit.MILLISECONDS) > limit.toMilliseconds();
+    }
+    
+    /** true iff timer is running (even if it is expired) */
+    public boolean isRunning() {
+        return stopwatch.isRunning();
+    }
+    
+    // --- constructor methods
+    
+    public static CountdownTimer newInstanceStarted(Duration duration) {
+        return new CountdownTimer(duration).start();
+    }
+
+    public static CountdownTimer newInstancePaused(Duration duration) {
+        return new CountdownTimer(duration).pause();
+    }
+
+    /** block (on this object) until completed 
+     * @throws InterruptedException */
+    public synchronized void waitForExpiry() throws InterruptedException {
+        while (waitOnForExpiry(this)) {};
+    }
+
+    /** as {@link #waitForExpiry()} but catches and wraps InterruptedException as unchecked RuntimeInterruptedExcedption */
+    public synchronized void waitForExpiryUnchecked() {
+        waitOnForExpiryUnchecked(this);
+    }
+
+    /** block on the given argument until the timer is completed or the object receives a notified;
+     * callers must be synchronized on the waitTarget
+     * @return true if the object is notified (or receives a spurious wake), false if the duration is expired 
+     * @throws InterruptedException */
+    public boolean waitOnForExpiry(Object waitTarget) throws InterruptedException {
+        Duration remainder = getDurationRemaining();
+        if (remainder.toMilliseconds() <= 0) 
+            return false;
+        waitTarget.wait(remainder.toMilliseconds());
+        return true;
+    }
+    /** as {@link #waitOnForExpiry(Object)} but catches and wraps InterruptedException as unchecked RuntimeInterruptedExcedption */
+    public boolean waitOnForExpiryUnchecked(Object waitTarget) {
+        try {
+            return waitOnForExpiry(waitTarget);
+        } catch (InterruptedException e) {
+            throw Exceptions.propagate(e);
+        }        
+    }
+    
+}