You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@brooklyn.apache.org by he...@apache.org on 2015/11/09 13:55:12 UTC

[03/21] incubator-brooklyn git commit: Catalog items configuration support

Catalog items configuration support

Functionality to let catalog items document what configuration they accept as input. Useful for YAML plans to let users know what configuration they support and for UIs to list available inputs and validate them.


Project: http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/commit/5fb47710
Tree: http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/tree/5fb47710
Diff: http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/diff/5fb47710

Branch: refs/heads/master
Commit: 5fb47710f7e6a6b3d460e65affc1b7472413a4eb
Parents: 92d8560
Author: Svetoslav Neykov <sv...@cloudsoftcorp.com>
Authored: Wed Oct 28 19:13:17 2015 +0200
Committer: Svetoslav Neykov <sv...@cloudsoftcorp.com>
Committed: Thu Nov 5 15:23:33 2015 +0200

----------------------------------------------------------------------
 .../brooklyn/api/catalog/CatalogItem.java       |  16 +-
 .../catalog/internal/BasicBrooklynCatalog.java  |   5 +
 .../catalog/internal/CatalogClasspathDo.java    |  13 +-
 .../core/catalog/internal/CatalogInputDto.java  | 240 +++++++++++++++++++
 .../catalog/internal/CatalogItemBuilder.java    |  10 +
 .../core/catalog/internal/CatalogItemDo.java    |   7 +
 .../internal/CatalogItemDtoAbstract.java        |  17 +-
 .../brooklyn/core/mgmt/ha/OsgiManager.java      |   1 +
 .../apache/brooklyn/util/core/osgi/Osgis.java   |   1 +
 .../internal/CatalogInputDtoClassTest.java      |  65 +++++
 .../internal/CatalogInputDtoYamlTest.java       | 185 ++++++++++++++
 .../core/catalog/internal/CatalogInputTest.java | 139 +++++++++++
 .../core/catalog/internal/CatalogLoadTest.java  |   2 +-
 .../catalog/internal/TestToSpecTransformer.java | 118 +++++++++
 ...che.brooklyn.core.plan.PlanToSpecTransformer |  19 ++
 .../rest/transform/CatalogTransformer.java      |  12 +-
 .../rest/transform/EntityTransformer.java       |   7 +
 .../brooklyn-test-osgi-more-entities_0.2.0.jar  | Bin 15622 -> 15745 bytes
 .../test/osgi/entities/more/MoreEntity.java     |   3 +
 19 files changed, 848 insertions(+), 12 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5fb47710/api/src/main/java/org/apache/brooklyn/api/catalog/CatalogItem.java
----------------------------------------------------------------------
diff --git a/api/src/main/java/org/apache/brooklyn/api/catalog/CatalogItem.java b/api/src/main/java/org/apache/brooklyn/api/catalog/CatalogItem.java
index de07640..e5a2636 100644
--- a/api/src/main/java/org/apache/brooklyn/api/catalog/CatalogItem.java
+++ b/api/src/main/java/org/apache/brooklyn/api/catalog/CatalogItem.java
@@ -19,6 +19,7 @@
 package org.apache.brooklyn.api.catalog;
 
 import java.util.Collection;
+import java.util.List;
 
 import javax.annotation.Nullable;
 
@@ -27,6 +28,7 @@ import org.apache.brooklyn.api.mgmt.rebind.Rebindable;
 import org.apache.brooklyn.api.mgmt.rebind.mementos.CatalogItemMemento;
 import org.apache.brooklyn.api.objs.BrooklynObject;
 import org.apache.brooklyn.api.typereg.OsgiBundleWithUrl;
+import org.apache.brooklyn.config.ConfigKey;
 
 import com.google.common.annotations.Beta;
 
@@ -45,6 +47,15 @@ public interface CatalogItem<T,SpecT> extends BrooklynObject, Rebindable {
         public boolean isNamed();
     }
 
+    public static interface CatalogInput<T> {
+        /** Short name, to be used in UI */
+        String getLabel();
+        /** Visible by default in UI, not all inputs may be visible at once */
+        boolean isPinned();
+        /** Type information for the input */
+        ConfigKey<T> getType();
+    }
+
     /**
      * @throws UnsupportedOperationException; config not supported for catalog items
      */
