http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/cf2f7a93/utils/common/src/main/java/org/apache/brooklyn/config/ConfigKey.java ---------------------------------------------------------------------- diff --git a/utils/common/src/main/java/org/apache/brooklyn/config/ConfigKey.java b/utils/common/src/main/java/org/apache/brooklyn/config/ConfigKey.java new file mode 100644 index 0000000..f2f31fe --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/config/ConfigKey.java @@ -0,0 +1,94 @@ +/* + * 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.config; + +import java.util.Collection; + +import javax.annotation.Nullable; + +import com.google.common.reflect.TypeToken; + +/** + * Represents the name of a piece of typed configuration data for an entity. + * <p> + * Two ConfigKeys should be considered equal if they have the same FQN. + */ +public interface ConfigKey<T> { + /** + * Returns the description of the configuration parameter, for display. + */ + String getDescription(); + + /** + * Returns the name of the configuration parameter, in a dot-separated namespace (FQN). + */ + String getName(); + + /** + * Returns the constituent parts of the configuration parameter name as a {@link Collection}. + */ + Collection<String> getNameParts(); + + /** + * Returns the Guava TypeToken, including info on generics. + */ + TypeToken<T> getTypeToken(); + + /** + * Returns the type of the configuration parameter data. + * <p> + * This returns a "super" of T only in the case where T is generified, + * and in such cases it returns the Class instance for the unadorned T --- + * i.e. for List<String> this returns Class<List> --- + * this is of course because there is no actual Class<List<String>> instance. + */ + Class<? super T> getType(); + + /** + * Returns the name of of the configuration parameter data type, as a {@link String}. + */ + String getTypeName(); + + /** + * Returns the default value of the configuration parameter. + */ + T getDefaultValue(); + + /** + * Returns true if a default configuration value has been set. + */ + boolean hasDefaultValue(); + + /** + * @return True if the configuration can be changed at runtime. + */ + boolean isReconfigurable(); + + /** + * @return The inheritance model, or <code>null</code> for the default in any context. + */ + @Nullable ConfigInheritance getInheritance(); + + /** Interface for elements which want to be treated as a config key without actually being one + * (e.g. config attribute sensors). + */ + public interface HasConfigKey<T> { + public ConfigKey<T> getConfigKey(); + } +}
http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/cf2f7a93/utils/common/src/main/java/org/apache/brooklyn/config/ConfigMap.java ---------------------------------------------------------------------- diff --git a/utils/common/src/main/java/org/apache/brooklyn/config/ConfigMap.java b/utils/common/src/main/java/org/apache/brooklyn/config/ConfigMap.java new file mode 100644 index 0000000..665bbf6 --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/config/ConfigMap.java @@ -0,0 +1,86 @@ +/* + * 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.config; + +import java.util.Map; + +import org.apache.brooklyn.config.ConfigKey.HasConfigKey; +import org.apache.brooklyn.util.guava.Maybe; + +import com.google.common.base.Predicate; + +public interface ConfigMap { + + /** @see #getConfig(ConfigKey, Object), with default value as per the key, or null */ + public <T> T getConfig(ConfigKey<T> key); + + /** @see #getConfig(ConfigKey, Object), with default value as per the key, or null */ + public <T> T getConfig(HasConfigKey<T> key); + + /** + * @see #getConfig(ConfigKey, Object), with provided default value if not set + * @deprecated since 0.7.0; use {@link #getConfig(HasConfigKey)} + */ + @Deprecated + public <T> T getConfig(HasConfigKey<T> key, T defaultValue); + + /** + * Returns value stored against the given key, + * resolved (if it is a Task, possibly blocking), and coerced to the appropriate type, + * or given default value if not set, + * unless the default value is null in which case it returns the default. + * + * @deprecated since 0.7.0; use {@link #getConfig(ConfigKey)} + */ + @Deprecated + public <T> T getConfig(ConfigKey<T> key, T defaultValue); + + /** as {@link #getConfigRaw(ConfigKey)} but returning null if not present + * @deprecated since 0.7.0 use {@link #getConfigRaw(ConfigKey)} */ + @Deprecated + public Object getRawConfig(ConfigKey<?> key); + + /** returns the value stored against the given key, + * <b>not</b> any default, + * <b>not</b> resolved (and guaranteed non-blocking), + * and <b>not</b> type-coerced. + * @param key key to look up + * @param includeInherited for {@link ConfigMap} instances which have an inheritance hierarchy, + * whether to traverse it or not; has no effects where there is no inheritance + * @return raw, unresolved, uncoerced value of key in map, + * but <b>not</b> any default on the key + */ + public Maybe<Object> getConfigRaw(ConfigKey<?> key, boolean includeInherited); + + /** returns a map of all config keys to their raw (unresolved+uncoerced) contents */ + public Map<ConfigKey<?>,Object> getAllConfig(); + + /** returns submap matching the given filter predicate; see ConfigPredicates for common predicates */ + public ConfigMap submap(Predicate<ConfigKey<?>> filter); + + /** returns a read-only map view which has string keys (corresponding to the config key names); + * callers encouraged to use the typed keys (and so not use this method), + * but in some compatibility areas having a Properties-like view is useful */ + public Map<String,Object> asMapWithStringKeys(); + + public int size(); + + public boolean isEmpty(); + +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/cf2f7a93/utils/common/src/main/java/org/apache/brooklyn/config/StringConfigMap.java ---------------------------------------------------------------------- diff --git a/utils/common/src/main/java/org/apache/brooklyn/config/StringConfigMap.java b/utils/common/src/main/java/org/apache/brooklyn/config/StringConfigMap.java new file mode 100644 index 0000000..e0e8e8f --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/config/StringConfigMap.java @@ -0,0 +1,35 @@ +/* + * 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.config; + +import java.util.Map; + +/** convenience extension where map is principally strings or converted to strings + * (supporting BrooklynProperties) */ +public interface StringConfigMap extends ConfigMap { + /** @see #getFirst(java.util.Map, String...) */ + public String getFirst(String... keys); + /** returns the value of the first key which is defined + * <p> + * takes the following flags: + * 'warnIfNone' or 'failIfNone' (both taking a boolean (to use default message) or a string (which is the message)); + * and 'defaultIfNone' (a default value to return if there is no such property); + * defaults to no warning and null default value */ + public String getFirst(@SuppressWarnings("rawtypes") Map flags, String... keys); +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/cf2f7a93/utils/common/src/main/java/org/apache/brooklyn/test/Asserts.java ---------------------------------------------------------------------- diff --git a/utils/common/src/main/java/org/apache/brooklyn/test/Asserts.java b/utils/common/src/main/java/org/apache/brooklyn/test/Asserts.java new file mode 100644 index 0000000..20fc98d --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/test/Asserts.java @@ -0,0 +1,499 @@ +/* + * 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.test; + +import groovy.lang.Closure; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.brooklyn.test.Asserts; +import org.apache.brooklyn.util.collections.MutableSet; +import org.apache.brooklyn.util.exceptions.Exceptions; +import org.apache.brooklyn.util.time.Duration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.annotations.Beta; +import com.google.common.base.Predicate; +import com.google.common.base.Predicates; +import com.google.common.base.Supplier; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; + +/** + * TODO should move this to new package brooklyn.util.assertions + * and TODO should add a repeating() method which returns an AssertingRepeater extending Repeater + * and: + * <li> adds support for requireAllIterationsTrue + * <li> convenience run methods equivalent to succeedsEventually and succeedsContinually + */ +@Beta +public class Asserts { + + /** + * The default timeout for assertions. Alter in individual tests by giving a + * "timeout" entry in method flags. + */ + public static final Duration DEFAULT_TIMEOUT = Duration.THIRTY_SECONDS; + + private static final Logger log = LoggerFactory.getLogger(Asserts.class); + + private Asserts() {} + + // --- selected routines from testng.Assert for visibility without needing that package + + /** + * Asserts that a condition is true. If it isn't, + * an AssertionError, with the given message, is thrown. + * @param condition the condition to evaluate + * @param message the assertion error message + */ + public static void assertTrue(boolean condition, String message) { + if (!condition) fail(message); + } + + /** + * Asserts that a condition is false. If it isn't, + * an AssertionError, with the given message, is thrown. + * @param condition the condition to evaluate + * @param message the assertion error message + */ + public static void assertFalse(boolean condition, String message) { + if (condition) fail(message); + } + + /** + * Fails a test with the given message. + * @param message the assertion error message + */ + public static AssertionError fail(String message) { + throw new AssertionError(message); + } + + public static void assertEqualsIgnoringOrder(Iterable<?> actual, Iterable<?> expected) { + assertEqualsIgnoringOrder(actual, expected, false, null); + } + + public static void assertEqualsIgnoringOrder(Iterable<?> actual, Iterable<?> expected, boolean logDuplicates, String errmsg) { + Set<?> actualSet = Sets.newLinkedHashSet(actual); + Set<?> expectedSet = Sets.newLinkedHashSet(expected); + Set<?> extras = Sets.difference(actualSet, expectedSet); + Set<?> missing = Sets.difference(expectedSet, actualSet); + List<Object> duplicates = Lists.newArrayList(actual); + for (Object a : actualSet) { + duplicates.remove(a); + } + String fullErrmsg = "extras="+extras+"; missing="+missing + + (logDuplicates ? "; duplicates="+MutableSet.copyOf(duplicates) : "") + +"; actualSize="+Iterables.size(actual)+"; expectedSize="+Iterables.size(expected) + +"; actual="+actual+"; expected="+expected+"; "+errmsg; + assertTrue(extras.isEmpty(), fullErrmsg); + assertTrue(missing.isEmpty(), fullErrmsg); + assertTrue(Iterables.size(actual) == Iterables.size(expected), fullErrmsg); + assertTrue(actualSet.equals(expectedSet), fullErrmsg); // should be covered by extras/missing/size test + } + + // --- new routines + + public static <T> void eventually(Supplier<? extends T> supplier, Predicate<T> predicate) { + eventually(ImmutableMap.<String,Object>of(), supplier, predicate); + } + + public static <T> void eventually(Map<String,?> flags, Supplier<? extends T> supplier, Predicate<T> predicate) { + eventually(flags, supplier, predicate, (String)null); + } + + public static <T> void eventually(Map<String,?> flags, Supplier<? extends T> supplier, Predicate<T> predicate, String errMsg) { + Duration timeout = toDuration(flags.get("timeout"), Duration.ONE_SECOND); + Duration period = toDuration(flags.get("period"), Duration.millis(10)); + long periodMs = period.toMilliseconds(); + long startTime = System.currentTimeMillis(); + long expireTime = startTime+timeout.toMilliseconds(); + + boolean first = true; + T supplied = supplier.get(); + while (first || System.currentTimeMillis() <= expireTime) { + supplied = supplier.get(); + if (predicate.apply(supplied)) { + return; + } + first = false; + if (periodMs > 0) sleep(periodMs); + } + fail("supplied="+supplied+"; predicate="+predicate+(errMsg!=null?"; "+errMsg:"")); + } + + // TODO improve here -- these methods aren't very useful without timeouts + public static <T> void continually(Supplier<? extends T> supplier, Predicate<T> predicate) { + continually(ImmutableMap.<String,Object>of(), supplier, predicate); + } + + public static <T> void continually(Map<String,?> flags, Supplier<? extends T> supplier, Predicate<? super T> predicate) { + continually(flags, supplier, predicate, (String)null); + } + + public static <T> void continually(Map<String,?> flags, Supplier<? extends T> supplier, Predicate<T> predicate, String errMsg) { + Duration duration = toDuration(flags.get("timeout"), Duration.ONE_SECOND); + Duration period = toDuration(flags.get("period"), Duration.millis(10)); + long periodMs = period.toMilliseconds(); + long startTime = System.currentTimeMillis(); + long expireTime = startTime+duration.toMilliseconds(); + + boolean first = true; + while (first || System.currentTimeMillis() <= expireTime) { + assertTrue(predicate.apply(supplier.get()), "supplied="+supplier.get()+"; predicate="+predicate+(errMsg!=null?"; "+errMsg:"")); + if (periodMs > 0) sleep(periodMs); + first = false; + } + } + + + /** + * Asserts given runnable succeeds in default duration. + * @see #DEFAULT_TIMEOUT + */ + public static void succeedsEventually(Runnable r) { + succeedsEventually(ImmutableMap.<String,Object>of(), r); + } + + public static void succeedsEventually(Map<String,?> flags, Runnable r) { + succeedsEventually(flags, toCallable(r)); + } + + /** + * Asserts given callable succeeds (runs without failure) in default duration. + * @see #DEFAULT_TIMEOUT + */ + public static <T> T succeedsEventually(Callable<T> c) { + return succeedsEventually(ImmutableMap.<String,Object>of(), c); + } + + // FIXME duplication with TestUtils.BooleanWithMessage + public static class BooleanWithMessage { + boolean value; String message; + public BooleanWithMessage(boolean value, String message) { + this.value = value; this.message = message; + } + public boolean asBoolean() { + return value; + } + public String toString() { + return message; + } + } + + /** + * Convenience method for cases where we need to test until something is true. + * + * The runnable will be invoked periodically until it succesfully concludes. + * <p> + * The following flags are supported: + * <ul> + * <li>abortOnError (boolean, default true) + * <li>abortOnException - (boolean, default false) + * <li>timeout - (a Duration or an integer in millis, defaults to 30*SECONDS) + * <li>period - (a Duration or an integer in millis, for fixed retry time; if not set, defaults to exponentially increasing from 1 to 500ms) + * <li>minPeriod - (a Duration or an integer in millis; only used if period not explicitly set; the minimum period when exponentially increasing; defaults to 1ms) + * <li>maxPeriod - (a Duration or an integer in millis; only used if period not explicitly set; the maximum period when exponentially increasing; defaults to 500ms) + * <li>maxAttempts - (integer, Integer.MAX_VALUE) + * </ul> + * + * The following flags are deprecated: + * <ul> + * <li>useGroovyTruth - (defaults to false; any result code apart from 'false' will be treated as success including null; ignored for Runnables which aren't Callables) + * </ul> + * + * @param flags, accepts the flags listed above + * @param r + * @param finallyBlock + */ + public static <T> T succeedsEventually(Map<String,?> flags, Callable<T> c) { + boolean abortOnException = get(flags, "abortOnException", false); + boolean abortOnError = get(flags, "abortOnError", false); + boolean useGroovyTruth = get(flags, "useGroovyTruth", false); + boolean logException = get(flags, "logException", true); + + // To speed up tests, default is for the period to start small and increase... + Duration duration = toDuration(flags.get("timeout"), DEFAULT_TIMEOUT); + Duration fixedPeriod = toDuration(flags.get("period"), null); + Duration minPeriod = (fixedPeriod != null) ? fixedPeriod : toDuration(flags.get("minPeriod"), Duration.millis(1)); + Duration maxPeriod = (fixedPeriod != null) ? fixedPeriod : toDuration(flags.get("maxPeriod"), Duration.millis(500)); + int maxAttempts = get(flags, "maxAttempts", Integer.MAX_VALUE); + int attempt = 0; + long startTime = System.currentTimeMillis(); + try { + Throwable lastException = null; + T result = null; + long lastAttemptTime = 0; + long expireTime = startTime+duration.toMilliseconds(); + long sleepTimeBetweenAttempts = minPeriod.toMilliseconds(); + + while (attempt < maxAttempts && lastAttemptTime < expireTime) { + try { + attempt++; + lastAttemptTime = System.currentTimeMillis(); + result = c.call(); + if (log.isTraceEnabled()) log.trace("Attempt {} after {} ms: {}", new Object[] {attempt, System.currentTimeMillis() - startTime, result}); + if (useGroovyTruth) { + if (groovyTruth(result)) return result; + } else if (Boolean.FALSE.equals(result)) { + if (result instanceof BooleanWithMessage) + log.warn("Test returned an instance of BooleanWithMessage but useGroovyTruth is not set! " + + "The result of this probably isn't what you intended."); + // FIXME surprising behaviour, "false" result here is acceptable + return result; + } else { + return result; + } + lastException = null; + } catch(Throwable e) { + lastException = e; + if (log.isTraceEnabled()) log.trace("Attempt {} after {} ms: {}", new Object[] {attempt, System.currentTimeMillis() - startTime, e.getMessage()}); + if (abortOnException) throw e; + if (abortOnError && e instanceof Error) throw e; + } + long sleepTime = Math.min(sleepTimeBetweenAttempts, expireTime-System.currentTimeMillis()); + if (sleepTime > 0) Thread.sleep(sleepTime); + sleepTimeBetweenAttempts = Math.min(sleepTimeBetweenAttempts*2, maxPeriod.toMilliseconds()); + } + + log.info("succeedsEventually exceeded max attempts or timeout - {} attempts lasting {} ms, for {}", new Object[] {attempt, System.currentTimeMillis()-startTime, c}); + if (lastException != null) + throw lastException; + throw fail("invalid result: "+result); + } catch (Throwable t) { + if (logException) log.info("failed succeeds-eventually, "+attempt+" attempts, "+ + (System.currentTimeMillis()-startTime)+"ms elapsed "+ + "(rethrowing): "+t); + throw propagate(t); + } + } + + public static <T> void succeedsContinually(Runnable r) { + succeedsContinually(ImmutableMap.<String,Object>of(), r); + } + + public static <T> void succeedsContinually(Map<?,?> flags, Runnable r) { + succeedsContinually(flags, toCallable(r)); + } + + public static <T> T succeedsContinually(Callable<T> c) { + return succeedsContinually(ImmutableMap.<String,Object>of(), c); + } + + public static <T> T succeedsContinually(Map<?,?> flags, Callable<T> job) { + Duration duration = toDuration(flags.get("timeout"), Duration.ONE_SECOND); + Duration period = toDuration(flags.get("period"), Duration.millis(10)); + long periodMs = period.toMilliseconds(); + long startTime = System.currentTimeMillis(); + long expireTime = startTime+duration.toMilliseconds(); + int attempt = 0; + + boolean first = true; + T result = null; + while (first || System.currentTimeMillis() <= expireTime) { + attempt++; + try { + result = job.call(); + } catch (Exception e) { + log.info("succeedsContinually failed - {} attempts lasting {} ms, for {} (rethrowing)", new Object[] {attempt, System.currentTimeMillis()-startTime, job}); + throw propagate(e); + } + if (periodMs > 0) sleep(periodMs); + first = false; + } + return result; + } + + private static Duration toDuration(Object duration, Duration defaultVal) { + if (duration == null) + return defaultVal; + else + return Duration.of(duration); + } + + public static void assertFails(Runnable r) { + assertFailsWith(toCallable(r), Predicates.alwaysTrue()); + } + + public static void assertFails(Callable<?> c) { + assertFailsWith(c, Predicates.alwaysTrue()); + } + + public static void assertFailsWith(Callable<?> c, final Closure<Boolean> exceptionChecker) { + assertFailsWith(c, new Predicate<Throwable>() { + public boolean apply(Throwable input) { + return exceptionChecker.call(input); + } + }); + } + + public static void assertFailsWith(Runnable c, final Class<? extends Throwable> validException, final Class<? extends Throwable> ...otherValidExceptions) { + final List<Class<?>> validExceptions = ImmutableList.<Class<?>>builder() + .add(validException) + .addAll(ImmutableList.copyOf(otherValidExceptions)) + .build(); + + assertFailsWith(c, new Predicate<Throwable>() { + public boolean apply(Throwable e) { + for (Class<?> validException: validExceptions) { + if (validException.isInstance(e)) return true; + } + fail("Test threw exception of unexpected type "+e.getClass()+"; expecting "+validExceptions); + return false; + } + }); + } + + public static void assertFailsWith(Runnable r, Predicate<? super Throwable> exceptionChecker) { + assertFailsWith(toCallable(r), exceptionChecker); + } + + public static void assertFailsWith(Callable<?> c, Predicate<? super Throwable> exceptionChecker) { + boolean failed = false; + try { + c.call(); + } catch (Throwable e) { + failed = true; + if (!exceptionChecker.apply(e)) { + log.debug("Test threw invalid exception (failing)", e); + fail("Test threw invalid exception: "+e); + } + log.debug("Test for exception successful ("+e+")"); + } + if (!failed) fail("Test code should have thrown exception but did not"); + } + + public static void assertReturnsEventually(final Runnable r, Duration timeout) throws InterruptedException, ExecutionException, TimeoutException { + final AtomicReference<Throwable> throwable = new AtomicReference<Throwable>(); + Runnable wrappedR = new Runnable() { + @Override public void run() { + try { + r.run(); + } catch (Throwable t) { + throwable.set(t); + throw Exceptions.propagate(t); + } + } + }; + Thread thread = new Thread(wrappedR, "assertReturnsEventually("+r+")"); + try { + thread.start(); + thread.join(timeout.toMilliseconds()); + if (thread.isAlive()) { + throw new TimeoutException("Still running: r="+r+"; thread="+Arrays.toString(thread.getStackTrace())); + } + } catch (InterruptedException e) { + throw Exceptions.propagate(e); + } finally { + thread.interrupt(); + } + + if (throwable.get() != null) { + throw new ExecutionException(throwable.get()); + } + } + + public static <T> void assertThat(T object, Predicate<T> condition) { + if (condition.apply(object)) return; + fail("Failed "+condition+": "+object); + } + + @SuppressWarnings("rawtypes") + private static boolean groovyTruth(Object o) { + // TODO Doesn't handle matchers (see http://docs.codehaus.org/display/GROOVY/Groovy+Truth) + if (o == null) { + return false; + } else if (o instanceof Boolean) { + return (Boolean)o; + } else if (o instanceof String) { + return !((String)o).isEmpty(); + } else if (o instanceof Collection) { + return !((Collection)o).isEmpty(); + } else if (o instanceof Map) { + return !((Map)o).isEmpty(); + } else if (o instanceof Iterator) { + return ((Iterator)o).hasNext(); + } else if (o instanceof Enumeration) { + return ((Enumeration)o).hasMoreElements(); + } else { + return true; + } + } + + @SuppressWarnings("unchecked") + private static <T> T get(Map<String,?> map, String key, T defaultVal) { + Object val = map.get(key); + return (T) ((val == null) ? defaultVal : val); + } + + private static Callable<?> toCallable(Runnable r) { + return (r instanceof Callable) ? (Callable<?>)r : new RunnableAdapter<Void>(r, null); + } + + /** Same as {@link java.util.concurrent.Executors#callable(Runnable)}, except includes toString() */ + static final class RunnableAdapter<T> implements Callable<T> { + final Runnable task; + final T result; + RunnableAdapter(Runnable task, T result) { + this.task = task; + this.result = result; + } + public T call() { + task.run(); + return result; + } + @Override + public String toString() { + return "RunnableAdapter("+task+")"; + } + } + + private static void sleep(long periodMs) { + if (periodMs > 0) { + try { + Thread.sleep(periodMs); + } catch (InterruptedException e) { + throw propagate(e); + } + } + } + + private static RuntimeException propagate(Throwable t) { + if (t instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + if (t instanceof RuntimeException) throw (RuntimeException)t; + if (t instanceof Error) throw (Error)t; + throw new RuntimeException(t); + } +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/cf2f7a93/utils/common/src/main/java/org/apache/brooklyn/util/CommandLineUtil.java ---------------------------------------------------------------------- diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/CommandLineUtil.java b/utils/common/src/main/java/org/apache/brooklyn/util/CommandLineUtil.java new file mode 100644 index 0000000..b072630 --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/CommandLineUtil.java @@ -0,0 +1,53 @@ +/* + * 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; + +import java.util.List; + +// FIXME move to brooklyn.util.cli.CommandLineArgs, and change get to "remove" +public class CommandLineUtil { + + public static String getCommandLineOption (List<String> args, String param){ + return getCommandLineOption(args, param, null); + } + + /** given a list of args, e.g. --name Foo --parent Bob + * will return "Foo" as param name, and remove those entries from the args list + */ + public static String getCommandLineOption(List<String> args, String param, String defaultValue) { + int i = args.indexOf(param); + if (i >= 0) { + String result = args.get(i + 1); + args.remove(i + 1); + args.remove(i); + return result; + } else { + return defaultValue; + } + } + + public static int getCommandLineOptionInt(List<String> args, String param, int defaultValue) { + String s = getCommandLineOption(args, param,null); + if (s == null) return defaultValue; + return Integer.parseInt(s); + } + + //we don't want instances. + private CommandLineUtil(){} +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/cf2f7a93/utils/common/src/main/java/org/apache/brooklyn/util/JavaGroovyEquivalents.java ---------------------------------------------------------------------- diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/JavaGroovyEquivalents.java b/utils/common/src/main/java/org/apache/brooklyn/util/JavaGroovyEquivalents.java new file mode 100644 index 0000000..94f9a04 --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/JavaGroovyEquivalents.java @@ -0,0 +1,180 @@ +/* + * 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; + +import groovy.lang.Closure; +import groovy.lang.GString; +import groovy.time.TimeDuration; + +import java.util.Collection; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.Executors; + +import org.apache.brooklyn.util.time.Duration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.base.Function; +import com.google.common.base.Predicate; +import com.google.common.collect.Iterables; +import com.google.common.collect.Maps; + +// FIXME move to brooklyn.util.groovy +public class JavaGroovyEquivalents { + + private static final Logger log = LoggerFactory.getLogger(JavaGroovyEquivalents.class); + + public static String join(Collection<?> collection, String separator) { + StringBuffer result = new StringBuffer(); + Iterator<?> ci = collection.iterator(); + if (ci.hasNext()) result.append(asNonnullString(ci.next())); + while (ci.hasNext()) { + result.append(separator); + result.append(asNonnullString(ci.next())); + } + return result.toString(); + } + + /** simple elvislike operators; uses groovy truth */ + @SuppressWarnings("unchecked") + public static <T> Collection<T> elvis(Collection<T> preferred, Collection<?> fallback) { + // TODO Would be nice to not cast, but this is groovy equivalent! Let's fix generics in stage 2 + return groovyTruth(preferred) ? preferred : (Collection<T>) fallback; + } + public static String elvis(String preferred, String fallback) { + return groovyTruth(preferred) ? preferred : fallback; + } + public static String elvisString(Object preferred, Object fallback) { + return elvis(asString(preferred), asString(fallback)); + } + public static <T> T elvis(T preferred, T fallback) { + return groovyTruth(preferred) ? preferred : fallback; + } + public static <T> T elvis(Iterable<?> preferences) { + return elvis(Iterables.toArray(preferences, Object.class)); + } + public static <T> T elvis(Object... preferences) { + if (preferences.length == 0) throw new IllegalArgumentException("preferences must not be empty for elvis"); + for (Object contender : preferences) { + if (groovyTruth(contender)) return (T) fix(contender); + } + return (T) fix(preferences[preferences.length-1]); + } + + public static Object fix(Object o) { + if (o instanceof GString) return (o.toString()); + return o; + } + + public static String asString(Object o) { + if (o==null) return null; + return o.toString(); + } + public static String asNonnullString(Object o) { + if (o==null) return "null"; + return o.toString(); + } + + public static boolean groovyTruth(Collection<?> c) { + return c != null && !c.isEmpty(); + } + public static boolean groovyTruth(String s) { + return s != null && !s.isEmpty(); + } + public static boolean groovyTruth(Object o) { + // TODO Doesn't handle matchers (see http://docs.codehaus.org/display/GROOVY/Groovy+Truth) + if (o == null) { + return false; + } else if (o instanceof Boolean) { + return (Boolean)o; + } else if (o instanceof String) { + return !((String)o).isEmpty(); + } else if (o instanceof Collection) { + return !((Collection)o).isEmpty(); + } else if (o instanceof Map) { + return !((Map)o).isEmpty(); + } else if (o instanceof Iterator) { + return ((Iterator)o).hasNext(); + } else if (o instanceof Enumeration) { + return ((Enumeration)o).hasMoreElements(); + } else { + return true; + } + } + + public static <T> Predicate<T> groovyTruthPredicate() { + return new Predicate<T>() { + @Override public boolean apply(T val) { + return groovyTruth(val); + } + }; + } + + public static Function<Object,Boolean> groovyTruthFunction() { + return new Function<Object, Boolean>() { + @Override public Boolean apply(Object input) { + return groovyTruth(input); + } + }; + } + + public static <K,V> Map<K,V> mapOf(K key1, V val1) { + Map<K,V> result = Maps.newLinkedHashMap(); + result.put(key1, val1); + return result; + } + + /** @deprecated since 0.6.0 use {@link Duration#of(Object)} */ + @Deprecated + public static TimeDuration toTimeDuration(Object duration) { + // TODO Lazy coding here for large number values; but refactoring away from groovy anyway... + + if (duration == null) { + return null; + } else if (duration instanceof TimeDuration) { + return (TimeDuration) duration; + } else if (duration instanceof Number) { + long d = ((Number)duration).longValue(); + if (d <= Integer.MAX_VALUE && d >= Integer.MIN_VALUE) { + return new TimeDuration(0,0,0,(int)d); + } else { + log.warn("Number "+d+" too large to convert to TimeDuration; using Integer.MAX_VALUE instead"); + return new TimeDuration(0,0,0,Integer.MAX_VALUE); + } + } else { + throw new IllegalArgumentException("Cannot convert "+duration+" of type "+duration.getClass().getName()+" to a TimeDuration"); + } + } + + public static <T> Predicate<T> toPredicate(final Closure<Boolean> c) { + return new Predicate<T>() { + @Override public boolean apply(T input) { + return c.call(input); + } + }; + } + + @SuppressWarnings("unchecked") + public static <T> Callable<T> toCallable(final Runnable job) { + return (Callable<T>) ((job instanceof Callable) ? (Callable<T>)job : Executors.callable(job)); + } +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/cf2f7a93/utils/common/src/main/java/org/apache/brooklyn/util/ShellUtils.java ---------------------------------------------------------------------- diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/ShellUtils.java b/utils/common/src/main/java/org/apache/brooklyn/util/ShellUtils.java new file mode 100644 index 0000000..7a9b1af --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/ShellUtils.java @@ -0,0 +1,180 @@ +/* + * 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; + +import groovy.io.GroovyPrintStream; +import groovy.time.TimeDuration; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.PrintStream; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.brooklyn.util.exceptions.Exceptions; +import org.apache.brooklyn.util.stream.StreamGobbler; +import org.apache.brooklyn.util.stream.Streams; +import org.apache.brooklyn.util.text.Strings; +import org.slf4j.Logger; + +import com.google.common.collect.Maps; +import com.google.common.io.Closer; + +/** + * @deprecated since 0.7; does not return exit status, stderr, etc, so utility is of very limited use; and is not used in core brooklyn at all!; + * use ProcessTool or SystemProcessTaskFactory. + */ +@Deprecated +public class ShellUtils { + + public static long TIMEOUT = 60*1000; + + /** + * Executes the given command. + * <p> + * Uses {@code bash -l -c cmd} (to have a good PATH set), and defaults for other fields. + * <p> + * requires a logger and a context object (whose toString is used in the logger and in error messages) + * optionally takes a string to use as input to the command + * + * @see {@link #exec(String, String, Logger, Object)} + */ + public static String[] exec(String cmd, Logger log, Object context) { + return exec(cmd, null, log, context); + } + /** @see {@link #exec(String[], String[], File, String, Logger, Object)} */ + public static String[] exec(String cmd, String input, Logger log, Object context) { + return exec(new String[] { "bash", "-l", "-c", cmd }, null, null, input, log, context); + } + /** @see {@link #exec(Map, String[], String[], File, String, Logger, Object)} */ + public static String[] exec(Map flags, String cmd, Logger log, Object context) { + return exec(flags, new String[] { "bash", "-l", "-c", cmd }, null, null, null, log, context); + } + /** @see {@link #exec(Map, String[], String[], File, String, Logger, Object)} */ + public static String[] exec(Map flags, String cmd, String input, Logger log, Object context) { + return exec(flags, new String[] { "bash", "-l", "-c", cmd }, null, null, input, log, context); + } + /** @see {@link #exec(Map, String[], String[], File, String, Logger, Object)} */ + public static String[] exec(String[] cmd, String[] envp, File dir, String input, Logger log, Object context) { + return exec(Maps.newLinkedHashMap(), cmd, envp, dir, input, log, context); + } + + private static long getTimeoutMs(Map flags) { + long timeout = TIMEOUT; + + Object tf = flags.get("timeout"); + + if (tf instanceof Number) { + timeout = ((Number) tf).longValue(); + } else if (tf instanceof TimeDuration) { + timeout = ((TimeDuration) tf).toMilliseconds(); + } + + //if (tf != null) timeout = tf; + + return timeout; + } + + /** + * Executes the given command. + * <p> + * Uses the given environmnet (inherited if null) and cwd ({@literal .} if null), + * feeding it the given input stream (if not null) and logging I/O at debug (if not null). + * <p> + * flags: timeout (Duration), 0 for forever; default 60 seconds + * + * @throws IllegalStateException if return code non-zero + * @return lines from stdout. + */ + public static String[] exec(Map flags, final String[] cmd, String[] envp, File dir, String input, final Logger log, final Object context) { + if (log.isDebugEnabled()) { + log.debug("Running local command: {}% {}", context, Strings.join(cmd, " ")); + } + Closer closer = Closer.create(); + try { + final Process proc = Runtime.getRuntime().exec(cmd, envp, dir); // Call *execute* on the string + ByteArrayOutputStream stdoutB = new ByteArrayOutputStream(); + ByteArrayOutputStream stderrB = new ByteArrayOutputStream(); + PrintStream stdoutP = new GroovyPrintStream(stdoutB); + PrintStream stderrP = new GroovyPrintStream(stderrB); + @SuppressWarnings("resource") + StreamGobbler stdoutG = new StreamGobbler(proc.getInputStream(), stdoutP, log).setLogPrefix("["+context+":stdout] "); + stdoutG.start(); + closer.register(stdoutG); + @SuppressWarnings("resource") + StreamGobbler stderrG = new StreamGobbler(proc.getErrorStream(), stderrP, log).setLogPrefix("["+context+":stderr] "); + stderrG.start(); + closer.register(stderrG); + if (input!=null && input.length()>0) { + proc.getOutputStream().write(input.getBytes()); + proc.getOutputStream().flush(); + } + + final long timeout = getTimeoutMs(flags); + final AtomicBoolean ended = new AtomicBoolean(false); + final AtomicBoolean killed = new AtomicBoolean(false); + + //if a timeout was specified, this thread will kill the process. This is a work around because the process.waitFor' + //doesn't accept a timeout. + Thread timeoutThread = new Thread(new Runnable() { + public void run() { + if (timeout <= 0) return; + try { + Thread.sleep(timeout); + if (!ended.get()) { + if (log.isDebugEnabled()) { + log.debug("Timeout exceeded for "+context+"% "+Strings.join(cmd, " ")); + } + proc.destroy(); + killed.set(true); + } + } catch (Exception e) { } + } + }); + if (timeout > 0) timeoutThread.start(); + int exitCode = proc.waitFor(); + ended.set(true); + if (timeout > 0) timeoutThread.interrupt(); + + stdoutG.blockUntilFinished(); + stderrG.blockUntilFinished(); + if (exitCode!=0 || killed.get()) { + String message = killed.get() ? "terminated after timeout" : "exit code "+exitCode; + if (log.isDebugEnabled()) { + log.debug("Completed local command (problem, throwing): "+context+"% "+Strings.join(cmd, " ")+" - "+message); + } + String e = "Command failed ("+message+"): "+Strings.join(cmd, " "); + log.warn(e+"\n"+stdoutB+(stderrB.size()>0 ? "\n--\n"+stderrB : "")); + throw new IllegalStateException(e+" (details logged)"); + } + if (log.isDebugEnabled()) { + log.debug("Completed local command: "+context+"% "+Strings.join(cmd, " ")+" - exit code 0"); + } + return stdoutB.toString().split("\n"); + } catch (IOException e) { + throw Exceptions.propagate(e); + } catch (InterruptedException e) { + throw Exceptions.propagate(e); + } finally { + Streams.closeQuietly(closer); + } + } + +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/cf2f7a93/utils/common/src/main/java/org/apache/brooklyn/util/collections/CollectionFunctionals.java ---------------------------------------------------------------------- diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/collections/CollectionFunctionals.java b/utils/common/src/main/java/org/apache/brooklyn/util/collections/CollectionFunctionals.java new file mode 100644 index 0000000..8446e55 --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/collections/CollectionFunctionals.java @@ -0,0 +1,242 @@ +/* + * 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.collections; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.annotation.Nullable; + +import org.apache.brooklyn.util.collections.QuorumCheck.QuorumChecks; + +import com.google.common.base.Function; +import com.google.common.base.Functions; +import com.google.common.base.Preconditions; +import com.google.common.base.Predicate; +import com.google.common.base.Predicates; +import com.google.common.base.Supplier; +import com.google.common.collect.Iterables; +import com.google.common.collect.Sets; + +/** things which it seems should be in guava, but i can't find + * @author alex */ +public class CollectionFunctionals { + + private static final class EqualsSetPredicate implements Predicate<Iterable<?>> { + private final Iterable<?> target; + + private EqualsSetPredicate(Iterable<?> target) { + this.target = target; + } + + @Override + public boolean apply(@Nullable Iterable<?> input) { + if (input==null) return false; + return Sets.newHashSet(target).equals(Sets.newHashSet(input)); + } + } + + private static final class KeysOfMapFunction<K> implements Function<Map<K, ?>, Set<K>> { + @Override + public Set<K> apply(Map<K, ?> input) { + if (input==null) return null; + return input.keySet(); + } + + @Override public String toString() { return "keys"; } + } + + private static final class SizeSupplier implements Supplier<Integer> { + private final Iterable<?> collection; + + private SizeSupplier(Iterable<?> collection) { + this.collection = collection; + } + + @Override + public Integer get() { + return Iterables.size(collection); + } + + @Override public String toString() { return "sizeSupplier("+collection+")"; } + } + + public static final class SizeFunction implements Function<Iterable<?>, Integer> { + private final Integer valueIfInputNull; + + private SizeFunction(Integer valueIfInputNull) { + this.valueIfInputNull = valueIfInputNull; + } + + @Override + public Integer apply(Iterable<?> input) { + if (input==null) return valueIfInputNull; + return Iterables.size(input); + } + + @Override public String toString() { return "sizeFunction"; } + } + + public static Supplier<Integer> sizeSupplier(final Iterable<?> collection) { + return new SizeSupplier(collection); + } + + public static Function<Iterable<?>, Integer> sizeFunction() { return sizeFunction(null); } + + public static Function<Iterable<?>, Integer> sizeFunction(final Integer valueIfInputNull) { + return new SizeFunction(valueIfInputNull); + } + + public static final class FirstElementFunction<T> implements Function<Iterable<? extends T>, T> { + private FirstElementFunction() { + } + + @Override + public T apply(Iterable<? extends T> input) { + if (input==null) return null; + return Iterables.get(input, 0); + } + + @Override public String toString() { return "firstElementFunction"; } + } + + public static <T> Function<Iterable<? extends T>, T> firstElement() { + return new FirstElementFunction<T>(); + } + + public static <K> Function<Map<K,?>,Set<K>> keys() { + return new KeysOfMapFunction<K>(); + } + + public static <K> Function<Map<K, ?>, Integer> mapSize() { + return mapSize(null); + } + + public static <K> Function<Map<K, ?>, Integer> mapSize(Integer valueIfNull) { + return Functions.compose(CollectionFunctionals.sizeFunction(valueIfNull), CollectionFunctionals.<K>keys()); + } + + /** default guava Equals predicate will reflect order of target, and will fail when matching against a list; + * this treats them both as sets */ + public static Predicate<Iterable<?>> equalsSetOf(Object... target) { + return equalsSet(Arrays.asList(target)); + } + public static Predicate<Iterable<?>> equalsSet(final Iterable<?> target) { + return new EqualsSetPredicate(target); + } + + public static Predicate<Iterable<?>> sizeEquals(int targetSize) { + return Predicates.compose(Predicates.equalTo(targetSize), CollectionFunctionals.sizeFunction()); + } + + public static Predicate<Iterable<?>> empty() { + return sizeEquals(0); + } + + public static Predicate<Iterable<?>> notEmpty() { + return Predicates.not(empty()); + } + + public static <K> Predicate<Map<K,?>> mapSizeEquals(int targetSize) { + return Predicates.compose(Predicates.equalTo(targetSize), CollectionFunctionals.<K>mapSize()); + } + + public static <T,I extends Iterable<T>> Function<I, List<T>> limit(final int max) { + return new LimitFunction<T,I>(max); + } + + private static final class LimitFunction<T, I extends Iterable<T>> implements Function<I, List<T>> { + private final int max; + private LimitFunction(int max) { + this.max = max; + } + @Override + public List<T> apply(I input) { + if (input==null) return null; + MutableList<T> result = MutableList.of(); + for (T i: input) { + result.add(i); + if (result.size()>=max) + return result; + } + return result; + } + } + + // --------- + public static <I,T extends Collection<I>> Predicate<T> contains(I item) { + return new CollectionContains<I,T>(item); + } + + private static final class CollectionContains<I,T extends Collection<I>> implements Predicate<T> { + private final I item; + private CollectionContains(I item) { + this.item = item; + } + @Override + public boolean apply(T input) { + if (input==null) return false; + return input.contains(item); + } + @Override + public String toString() { + return "contains("+item+")"; + } + } + + // --------- + + public static <T,TT extends Iterable<T>> Predicate<TT> all(Predicate<T> attributeSatisfies) { + return quorum(QuorumChecks.all(), attributeSatisfies); + } + + public static <T,TT extends Iterable<T>> Predicate<TT> quorum(QuorumCheck quorumCheck, Predicate<T> attributeSatisfies) { + return new QuorumSatisfies<T, TT>(quorumCheck, attributeSatisfies); + } + + + private static final class QuorumSatisfies<I,T extends Iterable<I>> implements Predicate<T> { + private final Predicate<I> itemCheck; + private final QuorumCheck quorumCheck; + private QuorumSatisfies(QuorumCheck quorumCheck, Predicate<I> itemCheck) { + this.itemCheck = Preconditions.checkNotNull(itemCheck, "itemCheck"); + this.quorumCheck = Preconditions.checkNotNull(quorumCheck, "quorumCheck"); + } + @Override + public boolean apply(T input) { + if (input==null) return false; + int sizeHealthy = 0, totalSize = 0; + for (I item: input) { + totalSize++; + if (itemCheck.apply(item)) sizeHealthy++; + } + return quorumCheck.isQuorate(sizeHealthy, totalSize); + } + @Override + public String toString() { + return quorumCheck.toString()+"("+itemCheck+")"; + } + } + + + +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/cf2f7a93/utils/common/src/main/java/org/apache/brooklyn/util/collections/Jsonya.java ---------------------------------------------------------------------- diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/collections/Jsonya.java b/utils/common/src/main/java/org/apache/brooklyn/util/collections/Jsonya.java new file mode 100644 index 0000000..ef7f451 --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/collections/Jsonya.java @@ -0,0 +1,581 @@ +/* + * 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.collections; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.Stack; + +import javax.annotation.Nonnull; + +import org.apache.brooklyn.util.guava.Maybe; +import org.apache.brooklyn.util.text.StringEscapes.JavaStringEscapes; + +import com.google.common.annotations.Beta; +import com.google.common.base.Function; +import com.google.common.base.Functions; +import com.google.common.base.Preconditions; +import com.google.common.base.Throwables; +import com.google.common.primitives.Primitives; + +/** Jsonya = JSON-yet-another (tool) + * <p> + * provides conveniences for working with maps and lists containing maps and lists, + * and other datatypes too, easily convertible to json. + * <p> + * see {@link JsonyaTest} for examples + * + * @since 0.6.0 + **/ +@Beta +public class Jsonya { + + private Jsonya() {} + + /** creates a {@link Navigator} backed by the given map (focussed at the root) */ + public static <T extends Map<?,?>> Navigator<T> of(T map) { + return new Navigator<T>(map, MutableMap.class); + } + + /** creates a {@link Navigator} backed by the map at the focus of the given navigator */ + public static <T extends Map<?,?>> Navigator<T> of(Navigator<T> navigator) { + return new Navigator<T>(navigator.getFocusMap(), MutableMap.class); + } + + /** creates a {@link Navigator} backed by a newly created map; + * the map can be accessed by {@link Navigator#getMap()} */ + public static Navigator<MutableMap<Object,Object>> newInstance() { + return new Navigator<MutableMap<Object,Object>>(new MutableMap<Object,Object>(), MutableMap.class); + } + /** convenience for {@link Navigator#at(Object, Object...)} on a {@link #newInstance()} */ + public static Navigator<MutableMap<Object,Object>> at(Object ...pathSegments) { + return newInstance().atArray(pathSegments); + } + + /** as {@link #newInstance()} but using the given translator to massage objects inserted into the Jsonya structure */ + public static Navigator<MutableMap<Object,Object>> newInstanceTranslating(Function<Object,Object> translator) { + return newInstance().useTranslator(translator); + } + + /** as {@link #newInstanceTranslating(Function)} using an identity function + * (functionally equivalent to {@link #newInstance()} but explicit about it */ + public static Navigator<MutableMap<Object,Object>> newInstanceLiteral() { + return newInstanceTranslating(Functions.identity()); + } + + /** as {@link #newInstanceTranslating(Function)} using a function which only supports JSON primitives: + * maps and collections are traversed, strings and primitives are inserted, and everything else has toString applied. + * see {@link JsonPrimitiveDeepTranslator} */ + public static Navigator<MutableMap<Object,Object>> newInstancePrimitive() { + return newInstanceTranslating(new JsonPrimitiveDeepTranslator()); + } + + /** convenience for converting an object x to something which consists only of json primitives, doing + * {@link #toString()} on anything which is not recognised. see {@link JsonPrimitiveDeepTranslator} */ + public static Object convertToJsonPrimitive(Object x) { + if (x==null) return null; + if (x instanceof Map) return newInstancePrimitive().put((Map<?,?>)x).getRootMap(); + return newInstancePrimitive().put("data", x).getRootMap().get("data"); + } + + /** tells whether {@link #convertToJsonPrimitive(Object)} returns an object which is identical to + * the equivalent literal json structure. this is typically equivalent to saying serializing to json then + * deserializing will produce something where the result is equal to the input, + * modulo a few edge cases such as longs becoming ints. + * note that the converse (input equal to output) may not be the case, + * e.g. if the input contains special subclasses of collections of maps who care about type preservation. */ + public static boolean isJsonPrimitiveCompatible(Object x) { + if (x==null) return true; + return convertToJsonPrimitive(x).equals(x); + } + + @SuppressWarnings({"rawtypes","unchecked"}) + public static class Navigator<T extends Map<?,?>> { + + protected final Object root; + protected final Class<? extends Map> mapType; + protected Object focus; + protected Stack<Object> focusStack = new Stack<Object>(); + protected Function<Object,Void> creationInPreviousFocus; + protected Function<Object,Object> translator; + + public Navigator(Object backingStore, Class<? extends Map> mapType) { + this.root = Preconditions.checkNotNull(backingStore); + this.focus = backingStore; + this.mapType = mapType; + } + + // -------------- access and configuration + + /** returns the object at the focus, or null if none */ + public Object get() { + return focus; + } + + /** as {@link #get()} but always wrapped in a {@link Maybe}, absent if null */ + public @Nonnull Maybe<Object> getMaybe() { + return Maybe.fromNullable(focus); + } + + /** returns the object at the focus, casted to the given type, null if none + * @throws ClassCastException if object exists here but of the wrong type */ + public <V> V get(Class<V> type) { + return (V)focus; + } + + /** as {@link #get(Class)} but always wrapped in a {@link Maybe}, absent if null + * @throws ClassCastException if object exists here but of the wrong type */ + public @Nonnull <V> Maybe<V> getMaybe(Class<V> type) { + return Maybe.fromNullable(get(type)); + } + + /** gets the object at the indicated path from the current focus + * (without changing the path to that focus; use {@link #at(Object, Object...)} to change focus) */ + // Jun 2014, semantics changed so that focus does not change, which is more natural + public Object get(Object pathSegment, Object ...furtherPathSegments) { + push(); + at(pathSegment, furtherPathSegments); + Object result = get(); + pop(); + return result; + } + + public Navigator<T> root() { + focus = root; + return this; + } + + /** returns the object at the root */ + public Object getRoot() { + return root; + } + + /** returns the {@link Map} at the root, throwing if root is not a map */ + public T getRootMap() { + return (T) root; + } + + /** returns a {@link Map} at the given focus, creating if needed (so never null), + * throwing if it exists already and is not a map */ + public T getFocusMap() { + map(); + return (T)focus; + } + + /** as {@link #getFocusMap()} but always wrapped in a {@link Maybe}, absent if null + * @throws ClassCastException if object exists here but of the wrong type */ + public @Nonnull Maybe<T> getFocusMapMaybe() { + return Maybe.fromNullable(getFocusMap()); + } + + /** specifies a translator function to use when new data is added; + * by default everything is added as a literal (ie {@link Functions#identity()}), + * but if you want to do translation on the way in, + * set a translation function + * <p> + * note that translation should be idempotent as implementation may apply it multiple times in certain cases + */ + public Navigator<T> useTranslator(Function<Object,Object> translator) { + this.translator = translator; + return this; + } + + protected Object translate(Object x) { + if (translator==null) return x; + return translator.apply(x); + } + + protected Object translateKey(Object x) { + if (translator==null) return x; + // this could return the toString to make it strict json + // but json libraries seem to do that so not strictly necessary + return translator.apply(x); + } + + // ------------- navigation (map mainly) + + /** pushes the current focus to a stack, so that this location will be restored on the corresponding {@link #pop()} */ + public Navigator<T> push() { + focusStack.push(focus); + return this; + } + + /** pops the most recently pushed focus, so that it returns to the last location {@link #push()}ed */ + public Navigator<T> pop() { + focus = focusStack.pop(); + return this; + } + + /** returns the navigator moved to focus at the indicated key sequence in the given map */ + public Navigator<T> at(Object pathSegment, Object ...furtherPathSegments) { + down(pathSegment); + return atArray(furtherPathSegments); + } + public Navigator<T> atArray(Object[] furtherPathSegments) { + for (Object p: furtherPathSegments) + down(p); + return this; + } + + /** ensures the given focus is a map, creating if needed (and creating inside the list if it is in a list) */ + public Navigator<T> map() { + if (focus==null) { + focus = newMap(); + creationInPreviousFocus.apply(focus); + } + if (focus instanceof List) { + Map m = newMap(); + ((List)focus).add(translate(m)); + focus = m; + return this; + } + if (!(focus instanceof Map)) + throw new IllegalStateException("focus here is "+focus+"; expected a map"); + return this; + } + + /** puts the given key-value pair at the current focus (or multiple such), + * creating a map if needed, replacing any values stored against keys supplied here; + * if you wish to merge deep maps, see {@link #add(Object, Object...)} */ + public Navigator<T> put(Object k1, Object v1, Object ...kvOthers) { + map(); + putInternal((Map)focus, k1, v1, kvOthers); + return this; + } + + public Navigator<T> putIfNotNull(Object k1, Object v1) { + if (v1!=null) { + map(); + putInternal((Map)focus, k1, v1); + } + return this; + } + + protected void putInternal(Map target, Object k1, Object v1, Object ...kvOthers) { + assert (kvOthers.length % 2) == 0 : "even number of arguments required for put"; + target.put(translateKey(k1), translate(v1)); + for (int i=0; i<kvOthers.length; ) { + target.put(translateKey(kvOthers[i++]), translate(kvOthers[i++])); + } + } + + /** as {@link #put(Object, Object, Object...)} for the kv-pairs in the given map; ignores null for convenience */ + public Navigator<T> put(Map map) { + map(); + if (map==null) return this; + ((Map)focus).putAll((Map)translate(map)); + return this; + } + + protected Map newMap() { + try { + return mapType.newInstance(); + } catch (Exception e) { + throw Throwables.propagate(e); + } + } + + /** utility for {@link #at(Object, Object...)}, taking one argument at a time */ + protected Navigator<T> down(final Object pathSegment) { + if (focus instanceof List) { + return downList(pathSegment); + } + if ((focus instanceof Map) || focus==null) { + return downMap(pathSegment); + } + throw new IllegalStateException("focus here is "+focus+"; cannot descend to '"+pathSegment+"'"); + } + + protected Navigator<T> downMap(Object pathSegmentO) { + final Object pathSegment = translateKey(pathSegmentO); + final Map givenParentMap = (Map)focus; + if (givenParentMap!=null) { + creationInPreviousFocus = null; + focus = givenParentMap.get(pathSegment); + } + if (focus==null) { + final Function<Object, Void> previousCreation = creationInPreviousFocus; + creationInPreviousFocus = new Function<Object, Void>() { + public Void apply(Object input) { + creationInPreviousFocus = null; + Map parentMap = givenParentMap; + if (parentMap==null) { + parentMap = newMap(); + previousCreation.apply(parentMap); + } + parentMap.put(pathSegment, translate(input)); + return null; + } + }; + } + return this; + } + + protected Navigator<T> downList(final Object pathSegment) { + if (!(pathSegment instanceof Integer)) + throw new IllegalStateException("focus here is a list ("+focus+"); cannot descend to '"+pathSegment+"'"); + final List givenParentList = (List)focus; + // previous focus always non-null + creationInPreviousFocus = null; + focus = givenParentList.get((Integer)pathSegment); + if (focus==null) { + // don't need to worry about creation here; we don't create list entries simply by navigating + // TODO a nicer architecture would create a new object with focus for each traversal + // in that case we could create, filling other positions with null; but is there a need? + creationInPreviousFocus = new Function<Object, Void>() { + public Void apply(Object input) { + throw new IllegalStateException("cannot create "+input+" here because we are at a non-existent position in a list"); + } + }; + } + return this; + } + + // ------------- navigation (list mainly) + + /** ensures the given focus is a list */ + public Navigator<T> list() { + if (focus==null) { + focus = newList(); + creationInPreviousFocus.apply(focus); + } + if (!(focus instanceof List)) + throw new IllegalStateException("focus here is "+focus+"; expected a list"); + return this; + } + + protected List newList() { + return new ArrayList(); + } + + /** adds the given items to the focus, whether a list or a map, + * creating the focus as a map if it doesn't already exist. + * to add items to a list which might not exist, precede by a call to {@link #list()}. + * <p> + * when adding items to a list, iterable and array arguments are flattened because + * that makes the most sense when working with deep maps (adding one map to another where both contain lists, for example); + * to prevent flattening use {@link #addUnflattened(Object, Object...)} + * <p> + * when adding to a map, arguments will be treated as things to put into the map, + * accepting either multiple arguments, as key1, value1, key2, value2, ... + * (and must be an event number); or a single argument which must be a map, + * in which case the value for each key in the supplied map is added to any existing value against that key in the target map + * (in other words, it will do a "deep put", where nested maps are effectively merged) + * <p> + * this implementation will currently throw if you attempt to add a non-map to anything present which is not a list; + * auto-conversion to a list may be added in a future version + * */ + public Navigator<T> add(Object o1, Object ...others) { + if (focus==null) map(); + addInternal(focus, focus, o1, others); + return this; + } + + /** adds the given arguments to a list at this point (will not descend into maps, and will not flatten lists) */ + public Navigator<T> addUnflattened(Object o1, Object ...others) { + ((Collection)focus).add(translate(o1)); + for (Object oi: others) ((Collection)focus).add(translate(oi)); + return this; + } + + protected void addInternal(Object initialFocus, Object currentFocus, Object o1, Object ...others) { + if (currentFocus instanceof Map) { + Map target = (Map)currentFocus; + Map source; + if (others.length==0) { + // add as a map + if (o1==null) + // ignore if null + return ; + if (!(o1 instanceof Map)) + throw new IllegalStateException("cannot add: focus here is "+currentFocus+" (in "+initialFocus+"); expected a collection, or a map (with a map being added, not "+o1+")"); + source = (Map)translate(o1); + } else { + // build a source map from the arguments as key-value pairs + if ((others.length % 2)==0) + throw new IllegalArgumentException("cannot add an odd number of arguments to a map" + + " ("+o1+" then "+Arrays.toString(others)+" in "+currentFocus+" in "+initialFocus+")"); + source = MutableMap.of(translateKey(o1), translate(others[0])); + for (int i=1; i<others.length; ) + source.put(translateKey(others[i++]), translate(others[i++])); + } + // and add the source map to the target + for (Object entry : source.entrySet()) { + Object key = ((Map.Entry)entry).getKey(); + Object sv = ((Map.Entry)entry).getValue(); + Object tv = target.get(key); + if (!target.containsKey(key)) { + target.put(key, sv); + } else { + addInternal(initialFocus, tv, sv); + } + } + return; + } + // lists are easy to add to, but remember we have to flatten + if (!(currentFocus instanceof Collection)) + // TODO a nicer architecture might replace the current target with a list (also above where single non-map argument is supplied) + throw new IllegalStateException("cannot add: focus here is "+currentFocus+"; expected a collection"); + addFlattened((Collection)currentFocus, o1); + for (Object oi: others) addFlattened((Collection)currentFocus, oi); + } + + protected void addFlattened(Collection target, Object item) { + if (item instanceof Iterable) { + for (Object i: (Iterable)item) + addFlattened(target, i); + return; + } + if (item.getClass().isArray()) { + for (Object i: ((Object[])item)) + addFlattened(target, i); + return; + } + // nothing to flatten + target.add(translate(item)); + } + + /** Returns JSON serialized output for given focus in the given jsonya; + * applies a naive toString for specialized types */ + @Override + public String toString() { + return render(get()); + } + } + + public static String render(Object focus) { + if (focus instanceof Map) { + StringBuilder sb = new StringBuilder(); + sb.append("{"); + boolean first = true; + for (Object entry: ((Map<?,?>)focus).entrySet()) { + if (!first) sb.append(","); + else first = false; + sb.append(" "); + sb.append( render(((Map.Entry<?,?>)entry).getKey()) ); + sb.append(": "); + sb.append( render(((Map.Entry<?,?>)entry).getValue()) ); + } + sb.append(" }"); + return sb.toString(); + } + if (focus instanceof Collection) { + StringBuilder sb = new StringBuilder(); + sb.append("["); + boolean first = true; + for (Object entry: (Collection<?>)focus) { + if (!first) sb.append(","); + else first = false; + sb.append( render(entry) ); + } + sb.append(" ]"); + return sb.toString(); + } + if (focus instanceof String) { + return JavaStringEscapes.wrapJavaString((String)focus); + } + if (focus == null || focus instanceof Number || focus instanceof Boolean) + return ""+focus; + + return render(""+focus); + } + + /** Converts an object to one which uses standard JSON objects where possible + * (strings, numbers, booleans, maps, lists), and uses toString elsewhere */ + public static class JsonPrimitiveDeepTranslator implements Function<Object,Object> { + public static JsonPrimitiveDeepTranslator INSTANCE = new JsonPrimitiveDeepTranslator(); + + /** No need to instantiate except when subclassing. Use static {@link #INSTANCE}. */ + protected JsonPrimitiveDeepTranslator() {} + + @Override + public Object apply(Object input) { + return apply(input, new HashSet<Object>()); + } + + protected Object apply(Object input, Set<Object> stack) { + if (input==null) return applyNull(stack); + + if (isPrimitiveOrBoxer(input.getClass())) + return applyPrimitiveOrBoxer(input, stack); + + if (input instanceof String) + return applyString((String)input, stack); + + stack = new HashSet<Object>(stack); + if (!stack.add(input)) + // fail if object is self-recursive; don't even try toString as that is dangerous + // (extra measure of safety, since maps and lists generally fail elsewhere with recursive entries, + // eg in hashcode or toString) + return "[REF_ANCESTOR:"+stack.getClass()+"]"; + + if (input instanceof Collection<?>) + return applyCollection( (Collection<?>)input, stack ); + + if (input instanceof Map<?,?>) + return applyMap( (Map<?,?>)input, stack ); + + return applyOther(input, stack); + } + + protected Object applyNull(Set<Object> stack) { + return null; + } + + protected Object applyPrimitiveOrBoxer(Object input, Set<Object> stack) { + return input; + } + + protected Object applyString(String input, Set<Object> stack) { + return input.toString(); + } + + protected Object applyCollection(Collection<?> input, Set<Object> stack) { + MutableList<Object> result = MutableList.of(); + + for (Object xi: input) + result.add(apply(xi, stack)); + + return result; + } + + protected Object applyMap(Map<?, ?> input, Set<Object> stack) { + MutableMap<Object, Object> result = MutableMap.of(); + + for (Map.Entry<?,?> xi: input.entrySet()) + result.put(apply(xi.getKey(), stack), apply(xi.getValue(), stack)); + + return result; + } + + protected Object applyOther(Object input, Set<Object> stack) { + return input.toString(); + } + + public static boolean isPrimitiveOrBoxer(Class<?> type) { + return Primitives.allPrimitiveTypes().contains(type) || Primitives.allWrapperTypes().contains(type); + } + } + +}
