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/04/21 22:41:27 UTC

[08/16] incubator-brooklyn git commit: support for multi-item catalog yaml

support for multi-item catalog yaml

adds many tests, and the rest of the features - template and policy and location; and source yaml.

also a few significant REST API changes:
* /v1/catalog/create API change returns a map (breaking)
* catalog items include more information for entity and policies


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

Branch: refs/heads/master
Commit: eaaca7874bc092acc5abac848eba30648836fd53
Parents: 08cb3e7
Author: Alex Heneveld <al...@cloudsoftcorp.com>
Authored: Fri Apr 3 13:29:28 2015 -0400
Committer: Alex Heneveld <al...@cloudsoftcorp.com>
Committed: Thu Apr 16 01:25:39 2015 -0500

----------------------------------------------------------------------
 .../java/brooklyn/catalog/BrooklynCatalog.java  |   4 +
 .../java/brooklyn/location/LocationSpec.java    |   9 +-
 .../brooklyn/catalog/CatalogPredicates.java     |  12 +-
 .../catalog/internal/BasicBrooklynCatalog.java  | 214 +++++++++++++++----
 .../brooklyn/catalog/internal/CatalogUtils.java |   6 +
 .../location/basic/BasicLocationRegistry.java   |   2 +-
 .../location/basic/CatalogLocationResolver.java |   3 +-
 .../brooklyn/camp/lite/CampYamlLiteTest.java    |   6 +-
 .../entity/rebind/RebindCatalogItemTest.java    |  10 +-
 docs/guide/misc/release-notes.md                |   4 +
 .../camp/brooklyn/AbstractYamlTest.java         |   2 +-
 .../CatalogOsgiVersionMoreEntityTest.java       |  11 +-
 .../brooklyn/catalog/CatalogYamlEntityTest.java | 103 ++++++++-
 .../catalog/CatalogYamlLocationTest.java        | 124 +++++++++--
 .../brooklyn/catalog/CatalogYamlPolicyTest.java |  70 +++++-
 .../catalog/CatalogYamlTemplateTest.java        |  77 +++++++
 .../src/main/webapp/assets/js/view/catalog.js   |  57 +++--
 .../assets/tpl/catalog/add-catalog-entry.html   |   4 +-
 .../webapp/assets/tpl/catalog/add-entity.html   |  30 ---
 .../webapp/assets/tpl/catalog/add-yaml.html     |  30 +++
 .../main/java/brooklyn/rest/api/CatalogApi.java |  19 +-
 .../java/brooklyn/rest/api/LocationApi.java     |   5 +-
 .../rest/domain/CatalogEntitySummary.java       |   6 +
 .../rest/domain/CatalogItemSummary.java         |   3 +
 .../rest/domain/CatalogPolicySummary.java       |   3 +
 .../rest/resources/CatalogResource.java         |  66 +++---
 .../rest/resources/LocationResource.java        |   4 +-
 .../rest/transform/CatalogTransformer.java      |  44 +++-
 .../rest/resources/CatalogResourceTest.java     |  67 +++---
 .../src/main/java/brooklyn/util/yaml/Yamls.java |  12 ++
 30 files changed, 786 insertions(+), 221 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/eaaca787/api/src/main/java/brooklyn/catalog/BrooklynCatalog.java