@@ -56,7 +67,8 @@ public interface CatalogItem<T,SpecT> extends BrooklynObject, Rebindable {
      */
     @Override
     SubscriptionSupport subscriptions();
-    
+
+    /** @deprecated since 0.7.0 in favour of {@link CatalogBundle}, kept for rebind compatibility */
     @Deprecated
     public static interface CatalogItemLibraries {
         Collection<String> getBundles();
@@ -94,6 +106,8 @@ public interface CatalogItem<T,SpecT> extends BrooklynObject, Rebindable {
 
     public String getVersion();
 
+    public List<CatalogInput<?>> getInputs();
+
     public Collection<CatalogBundle> getLibraries();
 
     public String toXmlString();

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5fb47710/core/src/main/java/org/apache/brooklyn/core/catalog/internal/BasicBrooklynCatalog.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/core/catalog/internal/BasicBrooklynCatalog.java b/core/src/main/java/org/apache/brooklyn/core/catalog/internal/BasicBrooklynCatalog.java
index c176a42..e9f8d34 100644
--- a/core/src/main/java/org/apache/brooklyn/core/catalog/internal/BasicBrooklynCatalog.java
+++ b/core/src/main/java/org/apache/brooklyn/core/catalog/internal/BasicBrooklynCatalog.java
@@ -32,6 +32,7 @@ import javax.annotation.Nullable;
 import org.apache.brooklyn.api.catalog.BrooklynCatalog;
 import org.apache.brooklyn.api.catalog.CatalogItem;
 import org.apache.brooklyn.api.catalog.CatalogItem.CatalogBundle;
+import org.apache.brooklyn.api.catalog.CatalogItem.CatalogInput;
 import org.apache.brooklyn.api.catalog.CatalogItem.CatalogItemType;
 import org.apache.brooklyn.api.internal.AbstractBrooklynObjectSpec;
 import org.apache.brooklyn.api.location.Location;
@@ -420,6 +421,9 @@ public class BasicBrooklynCatalog implements BrooklynCatalog {
         // (this load is required for the scan below and I think also for yaml resolution)
         CatalogUtils.installLibraries(mgmt, libraryBundlesNew);
 
+        List<?> inputsRaw = MutableList.copyOf(getFirstAs(itemMetadata, List.class, "brooklyn.inputs", "inputs").orNull());
+        List<CatalogInput<?>> inputs = CatalogInputDto.ParseYamlInputs.parseInputs(inputsRaw, CatalogUtils.newClassLoadingContext(mgmt, "<catalog_input_parser>", libraryBundles));
+
         Boolean scanJavaAnnotations = getFirstAs(itemMetadata, Boolean.class, "scanJavaAnnotations", "scan_java_annotations").orNull();
         if (scanJavaAnnotations==null || !scanJavaAnnotations) {
             // don't scan
@@ -567,6 +571,7 @@ public class BasicBrooklynCatalog implements BrooklynCatalog {
         String sourcePlanYaml = planInterpreter.getPlanYaml();
         
         CatalogItemDtoAbstract<?, ?> dto = createItemBuilder(itemType, symbolicName, version)
+            .inputs(inputs)
             .libraries(libraryBundles)
             .displayName(displayName)
             .description(description)

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5fb47710/core/src/main/java/org/apache/brooklyn/core/catalog/internal/CatalogClasspathDo.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/core/catalog/internal/CatalogClasspathDo.java b/core/src/main/java/org/apache/brooklyn/core/catalog/internal/CatalogClasspathDo.java
index a8b7bbe..7b2fb53 100644
--- a/core/src/main/java/org/apache/brooklyn/core/catalog/internal/CatalogClasspathDo.java
+++ b/core/src/main/java/org/apache/brooklyn/core/catalog/internal/CatalogClasspathDo.java
@@ -24,15 +24,14 @@ import java.io.InputStream;
 import java.lang.reflect.Modifier;
 import java.net.URL;
 import java.util.Arrays;
+import java.util.List;
 import java.util.Set;
 
 import javax.annotation.Nullable;
 
-import org.reflections.util.ClasspathHelper;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 import org.apache.brooklyn.api.catalog.Catalog;
 import org.apache.brooklyn.api.catalog.CatalogItem;
+import org.apache.brooklyn.api.catalog.CatalogItem.CatalogInput;
 import org.apache.brooklyn.api.entity.Application;
 import org.apache.brooklyn.api.entity.Entity;
 import org.apache.brooklyn.api.entity.ImplementedBy;
@@ -49,6 +48,9 @@ import org.apache.brooklyn.util.os.Os;
 import org.apache.brooklyn.util.stream.Streams;
 import org.apache.brooklyn.util.text.Strings;
 import org.apache.brooklyn.util.time.Time;
+import org.reflections.util.ClasspathHelper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import com.google.common.annotations.Beta;
 import com.google.common.base.Preconditions;
@@ -310,6 +312,7 @@ public class CatalogClasspathDo {
         }
         if (log.isTraceEnabled())
             log.trace("adding to catalog: "+c+" (from catalog "+catalog+")");
+        item.setInputs(getJavaTypeInputs(c));
         catalog.addEntry(item);
         return item;
     }
@@ -349,4 +352,8 @@ public class CatalogClasspathDo {
         classloader.addFirst(loader);
     }
 
+    private List<CatalogInput<?>> getJavaTypeInputs(Class<?> c) {
+        return CatalogInputDto.ParseClassInputs.parseInputs(c);
+    }
+
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5fb47710/core/src/main/java/org/apache/brooklyn/core/catalog/internal/CatalogInputDto.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/core/catalog/internal/CatalogInputDto.java b/core/src/main/java/org/apache/brooklyn/core/catalog/internal/CatalogInputDto.java
new file mode 100644
index 0000000..c549137
--- /dev/null
+++ b/core/src/main/java/org/apache/brooklyn/core/catalog/internal/CatalogInputDto.java
@@ -0,0 +1,240 @@
+/*
+ * 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.core.catalog.internal;
+
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.brooklyn.api.catalog.CatalogConfig;
+import org.apache.brooklyn.api.catalog.CatalogItem.CatalogInput;
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.entity.EntityType;
+import org.apache.brooklyn.config.ConfigKey;
+import org.apache.brooklyn.core.config.BasicConfigKey;
+import org.apache.brooklyn.core.entity.EntityDynamicType;
+import org.apache.brooklyn.core.mgmt.classloading.BrooklynClassLoadingContext;
+import org.apache.brooklyn.core.objs.BrooklynTypes;
+import org.apache.brooklyn.util.collections.MutableList;
+import org.apache.brooklyn.util.guava.Maybe;
+import org.apache.brooklyn.util.text.StringPredicates;
+
+import com.google.common.base.Function;
+import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.reflect.TypeToken;
+
+public class CatalogInputDto<T> implements CatalogInput<T> {
+    private static final String DEFAULT_TYPE = "string";
+    private static final Map<String, Class<?>> BUILT_IN_TYPES = ImmutableMap.<String, Class<?>>builder()
+            .put(DEFAULT_TYPE, String.class)
+            .put("integer", Integer.class)
+            .put("long", Long.class)
+            .put("float", Float.class)
+            .put("double", Double.class)
+            .put("timestamp", Date.class)
+            .build();
+
+    private static final Map<String, Predicate<?>> BUILT_IN_CONSTRAINTS = ImmutableMap.<String, Predicate<?>>of(
+            "required", StringPredicates.isNonBlank());
+
+    private String label;
+    private boolean pinned;
+    private ConfigKey<T> type;
+
+    public CatalogInputDto(String label, boolean pinned, ConfigKey<T> type) {
+        this.label = label;
+        this.pinned = pinned;
+        this.type = type;
+    }
+
+    @Override
+    public String getLabel() {
+        return label;
+    }
+
+    @Override
+    public boolean isPinned() {
+        return pinned;
+    }
+
+    @Override
+    public ConfigKey<T> getType() {
+        return type;
+    }
+
+    public static final class ParseYamlInputs {
+        @SuppressWarnings({ "unchecked", "rawtypes" })
+        public static List<CatalogInput<?>> parseInputs(List<?> inputsRaw, BrooklynClassLoadingContext loader) {
+            List<CatalogInput<?>> inputs = new ArrayList<>(inputsRaw.size());
+            for (Object obj : inputsRaw) {
+                Map inputDef;
+                if (obj instanceof String) {
+                    inputDef = ImmutableMap.of("name", obj);
+                } else if (obj instanceof Map) {
+                    inputDef = (Map) obj;
+                } else {
+                    throw new IllegalArgumentException("Catalog input definition expected to be a map, but is " + obj.getClass() + " instead: " + obj);
+                }
+                String name = (String)inputDef.get("name");
+                String label = (String)inputDef.get("label");
+                String description = (String)inputDef.get("description");
+                String type = (String)inputDef.get("type");
+                Object defaultValue = inputDef.get("default");
+                Predicate<?> constraints = parseConstraints(inputDef.get("constraints"), loader);
+
+                if (name == null) {
+                    throw new IllegalArgumentException("'name' value missing from input definition " + obj + " but is required. Check for typos.");
+                }
+
+                ConfigKey inputType = BasicConfigKey.builder(inferType(type, loader))
+                        .name(name)
+                        .description(description)
+                        .defaultValue(defaultValue)
+                        .constraint(constraints)
+                        .build();
+                inputs.add(new CatalogInputDto(Maybe.fromNullable(label).or(name), true, inputType));
+            }
+            return inputs;
+        }
+
+        @SuppressWarnings({ "rawtypes" })
+        private static TypeToken inferType(String typeRaw, BrooklynClassLoadingContext loader) {
+            if (typeRaw == null) return TypeToken.of(String.class);
+            String type = typeRaw.trim();
+            if (BUILT_IN_TYPES.containsKey(type)) {
+                return TypeToken.of(BUILT_IN_TYPES.get(type));
+            } else {
+                // Assume it's a Java type
+                Maybe<Class<?>> inputType = loader.tryLoadClass(type);
+                if (inputType.isPresent()) {
+                    return TypeToken.of(inputType.get());
+                } else {
+                    throw new IllegalArgumentException("The type '" + type + "' for a catalog input not recognised as a built-in (" + BUILT_IN_TYPES.keySet() + ") or a java type");
+                }
+            }
+        }
+    
+        @SuppressWarnings({ "unchecked", "rawtypes" })
+        private static Predicate parseConstraints(Object obj, BrooklynClassLoadingContext loader) {
+            List constraintsRaw;
+            if (obj == null) {
+                constraintsRaw = ImmutableList.of();
+            } else if (obj instanceof String) {
+                constraintsRaw = ImmutableList.of(obj);
+            } else if (obj instanceof List) {
+                constraintsRaw = (List) obj;
+            } else {
+                throw new IllegalArgumentException ("The constraint '" + obj + "' for a catalog input is invalid format - string or list supported");
+            }
+            List<Predicate> constraints = new ArrayList(constraintsRaw.size());
+            for (Object untypedConstraint : constraintsRaw) {
+                String constraint = (String)untypedConstraint;
+                if (BUILT_IN_CONSTRAINTS.containsKey(constraint)) {
+                    constraints.add(BUILT_IN_CONSTRAINTS.get(constraint));
+                } else {
+                    throw new IllegalArgumentException("The constraint '" + constraint + "' for a catalog input is not recognized as a built-in (" + BUILT_IN_CONSTRAINTS.keySet() + ")");
+                }
+            }
+            if (!constraints.isEmpty()) {
+                if (constraints.size() == 1) {
+                    return constraints.get(0);
+                } else {
+                    return Predicates.and((List<Predicate<Object>>)(List) constraints);
+                }
+            } else {
+                return Predicates.alwaysTrue();
+            }
+        }
+    }
+
+    public static final class ParseClassInputs {
+        private static final class WeightedCatalogInput {
+            private Double weight;
+            private CatalogInput<?> input;
+            public WeightedCatalogInput(Double weight, CatalogInput<?> input) {
+                this.weight = weight;
+                this.input = input;
+            }
+            public Double getWeight() {return weight; }
+            public CatalogInput<?> getInput() { return input; }
+        }
+        private static final class InputsComparator implements Comparator<WeightedCatalogInput> {
+            @Override
+            public int compare(WeightedCatalogInput o1, WeightedCatalogInput o2) {
+                if (o1.getWeight() == o2.getWeight()) {
+                    return 0;
+                } else if (o1.getWeight() == null) {
+                    return 1;
+                } else if (o2.getWeight() == null) {
+                    return -1;
+                } else {
+                    return Double.compare(o1.getWeight(),  o2.getWeight());
+                }
+            }
+        }
+        private static final class InputsTransformer implements Function<WeightedCatalogInput, CatalogInput<?>> {
+            @Override
+            public CatalogInput<?> apply(WeightedCatalogInput input) {
+                return input.getInput();
+            }
+        }
+
+        public static List<CatalogInput<?>> parseInputs(Class<?> c) {
+            MutableList<WeightedCatalogInput> inputs = MutableList.<WeightedCatalogInput>of();
+            if (Entity.class.isAssignableFrom(c)) {
+                @SuppressWarnings("unchecked")
+                Class<? extends Entity> entityClass = (Class<? extends Entity>) c;
+                EntityDynamicType dynamicType = BrooklynTypes.getDefinedEntityType(entityClass);
+                EntityType type = dynamicType.getSnapshot();
+                for (ConfigKey<?> x: type.getConfigKeys()) {
+                    WeightedCatalogInput fieldConfig = getFieldConfig(x, dynamicType.getConfigKeyField(x.getName()));
+                    inputs.appendIfNotNull(fieldConfig);
+                }
+                Collections.sort(inputs, new InputsComparator());
+                return FluentIterable.from(inputs)
+                        .transform(new InputsTransformer()).toList();
+            } else {
+                return ImmutableList.<CatalogInput<?>>of();
+            }
+        }
+
+        public static WeightedCatalogInput getFieldConfig(ConfigKey<?> config, Field configKeyField) {
+            if (configKeyField == null) return null;
+            CatalogConfig catalogConfig = configKeyField.getAnnotation(CatalogConfig.class);
+            String label = config.getName();
+            Double priority = null;
+            if (catalogConfig != null) {
+                label = Maybe.fromNullable(catalogConfig.label()).or(config.getName());
+                priority = catalogConfig.priority();
+            }
+            @SuppressWarnings({ "unchecked", "rawtypes" })
+            CatalogInput<?> input = new CatalogInputDto(label, priority != null, config);
+            return new WeightedCatalogInput(priority, input);
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5fb47710/core/src/main/java/org/apache/brooklyn/core/catalog/internal/CatalogItemBuilder.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/core/catalog/internal/CatalogItemBuilder.java b/core/src/main/java/org/apache/brooklyn/core/catalog/internal/CatalogItemBuilder.java
index 8918a74..f1e4711 100644
--- a/core/src/main/java/org/apache/brooklyn/core/catalog/internal/CatalogItemBuilder.java
+++ b/core/src/main/java/org/apache/brooklyn/core/catalog/internal/CatalogItemBuilder.java
@@ -20,8 +20,10 @@ package org.apache.brooklyn.core.catalog.internal;
 
 import java.util.Collection;
 import java.util.Collections;
+import java.util.List;
 
 import org.apache.brooklyn.api.catalog.CatalogItem.CatalogBundle;
+import org.apache.brooklyn.api.catalog.CatalogItem.CatalogInput;
 
 import com.google.common.base.Preconditions;
 
@@ -104,6 +106,11 @@ public class CatalogItemBuilder<CatalogItemType extends CatalogItemDtoAbstract<?
         return this;
     }
 
+    public CatalogItemBuilder<CatalogItemType> inputs(List<CatalogInput<?>> inputs) {
+        dto.setInputs(inputs);
+        return this;
+    }
+
     public CatalogItemBuilder<CatalogItemType> libraries(Collection<CatalogBundle> libraries) {
         dto.setLibraries(libraries);
         return this;
@@ -118,6 +125,9 @@ public class CatalogItemBuilder<CatalogItemType extends CatalogItemDtoAbstract<?
         Preconditions.checkNotNull(dto.getSymbolicName());
         Preconditions.checkNotNull(dto.getVersion());
 
+        if (dto.getInputs() == null) {
+            dto.setInputs(Collections.<CatalogInput<?>>emptyList());
+        }
         if (dto.getLibraries() == null) {
             dto.setLibraries(Collections.<CatalogBundle>emptyList());
         }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5fb47710/core/src/main/java/org/apache/brooklyn/core/catalog/internal/CatalogItemDo.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/core/catalog/internal/CatalogItemDo.java b/core/src/main/java/org/apache/brooklyn/core/catalog/internal/CatalogItemDo.java
index 2aa8479..e053374 100644
--- a/core/src/main/java/org/apache/brooklyn/core/catalog/internal/CatalogItemDo.java
+++ b/core/src/main/java/org/apache/brooklyn/core/catalog/internal/CatalogItemDo.java
@@ -19,6 +19,7 @@
 package org.apache.brooklyn.core.catalog.internal;
 
 import java.util.Collection;
+import java.util.List;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -177,6 +178,11 @@ public class CatalogItemDo<T,SpecT> implements CatalogItem<T,SpecT>, BrooklynObj
         return itemDto.getVersion();
     }
 
+    @Override
+    public List<CatalogInput<?>> getInputs() {
+        return itemDto.getInputs();
+    }
+
     @Nonnull  // but it is still null sometimes, see in CatalogDo.loadJavaClass
     @Override
     public Collection<CatalogBundle> getLibraries() {
@@ -192,6 +198,7 @@ public class CatalogItemDo<T,SpecT> implements CatalogItem<T,SpecT>, BrooklynObj
     }
     
     @SuppressWarnings("unchecked")
+    @Deprecated
     Class<? extends T> loadJavaClass(final ManagementContext mgmt) {
         if (javaClass!=null) return javaClass;
         javaClass = (Class<T>)CatalogUtils.newClassLoadingContext(mgmt, getId(), getLibraries(), catalog.getRootClassLoader()).loadClass(getJavaType());

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5fb47710/core/src/main/java/org/apache/brooklyn/core/catalog/internal/CatalogItemDtoAbstract.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/core/catalog/internal/CatalogItemDtoAbstract.java b/core/src/main/java/org/apache/brooklyn/core/catalog/internal/CatalogItemDtoAbstract.java
index 414b3e6..e95b0dd 100644
--- a/core/src/main/java/org/apache/brooklyn/core/catalog/internal/CatalogItemDtoAbstract.java
+++ b/core/src/main/java/org/apache/brooklyn/core/catalog/internal/CatalogItemDtoAbstract.java
@@ -20,6 +20,7 @@ package org.apache.brooklyn.core.catalog.internal;
 
 import java.util.Collection;
 import java.util.Collections;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
@@ -61,6 +62,7 @@ public abstract class CatalogItemDtoAbstract<T, SpecT> extends AbstractBrooklynO
     private @Deprecated @SetFromFlag String type;
     private @SetFromFlag String planYaml;
 
+    private @SetFromFlag List<CatalogInput<?>> inputs;
     private @SetFromFlag Collection<CatalogBundle> libraries;
     private @SetFromFlag Set<Object> tags = Sets.newLinkedHashSet();
     private @SetFromFlag boolean deprecated;
@@ -108,11 +110,13 @@ public abstract class CatalogItemDtoAbstract<T, SpecT> extends AbstractBrooklynO
         return type;
     }
 
+    @Override
     @Deprecated
     public String getName() {
         return getDisplayName();
     }
 
+    @Override
     @Deprecated
     public String getRegisteredTypeName() {
         return getSymbolicName();
@@ -170,6 +174,11 @@ public abstract class CatalogItemDtoAbstract<T, SpecT> extends AbstractBrooklynO
     public void setDisabled(boolean disabled) {
         this.disabled = disabled;
     }
+    
+    @Override
+    public List<CatalogInput<?>> getInputs() {
+        return inputs;
+    }
 
     @Nonnull
     @Override
@@ -188,7 +197,7 @@ public abstract class CatalogItemDtoAbstract<T, SpecT> extends AbstractBrooklynO
 
     @Override
     public int hashCode() {
-        return Objects.hashCode(symbolicName, planYaml, javaType, nullIfEmpty(libraries), version, getCatalogItemId());
+        return Objects.hashCode(symbolicName, planYaml, javaType, nullIfEmpty(inputs), nullIfEmpty(libraries), version, getCatalogItemId());
     }
 
     @Override
@@ -200,6 +209,7 @@ public abstract class CatalogItemDtoAbstract<T, SpecT> extends AbstractBrooklynO
         if (!Objects.equal(symbolicName, other.symbolicName)) return false;
         if (!Objects.equal(planYaml, other.planYaml)) return false;
         if (!Objects.equal(javaType, other.javaType)) return false;
+        if (!Objects.equal(nullIfEmpty(inputs), nullIfEmpty(other.inputs))) return false;
         if (!Objects.equal(nullIfEmpty(libraries), nullIfEmpty(other.libraries))) return false;
         if (!Objects.equal(getCatalogItemId(), other.getCatalogItemId())) return false;
         if (!Objects.equal(version, other.version)) return false;
@@ -224,6 +234,7 @@ public abstract class CatalogItemDtoAbstract<T, SpecT> extends AbstractBrooklynO
         return getClass().getSimpleName()+"["+getId()+"/"+getDisplayName()+"]";
     }
 
+    @Override
     public abstract Class<SpecT> getSpecType();
 
     transient CatalogXmlSerializer serializer;
@@ -371,6 +382,10 @@ public abstract class CatalogItemDtoAbstract<T, SpecT> extends AbstractBrooklynO
         this.planYaml = planYaml;
     }
 
+    protected void setInputs(List<CatalogInput<?>> inputs) {
+        this.inputs = inputs;
+    }
+
     protected void setLibraries(Collection<CatalogBundle> libraries) {
         this.libraries = libraries;
     }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5fb47710/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/OsgiManager.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/OsgiManager.java b/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/OsgiManager.java
index 0e941bd..306a05d 100644
--- a/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/OsgiManager.java
+++ b/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/OsgiManager.java
@@ -91,6 +91,7 @@ public class OsgiManager {
             final AtomicReference<DeletionResult> deletionResult = new AtomicReference<DeletionResult>();
             Repeater.create("Delete OSGi cache dir")
                     .until(new Callable<Boolean>() {
+                        @Override
                         public Boolean call() {
                             deletionResult.set(Os.deleteRecursively(osgiCacheDir));
                             return deletionResult.get().wasSuccessful();

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5fb47710/core/src/main/java/org/apache/brooklyn/util/core/osgi/Osgis.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/util/core/osgi/Osgis.java b/core/src/main/java/org/apache/brooklyn/util/core/osgi/Osgis.java
index 6d5dc91..42cebc5 100644
--- a/core/src/main/java/org/apache/brooklyn/util/core/osgi/Osgis.java
+++ b/core/src/main/java/org/apache/brooklyn/util/core/osgi/Osgis.java
@@ -246,6 +246,7 @@ public class Osgis {
             return Joiner.on(";").join(parts);
         }
         
+        @Override
         public String toString() {
             return getClass().getCanonicalName()+"["+getConstraintsDescription()+"]";
         }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5fb47710/core/src/test/java/org/apache/brooklyn/core/catalog/internal/CatalogInputDtoClassTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/brooklyn/core/catalog/internal/CatalogInputDtoClassTest.java b/core/src/test/java/org/apache/brooklyn/core/catalog/internal/CatalogInputDtoClassTest.java
new file mode 100644
index 0000000..7602c2a
--- /dev/null
+++ b/core/src/test/java/org/apache/brooklyn/core/catalog/internal/CatalogInputDtoClassTest.java
@@ -0,0 +1,65 @@
+/*
+ * 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.core.catalog.internal;
+
+import static org.testng.Assert.assertEquals;
+
+import java.util.List;
+
+import org.apache.brooklyn.api.catalog.CatalogConfig;
+import org.apache.brooklyn.api.catalog.CatalogItem.CatalogInput;
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.config.ConfigKey;
+import org.apache.brooklyn.core.config.ConfigKeys;
+import org.testng.annotations.Test;
+
+import com.google.common.base.Predicate;
+import com.google.common.reflect.TypeToken;
+
+public class CatalogInputDtoClassTest {
+    public interface CatalogInputTestEntity extends Entity {
+        @CatalogConfig(label="String Key", priority=3)
+        ConfigKey<String> STRING_KEY = ConfigKeys.newStringConfigKey("string_key");
+
+        @CatalogConfig(label="Integer Key", priority=2)
+        ConfigKey<Integer> INTEGER_KEY = ConfigKeys.newIntegerConfigKey("integer_key");
+
+        @SuppressWarnings("serial")
+        @CatalogConfig(label="Predicate Key", priority=1)
+        ConfigKey<Predicate<String>> PREDICATE_KEY = ConfigKeys.newConfigKey(new TypeToken<Predicate<String>>() {}, "predicate_key");
+
+        ConfigKey<String> UNPINNNED_KEY = ConfigKeys.newStringConfigKey("unpinned_key");
+    }
+
+    @Test
+    public void testFullDefinition() {
+        List<CatalogInput<?>> inputs = CatalogInputDto.ParseClassInputs.parseInputs(CatalogInputTestEntity.class);
+        assertInput(inputs.get(0), "Predicate Key", true, CatalogInputTestEntity.PREDICATE_KEY);
+        assertInput(inputs.get(1), "Integer Key", true, CatalogInputTestEntity.INTEGER_KEY);
+        assertInput(inputs.get(2), "String Key", true, CatalogInputTestEntity.STRING_KEY);
+        assertInput(inputs.get(3), "unpinned_key", false, CatalogInputTestEntity.UNPINNNED_KEY);
+    }
+
+    private void assertInput(CatalogInput<?> input, String label, boolean pinned, ConfigKey<?> type) {
+        assertEquals(input.getLabel(), label);
+        assertEquals(input.isPinned(), pinned);
+        assertEquals(input.getType(), type);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5fb47710/core/src/test/java/org/apache/brooklyn/core/catalog/internal/CatalogInputDtoYamlTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/brooklyn/core/catalog/internal/CatalogInputDtoYamlTest.java b/core/src/test/java/org/apache/brooklyn/core/catalog/internal/CatalogInputDtoYamlTest.java
new file mode 100644
index 0000000..6e65904
--- /dev/null
+++ b/core/src/test/java/org/apache/brooklyn/core/catalog/internal/CatalogInputDtoYamlTest.java
@@ -0,0 +1,185 @@
+/*
+ * 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.core.catalog.internal;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+
+import java.util.List;
+
+import org.apache.brooklyn.api.catalog.CatalogItem.CatalogInput;
+import org.apache.brooklyn.api.mgmt.ManagementContext;
+import org.apache.brooklyn.config.ConfigKey;
+import org.apache.brooklyn.core.mgmt.classloading.BrooklynClassLoadingContext;
+import org.apache.brooklyn.core.mgmt.classloading.JavaBrooklynClassLoadingContext;
+import org.apache.brooklyn.core.test.entity.LocalManagementContextForTests;
+import org.apache.brooklyn.util.text.StringPredicates;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.common.reflect.TypeToken;
+
+public class CatalogInputDtoYamlTest {
+    private ManagementContext mgmt;
+
+    @BeforeMethod(alwaysRun=true)
+    public void setUp() {
+        mgmt = LocalManagementContextForTests.newInstance();
+    }
+
+    @Test
+    public void testInlineName() {
+        String name = "minRam";
+        CatalogInput<?> input = parse(name);
+        assertEquals(input.getLabel(), name);
+        assertTrue(input.isPinned());
+        ConfigKey<?> type = input.getType();
+        assertEquals(type.getName(), name);
+        assertEquals(type.getTypeToken(), TypeToken.of(String.class));
+        assertNull(type.getDefaultValue());
+        assertNull(type.getDescription());
+        assertNull(type.getInheritance());
+        assertConstraint(type.getConstraint(), Predicates.alwaysTrue());
+    }
+
+    @Test
+    public void testOnlyName() {
+        String name = "minRam";
+        CatalogInput<?> input = parse(ImmutableMap.of("name", name));
+        assertEquals(input.getLabel(), name);
+        assertEquals(input.getType().getName(), name);
+        assertEquals(input.getType().getTypeToken(), TypeToken.of(String.class));
+    }
+
+    @Test
+    public void testUnusualName() {
+        parse(ImmutableMap.of("name", "name with spaces"));
+    }
+
+    @Test
+    public void testFullDefinition() {
+        String name = "minRam";
+        String label = "Minimum Ram";
+        String description = "Some description";
+        String inputType = "string";
+        String defaultValue = "VALUE";
+        String constraint = "required";
+        CatalogInput<?> input = parse(ImmutableMap.builder()
+                .put("name", name)
+                .put("label", label)
+                .put("description", description)
+                .put("type", inputType)
+                .put("default", defaultValue)
+                .put("constraints", constraint)
+                .build());
+
+        assertEquals(input.getLabel(), label);
+        assertTrue(input.isPinned());
+
+        ConfigKey<?> type = input.getType();
+        assertEquals(type.getName(), name);
+        assertEquals(type.getTypeToken(), TypeToken.of(String.class));
+        assertEquals(type.getDefaultValue(), defaultValue);
+        assertEquals(type.getDescription(), description);
+        assertNull(type.getInheritance());
+        assertConstraint(type.getConstraint(), StringPredicates.isNonBlank());
+    }
+
+    @Test
+    public void testUnexpectedType() {
+        String name = "1234";
+        String label = "1234";
+        String description = "5678.56";
+        String defaultValue = "444.12";
+        CatalogInput<?> input = parse(ImmutableMap.of(
+                "name", name,
+                "label", label,
+                "description", description,
+                "default", defaultValue));
+
+        assertEquals(input.getLabel(), name);
+        assertTrue(input.isPinned());
+
+        ConfigKey<?> type = input.getType();
+        assertEquals(type.getName(), name);
+        assertEquals(type.getDefaultValue(), defaultValue);
+        assertEquals(type.getDescription(), description);
+        assertNull(type.getInheritance());
+    }
+
+    @Test
+    public void testConstraintAsArray() {
+        String name = "minRam";
+        String constraint = "required";
+        CatalogInput<?> input = parse(ImmutableMap.of(
+                "name", name,
+                "constraints", ImmutableList.of(constraint)));
+        ConfigKey<?> type = input.getType();
+        assertConstraint(type.getConstraint(), StringPredicates.isNonBlank());
+    }
+
+    @Test(expectedExceptions = IllegalArgumentException.class)
+    public void testMissingName() {
+        parse(ImmutableMap.of(
+                "type", "string"));
+    }
+
+    @Test
+    public void testJavaType() {
+        String name = "minRam";
+        CatalogInput<?> input = parse(ImmutableMap.of(
+                "name", name,
+                "type", CatalogInputDtoYamlTest.class.getName()));
+        assertEquals(input.getType().getTypeToken(), TypeToken.of(CatalogInputDtoYamlTest.class));
+    }
+
+    @Test(expectedExceptions = IllegalArgumentException.class)
+    public void testInvalidType() {
+        String name = "minRam";
+        parse(ImmutableMap.of(
+                "name", name,
+                "type", "missing_type"));
+    }
+
+    @Test(expectedExceptions = IllegalArgumentException.class)
+    public void testInvalidConstraint() {
+        String name = "minRam";
+        parse(ImmutableMap.of(
+                "name", name,
+                "type", "missing_type"));
+    }
+
+    private CatalogInput<?> parse(Object def) {
+        BrooklynClassLoadingContext loader = JavaBrooklynClassLoadingContext.create(mgmt);
+        List<CatalogInput<?>> inputs = CatalogInputDto.ParseYamlInputs.parseInputs(ImmutableList.of(def), loader);
+        return Iterables.getOnlyElement(inputs);
+    }
+
+    private void assertConstraint(Predicate<?> actual, Predicate<?> expected) {
+        //How to compare predicates correctly, re-creating the same predicate doesn't work
+        assertEquals(actual.toString(), expected.toString());
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5fb47710/core/src/test/java/org/apache/brooklyn/core/catalog/internal/CatalogInputTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/brooklyn/core/catalog/internal/CatalogInputTest.java b/core/src/test/java/org/apache/brooklyn/core/catalog/internal/CatalogInputTest.java
new file mode 100644
index 0000000..8b477b9
--- /dev/null
+++ b/core/src/test/java/org/apache/brooklyn/core/catalog/internal/CatalogInputTest.java
@@ -0,0 +1,139 @@
+/*
+ * 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.core.catalog.internal;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+import java.util.List;
+
+import org.apache.brooklyn.api.catalog.BrooklynCatalog;
+import org.apache.brooklyn.api.catalog.CatalogItem;
+import org.apache.brooklyn.api.catalog.CatalogItem.CatalogInput;
+import org.apache.brooklyn.api.entity.EntitySpec;
+import org.apache.brooklyn.api.mgmt.ManagementContext;
+import org.apache.brooklyn.core.mgmt.osgi.OsgiTestResources;
+import org.apache.brooklyn.core.test.entity.LocalManagementContextForTests;
+import org.apache.brooklyn.entity.stock.BasicEntity;
+import org.apache.brooklyn.test.support.TestResourceUnavailableException;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.Iterables;
+import com.google.common.reflect.TypeToken;
+
+public class CatalogInputTest {
+    private ManagementContext mgmt;
+    private BrooklynCatalog catalog;
+    private String spec;
+
+    @BeforeMethod(alwaysRun=true)
+    public void setUp() {
+        mgmt = LocalManagementContextForTests.newInstanceWithOsgi();
+        catalog = mgmt.getCatalog();
+        spec = TestToSpecTransformer.registerSpec(EntitySpec.create(BasicEntity.class));
+    }
+
+    @Test
+    public void testYamlInputsParsed() {
+        CatalogItem<?, ?> item = add(
+                "brooklyn.catalog:",
+                "  id: test.inputs",
+                "  version: 0.0.1",
+                "  inputs:",
+                "  - simple",
+                "  - name: explicit_name",
+                "  - name: third_input",
+                "    type: integer",
+                "  item: " + spec);
+        List<CatalogInput<?>> inputs = item.getInputs();
+        assertEquals(inputs.size(), 3);
+        CatalogInput<?> firstInput = inputs.get(0);
+        assertEquals(firstInput.getLabel(), "simple");
+        assertEquals(firstInput.isPinned(), true);
+        assertEquals(firstInput.getType().getName(), "simple");
+        assertEquals(firstInput.getType().getTypeToken(), TypeToken.of(String.class));
+        
+        CatalogInput<?> secondInput = inputs.get(1);
+        assertEquals(secondInput.getLabel(), "explicit_name");
+        assertEquals(secondInput.isPinned(), true);
+        assertEquals(secondInput.getType().getName(), "explicit_name");
+        assertEquals(secondInput.getType().getTypeToken(), TypeToken.of(String.class));
+        
+        CatalogInput<?> thirdInput = inputs.get(2);
+        assertEquals(thirdInput.getLabel(), "third_input");
+        assertEquals(thirdInput.isPinned(), true);
+        assertEquals(thirdInput.getType().getName(), "third_input");
+        assertEquals(thirdInput.getType().getTypeToken(), TypeToken.of(Integer.class));
+    }
+
+    @Test
+    public void testOsgiType() {
+        TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), OsgiTestResources.BROOKLYN_TEST_OSGI_ENTITIES_PATH);
+
+        CatalogItem<?, ?> item = add(
+                "brooklyn.catalog:",
+                "  id: test.inputs",
+                "  version: 0.0.1",
+                "  libraries:",
+                "  - classpath://" + OsgiTestResources.BROOKLYN_TEST_OSGI_ENTITIES_PATH,
+                "  inputs:",
+                "  - name: simple",
+                "    type: " + OsgiTestResources.BROOKLYN_TEST_OSGI_ENTITIES_SIMPLE_ENTITY,
+                "  item: " + spec);
+        List<CatalogInput<?>> inputs = item.getInputs();
+        assertEquals(inputs.size(), 1);
+        CatalogInput<?> firstInput = inputs.get(0);
+        assertEquals(firstInput.getLabel(), "simple");
+        assertTrue(firstInput.isPinned());
+        assertEquals(firstInput.getType().getName(), "simple");
+        assertEquals(firstInput.getType().getTypeToken().getRawType().getName(), OsgiTestResources.BROOKLYN_TEST_OSGI_ENTITIES_SIMPLE_ENTITY);
+    }
+
+    @Test
+    public void testOsgiClassScanned() {
+        TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), OsgiTestResources.BROOKLYN_TEST_MORE_ENTITIES_V2_PATH);
+
+        addMulti("brooklyn.catalog:",
+            "    items:",
+            "    - scanJavaAnnotations: true",
+            "      version: 2.0.test_java",
+            "      libraries:",
+            "      - classpath://" + OsgiTestResources.BROOKLYN_TEST_MORE_ENTITIES_V2_PATH);
+
+        CatalogItem<?, ?> item = CatalogUtils.getCatalogItemOptionalVersion(mgmt, OsgiTestResources.BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY);
+        assertEquals(item.getVersion(), "2.0.test_java");
+        assertEquals(item.getLibraries().size(), 1);
+        CatalogInput<?> input = item.getInputs().get(0);
+        assertEquals(input.getLabel(), "more_config");
+        assertFalse(input.isPinned());
+        assertEquals(input.getType().getName(), "more_config");
+    }
+
+    private CatalogItem<?,?> add(String... def) {
+        return Iterables.getOnlyElement(addMulti(def));
+    }
+
+    private Iterable<? extends CatalogItem<?, ?>> addMulti(String... def) {
+        return catalog.addItems(Joiner.on('\n').join(def));
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5fb47710/core/src/test/java/org/apache/brooklyn/core/catalog/internal/CatalogLoadTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/brooklyn/core/catalog/internal/CatalogLoadTest.java b/core/src/test/java/org/apache/brooklyn/core/catalog/internal/CatalogLoadTest.java
index ba38ac1..d1862a6 100644
--- a/core/src/test/java/org/apache/brooklyn/core/catalog/internal/CatalogLoadTest.java
+++ b/core/src/test/java/org/apache/brooklyn/core/catalog/internal/CatalogLoadTest.java
@@ -46,7 +46,7 @@ public class CatalogLoadTest {
         return ResourceUtils.create(this).getResourceAsString(file);
     }
 
-    // CAMP YAML parsing not available in core, so YAML catalog tests are in camp, e.g. CatalogYamlEntitiesTest 
+    // CAMP YAML parsing not available in core, so YAML catalog tests are in camp, e.g. CatalogYamlEntityTest 
     
     @Test
     public void testLoadXmlCatalog() {

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5fb47710/core/src/test/java/org/apache/brooklyn/core/catalog/internal/TestToSpecTransformer.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/brooklyn/core/catalog/internal/TestToSpecTransformer.java b/core/src/test/java/org/apache/brooklyn/core/catalog/internal/TestToSpecTransformer.java
new file mode 100644
index 0000000..0dfe291
--- /dev/null
+++ b/core/src/test/java/org/apache/brooklyn/core/catalog/internal/TestToSpecTransformer.java
@@ -0,0 +1,118 @@
+/*
+ * 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.core.catalog.internal;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.apache.brooklyn.api.catalog.CatalogItem;
+import org.apache.brooklyn.api.entity.Application;
+import org.apache.brooklyn.api.entity.EntitySpec;
+import org.apache.brooklyn.api.internal.AbstractBrooklynObjectSpec;
+import org.apache.brooklyn.api.mgmt.ManagementContext;
+import org.apache.brooklyn.core.plan.PlanNotRecognizedException;
+import org.apache.brooklyn.core.plan.PlanToSpecTransformer;
+import org.apache.brooklyn.util.text.Identifiers;
+import org.apache.brooklyn.util.yaml.Yamls;
+
+/**
+ * Resolves previously registered specs by id.
+ * First create a spec and register it, keeping the returned ID:
+ * <pre> {@code
+ * String specId = TestToSpecTransformer.registerSpec(EntitySpec.create(BasicEntity.class));
+ * }</pre>
+ *
+ * Then build a plan to be resolved such as:
+ * <pre> {@code
+ *  brooklyn.catalog:
+ *    id: test.inputs
+ *    version: 0.0.1
+ *    item: <specId>
+ * } </pre>
+ */
+public class TestToSpecTransformer implements PlanToSpecTransformer {
+    private static final Map<String, AbstractBrooklynObjectSpec<?, ?>> REGISTERED_SPECS = new ConcurrentHashMap<>();
+    public static String registerSpec(AbstractBrooklynObjectSpec<?, ?> spec) {
+        String id = Identifiers.makeRandomId(10);
+        REGISTERED_SPECS.put(id, spec);
+        return id;
+    }
+
+    @Override
+    public void injectManagementContext(ManagementContext managementContext) {
+    }
+
+    @Override
+    public String getShortDescription() {
+        return "test";
+    }
+
+    @Override
+    public boolean accepts(String planType) {
+        return "test".equals(planType);
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public EntitySpec<? extends Application> createApplicationSpec(String plan) throws PlanNotRecognizedException {
+        return (EntitySpec<? extends Application>) getSpec(plan);
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public <T, SpecT extends AbstractBrooklynObjectSpec<? extends T, SpecT>> SpecT createCatalogSpec(CatalogItem<T, SpecT> item, Set<String> encounteredTypes) 
+            throws PlanNotRecognizedException {
+        return (SpecT) getSpecFromPlan(item.getPlanYaml());
+    }
+
+    private AbstractBrooklynObjectSpec<?,?> getSpecFromPlan(String plan) {
+        if (plan != null) {
+            Object planRaw = Yamls.parseAll(plan).iterator().next();
+            if (planRaw instanceof String) {
+                return getSpec((String)planRaw);
+            } else if (planRaw instanceof Map) {
+                // The catalog parser assumes it's dealing with CAMP specs so will helpfully
+                // prepend "type: " if it's an inline item.
+                return getSpec((String)((Map<?, ?>)planRaw).get("type"));
+            } else {
+                throw notRecognized();
+            }
+        } else {
+            throw notRecognized();
+        }
+    }
+
+    private AbstractBrooklynObjectSpec<?,?> getSpec(String plan) {
+        if (plan == null) {
+            throw notRecognized();
+        }
+        AbstractBrooklynObjectSpec<?, ?> spec = REGISTERED_SPECS.get(plan);
+        if (spec != null) {
+            return spec;
+        } else {
+            throw notRecognized();
+        }
+    }
+
+    private PlanNotRecognizedException notRecognized() {
+        return new PlanNotRecognizedException("Not recognized as registered spec");
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5fb47710/core/src/test/resources/META-INF/services/org.apache.brooklyn.core.plan.PlanToSpecTransformer
----------------------------------------------------------------------
diff --git a/core/src/test/resources/META-INF/services/org.apache.brooklyn.core.plan.PlanToSpecTransformer b/core/src/test/resources/META-INF/services/org.apache.brooklyn.core.plan.PlanToSpecTransformer
new file mode 100644
index 0000000..34d91b4
--- /dev/null
+++ b/core/src/test/resources/META-INF/services/org.apache.brooklyn.core.plan.PlanToSpecTransformer
@@ -0,0 +1,19 @@
+#
+# 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.
+#
+org.apache.brooklyn.core.catalog.internal.TestToSpecTransformer

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5fb47710/usage/rest-server/src/main/java/org/apache/brooklyn/rest/transform/CatalogTransformer.java
----------------------------------------------------------------------
diff --git a/usage/rest-server/src/main/java/org/apache/brooklyn/rest/transform/CatalogTransformer.java b/usage/rest-server/src/main/java/org/apache/brooklyn/rest/transform/CatalogTransformer.java
index 4e9895f..fdc21fe 100644
--- a/usage/rest-server/src/main/java/org/apache/brooklyn/rest/transform/CatalogTransformer.java
+++ b/usage/rest-server/src/main/java/org/apache/brooklyn/rest/transform/CatalogTransformer.java
@@ -22,11 +22,8 @@ import java.net.URI;
 import java.util.Map;
 import java.util.Set;
 
-import org.slf4j.LoggerFactory;
-import org.apache.brooklyn.config.ConfigKey;
-import org.apache.brooklyn.core.entity.EntityDynamicType;
-import org.apache.brooklyn.core.objs.BrooklynTypes;
 import org.apache.brooklyn.api.catalog.CatalogItem;
+import org.apache.brooklyn.api.catalog.CatalogItem.CatalogInput;
 import org.apache.brooklyn.api.catalog.CatalogItem.CatalogItemType;
 import org.apache.brooklyn.api.effector.Effector;
 import org.apache.brooklyn.api.entity.Entity;
@@ -37,6 +34,8 @@ import org.apache.brooklyn.api.location.LocationSpec;
 import org.apache.brooklyn.api.policy.Policy;
 import org.apache.brooklyn.api.policy.PolicySpec;
 import org.apache.brooklyn.api.sensor.Sensor;
+import org.apache.brooklyn.core.entity.EntityDynamicType;
+import org.apache.brooklyn.core.objs.BrooklynTypes;
 import org.apache.brooklyn.rest.domain.CatalogEntitySummary;
 import org.apache.brooklyn.rest.domain.CatalogItemSummary;
 import org.apache.brooklyn.rest.domain.CatalogLocationSummary;
@@ -50,6 +49,7 @@ import org.apache.brooklyn.rest.domain.SummaryComparators;
 import org.apache.brooklyn.rest.util.BrooklynRestResourceUtils;
 import org.apache.brooklyn.util.collections.MutableMap;
 import org.apache.brooklyn.util.exceptions.Exceptions;
+import org.slf4j.LoggerFactory;
 
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
@@ -69,8 +69,8 @@ public class CatalogTransformer {
             EntityDynamicType typeMap = BrooklynTypes.getDefinedEntityType(spec.getType());
             EntityType type = typeMap.getSnapshot();
 
-            for (ConfigKey<?> x: type.getConfigKeys())
-                config.add(EntityTransformer.entityConfigSummary(x, typeMap.getConfigKeyField(x.getName())));
+            for (CatalogInput<?> input: item.getInputs())
+                config.add(EntityTransformer.entityConfigSummary(input));
             for (Sensor<?> x: type.getSensors())
                 sensors.add(SensorTransformer.sensorSummaryForCatalog(x));
             for (Effector<?> x: type.getEffectors())

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5fb47710/usage/rest-server/src/main/java/org/apache/brooklyn/rest/transform/EntityTransformer.java
----------------------------------------------------------------------
diff --git a/usage/rest-server/src/main/java/org/apache/brooklyn/rest/transform/EntityTransformer.java b/usage/rest-server/src/main/java/org/apache/brooklyn/rest/transform/EntityTransformer.java
index 4e6e15b..9637e75 100644
--- a/usage/rest-server/src/main/java/org/apache/brooklyn/rest/transform/EntityTransformer.java
+++ b/usage/rest-server/src/main/java/org/apache/brooklyn/rest/transform/EntityTransformer.java
@@ -26,6 +26,7 @@ import java.util.List;
 import java.util.Map;
 
 import org.apache.brooklyn.api.catalog.CatalogConfig;
+import org.apache.brooklyn.api.catalog.CatalogItem.CatalogInput;
 import org.apache.brooklyn.api.entity.Application;
 import org.apache.brooklyn.api.entity.Entity;
 import org.apache.brooklyn.config.ConfigKey;
@@ -151,4 +152,10 @@ public class EntityTransformer {
         Double priority = catalogConfig==null ? null : catalogConfig.priority();
         return entityConfigSummary(config, label, priority, null);
     }
+
+    public static EntityConfigSummary entityConfigSummary(CatalogInput<?> input) {
+        Double priority = input.isPinned() ? Double.valueOf(1d) : null;
+        return entityConfigSummary(input.getType(), input.getLabel(), priority, null);
+    }
+
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5fb47710/utils/rt-osgi/src/test/resources/brooklyn/osgi/brooklyn-test-osgi-more-entities_0.2.0.jar
----------------------------------------------------------------------
diff --git a/utils/rt-osgi/src/test/resources/brooklyn/osgi/brooklyn-test-osgi-more-entities_0.2.0.jar b/utils/rt-osgi/src/test/resources/brooklyn/osgi/brooklyn-test-osgi-more-entities_0.2.0.jar
index 5587e25..c03ad4a 100644
Binary files a/utils/rt-osgi/src/test/resources/brooklyn/osgi/brooklyn-test-osgi-more-entities_0.2.0.jar and b/utils/rt-osgi/src/test/resources/brooklyn/osgi/brooklyn-test-osgi-more-entities_0.2.0.jar differ

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5fb47710/utils/rt-osgi/src/test/resources/dependencies/osgi/more-entities-v2/src/main/java/org/apache/brooklyn/test/osgi/entities/more/MoreEntity.java
----------------------------------------------------------------------
diff --git a/utils/rt-osgi/src/test/resources/dependencies/osgi/more-entities-v2/src/main/java/org/apache/brooklyn/test/osgi/entities/more/MoreEntity.java b/utils/rt-osgi/src/test/resources/dependencies/osgi/more-entities-v2/src/main/java/org/apache/brooklyn/test/osgi/entities/more/MoreEntity.java
index 2124f86..9d4d840 100644
--- a/utils/rt-osgi/src/test/resources/dependencies/osgi/more-entities-v2/src/main/java/org/apache/brooklyn/test/osgi/entities/more/MoreEntity.java
+++ b/utils/rt-osgi/src/test/resources/dependencies/osgi/more-entities-v2/src/main/java/org/apache/brooklyn/test/osgi/entities/more/MoreEntity.java
@@ -22,12 +22,15 @@ import org.apache.brooklyn.api.catalog.Catalog;
 import org.apache.brooklyn.api.effector.Effector;
 import org.apache.brooklyn.api.entity.Entity;
 import org.apache.brooklyn.api.entity.ImplementedBy;
+import org.apache.brooklyn.config.ConfigKey;
+import org.apache.brooklyn.core.config.ConfigKeys;
 import org.apache.brooklyn.core.effector.Effectors;
 
 @Catalog(name="More Entity v2")
 @ImplementedBy(MoreEntityImpl.class)
 public interface MoreEntity extends Entity {
 
+    public static final ConfigKey<String> MORE_CONFIG = ConfigKeys.newStringConfigKey("more_config");
     public static final Effector<String> SAY_HI = Effectors.effector(String.class, "sayHI")
         .description("says HI to an uppercased name")
         .parameter(String.class, "name")