----------------------------------------------------------------------
diff --git a/api/src/main/java/brooklyn/catalog/BrooklynCatalog.java b/api/src/main/java/brooklyn/catalog/BrooklynCatalog.java
index ce4386f..5b11cc6 100644
--- a/api/src/main/java/brooklyn/catalog/BrooklynCatalog.java
+++ b/api/src/main/java/brooklyn/catalog/BrooklynCatalog.java
@@ -90,7 +90,9 @@ public interface BrooklynCatalog {
      * Fails if the same version exists in catalog.
      *
      * @throws IllegalArgumentException if the yaml was invalid
+     * @deprecated since 0.7.0 use {@link #addItems(String, boolean)}
      */
+    @Deprecated
     CatalogItem<?,?> addItem(String yaml);
     
     /**
@@ -100,7 +102,9 @@ public interface BrooklynCatalog {
      * item exists with the same symbolicName and version
      *
      * @throws IllegalArgumentException if the yaml was invalid
+     * @deprecated since 0.7.0 use {@link #addItems(String, boolean)}
      */
+    @Deprecated
     CatalogItem<?,?> addItem(String yaml, boolean forceUpdate);
     
     /**

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/eaaca787/api/src/main/java/brooklyn/location/LocationSpec.java
----------------------------------------------------------------------
diff --git a/api/src/main/java/brooklyn/location/LocationSpec.java b/api/src/main/java/brooklyn/location/LocationSpec.java
index 7e3c4f9..10a6d7e 100644
--- a/api/src/main/java/brooklyn/location/LocationSpec.java
+++ b/api/src/main/java/brooklyn/location/LocationSpec.java
@@ -78,8 +78,11 @@ public class LocationSpec<T extends Location> extends AbstractBrooklynObjectSpec
      * original entity spec.
      */
     public static <T extends Location> LocationSpec<T> create(LocationSpec<T> spec) {
-        // FIXME Why can I not use LocationSpec<T>?
-        LocationSpec<?> result = create(spec.getType())
+        // need this to get LocationSpec<T> rather than LocationSpec<? extends T>
+        @SuppressWarnings("unchecked")
+        Class<T> exactType = (Class<T>)spec.getType();
+        
+        LocationSpec<T> result = create(exactType)
                 .displayName(spec.getDisplayName())
                 .tags(spec.getTags())
                 .configure(spec.getConfig())
@@ -176,7 +179,7 @@ public class LocationSpec<T extends Location> extends AbstractBrooklynObjectSpec
         return this;
     }
     
-    @SuppressWarnings("unchecked")
+    @SuppressWarnings({ "unchecked", "rawtypes" })
     public <E> LocationSpec<T> extensions(Map<Class<?>, ?> extensions) {
         for (Map.Entry<Class<?>, ?> entry : extensions.entrySet()) {
             extension((Class)entry.getKey(), entry.getValue());

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/eaaca787/core/src/main/java/brooklyn/catalog/CatalogPredicates.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/catalog/CatalogPredicates.java b/core/src/main/java/brooklyn/catalog/CatalogPredicates.java
index 4331a1a..2ffebdf 100644
--- a/core/src/main/java/brooklyn/catalog/CatalogPredicates.java
+++ b/core/src/main/java/brooklyn/catalog/CatalogPredicates.java
@@ -21,6 +21,7 @@ package brooklyn.catalog;
 import javax.annotation.Nullable;
 
 import brooklyn.catalog.CatalogItem.CatalogItemType;
+import brooklyn.catalog.internal.CatalogUtils;
 import brooklyn.entity.Application;
 import brooklyn.entity.Entity;
 import brooklyn.entity.proxying.EntitySpec;
@@ -126,5 +127,14 @@ public class CatalogPredicates {
             }
         };
     }
-    
+ 
+    public static <T,SpecT> Predicate<CatalogItem<T,SpecT>> isBestVersion(final ManagementContext mgmt) {
+        return new Predicate<CatalogItem<T,SpecT>>() {
+            @Override
+            public boolean apply(@Nullable CatalogItem<T,SpecT> item) {
+                return CatalogUtils.isBestVersion(mgmt, item);
+            }
+        };
+    }
+
 }
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/eaaca787/core/src/main/java/brooklyn/catalog/internal/BasicBrooklynCatalog.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/catalog/internal/BasicBrooklynCatalog.java b/core/src/main/java/brooklyn/catalog/internal/BasicBrooklynCatalog.java
index 8a562d0..09ef02f 100644
--- a/core/src/main/java/brooklyn/catalog/internal/BasicBrooklynCatalog.java
+++ b/core/src/main/java/brooklyn/catalog/internal/BasicBrooklynCatalog.java
@@ -72,6 +72,7 @@ import brooklyn.util.text.Strings;
 import brooklyn.util.time.Duration;
 import brooklyn.util.time.Time;
 import brooklyn.util.yaml.Yamls;
+import brooklyn.util.yaml.Yamls.YamlExtract;
 
 import com.google.common.base.Function;
 import com.google.common.base.Predicate;
@@ -322,6 +323,11 @@ public class BasicBrooklynCatalog implements BrooklynCatalog {
         String yaml = loadedItem.getPlanYaml();
 
         if (yaml!=null) {
+            // preferred way is to parse the yaml, to resolve references late;
+            // the parsing on load is to populate some fields, but it is optional.
+            // TODO messy for location and policy that we need brooklyn.{locations,policies} root of the yaml, but it works;
+            // see related comment when the yaml is set, in addAbstractCatalogItems
+            // (not sure if anywhere else relies on that syntax; if not, it should be easy to fix!)
             DeploymentPlan plan = makePlanFromYaml(yaml);
             BrooklynClassLoadingContext loader = CatalogUtils.newClassLoadingContext(mgmt, item);
             SpecT spec;
@@ -384,7 +390,6 @@ public class BasicBrooklynCatalog implements BrooklynCatalog {
         }
     }
 
-    @SuppressWarnings("unchecked")
     private <T, SpecT> SpecT createPolicySpec(DeploymentPlan plan, BrooklynClassLoadingContext loader) {
         //Would ideally re-use io.brooklyn.camp.brooklyn.spi.creation.BrooklynEntityDecorationResolver.PolicySpecResolver
         //but it is CAMP specific and there is no easy way to get hold of it.
@@ -395,6 +400,11 @@ public class BasicBrooklynCatalog implements BrooklynCatalog {
 
         Object policy = Iterables.getOnlyElement((Iterable<?>)policies);
 
+        return createPolicySpec(loader, policy);
+    }
+
+    @SuppressWarnings("unchecked")
+    private <T, SpecT> SpecT createPolicySpec(BrooklynClassLoadingContext loader, Object policy) {
         Map<String, Object> config;
         if (policy instanceof String) {
             config = ImmutableMap.<String, Object>of("type", policy);
@@ -413,7 +423,6 @@ public class BasicBrooklynCatalog implements BrooklynCatalog {
         return (SpecT) spec;
     }
     
-    @SuppressWarnings("unchecked")
     private <T, SpecT> SpecT createLocationSpec(DeploymentPlan plan, BrooklynClassLoadingContext loader) {
         // See #createPolicySpec; this impl is modeled on that.
         // spec.catalogItemId is set by caller
@@ -424,6 +433,11 @@ public class BasicBrooklynCatalog implements BrooklynCatalog {
 
         Object location = Iterables.getOnlyElement((Iterable<?>)locations);
 
+        return createLocationSpec(loader, location); 
+    }
+
+    @SuppressWarnings("unchecked")
+    private <T, SpecT> SpecT createLocationSpec(BrooklynClassLoadingContext loader, Object location) {
         Map<String, Object> config;
         if (location instanceof String) {
             config = ImmutableMap.<String, Object>of("type", location);
@@ -457,7 +471,7 @@ public class BasicBrooklynCatalog implements BrooklynCatalog {
             } else {
                 throw new IllegalStateException("No class or resolver found for location type "+type);
             }
-        } 
+        }
     }
 
     @SuppressWarnings("unchecked")
@@ -509,7 +523,9 @@ public class BasicBrooklynCatalog implements BrooklynCatalog {
         }
         if (foundKey==null) return Maybe.absent("Missing entry '"+firstKey+"'");
         value = map.get(foundKey);
-        if (!type.isInstance(value)) return Maybe.absent("Entry for '"+firstKey+"' should be of type "+type+", not "+value.getClass());
+        if (type.equals(String.class) && Number.class.isInstance(value)) value = value.toString();
+        if (!type.isInstance(value)) 
+            throw new IllegalArgumentException("Entry for '"+firstKey+"' should be of type "+type+", not "+value.getClass());
         return Maybe.of((T)value);
     }
     
@@ -518,7 +534,7 @@ public class BasicBrooklynCatalog implements BrooklynCatalog {
         return (Maybe<Map<?,?>>)(Maybe) getFirstAs(map, Map.class, firstKey, otherKeys);
     }
 
-    private List<CatalogItemDtoAbstract<?,?>> getAbstractCatalogItems(String yaml) {
+    private List<CatalogItemDtoAbstract<?,?>> addAbstractCatalogItems(String yaml, Boolean whenAddingAsDtoShouldWeForce) {
         Map<?,?> itemDef = Yamls.getAs(Yamls.parseAll(yaml), Map.class);
         Map<?,?> catalogMetadata = getFirstAsMap(itemDef, "brooklyn.catalog", "catalog").orNull();
         if (catalogMetadata==null)
@@ -527,30 +543,36 @@ public class BasicBrooklynCatalog implements BrooklynCatalog {
 
         List<CatalogItemDtoAbstract<?, ?>> result = MutableList.of();
         
-        addAbstractCatalogItems(catalogMetadata, result, null);
+        addAbstractCatalogItems(Yamls.getTextOfYamlAtPath(yaml, "brooklyn.catalog").getMatchedYamlTextOrWarn(), 
+            catalogMetadata, result, null, whenAddingAsDtoShouldWeForce);
         
         itemDef.remove("brooklyn.catalog");
         catalogMetadata.remove("item");
         catalogMetadata.remove("items");
         if (!itemDef.isEmpty()) {
-            log.debug("Reading brooklyn.catalog peer keys as item");
+            log.debug("Reading brooklyn.catalog peer keys as item ('top-level syntax')");
             Map<String,?> rootItem = MutableMap.of("item", itemDef);
-            addAbstractCatalogItems(rootItem, result, catalogMetadata);
+            String rootItemYaml = yaml;
+            YamlExtract yamlExtract = Yamls.getTextOfYamlAtPath(rootItemYaml, "brooklyn.catalog");
+            String match = yamlExtract.withOriginalIndentation(true).withKeyIncluded(true).getMatchedYamlTextOrWarn();
+            if (match!=null) {
+                if (rootItemYaml.startsWith(match)) rootItemYaml = Strings.removeFromStart(rootItemYaml, match);
+                else rootItemYaml = Strings.replaceAllNonRegex(rootItemYaml, "\n"+match, "");
+            }
+            addAbstractCatalogItems("item:\n"+makeAsIndentedObject(rootItemYaml), rootItem, result, catalogMetadata, whenAddingAsDtoShouldWeForce);
         }
         
         return result;
     }
 
-    enum CatalogItemTypes { ENTITY, TEMPLATE, POLICY, LOCATION }
-    
     @SuppressWarnings("unchecked")
-    private void addAbstractCatalogItems(Map<?,?> itemMetadata, List<CatalogItemDtoAbstract<?, ?>> result, Map<?,?> parentMetadata) {
+    private void addAbstractCatalogItems(String sourceYaml, Map<?,?> itemMetadata, List<CatalogItemDtoAbstract<?, ?>> result, Map<?,?> parentMetadata, Boolean whenAddingAsDtoShouldWeForce) {
+
+        if (sourceYaml==null) sourceYaml = new Yaml().dump(itemMetadata);
 
         // TODO:
-//        get yaml
-//        (tests)
-//        docs used as test casts -- (doc assertions that these match -- or import -- known test casesyamls)
-//        multiple versions in catalog page
+//        docs used as test cases -- (doc assertions that these match -- or import -- known test cases yamls)
+//        multiple versions in web app root
 
         Map<Object,Object> catalogMetadata = MutableMap.builder().putAll(parentMetadata).putAll(itemMetadata).build();
         
@@ -565,16 +587,22 @@ public class BasicBrooklynCatalog implements BrooklynCatalog {
         Object item = catalogMetadata.remove("item");
 
         if (items!=null) {
+            int count = 0;
             for (Map<?,?> i: ((List<Map<?,?>>)items)) {
-                addAbstractCatalogItems(i, result, catalogMetadata);
+                addAbstractCatalogItems(Yamls.getTextOfYamlAtPath(sourceYaml, "items", count).getMatchedYamlTextOrWarn(), 
+                    i, result, catalogMetadata, whenAddingAsDtoShouldWeForce);
+                count++;
             }
         }
         
         if (item==null) return;
+
+        // now look at the actual item, first correcting the sourceYaml and interpreting the catalog metadata
+        String itemYaml = Yamls.getTextOfYamlAtPath(sourceYaml, "item").getMatchedYamlTextOrWarn();
+        if (itemYaml!=null) sourceYaml = itemYaml;
+        else sourceYaml = new Yaml().dump(item);
         
-        //now parse the metadata and apply to item
-        
-        CatalogItemTypes itemType = TypeCoercions.coerce(getFirstAs(catalogMetadata, String.class, "item.type", "itemType", "item_type").or(CatalogItemTypes.ENTITY.toString()), CatalogItemTypes.class);
+        CatalogItemType itemType = TypeCoercions.coerce(getFirstAs(catalogMetadata, Object.class, "item.type", "itemType", "item_type").orNull(), CatalogItemType.class);
         
         String id = getFirstAs(catalogMetadata, String.class, "id").orNull();
         String version = getFirstAs(catalogMetadata, String.class, "version").orNull();
@@ -587,19 +615,25 @@ public class BasicBrooklynCatalog implements BrooklynCatalog {
                 Strings.isNonBlank(name) && !name.equals(displayName)) {
             log.warn("Name property will be ignored due to the existence of displayName and at least one of id, symbolicName");
         }
-        
-        // TODO use src yaml if avail
-        String yaml = new Yaml().dump(item);
-        
+                
         DeploymentPlan plan = null;
         try {
-            plan = makePlanFromYaml(yaml);
+            plan = makePlanFromYaml(sourceYaml);
         } catch (Exception e) {
             Exceptions.propagateIfFatal(e);
-            if (itemType==CatalogItemTypes.ENTITY || itemType==CatalogItemTypes.TEMPLATE)
-                log.warn("Could not parse item YAML for "+itemType+" (registering anyway): "+e+"\n"+yaml);
+            if (itemType==CatalogItemType.ENTITY || itemType==CatalogItemType.TEMPLATE)
+                log.warn("Could not parse item YAML for "+itemType+" (registering anyway): "+e+"\n"+sourceYaml);
         }
         
+        if (Strings.isBlank(id)) {
+            // let ID be inferred, especially from name, to support style where only "name" is specified, with inline version
+            if (Strings.isNonBlank(symbolicName) && Strings.isNonBlank(version)) {
+                id = symbolicName + ":" + version;
+            } else if (Strings.isNonBlank(name)) {
+                id = name;
+            }
+        }
+
         final String catalogSymbolicName;
         if (Strings.isNonBlank(symbolicName)) {
             catalogSymbolicName = symbolicName;
@@ -609,8 +643,6 @@ public class BasicBrooklynCatalog implements BrooklynCatalog {
             } else {
                 catalogSymbolicName = id;
             }
-        } else if (Strings.isNonBlank(name)) {
-            catalogSymbolicName = name;
         } else if (plan!=null && Strings.isNonBlank(plan.getName())) {
             catalogSymbolicName = plan.getName();
         } else if (plan!=null && plan.getServices().size()==1) {
@@ -620,8 +652,8 @@ public class BasicBrooklynCatalog implements BrooklynCatalog {
             }
             catalogSymbolicName = svc.getServiceType();
         } else {
-            log.error("Can't infer catalog item symbolicName from the following plan:\n" + yaml);
-            throw new IllegalStateException("Can't infer catalog item symbolicName from catalog item description");
+            log.error("Can't infer catalog item symbolicName from the following plan:\n" + sourceYaml);
+            throw new IllegalStateException("Can't infer catalog item symbolicName from catalog item metadata");
         }
 
         final String catalogVersion;
@@ -668,23 +700,103 @@ public class BasicBrooklynCatalog implements BrooklynCatalog {
         String versionedId = CatalogUtils.getVersionedId(catalogSymbolicName, catalogVersion!=null ? catalogVersion : NO_VERSION);
         BrooklynClassLoadingContext loader = CatalogUtils.newClassLoadingContext(mgmt, versionedId, libraries);
         
-        // TODO for entities, we parse. for templates we don't. (?)
-        AbstractBrooklynObjectSpec<?, ?> spec = createSpec(catalogSymbolicName, plan, loader);
+        CatalogItemType inferredItemType = inferType(plan);
+        boolean usePlan;
+        if (inferredItemType!=null) {
+            if (itemType!=null) {
+                if (itemType==CatalogItemType.TEMPLATE && inferredItemType==CatalogItemType.ENTITY) {
+                    // template - we use the plan, but coupled with the type to make a catalog template item
+                    usePlan = true;
+                } else if (itemType==inferredItemType) {
+                    // if the plan showed the type, it is either a parsed entity or a legacy spec type
+                    usePlan = true;
+                    if (itemType==CatalogItemType.TEMPLATE || itemType==CatalogItemType.ENTITY) {
+                        // normal
+                    } else {
+                        log.warn("Legacy syntax used for "+itemType+" "+catalogSymbolicName+": should declare item.type and specify type");
+                    }
+                } else {
+                    throw new IllegalStateException("Explicit type "+itemType+" "+catalogSymbolicName+" does not match blueprint inferred type "+inferredItemType);
+                }
+            } else {
+                // no type was declared, use the inferred type from the plan
+                itemType = inferredItemType;
+                usePlan = true;
+            }
+        } else if (itemType!=null) {
+            if (itemType==CatalogItemType.TEMPLATE || itemType==CatalogItemType.ENTITY) {
+                log.warn("Incomplete plan detected for "+itemType+" "+catalogSymbolicName+"; will likely fail subsequently");
+                usePlan = true;
+            } else {
+                usePlan = false;
+            }
+        } else {
+            throw new IllegalStateException("Unable to detect type for "+catalogSymbolicName+"; error in catalog metadata or blueprint");
+        }
+        
+        AbstractBrooklynObjectSpec<?, ?> spec;
+        if (usePlan) {
+            spec = createSpecFromPlan(catalogSymbolicName, plan, loader);
+        } else {
+            String key;
+            switch (itemType) {
+            // we don't actually need the spec here, since the yaml is what is used at load time,
+            // but it isn't a bad idea to confirm that it resolves
+            case POLICY: 
+                spec = createPolicySpec(loader, item);
+                key = POLICIES_KEY;
+                break;
+            case LOCATION: 
+                spec = createLocationSpec(loader, item); 
+                key = LOCATIONS_KEY;
+                break;
+            default: throw new IllegalStateException("Cannot create "+itemType+" "+catalogSymbolicName+"; invalid metadata or blueprint");
+            }
+            // TODO currently we then convert yaml to legacy brooklyn.{location,policy} syntax for subsequent usage; 
+            // would be better to (in the other path) convert to {type: ..., brooklyn.config: ... } format and expect that elsewhere
+            sourceYaml = key + ":\n" + makeAsIndentedList(sourceYaml);
+        }
 
-        CatalogItemDtoAbstract<?, ?> dto = createItemBuilder(spec, catalogSymbolicName, catalogVersion)
+        CatalogItemDtoAbstract<?, ?> dto = createItemBuilder(itemType, spec, catalogSymbolicName, catalogVersion)
             .libraries(libraries)
             .displayName(catalogDisplayName)
             .description(catalogDescription)
             .deprecated(catalogDeprecated)
             .iconUrl(catalogIconUrl)
-            .plan(yaml)
+            .plan(sourceYaml)
             .build();
 
         dto.setManagementContext((ManagementContextInternal) mgmt);
+        if (whenAddingAsDtoShouldWeForce!=null) {
+            addItemDto(dto, whenAddingAsDtoShouldWeForce);
+        }
         result.add(dto);
     }
 
-    private AbstractBrooklynObjectSpec<?,?> createSpec(String symbolicName, DeploymentPlan plan, BrooklynClassLoadingContext loader) {
+    private String makeAsIndentedList(String yaml) {
+        String[] lines = yaml.split("\n");
+        lines[0] = "- "+lines[0];
+        for (int i=1; i<lines.length; i++)
+            lines[i] = "  " + lines[i];
+        return Strings.join(lines, "\n");
+    }
+
+    private String makeAsIndentedObject(String yaml) {
+        String[] lines = yaml.split("\n");
+        for (int i=0; i<lines.length; i++)
+            lines[i] = "  " + lines[i];
+        return Strings.join(lines, "\n");
+    }
+
+    private CatalogItemType inferType(DeploymentPlan plan) {
+        if (plan==null) return null;
+        if (isEntityPlan(plan)) return CatalogItemType.ENTITY; 
+        if (isPolicyPlan(plan)) return CatalogItemType.POLICY;
+        if (isLocationPlan(plan)) return CatalogItemType.LOCATION;
+        return null;
+    }
+
+    private AbstractBrooklynObjectSpec<?,?> createSpecFromPlan(String symbolicName, DeploymentPlan plan, BrooklynClassLoadingContext loader) {
         if (isPolicyPlan(plan)) {
             return createPolicySpec(plan, loader);
         } else if (isLocationPlan(plan)) {
@@ -694,7 +806,20 @@ public class BasicBrooklynCatalog implements BrooklynCatalog {
         }
     }
 
-    private CatalogItemBuilder<?> createItemBuilder(AbstractBrooklynObjectSpec<?, ?> spec, String itemId, String version) {
+    private CatalogItemBuilder<?> createItemBuilder(CatalogItemType itemType, AbstractBrooklynObjectSpec<?, ?> spec, String itemId, String version) {
+        if (itemType!=null) {
+            switch (itemType) {
+            case ENTITY: return CatalogItemBuilder.newEntity(itemId, version);
+            case TEMPLATE: return CatalogItemBuilder.newTemplate(itemId, version);
+            case POLICY: return CatalogItemBuilder.newPolicy(itemId, version);
+            case LOCATION: return CatalogItemBuilder.newLocation(itemId, version);
+            }
+            log.warn("Legacy syntax for "+itemId+"; unexpected item.type "+itemType);
+        } else {
+            log.warn("Legacy syntax for "+itemId+"; no item.type declared or inferred");
+        }
+        
+        // @deprecated - should not come here
         if (spec instanceof EntitySpec) {
             if (isApplicationSpec((EntitySpec<?>)spec)) {
                 return CatalogItemBuilder.newTemplate(itemId, version);
@@ -714,12 +839,16 @@ public class BasicBrooklynCatalog implements BrooklynCatalog {
         return !Boolean.TRUE.equals(spec.getConfig().get(EntityManagementUtils.WRAPPER_APP_MARKER));
     }
 
+    private boolean isEntityPlan(DeploymentPlan plan) {
+        return plan!=null && !plan.getServices().isEmpty() || !plan.getArtifacts().isEmpty();
+    }
+    
     private boolean isPolicyPlan(DeploymentPlan plan) {
-        return plan.getCustomAttributes().containsKey(POLICIES_KEY);
+        return !isEntityPlan(plan) && plan.getCustomAttributes().containsKey(POLICIES_KEY);
     }
 
     private boolean isLocationPlan(DeploymentPlan plan) {
-        return plan.getCustomAttributes().containsKey(LOCATIONS_KEY);
+        return !isEntityPlan(plan) && plan.getCustomAttributes().containsKey(LOCATIONS_KEY);
     }
 
     private DeploymentPlan makePlanFromYaml(String yaml) {
@@ -746,10 +875,11 @@ public class BasicBrooklynCatalog implements BrooklynCatalog {
     public List<? extends CatalogItem<?,?>> addItems(String yaml, boolean forceUpdate) {
         log.debug("Adding manual catalog item to "+mgmt+": "+yaml);
         checkNotNull(yaml, "yaml");
-        List<CatalogItemDtoAbstract<?, ?>> result = getAbstractCatalogItems(yaml);
-        for (CatalogItemDtoAbstract<?, ?> item: result) {
-            addItemDto(item, forceUpdate);
-        }
+        List<CatalogItemDtoAbstract<?, ?>> result = addAbstractCatalogItems(yaml, forceUpdate);
+        // previously we did this here, but now we do it on each item, in case #2 refers to #1
+//        for (CatalogItemDtoAbstract<?, ?> item: result) {
+//            addItemDto(item, forceUpdate);
+//        }
         return result;
     }
     

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/eaaca787/core/src/main/java/brooklyn/catalog/internal/CatalogUtils.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/catalog/internal/CatalogUtils.java b/core/src/main/java/brooklyn/catalog/internal/CatalogUtils.java
index 50ef436..c72fc9b 100644
--- a/core/src/main/java/brooklyn/catalog/internal/CatalogUtils.java
+++ b/core/src/main/java/brooklyn/catalog/internal/CatalogUtils.java
@@ -218,6 +218,12 @@ public class CatalogUtils {
         }
     }
 
+    public static boolean isBestVersion(ManagementContext mgmt, CatalogItem<?,?> item) {
+        CatalogItem<?, ?> bestVersion = getCatalogItemOptionalVersion(mgmt, item.getSymbolicName());
+        if (bestVersion==null) return false;
+        return (bestVersion.getVersion().equals(item.getVersion()));
+    }
+
     public static <T,SpecT> CatalogItem<T, SpecT> getCatalogItemOptionalVersion(ManagementContext mgmt, Class<T> type, String versionedId) {
         if (looksLikeVersionedId(versionedId)) {
             String id = getIdFromVersionedId(versionedId);

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/eaaca787/core/src/main/java/brooklyn/location/basic/BasicLocationRegistry.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/location/basic/BasicLocationRegistry.java b/core/src/main/java/brooklyn/location/basic/BasicLocationRegistry.java
index f0b466e..c59298b 100644
--- a/core/src/main/java/brooklyn/location/basic/BasicLocationRegistry.java
+++ b/core/src/main/java/brooklyn/location/basic/BasicLocationRegistry.java
@@ -84,7 +84,7 @@ import com.google.common.collect.Sets;
  * it goes through the following steps:
  * 
  * <ol>
- *   <li>Call {@link BrooklynCatalog#addItem(String)}
+ *   <li>Call {@link BrooklynCatalog#addItems(String)}
  *     <ol>
  *       <li>This automatically calls {@link #updateDefinedLocation(CatalogItem)}
  *       <li>A LocationDefinition is creating, using as its id the {@link CatalogItem#getSymbolicName()}.

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/eaaca787/core/src/main/java/brooklyn/location/basic/CatalogLocationResolver.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/location/basic/CatalogLocationResolver.java b/core/src/main/java/brooklyn/location/basic/CatalogLocationResolver.java
index 4823b05..591873d 100644
--- a/core/src/main/java/brooklyn/location/basic/CatalogLocationResolver.java
+++ b/core/src/main/java/brooklyn/location/basic/CatalogLocationResolver.java
@@ -39,6 +39,7 @@ import brooklyn.management.ManagementContext;
  */
 public class CatalogLocationResolver implements LocationResolver {
 
+    @SuppressWarnings("unused")
     private static final Logger log = LoggerFactory.getLogger(CatalogLocationResolver.class);
 
     public static final String NAME = "brooklyn.catalog";
@@ -51,7 +52,7 @@ public class CatalogLocationResolver implements LocationResolver {
     }
     
     @Override
-    @SuppressWarnings({ "rawtypes" })
+    @SuppressWarnings({ "rawtypes", "unchecked" })
     public Location newLocationFromString(Map locationFlags, String spec, brooklyn.location.LocationRegistry registry) {
         String id = spec.substring(NAME.length()+1);
         CatalogItem<?, ?> item = CatalogUtils.getCatalogItemOptionalVersion(managementContext, id);

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/eaaca787/core/src/test/java/brooklyn/camp/lite/CampYamlLiteTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/brooklyn/camp/lite/CampYamlLiteTest.java b/core/src/test/java/brooklyn/camp/lite/CampYamlLiteTest.java
index b9b4bca..2739c79 100644
--- a/core/src/test/java/brooklyn/camp/lite/CampYamlLiteTest.java
+++ b/core/src/test/java/brooklyn/camp/lite/CampYamlLiteTest.java
@@ -156,7 +156,7 @@ public class CampYamlLiteTest {
     public void testYamlServiceForCatalog() {
         TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_PATH);
 
-        CatalogItem<?, ?> realItem = mgmt.getCatalog().addItem(Streams.readFullyString(getClass().getResourceAsStream("test-app-service-blueprint.yaml")));
+        CatalogItem<?, ?> realItem = Iterables.getOnlyElement(mgmt.getCatalog().addItems(Streams.readFullyString(getClass().getResourceAsStream("test-app-service-blueprint.yaml"))));
         Iterable<CatalogItem<Object, Object>> retrievedItems = mgmt.getCatalog()
                 .getCatalogItems(CatalogPredicates.symbolicName(Predicates.equalTo("catalog-name")));
         
@@ -185,7 +185,7 @@ public class CampYamlLiteTest {
         String bundleUrl = OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_URL;
         String yaml = getSampleMyCatalogAppYaml(symbolicName, bundleUrl);
 
-        mgmt.getCatalog().addItem(yaml);
+        mgmt.getCatalog().addItems(yaml);
 
         assertMgmtHasSampleMyCatalogApp(symbolicName, bundleUrl);
     }
@@ -203,7 +203,7 @@ public class CampYamlLiteTest {
             CampPlatformWithJustBrooklynMgmt platform2 = new CampPlatformWithJustBrooklynMgmt(mgmt2);
             MockWebPlatform.populate(platform2, TestAppAssemblyInstantiator.class);
 
-            mgmt2.getCatalog().addItem(yaml);
+            mgmt2.getCatalog().addItems(yaml);
             String xml = ((BasicBrooklynCatalog) mgmt2.getCatalog()).toXmlString();
             ((BasicBrooklynCatalog) mgmt.getCatalog()).reset(CatalogDto.newDtoFromXmlContents(xml, "copy of temporary catalog"));
         } finally {

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/eaaca787/core/src/test/java/brooklyn/entity/rebind/RebindCatalogItemTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/brooklyn/entity/rebind/RebindCatalogItemTest.java b/core/src/test/java/brooklyn/entity/rebind/RebindCatalogItemTest.java
index 4036dc5..e0260c3 100644
--- a/core/src/test/java/brooklyn/entity/rebind/RebindCatalogItemTest.java
+++ b/core/src/test/java/brooklyn/entity/rebind/RebindCatalogItemTest.java
@@ -212,8 +212,9 @@ public class RebindCatalogItemTest extends RebindTestFixtureWithApp {
                 "  version: " + TEST_VERSION + "\n" +
                 "services:\n" +
                 "- type: io.camp.mock:AppServer";
+        addItem(origManagementContext, yaml);
+        
         BasicBrooklynCatalog catalog = (BasicBrooklynCatalog) origManagementContext.getCatalog();
-        catalog.addItem(yaml);
         String catalogXml = catalog.toXmlString();
         catalog.reset(CatalogDto.newDtoFromXmlContents(catalogXml, "Test reset"));
         rebindAndAssertCatalogsAreEqual();
@@ -227,9 +228,10 @@ public class RebindCatalogItemTest extends RebindTestFixtureWithApp {
                 "  version: " + TEST_VERSION + "\n" +
                 "services:\n" +
                 "- type: io.camp.mock:AppServer";
-        BasicBrooklynCatalog catalog = (BasicBrooklynCatalog) origManagementContext.getCatalog();
-        CatalogItem<?, ?> catalogItem = catalog.addItem(yaml);
+        CatalogItem<?, ?> catalogItem = addItem(origManagementContext, yaml);
         assertNotNull(catalogItem, "catalogItem");
+        BasicBrooklynCatalog catalog = (BasicBrooklynCatalog) origManagementContext.getCatalog();
+        
         catalogItem.setDeprecated(true);
         catalog.persist(catalogItem);
         rebindAndAssertCatalogsAreEqual();
@@ -238,7 +240,7 @@ public class RebindCatalogItemTest extends RebindTestFixtureWithApp {
     }
 
     protected CatalogItem<?, ?> addItem(ManagementContext mgmt, String yaml) {
-        CatalogItem<?, ?> added = mgmt.getCatalog().addItem(yaml);
+        CatalogItem<?, ?> added = Iterables.getOnlyElement(mgmt.getCatalog().addItems(yaml));
         LOG.info("Added item to catalog: {}, id={}", added, added.getId());
         assertCatalogContains(mgmt.getCatalog(), added);
         return added;

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/eaaca787/docs/guide/misc/release-notes.md
----------------------------------------------------------------------
diff --git a/docs/guide/misc/release-notes.md b/docs/guide/misc/release-notes.md
index 2b6dadf..1a324d6 100644
--- a/docs/guide/misc/release-notes.md
+++ b/docs/guide/misc/release-notes.md
@@ -47,6 +47,10 @@ For more information, please visit [brooklyn.io](http://brooklyn.io).
 * If `brooklyn.webconsole.security.https.required=true` is specified with no explicit port, 
   it now defaults to 8443; previously it would default to 8081 even in the case of `https`.
 
+* The /v1/catalog/create method now returns a map of ID to item map, instead of an item map, 
+  as the call supports multiple items defined in the YAML.
+  
+
 ### Community Activity
 
 Brooklyn has moved into the Apache Software Foundation.

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/eaaca787/usage/camp/src/test/java/io/brooklyn/camp/brooklyn/AbstractYamlTest.java
----------------------------------------------------------------------
diff --git a/usage/camp/src/test/java/io/brooklyn/camp/brooklyn/AbstractYamlTest.java b/usage/camp/src/test/java/io/brooklyn/camp/brooklyn/AbstractYamlTest.java
index dd7ad58..b1e6817 100644
--- a/usage/camp/src/test/java/io/brooklyn/camp/brooklyn/AbstractYamlTest.java
+++ b/usage/camp/src/test/java/io/brooklyn/camp/brooklyn/AbstractYamlTest.java
@@ -153,7 +153,7 @@ public abstract class AbstractYamlTest {
     }
 
     protected void addCatalogItem(String catalogYaml) {
-        mgmt().getCatalog().addItem(catalogYaml, forceUpdate);
+        mgmt().getCatalog().addItems(catalogYaml, forceUpdate);
     }
 
     protected void deleteCatalogEntity(String catalogItem) {

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/eaaca787/usage/camp/src/test/java/io/brooklyn/camp/brooklyn/catalog/CatalogOsgiVersionMoreEntityTest.java
----------------------------------------------------------------------
diff --git a/usage/camp/src/test/java/io/brooklyn/camp/brooklyn/catalog/CatalogOsgiVersionMoreEntityTest.java b/usage/camp/src/test/java/io/brooklyn/camp/brooklyn/catalog/CatalogOsgiVersionMoreEntityTest.java
index 3527a5d..19c9ef8 100644
--- a/usage/camp/src/test/java/io/brooklyn/camp/brooklyn/catalog/CatalogOsgiVersionMoreEntityTest.java
+++ b/usage/camp/src/test/java/io/brooklyn/camp/brooklyn/catalog/CatalogOsgiVersionMoreEntityTest.java
@@ -18,16 +18,19 @@
  */
 package io.brooklyn.camp.brooklyn.catalog;
 
-import brooklyn.test.TestResourceUnavailableException;
 import io.brooklyn.camp.brooklyn.AbstractYamlTest;
 import io.brooklyn.camp.brooklyn.spi.creation.BrooklynEntityMatcher;
 
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
+import brooklyn.catalog.CatalogItem;
+import brooklyn.catalog.CatalogItem.CatalogItemType;
+import brooklyn.catalog.internal.CatalogUtils;
 import brooklyn.entity.Entity;
 import brooklyn.management.osgi.OsgiVersionMoreEntityTest;
 import brooklyn.policy.Policy;
+import brooklyn.test.TestResourceUnavailableException;
 import brooklyn.util.ResourceUtils;
 
 import com.google.common.collect.Iterables;
@@ -45,6 +48,12 @@ public class CatalogOsgiVersionMoreEntityTest extends AbstractYamlTest {
         TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), "/brooklyn/osgi/brooklyn-test-osgi-more-entities_0.1.0.jar");
 
         addCatalogItem(getLocalResource("more-entity-v1-osgi-catalog.yaml"));
+        CatalogItem<?, ?> item = CatalogUtils.getCatalogItemOptionalVersion(mgmt(), "more-entity");
+        Assert.assertNotNull(item);
+        Assert.assertEquals(item.getVersion(), "1.0");
+        Assert.assertEquals(item.getCatalogItemType(), CatalogItemType.ENTITY);
+        Assert.assertEquals(item.getLibraries().size(), 1);
+        
         Entity app = createAndStartApplication("services: [ { type: 'more-entity:1.0' } ]");
         Entity moreEntity = Iterables.getOnlyElement(app.getChildren());
         

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/eaaca787/usage/camp/src/test/java/io/brooklyn/camp/brooklyn/catalog/CatalogYamlEntityTest.java
----------------------------------------------------------------------
diff --git a/usage/camp/src/test/java/io/brooklyn/camp/brooklyn/catalog/CatalogYamlEntityTest.java b/usage/camp/src/test/java/io/brooklyn/camp/brooklyn/catalog/CatalogYamlEntityTest.java
index 5b79982..1737718 100644
--- a/usage/camp/src/test/java/io/brooklyn/camp/brooklyn/catalog/CatalogYamlEntityTest.java
+++ b/usage/camp/src/test/java/io/brooklyn/camp/brooklyn/catalog/CatalogYamlEntityTest.java
@@ -25,6 +25,7 @@ import io.brooklyn.camp.brooklyn.AbstractYamlTest;
 
 import java.io.InputStream;
 import java.util.Collection;
+import java.util.List;
 
 import org.testng.Assert;
 import org.testng.annotations.Test;
@@ -38,6 +39,7 @@ import brooklyn.management.osgi.OsgiStandaloneTest;
 import brooklyn.management.osgi.OsgiTestResources;
 import brooklyn.test.TestResourceUnavailableException;
 import brooklyn.util.ResourceUtils;
+import brooklyn.util.collections.MutableList;
 
 import com.google.common.collect.Iterables;
 
@@ -59,6 +61,30 @@ public class CatalogYamlEntityTest extends AbstractYamlTest {
     }
 
     @Test
+    public void testAddCatalogItemAsSiblingOfCatalogMetadata() throws Exception {
+        TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_PATH);
+
+        String symbolicName = "my.catalog.app.id.load";
+        addCatalogItem(
+            "brooklyn.catalog:",
+            "  id: " + symbolicName,
+            "  name: My Catalog App",
+            "  description: My description",
+            "  icon_url: classpath://path/to/myicon.jpg",
+            "  version: " + TEST_VERSION,
+            "  libraries:",
+            "  - url: " + OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_URL,
+            "",
+            "services:",
+            "- type: " + SIMPLE_ENTITY_TYPE);
+
+        CatalogItem<?, ?> item = mgmt().getCatalog().getCatalogItem(symbolicName, TEST_VERSION);
+        assertEquals(item.getSymbolicName(), symbolicName);
+
+        deleteCatalogEntity(symbolicName);
+    }
+
+    @Test
     public void testAddCatalogItemWithoutVersion() throws Exception {
         TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_PATH);
 
@@ -76,6 +102,23 @@ public class CatalogYamlEntityTest extends AbstractYamlTest {
     }
 
     @Test
+    public void testAddCatalogItemWithInlinedVersion() throws Exception {
+        TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_PATH);
+
+        String id = "inline_version.app";
+        addCatalogItem(
+            "brooklyn.catalog:",
+            "  name: " + id+":"+TEST_VERSION,
+            "  libraries:",
+            "  - " + OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_URL,
+            "services:",
+            "- type: " + SIMPLE_ENTITY_TYPE);
+        CatalogItem<?, ?> catalogItem = mgmt().getCatalog().getCatalogItem(id, TEST_VERSION);
+        assertEquals(catalogItem.getVersion(), TEST_VERSION);
+        mgmt().getCatalog().deleteCatalogItem(id, TEST_VERSION);
+    }
+
+    @Test
     public void testLaunchApplicationReferencingCatalog() throws Exception {
         TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_PATH);
 
@@ -84,7 +127,7 @@ public class CatalogYamlEntityTest extends AbstractYamlTest {
     }
 
     @Test
-    public void testLaunchApplicationUnverionedCatalogReference() throws Exception {
+    public void testLaunchApplicationUnversionedCatalogReference() throws Exception {
         TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_PATH);
 
         String symbolicName = "my.catalog.app.id.fail";
@@ -106,6 +149,27 @@ public class CatalogYamlEntityTest extends AbstractYamlTest {
 
         String referencedSymbolicName = "my.catalog.app.id.referenced";
         String referrerSymbolicName = "my.catalog.app.id.referring";
+        addCatalogOSGiEntities(referencedSymbolicName, SIMPLE_ENTITY_TYPE, referrerSymbolicName, ver(referencedSymbolicName));
+
+        String yaml = "name: simple-app-yaml\n" +
+                      "location: localhost\n" +
+                      "services: \n" +
+                      "  - serviceType: " + ver(referrerSymbolicName);
+        Entity app = createAndStartApplication(yaml);
+
+        Entity simpleEntity = Iterables.getOnlyElement(app.getChildren());
+        assertEquals(simpleEntity.getEntityType().getName(), SIMPLE_ENTITY_TYPE);
+
+        deleteCatalogEntity(referencedSymbolicName);
+        deleteCatalogEntity(referrerSymbolicName);
+    }
+
+    @Test
+    public void testLaunchApplicationWithCatalogReferencingOtherCatalogInTwoSteps() throws Exception {
+        TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_PATH);
+
+        String referencedSymbolicName = "my.catalog.app.id.referenced";
+        String referrerSymbolicName = "my.catalog.app.id.referring";
         addCatalogOSGiEntity(referencedSymbolicName, SIMPLE_ENTITY_TYPE);
         addCatalogOSGiEntity(referrerSymbolicName, ver(referencedSymbolicName));
 
@@ -465,11 +529,32 @@ public class CatalogYamlEntityTest extends AbstractYamlTest {
             "  version: " + TEST_VERSION,
             "  libraries:",
             "  - url: " + OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_URL,
-            "",
-            "services:",
-            "- type: " + serviceType);
+            "  item:",
+            "    services:",
+            "    - type: " + serviceType);
     }
 
+    private void addCatalogOSGiEntities(String ...namesAndTypes) {
+        List<String> lines = MutableList.of(
+            "brooklyn.catalog:",
+            "  name: My Catalog App",
+            "  description: My description",
+            "  icon_url: classpath://path/to/myicon.jpg",
+            "  version: " + TEST_VERSION,
+            "  libraries:",
+            "  - url: " + OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_URL,
+            "  items:");
+        
+        for (int i=0; i<namesAndTypes.length; i+=2) {
+            lines.addAll(MutableList.of(
+            "  - id: " + namesAndTypes[i],
+            "    item:",
+            "      services:",
+            "      - type: " + namesAndTypes[i+1]));
+        }
+            
+        addCatalogItem(lines);
+    }
     private void addCatalogChildOSGiEntity(String symbolicName, String serviceType) {
         addCatalogItem(
             "brooklyn.catalog:",
@@ -480,11 +565,11 @@ public class CatalogYamlEntityTest extends AbstractYamlTest {
             "  version: " + TEST_VERSION,
             "  libraries:",
             "  - url: " + OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_URL,
-            "",
-            "services:",
-            "- type: " + BasicEntity.class.getName(),
-            "  brooklyn.children:",
-            "  - type: " + serviceType);
+            "  item:",
+            "    services:",
+            "    - type: " + BasicEntity.class.getName(),
+            "      brooklyn.children:",
+            "      - type: " + serviceType);
     }
 
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/eaaca787/usage/camp/src/test/java/io/brooklyn/camp/brooklyn/catalog/CatalogYamlLocationTest.java
----------------------------------------------------------------------
diff --git a/usage/camp/src/test/java/io/brooklyn/camp/brooklyn/catalog/CatalogYamlLocationTest.java b/usage/camp/src/test/java/io/brooklyn/camp/brooklyn/catalog/CatalogYamlLocationTest.java
index ef5aa0f..c0d41d2 100644
--- a/usage/camp/src/test/java/io/brooklyn/camp/brooklyn/catalog/CatalogYamlLocationTest.java
+++ b/usage/camp/src/test/java/io/brooklyn/camp/brooklyn/catalog/CatalogYamlLocationTest.java
@@ -22,16 +22,20 @@ import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertNull;
 import io.brooklyn.camp.brooklyn.AbstractYamlTest;
 
+import java.util.Collection;
 import java.util.List;
 
+import org.testng.annotations.AfterMethod;
 import org.testng.annotations.Test;
 
 import brooklyn.catalog.CatalogItem;
+import brooklyn.catalog.CatalogItem.CatalogBundle;
 import brooklyn.catalog.CatalogPredicates;
 import brooklyn.entity.Entity;
 import brooklyn.event.basic.BasicConfigKey;
 import brooklyn.location.Location;
 import brooklyn.location.LocationDefinition;
+import brooklyn.location.LocationSpec;
 import brooklyn.location.basic.LocalhostMachineProvisioningLocation;
 import brooklyn.management.osgi.OsgiStandaloneTest;
 import brooklyn.test.TestResourceUnavailableException;
@@ -46,13 +50,63 @@ public class CatalogYamlLocationTest extends AbstractYamlTest {
     private static final String LOCALHOST_LOCATION_TYPE = LocalhostMachineProvisioningLocation.class.getName();
     private static final String SIMPLE_LOCATION_TYPE = "brooklyn.osgi.tests.SimpleLocation";
 
+    @AfterMethod
+    public void tearDown() {
+        for (CatalogItem<Location, LocationSpec<?>> ci : mgmt().getCatalog().getCatalogItems(CatalogPredicates.IS_LOCATION)) {
+            mgmt().getCatalog().deleteCatalogItem(ci.getSymbolicName(), ci.getVersion());
+        }
+    }
+    
     @Test
     public void testAddCatalogItem() throws Exception {
         assertEquals(countCatalogLocations(), 0);
 
         String symbolicName = "my.catalog.location.id.load";
-        addCatalogOSGiLocation(symbolicName, SIMPLE_LOCATION_TYPE);
+        addCatalogLocation(symbolicName, LOCALHOST_LOCATION_TYPE, null);
+        assertAdded(symbolicName, LOCALHOST_LOCATION_TYPE);
+        removeAndAssert(symbolicName);
+    }
+
+    @Test
+    public void testAddCatalogItemOsgi() throws Exception {
+        assertEquals(countCatalogLocations(), 0);
+
+        String symbolicName = "my.catalog.location.id.load";
+        addCatalogLocation(symbolicName, SIMPLE_LOCATION_TYPE, getOsgiLibraries());
+        assertAdded(symbolicName, SIMPLE_LOCATION_TYPE);
+        assertOsgi(symbolicName);
+        removeAndAssert(symbolicName);
+    }
+
+    @Test
+    public void testAddCatalogItemTopLevelItemSyntax() throws Exception {
+        assertEquals(countCatalogLocations(), 0);
+
+        String symbolicName = "my.catalog.location.id.load";
+        addCatalogLocationTopLevelItemSyntax(symbolicName, LOCALHOST_LOCATION_TYPE, null);
+        assertAdded(symbolicName, LOCALHOST_LOCATION_TYPE);
+        removeAndAssert(symbolicName);
+    }
+
+    @Test
+    public void testAddCatalogItemOsgiTopLevelItemSyntax() throws Exception {
+        assertEquals(countCatalogLocations(), 0);
 
+        String symbolicName = "my.catalog.location.id.load";
+        addCatalogLocationTopLevelItemSyntax(symbolicName, SIMPLE_LOCATION_TYPE, getOsgiLibraries());
+        assertAdded(symbolicName, SIMPLE_LOCATION_TYPE);
+        assertOsgi(symbolicName);
+        removeAndAssert(symbolicName);
+    }
+
+    private void assertOsgi(String symbolicName) {
+        CatalogItem<?, ?> item = mgmt().getCatalog().getCatalogItem(symbolicName, TEST_VERSION);
+        Collection<CatalogBundle> libs = item.getLibraries();
+        assertEquals(libs.size(), 1);
+        assertEquals(Iterables.getOnlyElement(libs).getUrl(), Iterables.getOnlyElement(getOsgiLibraries()));
+    }
+
+    private void assertAdded(String symbolicName, String expectedJavaType) {
         CatalogItem<?, ?> item = mgmt().getCatalog().getCatalogItem(symbolicName, TEST_VERSION);
         assertEquals(item.getSymbolicName(), symbolicName);
         assertEquals(countCatalogLocations(), 1);
@@ -61,7 +115,12 @@ public class CatalogYamlLocationTest extends AbstractYamlTest {
         LocationDefinition def = mgmt().getLocationRegistry().getDefinedLocationByName(symbolicName);
         assertEquals(def.getId(), symbolicName);
         assertEquals(def.getName(), symbolicName);
-
+        
+        LocationSpec<?> spec = (LocationSpec<?>)mgmt().getCatalog().createSpec(item);
+        assertEquals(spec.getType().getName(), expectedJavaType);
+    }
+    
+    private void removeAndAssert(String symbolicName) {
         // Deleting item: should be gone from catalog, and from location registry
         deleteCatalogEntity(symbolicName);
 
@@ -72,7 +131,7 @@ public class CatalogYamlLocationTest extends AbstractYamlTest {
     @Test
     public void testLaunchApplicationReferencingLocationClass() throws Exception {
         String symbolicName = "my.catalog.location.id.launch";
-        addCatalogLocation(symbolicName, LOCALHOST_LOCATION_TYPE);
+        addCatalogLocation(symbolicName, LOCALHOST_LOCATION_TYPE, null);
         runLaunchApplicationReferencingLocation(symbolicName, LOCALHOST_LOCATION_TYPE);
 
         deleteCatalogEntity(symbolicName);
@@ -81,7 +140,25 @@ public class CatalogYamlLocationTest extends AbstractYamlTest {
     @Test
     public void testLaunchApplicationReferencingLocationSpec() throws Exception {
         String symbolicName = "my.catalog.location.id.launch";
-        addCatalogLocation(symbolicName, LOCALHOST_LOCATION_SPEC);
+        addCatalogLocation(symbolicName, LOCALHOST_LOCATION_SPEC, null);
+        runLaunchApplicationReferencingLocation(symbolicName, LOCALHOST_LOCATION_TYPE);
+
+        deleteCatalogEntity(symbolicName);
+    }
+
+    @Test
+    public void testLaunchApplicationReferencingLocationClassTopLevelItemSyntax() throws Exception {
+        String symbolicName = "my.catalog.location.id.launch";
+        addCatalogLocationTopLevelItemSyntax(symbolicName, LOCALHOST_LOCATION_TYPE, null);
+        runLaunchApplicationReferencingLocation(symbolicName, LOCALHOST_LOCATION_TYPE);
+
+        deleteCatalogEntity(symbolicName);
+    }
+
+    @Test
+    public void testLaunchApplicationReferencingLocationSpecTopLevelSyntax() throws Exception {
+        String symbolicName = "my.catalog.location.id.launch";
+        addCatalogLocationTopLevelItemSyntax(symbolicName, LOCALHOST_LOCATION_SPEC, null);
         runLaunchApplicationReferencingLocation(symbolicName, LOCALHOST_LOCATION_TYPE);
 
         deleteCatalogEntity(symbolicName);
@@ -90,7 +167,7 @@ public class CatalogYamlLocationTest extends AbstractYamlTest {
     @Test
     public void testLaunchApplicationReferencingOsgiLocation() throws Exception {
         String symbolicName = "my.catalog.location.id.launch";
-        addCatalogOSGiLocation(symbolicName, SIMPLE_LOCATION_TYPE);
+        addCatalogLocation(symbolicName, SIMPLE_LOCATION_TYPE, getOsgiLibraries());
         runLaunchApplicationReferencingLocation(symbolicName, SIMPLE_LOCATION_TYPE);
         
         deleteCatalogEntity(symbolicName);
@@ -114,30 +191,49 @@ public class CatalogYamlLocationTest extends AbstractYamlTest {
         assertEquals(location.getConfig(new BasicConfigKey<String>(String.class, "config3")), "config3");
     }
 
-    private void addCatalogLocation(String symbolicName, String serviceType) {
-        addCatalogLocation(symbolicName, serviceType, ImmutableList.<String>of());
-    }
-
-    private void addCatalogOSGiLocation(String symbolicName, String serviceType) {
+    private List<String> getOsgiLibraries() {
         TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_PATH);
-        addCatalogLocation(symbolicName, serviceType, ImmutableList.of(OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_URL));
+        return ImmutableList.of(OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_URL);
     }
     
-    private void addCatalogLocation(String symbolicName, String serviceType, List<String> libraries) {
+    private void addCatalogLocation(String symbolicName, String locationType, List<String> libraries) {
+        ImmutableList.Builder<String> yaml = ImmutableList.<String>builder().add(
+                "brooklyn.catalog:",
+                "  id: " + symbolicName,
+                "  name: My Catalog Location",
+                "  description: My description",
+                "  version: " + TEST_VERSION);
+        if (libraries!=null && libraries.size() > 0) {
+            yaml.add("  libraries:")
+                .addAll(Lists.transform(libraries, StringFunctions.prepend("  - url: ")));
+        }
+        yaml.add(
+                "  item.type: location",
+                "  item:",
+                "    type: " + locationType,
+                "    brooklyn.config:",
+                "      config1: config1",
+                "      config2: config2");
+        
+        
+        addCatalogItem(yaml.build());
+    }
+
+    private void addCatalogLocationTopLevelItemSyntax(String symbolicName, String locationType, List<String> libraries) {
         ImmutableList.Builder<String> yaml = ImmutableList.<String>builder().add(
                 "brooklyn.catalog:",
                 "  id: " + symbolicName,
                 "  name: My Catalog Location",
                 "  description: My description",
                 "  version: " + TEST_VERSION);
-        if (libraries.size() > 0) {
+        if (libraries!=null && libraries.size() > 0) {
             yaml.add("  libraries:")
                 .addAll(Lists.transform(libraries, StringFunctions.prepend("  - url: ")));
         }
         yaml.add(
                 "",
                 "brooklyn.locations:",
-                "- type: " + serviceType,
+                "- type: " + locationType,
                 "  brooklyn.config:",
                 "    config1: config1",
                 "    config2: config2");

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/eaaca787/usage/camp/src/test/java/io/brooklyn/camp/brooklyn/catalog/CatalogYamlPolicyTest.java
----------------------------------------------------------------------
diff --git a/usage/camp/src/test/java/io/brooklyn/camp/brooklyn/catalog/CatalogYamlPolicyTest.java b/usage/camp/src/test/java/io/brooklyn/camp/brooklyn/catalog/CatalogYamlPolicyTest.java
index d83c11a..b7370be 100644
--- a/usage/camp/src/test/java/io/brooklyn/camp/brooklyn/catalog/CatalogYamlPolicyTest.java
+++ b/usage/camp/src/test/java/io/brooklyn/camp/brooklyn/catalog/CatalogYamlPolicyTest.java
@@ -19,8 +19,6 @@
 package io.brooklyn.camp.brooklyn.catalog;
 
 import static org.testng.Assert.assertEquals;
-
-import brooklyn.test.TestResourceUnavailableException;
 import io.brooklyn.camp.brooklyn.AbstractYamlTest;
 
 import org.testng.annotations.Test;
@@ -31,6 +29,7 @@ import brooklyn.entity.Entity;
 import brooklyn.event.basic.BasicConfigKey;
 import brooklyn.management.osgi.OsgiStandaloneTest;
 import brooklyn.policy.Policy;
+import brooklyn.test.TestResourceUnavailableException;
 
 import com.google.common.collect.Iterables;
 
@@ -43,7 +42,21 @@ public class CatalogYamlPolicyTest extends AbstractYamlTest {
         assertEquals(countCatalogPolicies(), 0);
 
         String symbolicName = "my.catalog.policy.id.load";
-        addCatalogOSGiPolicy(symbolicName, SIMPLE_POLICY_TYPE);
+        addCatalogOsgiPolicy(symbolicName, SIMPLE_POLICY_TYPE);
+
+        CatalogItem<?, ?> item = mgmt().getCatalog().getCatalogItem(symbolicName, TEST_VERSION);
+        assertEquals(item.getSymbolicName(), symbolicName);
+        assertEquals(countCatalogPolicies(), 1);
+
+        deleteCatalogEntity(symbolicName);
+    }
+
+    @Test
+    public void testAddCatalogItemTopLevelSyntax() throws Exception {
+        assertEquals(countCatalogPolicies(), 0);
+
+        String symbolicName = "my.catalog.policy.id.load";
+        addCatalogOsgiPolicyTopLevelSyntax(symbolicName, SIMPLE_POLICY_TYPE);
 
         CatalogItem<?, ?> item = mgmt().getCatalog().getCatalogItem(symbolicName, TEST_VERSION);
         assertEquals(item.getSymbolicName(), symbolicName);
@@ -55,7 +68,7 @@ public class CatalogYamlPolicyTest extends AbstractYamlTest {
     @Test
     public void testLaunchApplicationReferencingPolicy() throws Exception {
         String symbolicName = "my.catalog.policy.id.launch";
-        addCatalogOSGiPolicy(symbolicName, SIMPLE_POLICY_TYPE);
+        addCatalogOsgiPolicy(symbolicName, SIMPLE_POLICY_TYPE);
         Entity app = createAndStartApplication(
             "name: simple-app-yaml",
             "location: localhost",
@@ -78,10 +91,35 @@ public class CatalogYamlPolicyTest extends AbstractYamlTest {
     }
 
     @Test
+    public void testLaunchApplicationReferencingPolicyTopLevelSyntax() throws Exception {
+        String symbolicName = "my.catalog.policy.id.launch";
+        addCatalogOsgiPolicyTopLevelSyntax(symbolicName, SIMPLE_POLICY_TYPE);
+        Entity app = createAndStartApplication(
+            "name: simple-app-yaml",
+            "location: localhost",
+            "services: ",
+            "  - type: brooklyn.entity.basic.BasicEntity\n" +
+            "    brooklyn.policies:\n" +
+            "    - type: " + ver(symbolicName),
+            "      brooklyn.config:",
+            "        config2: config2 override",
+            "        config3: config3");
+
+        Entity simpleEntity = Iterables.getOnlyElement(app.getChildren());
+        Policy policy = Iterables.getOnlyElement(simpleEntity.getPolicies());
+        assertEquals(policy.getPolicyType().getName(), SIMPLE_POLICY_TYPE);
+        assertEquals(policy.getConfig(new BasicConfigKey<String>(String.class, "config1")), "config1");
+        assertEquals(policy.getConfig(new BasicConfigKey<String>(String.class, "config2")), "config2 override");
+        assertEquals(policy.getConfig(new BasicConfigKey<String>(String.class, "config3")), "config3");
+
+        deleteCatalogEntity(symbolicName);
+    }
+    
+    @Test
     public void testLaunchApplicationWithCatalogReferencingOtherCatalog() throws Exception {
         String referencedSymbolicName = "my.catalog.policy.id.referenced";
         String referrerSymbolicName = "my.catalog.policy.id.referring";
-        addCatalogOSGiPolicy(referencedSymbolicName, SIMPLE_POLICY_TYPE);
+        addCatalogOsgiPolicy(referencedSymbolicName, SIMPLE_POLICY_TYPE);
 
         addCatalogItem(
             "brooklyn.catalog:",
@@ -112,7 +150,27 @@ public class CatalogYamlPolicyTest extends AbstractYamlTest {
         deleteCatalogEntity(referencedSymbolicName);
     }
 
-    private void addCatalogOSGiPolicy(String symbolicName, String serviceType) {
+    private void addCatalogOsgiPolicy(String symbolicName, String serviceType) {
+        TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_PATH);
+
+        addCatalogItem(
+            "brooklyn.catalog:",
+            "  id: " + symbolicName,
+            "  name: My Catalog Policy",
+            "  description: My description",
+            "  icon_url: classpath://path/to/myicon.jpg",
+            "  version: " + TEST_VERSION,
+            "  libraries:",
+            "  - url: " + OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_URL,
+            "  item_type: policy",
+            "  item:",
+            "    type: " + serviceType,
+            "    brooklyn.config:",
+            "      config1: config1",
+            "      config2: config2");
+    }
+
+    private void addCatalogOsgiPolicyTopLevelSyntax(String symbolicName, String serviceType) {
         TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_PATH);
 
         addCatalogItem(

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/eaaca787/usage/camp/src/test/java/io/brooklyn/camp/brooklyn/catalog/CatalogYamlTemplateTest.java
----------------------------------------------------------------------
diff --git a/usage/camp/src/test/java/io/brooklyn/camp/brooklyn/catalog/CatalogYamlTemplateTest.java b/usage/camp/src/test/java/io/brooklyn/camp/brooklyn/catalog/CatalogYamlTemplateTest.java
new file mode 100644
index 0000000..e473d2d
--- /dev/null
+++ b/usage/camp/src/test/java/io/brooklyn/camp/brooklyn/catalog/CatalogYamlTemplateTest.java
@@ -0,0 +1,77 @@
+/*
+ * 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 io.brooklyn.camp.brooklyn.catalog;
+
+import static org.testng.Assert.assertEquals;
+import io.brooklyn.camp.brooklyn.AbstractYamlTest;
+
+import org.testng.Assert;
+import org.testng.TestListenerAdapter;
+import org.testng.TestNG;
+import org.testng.annotations.Test;
+
+import brooklyn.catalog.CatalogItem;
+import brooklyn.catalog.CatalogItem.CatalogItemType;
+import brooklyn.management.osgi.OsgiStandaloneTest;
+import brooklyn.management.osgi.OsgiTestResources;
+import brooklyn.test.TestResourceUnavailableException;
+
+
+public class CatalogYamlTemplateTest extends AbstractYamlTest {
+    
+    private static final String SIMPLE_ENTITY_TYPE = OsgiTestResources.BROOKLYN_TEST_OSGI_ENTITIES_SIMPLE_ENTITY;
+
+    @Test
+    public void testAddCatalogItem() throws Exception {
+        TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_PATH);
+        
+        addCatalogItem(
+            "brooklyn.catalog:",
+            "  id: t1",
+            "  item_type: template",
+            "  name: My Catalog App",
+            "  description: My description",
+            "  icon_url: classpath://path/to/myicon.jpg",
+            "  version: " + TEST_VERSION,
+            "  libraries:",
+            "  - url: " + OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_URL,
+            "  item:",
+            "    services:",
+            "    # this sample comment should be included",
+            "    - type: " + SIMPLE_ENTITY_TYPE);
+
+        CatalogItem<?, ?> item = mgmt().getCatalog().getCatalogItem("t1", TEST_VERSION);
+        assertEquals(item.getCatalogItemType(), CatalogItemType.TEMPLATE);
+        Assert.assertTrue(item.getPlanYaml().indexOf("sample comment")>=0,
+            "YAML did not include original comments; it was:\n"+item.getPlanYaml());
+        Assert.assertFalse(item.getPlanYaml().indexOf("description")>=0,
+            "YAML included metadata which should have been excluded; it was:\n"+item.getPlanYaml());
+
+        deleteCatalogEntity("t1");
+    }
+
+    // convenience for running in eclipse when the TestNG plugin drags in old version of snake yaml
+    public static void main(String[] args) {
+        TestListenerAdapter tla = new TestListenerAdapter();
+        TestNG testng = new TestNG();
+        testng.setTestClasses(new Class[] { CatalogYamlTemplateTest.class });
+        testng.addListener(tla);
+        testng.run();
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/eaaca787/usage/jsgui/src/main/webapp/assets/js/view/catalog.js
----------------------------------------------------------------------
diff --git a/usage/jsgui/src/main/webapp/assets/js/view/catalog.js b/usage/jsgui/src/main/webapp/assets/js/view/catalog.js
index 6f75136..68b3c6f 100644
--- a/usage/jsgui/src/main/webapp/assets/js/view/catalog.js
+++ b/usage/jsgui/src/main/webapp/assets/js/view/catalog.js
@@ -24,7 +24,7 @@ define([
     "text!tpl/catalog/details-generic.html",
     "text!tpl/catalog/details-location.html",
     "text!tpl/catalog/add-catalog-entry.html",
-    "text!tpl/catalog/add-entity.html",
+    "text!tpl/catalog/add-yaml.html",
     "text!tpl/catalog/add-location.html",
     "text!tpl/catalog/nav-entry.html",
 
@@ -32,7 +32,7 @@ define([
 ], function(_, $, Backbone, Brooklyn,
         Location, Entity,
         CatalogPageHtml, DetailsEntityHtml, DetailsGenericHtml, LocationDetailsHtml,
-        AddCatalogEntryHtml, AddEntityHtml, AddLocationHtml, EntryHtml) {
+        AddCatalogEntryHtml, AddYamlHtml, AddLocationHtml, EntryHtml) {
 
     // Holds the currently active details type, e.g. applications, policies. Bit of a workaround
     // to share the active view with all instances of AccordionItemView, so clicking the 'reload
@@ -130,15 +130,21 @@ define([
         render: function (initialView) {
             this.$el.html(this.template());
             if (initialView) {
+                if (initialView == "entity") initialView = "yaml";
+                
                 this.$("[data-context='"+initialView+"']").addClass("active");
                 this.showFormForType(initialView)
             }
             return this;
         },
+        clearWithHtml: function(template) {
+            if (this.contextView) this.contextView.close();
+            this.context = undefined;
+            this.$(".btn").removeClass("active");
+            this.$("#catalog-add-form").html(template);
+        },
         beforeClose: function () {
-            if (this.contextView) {
-                this.contextView.close();
-            }
+            if (this.contextView) this.contextView.close();
         },
         showContext: function(event) {
             var $event = $(event.currentTarget);
@@ -152,13 +158,13 @@ define([
         },
         showFormForType: function (type) {
             this.context = type;
-            if (type == "entity") {
-                this.contextView = newEntityForm(this.options.parent);
+            if (type == "yaml" || type == "entity") {
+                this.contextView = newYamlForm(this, this.options.parent);
             } else if (type == "location") {
-                this.contextView = newLocationForm(this.options.parent);
+                this.contextView = newLocationForm(this, this.options.parent);
             } else if (type !== undefined) {
                 console.log("unknown catalog type " + type);
-                this.showFormForType("entity");
+                this.showFormForType("yaml");
                 return;
             }
             Backbone.history.navigate("/v1/catalog/new/" + type);
@@ -166,11 +172,10 @@ define([
         }
     });
 
-    function newEntityForm(parent) {
+    function newYamlForm(addView, addViewParent) {
         return new Brooklyn.view.Form({
-            template: _.template(AddEntityHtml),
+            template: _.template(AddYamlHtml),
             onSubmit: function (model) {
-                console.log("Submit entity", model.get("yaml"));
                 var submitButton = this.$(".catalog-submit-button");
                 // "loading" is an indicator to Bootstrap, not a string to display
                 submitButton.button("loading");
@@ -185,9 +190,13 @@ define([
                     .done(function (data, status, xhr) {
                         // Can extract location of new item with:
                         //model.url = Brooklyn.util.pathOf(xhr.getResponseHeader("Location"));
-                        self.close();  // one of the calls below should draw a different view
-                        parent.loadAccordionItem("entities", data.id);
-                        parent.loadAccordionItem("applications", data.id);
+                        if (_.size(data)==0) {
+                          addView.clearWithHtml( "No items supplied." );
+                        } else {
+                          addView.clearWithHtml( "Added: "+_.escape(_.keys(data).join(", ")) 
+                            + (_.size(data)==1 ? ". Loading..." : "") );
+                          addViewParent.loadAnyAccordionItem(_.size(data)==1 ? _.keys(data)[0] : undefined);
+                        }
                     })
                     .fail(function (xhr, status, error) {
                         submitButton.button("reset");
@@ -201,7 +210,7 @@ define([
     }
 
     // Could adapt to edit existing locations too.
-    function newLocationForm(parent) {
+    function newLocationForm(addView, addViewParent) {
         // Renders with config key list
         var body = new (Backbone.View.extend({
             beforeClose: function() {
@@ -235,10 +244,9 @@ define([
                 submitButton.button("loading");
                 location.set("config", configKeys);
                 location.save()
-                    .done(function (newModel) {
-                        newModel = new Location.Model(newModel);
-                        self.close();  // the call below should draw a different view
-                        parent.loadAccordionItem("locations", newModel.id);
+                    .done(function (data) {
+                        addView.clearWithHtml( "Added: "+data.id+". Loading..." ); 
+                        addViewParent.loadAccordionItem("locations", data.id);
                     })
                     .fail(function (response) {
                         submitButton.button("reset");
@@ -264,7 +272,7 @@ define([
             this.model = model;
         },
         url: function() {
-            return "/v1/catalog/" + this.name;
+            return "/v1/catalog/" + this.name+"?allVersions=true";
         }
     });
 
@@ -509,6 +517,13 @@ define([
             this.setDetailsView(newView);
         },
 
+        loadAnyAccordionItem: function (id) {
+            this.loadAccordionItem("entities", id);
+            this.loadAccordionItem("applications", id);
+            this.loadAccordionItem("policies", id);
+            this.loadAccordionItem("locations", id);
+        },
+
         loadAccordionItem: function (kind, id) {
             if (!this.accordion[kind]) {
                 console.error("No accordion for: " + kind);

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/eaaca787/usage/jsgui/src/main/webapp/assets/tpl/catalog/add-catalog-entry.html
----------------------------------------------------------------------
diff --git a/usage/jsgui/src/main/webapp/assets/tpl/catalog/add-catalog-entry.html b/usage/jsgui/src/main/webapp/assets/tpl/catalog/add-catalog-entry.html
index 6bd8c00..238dd77 100644
--- a/usage/jsgui/src/main/webapp/assets/tpl/catalog/add-catalog-entry.html
+++ b/usage/jsgui/src/main/webapp/assets/tpl/catalog/add-catalog-entry.html
@@ -18,11 +18,11 @@ under the License.
 -->
 <div class="catalog-details">
 
-    <h2>Add a new...</h2>
+    <h2>Add to Catalog</h2>
     <br/>
 
     <div data-toggle="buttons-radio">
-        <button class="btn btn-large show-context" data-context="entity">Entity</button>
+        <button class="btn btn-large show-context" data-context="yaml">YAML</button>
         <button class="btn btn-large show-context" data-context="location">Location</button>
     </div>
 

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/eaaca787/usage/jsgui/src/main/webapp/assets/tpl/catalog/add-entity.html
----------------------------------------------------------------------
diff --git a/usage/jsgui/src/main/webapp/assets/tpl/catalog/add-entity.html b/usage/jsgui/src/main/webapp/assets/tpl/catalog/add-entity.html
deleted file mode 100644
index 9c64e90..0000000
--- a/usage/jsgui/src/main/webapp/assets/tpl/catalog/add-entity.html
+++ /dev/null
@@ -1,30 +0,0 @@
-<!--
-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.
--->
-<form>
-    <label for="new-blueprint">Enter blueprint:</label>
-    <textarea id='new-blueprint' name='yaml' rows='5' cols='15'></textarea>
-
-    <button class='catalog-submit-button btn' data-loading-text='Saving...'>Submit</button>
-    <p class="catalog-save-error hide">
-        <span class="alert-error">
-            <strong>Error:</strong><br/>
-            <span class="catalog-error-message"></span>
-        </span>
-    </p>
-</form>

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/eaaca787/usage/jsgui/src/main/webapp/assets/tpl/catalog/add-yaml.html
----------------------------------------------------------------------
diff --git a/usage/jsgui/src/main/webapp/assets/tpl/catalog/add-yaml.html b/usage/jsgui/src/main/webapp/assets/tpl/catalog/add-yaml.html
new file mode 100644
index 0000000..9c64e90
--- /dev/null
+++ b/usage/jsgui/src/main/webapp/assets/tpl/catalog/add-yaml.html
@@ -0,0 +1,30 @@
+<!--
+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.
+-->
+<form>
+    <label for="new-blueprint">Enter blueprint:</label>
+    <textarea id='new-blueprint' name='yaml' rows='5' cols='15'></textarea>
+
+    <button class='catalog-submit-button btn' data-loading-text='Saving...'>Submit</button>
+    <p class="catalog-save-error hide">
+        <span class="alert-error">
+            <strong>Error:</strong><br/>
+            <span class="catalog-error-message"></span>
+        </span>
+    </p>
+</form>

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/eaaca787/usage/rest-api/src/main/java/brooklyn/rest/api/CatalogApi.java
----------------------------------------------------------------------
diff --git a/usage/rest-api/src/main/java/brooklyn/rest/api/CatalogApi.java b/usage/rest-api/src/main/java/brooklyn/rest/api/CatalogApi.java
index bf097e2..49549fd 100644
--- a/usage/rest-api/src/main/java/brooklyn/rest/api/CatalogApi.java
+++ b/usage/rest-api/src/main/java/brooklyn/rest/api/CatalogApi.java
@@ -64,7 +64,8 @@ public interface CatalogApi {
 
     @Consumes
     @POST
-    @ApiOperation(value = "Add a catalog item (e.g. new type of entity, policy or location) by uploading YAML descriptor", responseClass = "String")
+    @ApiOperation(value = "Add a catalog item (e.g. new type of entity, policy or location) by uploading YAML descriptor "
+        + "Return value is map of ID to CatalogItemSummary, with code 201 CREATED.", responseClass = "Response")
     public Response create(
             @ApiParam(name = "yaml", value = "YAML descriptor of catalog item", required = true)
             @Valid String yaml);
@@ -135,7 +136,9 @@ public interface CatalogApi {
         @ApiParam(name = "regex", value = "Regular expression to search for")
         @QueryParam("regex") @DefaultValue("") String regex,
         @ApiParam(name = "fragment", value = "Substring case-insensitive to search for")
-        @QueryParam("fragment") @DefaultValue("") String fragment);
+        @QueryParam("fragment") @DefaultValue("") String fragment,
+        @ApiParam(name = "allVersions", value = "Include all versions (defaults false, only returning the best version)")
+        @QueryParam("allVersions") @DefaultValue("false") boolean includeAllVersions);
 
     @GET
     @Path("/applications")
@@ -144,7 +147,9 @@ public interface CatalogApi {
             @ApiParam(name = "regex", value = "Regular expression to search for")
             @QueryParam("regex") @DefaultValue("") String regex,
             @ApiParam(name = "fragment", value = "Substring case-insensitive to search for")
-            @QueryParam("fragment") @DefaultValue("") String fragment);
+            @QueryParam("fragment") @DefaultValue("") String fragment,
+            @ApiParam(name = "allVersions", value = "Include all versions (defaults false, only returning the best version)")
+            @QueryParam("allVersions") @DefaultValue("false") boolean includeAllVersions);
 
     /** @deprecated since 0.7.0 use {@link #getEntity(String, String)} */
     @Deprecated
@@ -203,7 +208,9 @@ public interface CatalogApi {
             @ApiParam(name = "regex", value = "Regular expression to search for")
             @QueryParam("regex") @DefaultValue("") String regex,
             @ApiParam(name = "fragment", value = "Substring case-insensitive to search for")
-            @QueryParam("fragment") @DefaultValue("") String fragment);
+            @QueryParam("fragment") @DefaultValue("") String fragment,
+            @ApiParam(name = "allVersions", value = "Include all versions (defaults false, only returning the best version)")
+            @QueryParam("allVersions") @DefaultValue("false") boolean includeAllVersions);
     
     /** @deprecated since 0.7.0 use {@link #getPolicy(String, String)} */
     @Deprecated
@@ -236,7 +243,9 @@ public interface CatalogApi {
             @ApiParam(name = "regex", value = "Regular expression to search for")
             @QueryParam("regex") @DefaultValue("") String regex,
             @ApiParam(name = "fragment", value = "Substring case-insensitive to search for")
-            @QueryParam("fragment") @DefaultValue("") String fragment);
+            @QueryParam("fragment") @DefaultValue("") String fragment,
+            @ApiParam(name = "allVersions", value = "Include all versions (defaults false, only returning the best version)")
+            @QueryParam("allVersions") @DefaultValue("false") boolean includeAllVersions);
     
     /** @deprecated since 0.7.0 use {@link #getLocation(String, String)} */
     @Deprecated

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/eaaca787/usage/rest-api/src/main/java/brooklyn/rest/api/LocationApi.java
----------------------------------------------------------------------
diff --git a/usage/rest-api/src/main/java/brooklyn/rest/api/LocationApi.java b/usage/rest-api/src/main/java/brooklyn/rest/api/LocationApi.java
index a5c5207..5035c19 100644
--- a/usage/rest-api/src/main/java/brooklyn/rest/api/LocationApi.java
+++ b/usage/rest-api/src/main/java/brooklyn/rest/api/LocationApi.java
@@ -41,6 +41,7 @@ import brooklyn.rest.domain.LocationSummary;
 import com.wordnik.swagger.core.ApiOperation;
 import com.wordnik.swagger.core.ApiParam;
 
+@SuppressWarnings("deprecation")
 @Path("/v1/locations")
 @Apidoc("Locations")
 @Produces(MediaType.APPLICATION_JSON)
@@ -79,9 +80,7 @@ public interface LocationApi {
             @DefaultValue("false")
             @QueryParam("full") String fullConfig);
 
-    /**
-     * @deprecated since 0.7.0; use {@link CatalogApi#create(String)}
-     */
+    /** @deprecated since 0.7.0 use {@link CatalogApi#create(String)} with a location definition */
     @POST
     @ApiOperation(value = "Create a new location definition", responseClass = "String")
     @Deprecated

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/eaaca787/usage/rest-api/src/main/java/brooklyn/rest/domain/CatalogEntitySummary.java
----------------------------------------------------------------------
diff --git a/usage/rest-api/src/main/java/brooklyn/rest/domain/CatalogEntitySummary.java b/usage/rest-api/src/main/java/brooklyn/rest/domain/CatalogEntitySummary.java
index c08e82a..268437a 100644
--- a/usage/rest-api/src/main/java/brooklyn/rest/domain/CatalogEntitySummary.java
+++ b/usage/rest-api/src/main/java/brooklyn/rest/domain/CatalogEntitySummary.java
@@ -23,13 +23,19 @@ import java.util.Map;
 import java.util.Set;
 
 import org.codehaus.jackson.annotate.JsonProperty;
+import org.codehaus.jackson.map.annotate.JsonSerialize;
+import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion;
 
 public class CatalogEntitySummary extends CatalogItemSummary {
 
     private static final long serialVersionUID = 1063908984191424539L;
     
+    @JsonSerialize(include=Inclusion.NON_EMPTY)
     private final Set<EntityConfigSummary> config;
+    
+    @JsonSerialize(include=Inclusion.NON_EMPTY)
     private final Set<SensorSummary> sensors;
+    @JsonSerialize(include=Inclusion.NON_EMPTY)
     private final Set<EffectorSummary> effectors;
 
     public CatalogEntitySummary(

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/eaaca787/usage/rest-api/src/main/java/brooklyn/rest/domain/CatalogItemSummary.java
----------------------------------------------------------------------
diff --git a/usage/rest-api/src/main/java/brooklyn/rest/domain/CatalogItemSummary.java b/usage/rest-api/src/main/java/brooklyn/rest/domain/CatalogItemSummary.java
index db979e2..79b1d27 100644
--- a/usage/rest-api/src/main/java/brooklyn/rest/domain/CatalogItemSummary.java
+++ b/usage/rest-api/src/main/java/brooklyn/rest/domain/CatalogItemSummary.java
@@ -23,6 +23,7 @@ import java.net.URI;
 import java.util.Map;
 
 import org.apache.commons.lang.builder.EqualsBuilder;
+import org.codehaus.jackson.annotate.JsonIgnoreProperties;
 import org.codehaus.jackson.annotate.JsonProperty;
 import org.codehaus.jackson.map.annotate.JsonSerialize;
 import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion;
@@ -32,6 +33,8 @@ import com.google.common.collect.ImmutableMap;
 
 /** variant of Catalog*ItemDto objects for JS/JSON serialization;
  * see also, subclasses */
+@JsonIgnoreProperties(ignoreUnknown = true)
+// ignore unknown, ie properties from subclasses (entity)
 public class CatalogItemSummary implements HasId, HasName, Serializable {
 
     private static final long serialVersionUID = -823483595879417681L;

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/eaaca787/usage/rest-api/src/main/java/brooklyn/rest/domain/CatalogPolicySummary.java
----------------------------------------------------------------------
diff --git a/usage/rest-api/src/main/java/brooklyn/rest/domain/CatalogPolicySummary.java b/usage/rest-api/src/main/java/brooklyn/rest/domain/CatalogPolicySummary.java
index f30006a..75cc6ae 100644
--- a/usage/rest-api/src/main/java/brooklyn/rest/domain/CatalogPolicySummary.java
+++ b/usage/rest-api/src/main/java/brooklyn/rest/domain/CatalogPolicySummary.java
@@ -23,6 +23,8 @@ import java.util.Map;
 import java.util.Set;
 
 import org.codehaus.jackson.annotate.JsonProperty;
+import org.codehaus.jackson.map.annotate.JsonSerialize;
+import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion;
 
 import com.google.common.collect.ImmutableSet;
 
@@ -30,6 +32,7 @@ public class CatalogPolicySummary extends CatalogItemSummary {
 
     private static final long serialVersionUID = -588856488327394445L;
     
+    @JsonSerialize(include=Inclusion.NON_EMPTY)
     private final Set<PolicyConfigSummary> config;
 
     public CatalogPolicySummary(