diff --git a/api/src/main/java/org/apache/brooklyn/api/catalog/BrooklynCatalog.java b/api/src/main/java/org/apache/brooklyn/api/catalog/BrooklynCatalog.java index 9c7ed66d20..e03adbf271 100644 --- a/api/src/main/java/org/apache/brooklyn/api/catalog/BrooklynCatalog.java +++ b/api/src/main/java/org/apache/brooklyn/api/catalog/BrooklynCatalog.java @@ -53,6 +53,7 @@ public interface BrooklynCatalog { /** As {@link #getCatalogItem(String, String)} but only looking in legacy catalog * @deprecated since 0.12.0 only provided to allow TypeRegistry to see the legacy items */ + @Deprecated CatalogItem getCatalogItemLegacy(String symbolicName, String version); /** @return Deletes the item with the given {@link CatalogItem#getSymbolicName() @@ -70,6 +71,7 @@ public interface BrooklynCatalog { /** As non-legacy method but only looking in legacy catalog * @deprecated since 0.12.0 only provided to allow TypeRegistry to see the legacy items */ + @Deprecated CatalogItem getCatalogItemLegacy(Class type, String symbolicName, String version); /** @return All items in the catalog @@ -89,6 +91,7 @@ public interface BrooklynCatalog { /** As non-legacy method but only looking in legacy catalog * @deprecated since 0.12.0 only provided to allow TypeRegistry to see the legacy items */ + @Deprecated Iterable> getCatalogItemsLegacy(Predicate> filter); /** persists the catalog item to the object store, if persistence is enabled */ diff --git a/api/src/main/java/org/apache/brooklyn/api/typereg/RegisteredTypeLoadingContext.java b/api/src/main/java/org/apache/brooklyn/api/typereg/RegisteredTypeLoadingContext.java index d37666e820..925b6b1a5d 100644 --- a/api/src/main/java/org/apache/brooklyn/api/typereg/RegisteredTypeLoadingContext.java +++ b/api/src/main/java/org/apache/brooklyn/api/typereg/RegisteredTypeLoadingContext.java @@ -44,7 +44,7 @@ public interface RegisteredTypeLoadingContext { * the instantiator can avoid recursive cycles */ @Nonnull public Set getAlreadyEncounteredTypes(); - /** A loader to use, supplying additional search paths */ + /** A loader to use, supplying preferred or additional bundles and search paths */ @Nullable public BrooklynClassLoadingContext getLoader(); } 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 d1709700d2..f42c0a2234 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 @@ -1407,43 +1407,8 @@ public List> addItems(String yaml, boolean forceUpdat Maybe osgiManager = ((ManagementContextInternal)mgmt).getOsgiManager(); if (osgiManager.isPresent() && AUTO_WRAP_CATALOG_YAML_AS_BUNDLE) { // wrap in a bundle to be managed; need to get bundle and version from yaml - Map cm = BasicBrooklynCatalog.getCatalogMetadata(yaml); - VersionedName vn = BasicBrooklynCatalog.getVersionedName( cm, false ); - if (vn==null) { - // for better legacy compatibiity, if id specified at root use that - String id = (String) cm.get("id"); - if (Strings.isNonBlank(id)) { - vn = VersionedName.fromString(id); - } - vn = new VersionedName(vn!=null && Strings.isNonBlank(vn.getSymbolicName()) ? vn.getSymbolicName() : "brooklyn-catalog-bom-"+Identifiers.makeRandomId(8), - vn!=null && vn.getVersionString()!=null ? vn.getVersionString() : getFirstAs(cm, String.class, "version").or(NO_VERSION)); - } - log.debug("Wrapping supplied BOM as "+vn); - Manifest mf = new Manifest(); - mf.getMainAttributes().putValue(Constants.BUNDLE_SYMBOLICNAME, vn.getSymbolicName()); - mf.getMainAttributes().putValue(Constants.BUNDLE_VERSION, vn.getOsgiVersionString()); - mf.getMainAttributes().putValue(Constants.BUNDLE_MANIFESTVERSION, "2"); - mf.getMainAttributes().putValue(Attributes.Name.MANIFEST_VERSION.toString(), OSGI_MANIFEST_VERSION_VALUE); - mf.getMainAttributes().putValue(BROOKLYN_WRAPPED_BOM_BUNDLE, Boolean.TRUE.toString()); - - BundleMaker bm = new BundleMaker(mgmt); - File bf = bm.createTempBundle(vn.getSymbolicName(), mf, MutableMap.of( - new ZipEntry(CATALOG_BOM), (InputStream) new ByteArrayInputStream(yaml.getBytes())) ); - - OsgiBundleInstallationResult result = null; - try { - result = osgiManager.get().install(null, new FileInputStream(bf), true, true, forceUpdate).get(); - } catch (FileNotFoundException e) { - throw Exceptions.propagate(e); - } finally { - bf.delete(); - } - if (result.getCode().isError()) { - // rollback done by install call above - throw new IllegalStateException(result.getMessage()); - } - uninstallEmptyWrapperBundles(); - return toLegacyCatalogItems(result.getCatalogItemsInstalled()); + OsgiBundleInstallationResult result = addItemsOsgi(yaml, forceUpdate, osgiManager); + return toLegacyCatalogItems(result.getTypesInstalled()); // if all items pertaining to an older anonymous catalog.bom bundle have been overridden // we delete those later; see list of wrapper bundles kept in OsgiManager @@ -1452,10 +1417,73 @@ public List> addItems(String yaml, boolean forceUpdat return addItems(yaml, null, forceUpdate); } + /** Like {@link #addItems(String, boolean)} but returning the {@link OsgiBundleInstallationResult} for use from new environments. + * If not using OSGi the bundle/code/etc fields are null but the types will always be set. */ + @SuppressWarnings("deprecation") + public OsgiBundleInstallationResult addItemsBundleResult(String yaml, boolean forceUpdate) { + Maybe osgiManager = ((ManagementContextInternal)mgmt).getOsgiManager(); + if (osgiManager.isPresent() && AUTO_WRAP_CATALOG_YAML_AS_BUNDLE) { + // wrap in a bundle to be managed; need to get bundle and version from yaml + return addItemsOsgi(yaml, forceUpdate, osgiManager); + + // if all items pertaining to an older anonymous catalog.bom bundle have been overridden + // we delete those later; see list of wrapper bundles kept in OsgiManager + } + // fallback to non-OSGi for tests and other environments + List> items = addItems(yaml, null, forceUpdate); + OsgiBundleInstallationResult result = new OsgiBundleInstallationResult(); + for (CatalogItem ci: items) { + RegisteredType rt = mgmt.getTypeRegistry().get(ci.getId()); + result.getTypesInstalled().add(rt!=null ? rt : RegisteredTypes.of(ci)); + } + return result; + } + + protected OsgiBundleInstallationResult addItemsOsgi(String yaml, boolean forceUpdate, Maybe osgiManager) { + Map cm = BasicBrooklynCatalog.getCatalogMetadata(yaml); + VersionedName vn = BasicBrooklynCatalog.getVersionedName( cm, false ); + if (vn==null) { + // for better legacy compatibiity, if id specified at root use that + String id = (String) cm.get("id"); + if (Strings.isNonBlank(id)) { + vn = VersionedName.fromString(id); + } + vn = new VersionedName(vn!=null && Strings.isNonBlank(vn.getSymbolicName()) ? vn.getSymbolicName() : "brooklyn-catalog-bom-"+Identifiers.makeRandomId(8), + vn!=null && vn.getVersionString()!=null ? vn.getVersionString() : getFirstAs(cm, String.class, "version").or(NO_VERSION)); + } + log.debug("Wrapping supplied BOM as "+vn); + Manifest mf = new Manifest(); + mf.getMainAttributes().putValue(Constants.BUNDLE_SYMBOLICNAME, vn.getSymbolicName()); + mf.getMainAttributes().putValue(Constants.BUNDLE_VERSION, vn.getOsgiVersionString()); + mf.getMainAttributes().putValue(Constants.BUNDLE_MANIFESTVERSION, "2"); + mf.getMainAttributes().putValue(Attributes.Name.MANIFEST_VERSION.toString(), OSGI_MANIFEST_VERSION_VALUE); + mf.getMainAttributes().putValue(BROOKLYN_WRAPPED_BOM_BUNDLE, Boolean.TRUE.toString()); + + BundleMaker bm = new BundleMaker(mgmt); + File bf = bm.createTempBundle(vn.getSymbolicName(), mf, MutableMap.of( + new ZipEntry(CATALOG_BOM), (InputStream) new ByteArrayInputStream(yaml.getBytes())) ); + + OsgiBundleInstallationResult result = null; + try { + result = osgiManager.get().install(null, new FileInputStream(bf), true, true, forceUpdate).get(); + } catch (FileNotFoundException e) { + throw Exceptions.propagate(e); + } finally { + bf.delete(); + } + if (result.getCode().isError()) { + // rollback done by install call above + throw new IllegalStateException(result.getMessage()); + } + uninstallEmptyWrapperBundles(); + return result; + } + @SuppressWarnings("deprecation") - private List> toLegacyCatalogItems(Iterable itemIds) { + private List> toLegacyCatalogItems(Iterable list) { List> result = MutableList.of(); - for (String id: itemIds) { + for (RegisteredType t: list) { + String id = t.getId(); CatalogItem item = CatalogUtils.getCatalogItemOptionalVersion(mgmt, id); if (item==null) { // using new Type Registry (OSGi addition); diff --git a/core/src/main/java/org/apache/brooklyn/core/mgmt/entitlement/Entitlements.java b/core/src/main/java/org/apache/brooklyn/core/mgmt/entitlement/Entitlements.java index 60d998e01f..f7b5f724cc 100644 --- a/core/src/main/java/org/apache/brooklyn/core/mgmt/entitlement/Entitlements.java +++ b/core/src/main/java/org/apache/brooklyn/core/mgmt/entitlement/Entitlements.java @@ -60,6 +60,7 @@ public class Entitlements { // ------------------- individual permissions + // TODO applies to bundles and registered types; should pass object or probably better add more entitlements? public static EntitlementClass SEE_CATALOG_ITEM = new BasicEntitlementClassDefinition("catalog.see", String.class); public static EntitlementClass ADD_CATALOG_ITEM = new BasicEntitlementClassDefinition("catalog.add", Object.class); public static EntitlementClass MODIFY_CATALOG_ITEM = new BasicEntitlementClassDefinition("catalog.modify", StringAndArgument.class); diff --git a/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/OsgiArchiveInstaller.java b/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/OsgiArchiveInstaller.java index 2b837ba007..15430e592d 100644 --- a/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/OsgiArchiveInstaller.java +++ b/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/OsgiArchiveInstaller.java @@ -60,6 +60,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.google.common.base.Function; import com.google.common.base.Objects; import com.google.common.base.Preconditions; import com.google.common.collect.Iterables; @@ -446,7 +447,7 @@ public void run() { Iterable items = mgmt().getTypeRegistry().getMatching(RegisteredTypePredicates.containingBundle(result.getMetadata())); log.debug("Adding items from bundle "+result.getVersionedName()+": "+items); for (RegisteredType ci: items) { - result.catalogItemsInstalled.add(ci.getId()); + result.addType(ci); } } catch (Exception e) { // unable to install new items; rollback bundles @@ -494,15 +495,20 @@ public void run() { log.debug(result.message+" (Brooklyn load deferred)"); } else { startRunnable.run(); - if (!result.catalogItemsInstalled.isEmpty()) { + if (!result.typesInstalled.isEmpty()) { // show fewer info messages, only for 'interesting' and non-deferred installations // (rebind is deferred, as are tests, but REST is not) final int MAX_TO_LIST_EXPLICITLY = 5; - MutableList firstN = MutableList.copyOf(Iterables.limit(result.catalogItemsInstalled, MAX_TO_LIST_EXPLICITLY)); + Iterable firstN = Iterables.transform(MutableList.copyOf(Iterables.limit(result.typesInstalled, MAX_TO_LIST_EXPLICITLY)), + new Function() { + @Override public String apply(RegisteredType input) { + return input.getVersionedName().toString(); + } + }); log.info(result.message+", items: "+firstN+ - (result.catalogItemsInstalled.size() > MAX_TO_LIST_EXPLICITLY ? " (and others, "+result.catalogItemsInstalled.size()+" total)" : "") ); - if (log.isDebugEnabled() && result.catalogItemsInstalled.size()>MAX_TO_LIST_EXPLICITLY) { - log.debug(result.message+", all items: "+result.catalogItemsInstalled); + (result.typesInstalled.size() > MAX_TO_LIST_EXPLICITLY ? " (and others, "+result.typesInstalled.size()+" total)" : "") ); + if (log.isDebugEnabled() && result.typesInstalled.size()>MAX_TO_LIST_EXPLICITLY) { + log.debug(result.message+", all items: "+result.typesInstalled); } } else { log.debug(result.message+" (into Brooklyn), with no catalog items"); diff --git a/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/OsgiBundleInstallationResult.java b/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/OsgiBundleInstallationResult.java index c611553241..05221a6bd2 100644 --- a/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/OsgiBundleInstallationResult.java +++ b/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/OsgiBundleInstallationResult.java @@ -21,6 +21,7 @@ import java.util.List; import org.apache.brooklyn.api.typereg.ManagedBundle; +import org.apache.brooklyn.api.typereg.RegisteredType; import org.apache.brooklyn.util.collections.MutableList; import org.apache.brooklyn.util.osgi.VersionedName; import org.osgi.framework.Bundle; @@ -48,14 +49,21 @@ public enum ResultCode { /** bundle successfully installed to OSGi container but there was an error launching it, * either the OSGi bundle start, catalog items load, or (most commonly) validating the catalog items; * bundle may be installed (currently it is in most/all places, but behaviour TBC) so caller may have to uninstall it */ - ERROR_LAUNCHING_BUNDLE(true); + ERROR_LAUNCHING_BUNDLE(true), + // codes below used for deletion + BUNDLE_REMOVED(false), + ERROR_REMOVING_BUNDLE_IN_USE(true), + ERROR_REMOVING_BUNDLE_OTHER(true); final boolean isError; ResultCode(boolean isError) { this.isError = isError; } public boolean isError() { return isError; } } - final List catalogItemsInstalled = MutableList.of(); + final List typesInstalled = MutableList.of(); + /** @deprecated since 0.13.0 use {@link #typesInstalled} */ + @Deprecated + private final List catalogItemsInstalled = MutableList.of(); public String getMessage() { return message; @@ -69,6 +77,11 @@ public ManagedBundle getMetadata() { public ResultCode getCode() { return code; } + public List getTypesInstalled() { + return typesInstalled; + } + /** @deprecated since 0.13.0 use {@link #getTypesInstalled()} */ + @Deprecated public List getCatalogItemsInstalled() { return ImmutableList.copyOf(catalogItemsInstalled); } @@ -89,4 +102,8 @@ void setIgnoringAlreadyInstalled() { public String toString() { return OsgiBundleInstallationResult.class.getSimpleName()+"["+code+", "+metadata+", "+message+"]"; } + public void addType(RegisteredType ci) { + typesInstalled.add(ci); + catalogItemsInstalled.add(ci.getId()); + } } \ No newline at end of file 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 d0c7800c66..603927a45d 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 @@ -46,6 +46,7 @@ import org.apache.brooklyn.core.BrooklynVersion; import org.apache.brooklyn.core.catalog.internal.CatalogBundleLoader; import org.apache.brooklyn.core.config.ConfigKeys; +import org.apache.brooklyn.core.mgmt.ha.OsgiBundleInstallationResult.ResultCode; import org.apache.brooklyn.core.server.BrooklynServerConfig; import org.apache.brooklyn.core.server.BrooklynServerPaths; import org.apache.brooklyn.core.typereg.RegisteredTypePredicates; @@ -373,6 +374,11 @@ public ReferenceWithError install(@Nullable Manage return installer.install(); } + /** Convenience for {@link #uninstallUploadedBundle(ManagedBundle, boolean)} without forcing, and throwing on error */ + public OsgiBundleInstallationResult uninstallUploadedBundle(ManagedBundle bundleMetadata) { + return uninstallUploadedBundle(bundleMetadata, false).get(); + } + /** * Removes this bundle from Brooklyn management, * removes all catalog items it defined, @@ -382,27 +388,78 @@ public ReferenceWithError install(@Nullable Manage * behaviour of such things is not guaranteed. They will work for many things * but attempts to load new classes may fail. *

- * Callers should typically fail if anything from this bundle is in use. + * Callers should typically fail prior to invoking if anything from this bundle is in use. + *

+ * This does not throw but returns a reference containing errors and result for caller to inspect and handle. */ - public void uninstallUploadedBundle(ManagedBundle bundleMetadata) { - uninstallCatalogItemsFromBundle( bundleMetadata.getVersionedName() ); + public ReferenceWithError uninstallUploadedBundle(ManagedBundle bundleMetadata, boolean force) { + OsgiBundleInstallationResult result = new OsgiBundleInstallationResult(); + result.metadata = bundleMetadata; + List errors = MutableList.of(); + boolean uninstalledItems = false; - if (!managedBundlesRecord.remove(bundleMetadata)) { - throw new IllegalStateException("No such bundle registered: "+bundleMetadata); + try { + try { + Iterable itemsRemoved = uninstallCatalogItemsFromBundle( bundleMetadata.getVersionedName() ); + for (RegisteredType t: itemsRemoved) result.addType(t); + uninstalledItems = true; + } catch (Exception e) { + Exceptions.propagateIfFatal(e); + if (!force) Exceptions.propagate(e); + log.warn("Error uninstalling catalog items of "+bundleMetadata+": "+e); + errors.add(e); + } + + if (!managedBundlesRecord.remove(bundleMetadata)) { + Exception e = new IllegalStateException("No such bundle registered with Brooklyn when uninstalling: "+bundleMetadata); + if (!force) Exceptions.propagate(e); + log.warn(e.getMessage()); + errors.add(e); + } + try { + mgmt.getRebindManager().getChangeListener().onUnmanaged(bundleMetadata); + } catch (Exception e) { + Exceptions.propagateIfFatal(e); + if (!force) Exceptions.propagate(e); + log.warn("Error handling unmanagement of "+bundleMetadata+": "+e); + errors.add(e); + } + + Bundle bundle = framework.getBundleContext().getBundle(bundleMetadata.getOsgiUniqueUrl()); + result.bundle = bundle; + if (bundle==null) { + Exception e = new IllegalStateException("No such bundle installed in OSGi when uninstalling: "+bundleMetadata); + if (!force) Exceptions.propagate(e); + log.warn(e.getMessage()); + errors.add(e); + } else { + try { + bundle.stop(); + bundle.uninstall(); + } catch (BundleException e) { + Exceptions.propagateIfFatal(e); + if (!force) Exceptions.propagate(e); + log.warn("Error stopping and uninstalling "+bundleMetadata+": "+e); + errors.add(e); + } + } + } catch (Exception e) { + Exceptions.propagateIfFatal(e); + if (!force) Exceptions.propagate(e); + log.warn("Error removing "+bundleMetadata+": "+e); + errors.add(e); } - mgmt.getRebindManager().getChangeListener().onUnmanaged(bundleMetadata); - - Bundle bundle = framework.getBundleContext().getBundle(bundleMetadata.getOsgiUniqueUrl()); - if (bundle==null) { - throw new IllegalStateException("No such bundle installed: "+bundleMetadata); - } - try { - bundle.stop(); - bundle.uninstall(); - } catch (BundleException e) { - throw Exceptions.propagate(e); + if (errors.isEmpty()) { + result.message = "Uninstalled "+bundleMetadata+" (type count "+result.typesInstalled.size()+", OSGi "+result.bundle+")"; + result.code = ResultCode.BUNDLE_REMOVED; + return ReferenceWithError.newInstanceWithoutError(result); } + + RuntimeException e = Exceptions.create("Error removing bundle "+bundleMetadata, errors); + result.message = Exceptions.collapseText(e); + result.code = uninstalledItems ? ResultCode.ERROR_REMOVING_BUNDLE_OTHER : ResultCode.ERROR_REMOVING_BUNDLE_IN_USE; + return ReferenceWithError.newInstanceThrowingError(result, e); } @Beta diff --git a/core/src/main/java/org/apache/brooklyn/core/typereg/RegisteredTypePredicates.java b/core/src/main/java/org/apache/brooklyn/core/typereg/RegisteredTypePredicates.java index 0bf3490fb6..e6705efde4 100644 --- a/core/src/main/java/org/apache/brooklyn/core/typereg/RegisteredTypePredicates.java +++ b/core/src/main/java/org/apache/brooklyn/core/typereg/RegisteredTypePredicates.java @@ -20,6 +20,8 @@ import static com.google.common.base.Preconditions.checkNotNull; +import java.util.Set; + import javax.annotation.Nullable; import org.apache.brooklyn.api.entity.Application; @@ -175,6 +177,31 @@ public boolean apply(@Nullable RegisteredType item) { } } + /** Filters for the symbolic name or alias matching the given typeName. */ + public static Predicate nameOrAlias(final String typeName) { + return nameOrAlias(Predicates.equalTo(typeName)); + } + public static Predicate nameOrAlias(final Predicate filter) { + return new NameOrAliasMatches(filter); + } + + private static class NameOrAliasMatches implements Predicate { + private final Predicate filter; + + public NameOrAliasMatches(Predicate filter) { + this.filter = filter; + } + @Override + public boolean apply(@Nullable RegisteredType item) { + if (item==null) return false; + if (filter.apply(item.getSymbolicName())) return true; + for (String alias: item.getAliases()) { + if (filter.apply(alias)) return true; + } + return false; + } + } + public static Predicate tag(final Object tag) { return tags(CollectionFunctionals.any(Predicates.equalTo(tag))); } @@ -194,27 +221,84 @@ public boolean apply(@Nullable RegisteredType item) { } } - public static Predicate anySuperType(final Predicate> filter) { - return new AnySuperTypeMatches(filter); + public static Predicate anySuperType(final Predicate filter) { + return new AnySuperTypeSatisfies(filter); } - @SuppressWarnings({ "unchecked", "rawtypes" }) + /** True for any {@link RegisteredType} which has a type ancestor (or self) + * registered type which is equal to the given {@link RegisteredType} */ + public static Predicate subtypeOf(final RegisteredType filter) { + return anySuperType(Predicates.equalTo(filter)); + } + /** True for any {@link RegisteredType} which has a type ancestor + * class which is equal to or a subtype of the given class */ public static Predicate subtypeOf(final Class filter) { // the assignableFrom predicate checks if this class is assignable from the subsequent *input*. // in other words, we're checking if any input is a subtype of this class - return anySuperType((Predicate)Predicates.assignableFrom(filter)); + return anySuperType(new IsSubtypeOfClass(filter)); + } + private static class IsSubtypeOfClass implements Predicate { + private Class filter; + public IsSubtypeOfClass(Class filter) { + this.filter = filter; + } + @Override + public boolean apply(Object input) { + if (!(input instanceof Class)) return false; + return filter.isAssignableFrom((Class)input); + } + } + /** True for any {@link RegisteredType} which has a type ancestor (or self) + * whose registered type name or ID equals the string, or class name equals the string */ + public static Predicate subtypeOf(final String filter) { + return anySuperType(new EqualsClassOrTypeName(filter)); + } + private static class EqualsClassOrTypeName implements Predicate { + private String filter; + public EqualsClassOrTypeName(String typeOrClassOrName) { + this.filter = typeOrClassOrName; + } + @Override + public boolean apply(Object input) { + if (input instanceof RegisteredType) { + return ((RegisteredType)input).getSymbolicName().equals(filter) || ((RegisteredType)input).getSymbolicName().equals(filter); + } + if (input instanceof Class) input = ((Class)input).getName(); + return filter.equals(input); + } } + /** @deprecated since 0.13.0 use {@link AnySuperTypeSatisfies}, kept for persistence compatibility */ + @SuppressWarnings("unused") + @Deprecated private static class AnySuperTypeMatches implements Predicate { private final Predicate> filter; - @SuppressWarnings({ "rawtypes", "unchecked" }) - private AnySuperTypeMatches(Predicate filter) { + private AnySuperTypeMatches(Predicate> filter) { + this.filter = filter; + } + @Override + public boolean apply(@Nullable RegisteredType item) { + if (item==null) return false; + + Set candidateTypes = item.getSuperTypes(); + for (Object st: candidateTypes) { + if (st instanceof Class && filter.apply((Class)st)) return true; + } + return false; + } + } + + private static class AnySuperTypeSatisfies implements Predicate { + private final Predicate filter; + + private AnySuperTypeSatisfies(Predicate filter) { this.filter = filter; } @Override public boolean apply(@Nullable RegisteredType item) { if (item==null) return false; - return RegisteredTypes.isAnyTypeOrSuperSatisfying(item.getSuperTypes(), filter); + if (filter.apply(item)) return true; + return RegisteredTypes.isAnyTypeOrSuper(item.getSuperTypes(), filter); } } diff --git a/core/src/main/java/org/apache/brooklyn/core/typereg/RegisteredTypes.java b/core/src/main/java/org/apache/brooklyn/core/typereg/RegisteredTypes.java index 275d5c8f87..49ea861233 100644 --- a/core/src/main/java/org/apache/brooklyn/core/typereg/RegisteredTypes.java +++ b/core/src/main/java/org/apache/brooklyn/core/typereg/RegisteredTypes.java @@ -65,7 +65,6 @@ import com.google.common.base.Function; import com.google.common.base.Objects; import com.google.common.base.Predicate; -import com.google.common.base.Predicates; import com.google.common.collect.ComparisonChain; import com.google.common.collect.Iterables; import com.google.common.collect.Ordering; @@ -407,12 +406,50 @@ public static boolean isSubtypeOf(RegisteredType type, Class superType) { * to see whether any inherit from the given {@link Class} */ public static boolean isAnyTypeSubtypeOf(Set candidateTypes, Class superType) { if (superType == Object.class) return true; - return isAnyTypeOrSuperSatisfying(candidateTypes, Predicates.assignableFrom(superType)); + return isAnyTypeOrSuper(candidateTypes, new Predicate() { + @Override + public boolean apply(Object input) { + return input instanceof Class && superType.isAssignableFrom( (Class)input ); + } + }); + } + + /** + * Queries recursively the given types (either {@link Class} or {@link RegisteredType}) + * to see whether any inherit from the given type either in the registry or a java class */ + public static boolean isAnyTypeSubtypeOf(Set candidateTypes, String superType) { + if (Object.class.getName().equals(superType)) return true; + return isAnyTypeOrSuper(candidateTypes, new Predicate() { + @Override + public boolean apply(Object input) { + if (input instanceof Class) input = ((Class)input).getName(); + return superType.equals(input); + } + }); + } + + /** + * Queries recursively the given types (either {@link Class} or {@link RegisteredType}) + * to see whether any superclasses satisfy the given {@link Predicate} comparing as string or class */ + public static boolean isAnyTypeOrSuper(Set candidateTypes, Predicate filter) { + for (Object st: candidateTypes) { + if (filter.apply(st)) return true; + } + for (Object st: candidateTypes) { + if (st instanceof RegisteredType) { + if (isAnyTypeOrSuper(((RegisteredType)st).getSuperTypes(), filter)) return true; + } + } + return false; } /** * Queries recursively the given types (either {@link Class} or {@link RegisteredType}) - * to see whether any java superclasses satisfy the given {@link Predicate} */ + * to see whether any java superclasses satisfy the given {@link Predicate} on the {@link Class} + * @deprecated since 0.13.0 use {@link #isAnyTypeOrSuper(Set, Predicate)} accepting any object in the predicate, + * typically allowing string equivalence although it is valid to restrict to {@link Class} comparison + * (might be stricter in some OSGi cases) */ + @Deprecated public static boolean isAnyTypeOrSuperSatisfying(Set candidateTypes, Predicate> filter) { for (Object st: candidateTypes) { if (st instanceof Class) { diff --git a/rest/rest-api/src/main/java/org/apache/brooklyn/rest/api/BundleApi.java b/rest/rest-api/src/main/java/org/apache/brooklyn/rest/api/BundleApi.java new file mode 100644 index 0000000000..2eb5ed8a96 --- /dev/null +++ b/rest/rest-api/src/main/java/org/apache/brooklyn/rest/api/BundleApi.java @@ -0,0 +1,147 @@ +/* + * 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.rest.api; + +import java.util.List; + +import javax.validation.Valid; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.DefaultValue; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.apache.brooklyn.rest.domain.BundleInstallationRestResult; +import org.apache.brooklyn.rest.domain.BundleSummary; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; + +@Path("/catalog/bundles") +@Api("Catalog Bundles") +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +public interface BundleApi { + + @GET + @ApiOperation(value = "List bundles registered in the system including their types", + response = BundleSummary.class, + responseContainer = "List") + public List list( + @ApiParam(name = "versions", value = "Whether to list 'latest' for each symbolic-name or 'all' versions", + required = false, defaultValue = "latest") + @QueryParam("versions") + String versions, + @ApiParam(name = "detail", value = "Whether to include types and other detail info, default 'false'", + required = false, defaultValue = "false") + @QueryParam("detail") + boolean detail); + + @Path("/{symbolicName}") + @GET + @ApiOperation(value = "Get summaries for all versions of the given bundle, with more recent ones first (preferring non-SNAPSHOTs)", + response = BundleSummary.class, + responseContainer = "List") + public List listVersions( + @ApiParam(name = "symbolicName", value = "Bundle name to query", required = true) + @PathParam("symbolicName") + String symbolicName, + @ApiParam(name = "detail", value = "Whether to include types and other detail info, default 'false'", + required = false, defaultValue = "false") + @QueryParam("detail") + boolean detail); + + @Path("/{symbolicName}/{version}") + @GET + @ApiOperation(value = "Get detail on a specific bundle given its symbolic name and version", + response = BundleSummary.class) + public BundleSummary detail( + @ApiParam(name = "symbolicName", value = "Bundle name to query", required = true) + @PathParam("symbolicName") + String symbolicName, + @ApiParam(name = "version", value = "Version to query", required = true) + @PathParam("version") + String version); + + + @Path("/{symbolicName}/{version}") + @DELETE + @ApiOperation(value = "Removes a bundle, unregistering all the types it declares", + response = BundleInstallationRestResult.class) + public BundleInstallationRestResult remove( + @ApiParam(name = "symbolicName", value = "Bundle name to query", required = true) + @PathParam("symbolicName") + String symbolicName, + @ApiParam(name = "version", value = "Version to query", required = true) + @PathParam("version") + String version, + @ApiParam(name = "force", value = "Whether to forcibly remove it, even if in use and/or errors", required = false, defaultValue = "false") + @QueryParam("force") @DefaultValue("false") + Boolean force); + + @POST + @Consumes({MediaType.APPLICATION_JSON, "application/x-yaml", + // see http://stackoverflow.com/questions/332129/yaml-mime-type + "text/yaml", "text/x-yaml", "application/yaml"}) + @ApiOperation( + value = "Adds types to the registry from a given BOM YAML/JSON descriptor (creating a bundle with just this file in it)", + response = BundleInstallationRestResult.class + ) + @ApiResponses(value = { + @ApiResponse(code = 400, message = "Error processing the given YAML"), + @ApiResponse(code = 201, message = "Items added successfully") + }) + public Response createFromYaml( + @ApiParam(name = "yaml", value = "BOM YAML declaring the types to be installed", required = true) + @Valid String yaml, + @ApiParam(name="force", value="Force installation including replacing any different bundle of the same name and version") + @QueryParam("force") @DefaultValue("false") + Boolean forceUpdate); + + @POST + @Consumes({"application/x-zip", "application/x-jar"}) + @ApiOperation( + value = "Adds types to the registry from a given JAR or ZIP", + notes = "Accepts either an OSGi bundle JAR, or ZIP which will be turned into bundle JAR. Either format must " + + "contain a catalog.bom at the root of the archive, which must contain the bundle and version key.", + response = BundleInstallationRestResult.class) + @ApiResponses(value = { + @ApiResponse(code = 400, message = "Error processing the given archive, or the catalog.bom is invalid"), + @ApiResponse(code = 201, message = "Catalog items added successfully") + }) + public Response createFromArchive( + @ApiParam( + name = "archive", + value = "Bundle to install, in ZIP or JAR format, requiring catalog.bom containing bundle name and version", + required = true) + byte[] archive, + @ApiParam(name = "force", value = "Whether to forcibly remove it, even if in use and/or errors", required = false, defaultValue = "false") + @QueryParam("force") @DefaultValue("false") + Boolean force); + +} diff --git a/rest/rest-api/src/main/java/org/apache/brooklyn/rest/api/CatalogApi.java b/rest/rest-api/src/main/java/org/apache/brooklyn/rest/api/CatalogApi.java index 698f97fb56..6499f14818 100644 --- a/rest/rest-api/src/main/java/org/apache/brooklyn/rest/api/CatalogApi.java +++ b/rest/rest-api/src/main/java/org/apache/brooklyn/rest/api/CatalogApi.java @@ -153,6 +153,9 @@ public Response createFromUpload( @QueryParam("forceUpdate") @DefaultValue("false") boolean forceUpdate); + /** @deprecated since 0.13.0 delete the bundle via DELETE /catalog/bundles/xxx */ + // but we will probably keep this around for a while as many places use it + @Deprecated @DELETE @Path("/applications/{symbolicName}/{version}") @ApiOperation( @@ -170,6 +173,9 @@ public void deleteApplication( @ApiParam(name = "version", value = "The version identifier of the application or template to delete", required = true) @PathParam("version") String version) throws Exception; + /** @deprecated since 0.13.0 delete the bundle via DELETE /catalog/bundles/xxx */ + // but we will probably keep this around for a while as many places use it + @Deprecated @DELETE @Path("/entities/{symbolicName}/{version}") @ApiOperation( @@ -187,6 +193,9 @@ public void deleteEntity( @ApiParam(name = "version", value = "The version identifier of the entity or template to delete", required = true) @PathParam("version") String version) throws Exception; + /** @deprecated since 0.13.0 delete the bundle via DELETE /catalog/bundles/xxx */ + // but we will probably keep this around for a while as many places use it + @Deprecated @DELETE @Path("/policies/{policyId}/{version}") @ApiOperation( @@ -203,6 +212,9 @@ public void deletePolicy( @ApiParam(name = "version", value = "The version identifier of the policy to delete", required = true) @PathParam("version") String version) throws Exception; + /** @deprecated since 0.13.0 delete the bundle via DELETE /catalog/bundles/xxx */ + // but we will probably keep this around for a while as many places use it + @Deprecated @DELETE @Path("/locations/{locationId}/{version}") @ApiOperation( @@ -220,6 +232,9 @@ public void deleteLocation( @ApiParam(name = "version", value = "The version identifier of the location to delete", required = true) @PathParam("version") String version) throws Exception; + /** @deprecated since 0.13.0 use /catalog/bundles and /catalog/types?supertype=... */ + // but we will probably keep this around for a while as many places use it + @Deprecated @GET @Path("/entities") @ApiOperation(value = "List available entity types optionally matching a query", @@ -234,6 +249,9 @@ public List listEntities( @QueryParam("allVersions") @DefaultValue("false") boolean includeAllVersions); // bad name - it is just templates + /** @deprecated since 0.13.0 use /catalog/bundles and /catalog/types?supertype=... */ + // but we will probably keep this around for a while as many places use it + @Deprecated @GET @Path("/applications") @ApiOperation(value = "Fetch a list of templates (for applications) optionally matching a query", @@ -247,6 +265,9 @@ public List listApplications( @ApiParam(name = "allVersions", value = "Include all versions (defaults false, only returning the best version)") @QueryParam("allVersions") @DefaultValue("false") boolean includeAllVersions); + /** @deprecated since 0.13.0 use /catalog/bundles and /catalog/types?supertype=... */ + // but we will probably keep this around for a while as many places use it + @Deprecated @GET @Path("/entities/{symbolicName}/{version}") @ApiOperation( @@ -265,6 +286,9 @@ public CatalogEntitySummary getEntity( @ApiParam(name = "version", value = "The version identifier of the entity or template to retrieve", required = true) @PathParam("version") String version) throws Exception; + /** @deprecated since 0.13.0 use /catalog/bundles and /catalog/types?supertype=... */ + // but we will probably keep this around for a while as many places use it + @Deprecated @GET @Path("/applications/{symbolicName}/{version}") @ApiOperation( @@ -283,6 +307,9 @@ public CatalogEntitySummary getApplication( @ApiParam(name = "version", value = "The version identifier of the application to retrieve", required = true) @PathParam("version") String version) throws Exception; + /** @deprecated since 0.13.0 use /catalog/bundles and /catalog/types?supertype=... */ + // but we will probably keep this around for a while as many places use it + @Deprecated @GET @Path("/policies") @ApiOperation(value = "List available policies optionally matching a query", @@ -296,6 +323,9 @@ public List listPolicies( @ApiParam(name = "allVersions", value = "Include all versions (defaults false, only returning the best version)") @QueryParam("allVersions") @DefaultValue("false") boolean includeAllVersions); + /** @deprecated since 0.13.0 use /catalog/bundles and /catalog/types?supertype=... */ + // but we will probably keep this around for a while as many places use it + @Deprecated @GET @Path("/policies/{policyId}/{version}") @ApiOperation( @@ -313,6 +343,9 @@ public CatalogPolicySummary getPolicy( @ApiParam(name = "version", value = "The version identifier of the application to retrieve", required = true) @PathParam("version") String version) throws Exception; + /** @deprecated since 0.13.0 use /catalog/bundles and /catalog/types?supertype=... */ + // but we will probably keep this around for a while as many places use it + @Deprecated @GET @Path("/locations") @ApiOperation(value = "List available locations optionally matching a query", @@ -326,6 +359,9 @@ public List listLocations( @ApiParam(name = "allVersions", value = "Include all versions (defaults false, only returning the best version)") @QueryParam("allVersions") @DefaultValue("false") boolean includeAllVersions); + /** @deprecated since 0.13.0 use /catalog/bundles and /catalog/types?supertype=... */ + // but we will probably keep this around for a while as many places use it + @Deprecated @GET @Path("/locations/{locationId}/{version}") @ApiOperation( @@ -361,6 +397,10 @@ public Response getIcon( @ApiParam(name = "version", value = "version identifier of catalog item (application, entity, policy, location)", required=true) @PathParam("version") String version); + /** @deprecated since 0.13.0 use /catalog/bundles and /catalog/types?supertype=...; + * deprecation/disabling needs to be done in the bundle, and we might support deprecating/disabling bundles */ + // but we will probably keep this around for a while as many places use it + @Deprecated @POST @Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_OCTET_STREAM, MediaType.TEXT_PLAIN}) @ApiResponses(value = { @@ -373,6 +413,10 @@ public void setDeprecated( @ApiParam(name = "deprecated", value = "Whether or not the catalog item is deprecated", required = true) boolean deprecated); + /** @deprecated since 0.13.0 use /catalog/bundles and /catalog/types?supertype=...; + * deprecation/disabling needs to be done in the bundle, and we might support deprecating/disabling bundles */ + // but we will probably keep this around for a while as many places use it + @Deprecated @POST @Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_OCTET_STREAM, MediaType.TEXT_PLAIN}) @ApiResponses(value = { @@ -385,6 +429,9 @@ public void setDisabled( @ApiParam(name = "disabled", value = "Whether or not the catalog item is disabled", required = true) boolean disabled); + /** @deprecated since 0.13.0 use /catalog/bundles and /catalog/types?supertype=... */ + // but we will probably keep this around for a while as many places use it + @Deprecated @GET @Path("/enrichers") @ApiOperation(value = "List available enrichers types optionally matching a query", @@ -398,6 +445,9 @@ public List listEnrichers( @ApiParam(name = "allVersions", value = "Include all versions (defaults false, only returning the best version)") @QueryParam("allVersions") @DefaultValue("false") boolean includeAllVersions); + /** @deprecated since 0.13.0 use /catalog/bundles and /catalog/types?supertype=... */ + // but we will probably keep this around for a while as many places use it + @Deprecated @GET @Path("/enrichers/{enricherId}/{version}") @ApiOperation(value = "Fetch an enricher's definition from the catalog", @@ -412,6 +462,9 @@ public CatalogEnricherSummary getEnricher( @ApiParam(name = "version", value = "The version identifier of the enricher to retrieve", required = true) @PathParam("version") String version) throws Exception; + /** @deprecated since 0.13.0 delete the bundle via DELETE /catalog/bundles/xxx */ + // but we will probably keep this around for a while as many places use it + @Deprecated @DELETE @Path("/enrichers/{enricherId}/{version}") @ApiOperation(value = "Deletes a specific version of an enricher's definition from the catalog") diff --git a/rest/rest-api/src/main/java/org/apache/brooklyn/rest/api/TypeApi.java b/rest/rest-api/src/main/java/org/apache/brooklyn/rest/api/TypeApi.java new file mode 100644 index 0000000000..914dce2c6f --- /dev/null +++ b/rest/rest-api/src/main/java/org/apache/brooklyn/rest/api/TypeApi.java @@ -0,0 +1,100 @@ +/* + * 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.rest.api; + +import java.util.List; + +import javax.ws.rs.Consumes; +import javax.ws.rs.DefaultValue; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.apache.brooklyn.rest.domain.TypeDetail; +import org.apache.brooklyn.rest.domain.TypeSummary; + +import com.google.common.annotations.Beta; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; + +@Path("/catalog/types") +@Api("Catalog Types") +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +@Beta +public interface TypeApi { + + @GET + @ApiOperation(value = "List types registered in the system", + response = TypeSummary.class, + responseContainer = "List") + public List list( + @ApiParam(name = "supertype", value = "Supertype to require (beta, currently intended only for 'entity', 'policy', 'enricher', and 'location')", required = false) + @QueryParam("supertype") + String supertype, + @ApiParam(name = "versions", value = "Whether to list 'latest' of each symbolic-name or 'all' versions", + required = false, defaultValue = "latest") + @QueryParam("versions") + String versions, + @ApiParam(name = "regex", value = "Regular expression to search for (in name and description)") + @QueryParam("regex") @DefaultValue("") String regex, + @ApiParam(name = "fragment", value = "Substring case-insensitive to search for (in name and description)") + @QueryParam("fragment") @DefaultValue("") String fragment); + + @Path("/{nameOrAlias}") + @GET + @ApiOperation(value = "Get summaries for all versions and instances of a given type or alias, with best match first", + response = TypeSummary.class, + responseContainer = "List") + public List listVersions( + @ApiParam(name = "nameOrAlias", value = "Type name to query", required = true) + @PathParam("nameOrAlias") + String nameOrAlias); + + @Path("/{symbolicName}/{version}") + @GET + @ApiOperation(value = "Get detail on a given type and version, allowing 'latest' to match the most recent version (preferring non-SNAPSHOTs)", + response = TypeDetail.class) + public TypeDetail detail( + @ApiParam(name = "symbolicName", value = "Type name to query", required = true) + @PathParam("symbolicName") + String symbolicName, + @ApiParam(name = "version", value = "Version to query", required = true) + @PathParam("version") + String version); + + @Path("/{symbolicName}/{version}/icon") + @GET + @ApiOperation(value = "Returns the icon image registered for this item") + @Produces("application/image") + public Response icon( + @ApiParam(name = "symbolicName", value = "Type name to query", required = true) + @PathParam("symbolicName") + String symbolicName, + @ApiParam(name = "version", value = "Version to query", required = true) + @PathParam("version") + String version); + +} diff --git a/rest/rest-api/src/main/java/org/apache/brooklyn/rest/domain/BundleInstallationRestResult.java b/rest/rest-api/src/main/java/org/apache/brooklyn/rest/domain/BundleInstallationRestResult.java new file mode 100644 index 0000000000..48f15ee782 --- /dev/null +++ b/rest/rest-api/src/main/java/org/apache/brooklyn/rest/domain/BundleInstallationRestResult.java @@ -0,0 +1,67 @@ +/* + * 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.rest.domain; + +import java.util.Map; + +import org.apache.brooklyn.core.mgmt.ha.OsgiBundleInstallationResult; +import org.apache.brooklyn.core.mgmt.ha.OsgiBundleInstallationResult.ResultCode; +import org.apache.brooklyn.util.collections.MutableMap; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; + +public class BundleInstallationRestResult { + // as Osgi result, but without bundle, and with maps of catalog items installed + + private final String message; + private final String bundle; + private final OsgiBundleInstallationResult.ResultCode code; + @JsonInclude(Include.NON_EMPTY) + private Map types = MutableMap.of(); + + /** json internal only */ + @SuppressWarnings("unused") + private BundleInstallationRestResult() { + this.message = null; + this.bundle = null; + this.code = null; + } + + public BundleInstallationRestResult(String message, String bundle, ResultCode code) { + this.message = message; + this.bundle = bundle; + this.code = code; + } + + public String getMessage() { + return message; + } + public String getBundle() { + return bundle; + } + public OsgiBundleInstallationResult.ResultCode getCode() { + return code; + } + + public Map getTypes() { + return types; + } + +} \ No newline at end of file diff --git a/rest/rest-api/src/main/java/org/apache/brooklyn/rest/domain/BundleSummary.java b/rest/rest-api/src/main/java/org/apache/brooklyn/rest/domain/BundleSummary.java new file mode 100644 index 0000000000..c8f442be0a --- /dev/null +++ b/rest/rest-api/src/main/java/org/apache/brooklyn/rest/domain/BundleSummary.java @@ -0,0 +1,106 @@ +/* + * 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.rest.domain; + +import java.util.List; +import java.util.Map; + +import org.apache.brooklyn.api.typereg.ManagedBundle; +import org.apache.brooklyn.api.typereg.OsgiBundleWithUrl; +import org.apache.brooklyn.util.collections.MutableList; +import org.apache.brooklyn.util.collections.MutableMap; +import org.apache.brooklyn.util.javalang.JavaClassNames; +import org.apache.brooklyn.util.text.NaturalOrderComparator; +import org.apache.brooklyn.util.text.VersionComparator; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.google.common.collect.ComparisonChain; + +/** Summary info of {@link ManagedBundle} bundles in the catalog providing types, + * essentially the symbolic name and version. + * Extra fields listing the types may be added. + *

+ * These are comparable in alpha-then-version order with most recent preferring non-snapshot versions first, + * as per {@link VersionComparator}. */ +public class BundleSummary implements Comparable { + + private final String symbolicName; + private final String version; + + @JsonInclude(value=Include.ALWAYS) + private final List types = MutableList.of(); + + // not exported directly, but used to provide other top-level json fields + // for specific types + @JsonIgnore + private final Map others = MutableMap.of(); + + /** for json deserialization */ + BundleSummary() { + symbolicName = null; + version = null; + } + + public BundleSummary(OsgiBundleWithUrl bundle) { + symbolicName = bundle.getSymbolicName(); + version = bundle.getSuppliedVersionString(); + } + + /** Mutable map of other top-level metadata included on this DTO (eg listing config keys or effectors) */ + @JsonAnyGetter + public Map getExtraFields() { + return others; + } + @JsonAnySetter + public void setExtraField(String name, Object value) { + others.put(name, value); + } + + public void addType(TypeSummary type) { types.add(type); } + + @Override + public int compareTo(BundleSummary o2) { + BundleSummary o1 = this; + return ComparisonChain.start() + .compare(o1.symbolicName, o2.symbolicName, NaturalOrderComparator.INSTANCE) + .compare(o2.version, o1.version, VersionComparator.INSTANCE) + .result(); + } + + public String getSymbolicName() { + return symbolicName; + } + + public String getVersion() { + return version; + } + + public List getTypes() { + return types; + } + + @Override + public String toString() { + return JavaClassNames.cleanSimpleClassName(this)+"["+symbolicName+":"+version+"]"; + } +} diff --git a/rest/rest-api/src/main/java/org/apache/brooklyn/rest/domain/ConfigSummary.java b/rest/rest-api/src/main/java/org/apache/brooklyn/rest/domain/ConfigSummary.java index 22e88d6d36..77bf5fdd25 100644 --- a/rest/rest-api/src/main/java/org/apache/brooklyn/rest/domain/ConfigSummary.java +++ b/rest/rest-api/src/main/java/org/apache/brooklyn/rest/domain/ConfigSummary.java @@ -29,32 +29,49 @@ import org.apache.brooklyn.config.ConfigKey; import org.apache.brooklyn.util.collections.Jsonya; +import org.apache.brooklyn.util.text.StringPredicates; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.google.common.base.Function; +import com.google.common.base.Predicates; import com.google.common.collect.FluentIterable; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; -public abstract class ConfigSummary implements HasName, Serializable { +public class ConfigSummary implements HasName, Serializable { private static final long serialVersionUID = -2831796487073496730L; private final String name; private final String type; - @JsonSerialize(include = JsonSerialize.Inclusion.NON_NULL) + @JsonInclude(Include.NON_NULL) private final Object defaultValue; - @JsonSerialize(include = JsonSerialize.Inclusion.NON_NULL) + @JsonInclude(Include.NON_NULL) private final String description; @JsonSerialize private final boolean reconfigurable; - @JsonSerialize(include = JsonSerialize.Inclusion.NON_NULL) + @JsonInclude(Include.NON_NULL) private final String label; - @JsonSerialize(include = JsonSerialize.Inclusion.NON_NULL) + @JsonInclude(Include.NON_NULL) private final Double priority; - @JsonSerialize(include = JsonSerialize.Inclusion.NON_NULL) + @JsonInclude(Include.NON_EMPTY) private final List> possibleValues; + @JsonInclude(Include.NON_NULL) + private final Boolean pinned; + @JsonInclude(Include.NON_EMPTY) + private final List constraints; + @JsonInclude(Include.NON_EMPTY) + private final Map links; + + // json deserialization + ConfigSummary() { + this(null, null, null, null, false, null, null, null, null, null, null); + } + protected ConfigSummary( @JsonProperty("name") String name, @JsonProperty("type") String type, @@ -63,7 +80,10 @@ protected ConfigSummary( @JsonProperty("reconfigurable") boolean reconfigurable, @JsonProperty("label") String label, @JsonProperty("priority") Double priority, - @JsonProperty("possibleValues") List> possibleValues) { + @JsonProperty("possibleValues") List> possibleValues, + @JsonProperty("pinned") Boolean pinned, + @JsonProperty("constraints") List constraints, + @JsonProperty("links") Map links) { this.name = name; this.type = type; this.description = description; @@ -72,14 +92,12 @@ protected ConfigSummary( this.label = label; this.priority = priority; this.possibleValues = possibleValues; + this.pinned = pinned; + this.constraints = (constraints == null) ? ImmutableList.of() : ImmutableList.copyOf(constraints); + this.links = (links == null) ? ImmutableMap.of() : ImmutableMap.copyOf(links); } - protected ConfigSummary(ConfigKey config) { - this(config, null, null); - } - - @SuppressWarnings("rawtypes") - protected ConfigSummary(ConfigKey config, String label, Double priority) { + public ConfigSummary(ConfigKey config, String label, Double priority, Boolean pinned, Map links) { this.name = config.getName(); this.description = config.getDescription(); this.reconfigurable = config.isReconfigurable(); @@ -90,15 +108,19 @@ protected ConfigSummary(ConfigKey config, String label, Double priority) { */ this.label = label; this.priority = priority; + this.pinned = pinned; + this.constraints = !config.getConstraint().equals(Predicates.alwaysTrue()) + ? ImmutableList.of((config.getConstraint().getClass().equals(StringPredicates.isNonBlank().getClass()) ? "required" : config.getConstraint().toString())) + : ImmutableList.of(); if (config.getType().isEnum()) { this.type = Enum.class.getName(); - this.defaultValue = (config.getDefaultValue() == null) ? null : ((Enum) config.getDefaultValue()).name(); + this.defaultValue = (config.getDefaultValue() == null) ? null : ((Enum) config.getDefaultValue()).name(); this.possibleValues = FluentIterable - .from(Arrays.asList((Enum[])(config.getType().getEnumConstants()))) - .transform(new Function>() { + .from(Arrays.asList((Enum[])(config.getType().getEnumConstants()))) + .transform(new Function, Map>() { @Nullable @Override - public Map apply(@Nullable Enum input) { + public Map apply(@Nullable Enum input) { return ImmutableMap.of( "value", input != null ? input.name() : null, "description", input != null ? input.toString() : null); @@ -109,6 +131,7 @@ public Map apply(@Nullable Enum input) { this.defaultValue = Jsonya.convertToJsonPrimitive(config.getDefaultValue()); this.possibleValues = null; } + this.links = (links == null) ? ImmutableMap.of() : ImmutableMap.copyOf(links); } @Override @@ -145,7 +168,17 @@ public List> getPossibleValues() { return possibleValues; } - public abstract Map getLinks(); + public Boolean isPinned() { + return pinned; + } + + public List getConstraints() { + return constraints; + } + + public Map getLinks() { + return links; + } @Override public boolean equals(Object o) { @@ -159,12 +192,16 @@ public boolean equals(Object o) { Objects.equals(description, that.description) && Objects.equals(label, that.label) && Objects.equals(priority, that.priority) && - Objects.equals(possibleValues, that.possibleValues); + Objects.equals(possibleValues, that.possibleValues) && + Objects.equals(pinned, that.pinned) && + Objects.equals(constraints, that.constraints) && + Objects.equals(links, that.links); } @Override public int hashCode() { - return Objects.hash(name, type, defaultValue, description, reconfigurable, label, priority, possibleValues); + return Objects.hash(name, type, defaultValue, description, reconfigurable, label, priority, + possibleValues, pinned, constraints); } @Override @@ -178,6 +215,8 @@ public String toString() { ", label='" + label + '\'' + ", priority=" + priority + ", possibleValues=" + possibleValues + + ", pinned=" + pinned + + ", constraints=" + constraints + '}'; } } diff --git a/rest/rest-api/src/main/java/org/apache/brooklyn/rest/domain/EnricherConfigSummary.java b/rest/rest-api/src/main/java/org/apache/brooklyn/rest/domain/EnricherConfigSummary.java index 276cd6b354..de8e4d3dc4 100644 --- a/rest/rest-api/src/main/java/org/apache/brooklyn/rest/domain/EnricherConfigSummary.java +++ b/rest/rest-api/src/main/java/org/apache/brooklyn/rest/domain/EnricherConfigSummary.java @@ -20,60 +20,20 @@ import java.net.URI; import java.util.Map; -import java.util.Objects; import org.apache.brooklyn.config.ConfigKey; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import com.google.common.collect.ImmutableMap; - +/** @deprecated since 0.13.0 no different to ConfigSummary, use that */ +@Deprecated public class EnricherConfigSummary extends ConfigSummary { private static final long serialVersionUID = 4339330833863794513L; - @JsonSerialize(include = JsonSerialize.Inclusion.NON_NULL) - private final Map links; - - public EnricherConfigSummary( - @JsonProperty("name") String name, - @JsonProperty("type") String type, - @JsonProperty("description") String description, - @JsonProperty("defaultValue") Object defaultValue, - @JsonProperty("reconfigurable") boolean reconfigurable, - @JsonProperty("links") Map links) { - super(name, type, description, defaultValue, reconfigurable, null, null, null); - this.links = (links == null) ? ImmutableMap.of() : ImmutableMap.copyOf(links); - } - + @SuppressWarnings("unused") // json deserialization + private EnricherConfigSummary() {} + public EnricherConfigSummary(ConfigKey config, String label, Double priority, Map links) { - super(config, label, priority); - this.links = links != null ? ImmutableMap.copyOf(links) : null; - } - - @Override - public Map getLinks() { - return links; + super(config, label, priority, null, links); } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof EnricherConfigSummary)) return false; - if (!super.equals(o)) return false; - EnricherConfigSummary that = (EnricherConfigSummary) o; - return Objects.equals(links, that.links); - } - - @Override - public int hashCode() { - return Objects.hash(super.hashCode(), links); - } - - @Override - public String toString() { - return "EnricherConfigSummary{" + - "links=" + links + - '}'; - } } diff --git a/rest/rest-api/src/main/java/org/apache/brooklyn/rest/domain/EntityConfigSummary.java b/rest/rest-api/src/main/java/org/apache/brooklyn/rest/domain/EntityConfigSummary.java index aeba294dbb..131ae65699 100644 --- a/rest/rest-api/src/main/java/org/apache/brooklyn/rest/domain/EntityConfigSummary.java +++ b/rest/rest-api/src/main/java/org/apache/brooklyn/rest/domain/EntityConfigSummary.java @@ -21,30 +21,17 @@ import java.net.URI; import java.util.List; import java.util.Map; -import java.util.Objects; import org.apache.brooklyn.config.ConfigKey; -import org.apache.brooklyn.util.text.StringPredicates; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import com.google.common.base.Predicates; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; +/** @deprecated since 0.13.0 no different to {@link ConfigSummary}, use that */ +@Deprecated public class EntityConfigSummary extends ConfigSummary { private static final long serialVersionUID = -1336134336883426030L; - @JsonSerialize(include = JsonSerialize.Inclusion.NON_NULL) - private final Boolean pinned; - - @JsonSerialize(include = JsonSerialize.Inclusion.NON_NULL) - private final List constraints; - - @JsonSerialize(include = JsonSerialize.Inclusion.NON_NULL) - private final Map links; - public EntityConfigSummary( @JsonProperty("name") String name, @JsonProperty("type") String type, @@ -57,57 +44,11 @@ public EntityConfigSummary( @JsonProperty("pinned") Boolean pinned, @JsonProperty("constraints") List constraints, @JsonProperty("links") Map links) { - super(name, type, description, defaultValue, reconfigurable, label, priority, possibleValues); - this.pinned = pinned; - this.constraints = (constraints == null) ? ImmutableList.of() : ImmutableList.copyOf(constraints); - this.links = (links == null) ? ImmutableMap.of() : ImmutableMap.copyOf(links); + super(name, type, description, defaultValue, reconfigurable, label, priority, possibleValues, pinned, constraints, links); } public EntityConfigSummary(ConfigKey config, String label, Double priority, Boolean pinned, Map links) { - super(config, label, priority); - this.pinned = pinned; - this.constraints = !config.getConstraint().equals(Predicates.alwaysTrue()) - ? ImmutableList.of((config.getConstraint().getClass().equals(StringPredicates.isNonBlank().getClass()) ? "required" : config.getConstraint().toString())) - : ImmutableList.of(); - this.links = links != null ? ImmutableMap.copyOf(links) : null; - } - - public Boolean isPinned() { - return pinned; - } - - public List getConstraints() { - return constraints; + super(config, label, priority, pinned, links); } - @Override - public Map getLinks() { - return links; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - if (!super.equals(o)) return false; - EntityConfigSummary that = (EntityConfigSummary) o; - if (pinned != that.pinned) return false; - if (constraints != null ? !constraints.equals(that.constraints) : that.constraints != null) return false; - return links != null ? links.equals(that.links) : that.links == null; - } - - @Override - public int hashCode() { - return Objects.hash(super.hashCode(), links); - } - - @Override - public String toString() { - return "EntityConfigSummary{" + - "name='" + getName() + '\'' + - ", type='" + getType() + '\'' + - ", description='" + getDescription() + '\'' + - "links=" + links + - '}'; - } } diff --git a/rest/rest-api/src/main/java/org/apache/brooklyn/rest/domain/LocationConfigSummary.java b/rest/rest-api/src/main/java/org/apache/brooklyn/rest/domain/LocationConfigSummary.java index 21d5861efc..240921bdd5 100644 --- a/rest/rest-api/src/main/java/org/apache/brooklyn/rest/domain/LocationConfigSummary.java +++ b/rest/rest-api/src/main/java/org/apache/brooklyn/rest/domain/LocationConfigSummary.java @@ -21,18 +21,17 @@ import java.net.URI; import java.util.List; import java.util.Map; -import java.util.Objects; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import com.google.common.collect.ImmutableMap; +/** @deprecated since 0.13.0 no different to ConfigSummary, use that */ +@Deprecated public class LocationConfigSummary extends ConfigSummary { private static final long serialVersionUID = 2232321501735217002L; - @JsonSerialize(include = JsonSerialize.Inclusion.NON_NULL) - private final Map links; + @SuppressWarnings("unused") // json deserialization + private LocationConfigSummary() {} public LocationConfigSummary( @JsonProperty("name") String name, @@ -43,34 +42,11 @@ public LocationConfigSummary( @JsonProperty("label") String label, @JsonProperty("priority") Double priority, @JsonProperty("possibleValues") List> possibleValues, + @JsonProperty("pinned") Boolean pinned, + @JsonProperty("constraints") List constraints, @JsonProperty("links") Map links) { - super(name, type, description, defaultValue, reconfigurable, label, priority, possibleValues); - this.links = (links == null) ? ImmutableMap.of() : ImmutableMap.copyOf(links); + super(name, type, description, defaultValue, reconfigurable, label, priority, possibleValues, + pinned, constraints, links); } - @Override - public Map getLinks() { - return links; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof LocationConfigSummary)) return false; - if (!super.equals(o)) return false; - LocationConfigSummary that = (LocationConfigSummary) o; - return Objects.equals(links, that.links); - } - - @Override - public int hashCode() { - return Objects.hash(super.hashCode(), links); - } - - @Override - public String toString() { - return "LocationConfigSummary{" + - "links=" + links + - '}'; - } } diff --git a/rest/rest-api/src/main/java/org/apache/brooklyn/rest/domain/PolicyConfigSummary.java b/rest/rest-api/src/main/java/org/apache/brooklyn/rest/domain/PolicyConfigSummary.java index 8115ab97a5..338604514b 100644 --- a/rest/rest-api/src/main/java/org/apache/brooklyn/rest/domain/PolicyConfigSummary.java +++ b/rest/rest-api/src/main/java/org/apache/brooklyn/rest/domain/PolicyConfigSummary.java @@ -19,60 +19,39 @@ package org.apache.brooklyn.rest.domain; import java.net.URI; +import java.util.List; import java.util.Map; -import java.util.Objects; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import com.google.common.collect.ImmutableMap; import org.apache.brooklyn.config.ConfigKey; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** @deprecated since 0.13.0 no different to ConfigSummary, use that */ +@Deprecated public class PolicyConfigSummary extends ConfigSummary { private static final long serialVersionUID = 4339330833863794513L; - @JsonSerialize(include = JsonSerialize.Inclusion.NON_NULL) - private final Map links; - + @SuppressWarnings("unused") // json deserialization + private PolicyConfigSummary() {} + public PolicyConfigSummary( @JsonProperty("name") String name, @JsonProperty("type") String type, @JsonProperty("description") String description, @JsonProperty("defaultValue") Object defaultValue, @JsonProperty("reconfigurable") boolean reconfigurable, + @JsonProperty("label") String label, + @JsonProperty("priority") Double priority, + @JsonProperty("possibleValues") List> possibleValues, + @JsonProperty("pinned") Boolean pinned, + @JsonProperty("constraints") List constraints, @JsonProperty("links") Map links) { - super(name, type, description, defaultValue, reconfigurable, null, null, null); - this.links = (links == null) ? ImmutableMap.of() : ImmutableMap.copyOf(links); + super(name, type, description, defaultValue, reconfigurable, label, priority, possibleValues, pinned, constraints, links); } - + public PolicyConfigSummary(ConfigKey config, String label, Double priority, Map links) { - super(config, label, priority); - this.links = links != null ? ImmutableMap.copyOf(links) : null; - } - - @Override - public Map getLinks() { - return links; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof PolicyConfigSummary)) return false; - if (!super.equals(o)) return false; - PolicyConfigSummary that = (PolicyConfigSummary) o; - return Objects.equals(links, that.links); + super(config, label, priority, null, links); } - @Override - public int hashCode() { - return Objects.hash(super.hashCode(), links); - } - - @Override - public String toString() { - return "PolicyConfigSummary{" + - "links=" + links + - '}'; - } } diff --git a/rest/rest-api/src/main/java/org/apache/brooklyn/rest/domain/TaskSummary.java b/rest/rest-api/src/main/java/org/apache/brooklyn/rest/domain/TaskSummary.java index eef57e6516..e3b0e9509a 100644 --- a/rest/rest-api/src/main/java/org/apache/brooklyn/rest/domain/TaskSummary.java +++ b/rest/rest-api/src/main/java/org/apache/brooklyn/rest/domain/TaskSummary.java @@ -30,8 +30,9 @@ import org.apache.brooklyn.util.collections.Jsonya; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -58,14 +59,14 @@ public class TaskSummary implements HasId, Serializable { private final List children; private final LinkWithMetadata submittedByTask; - @JsonSerialize(include = JsonSerialize.Inclusion.NON_NULL) + @JsonInclude(Include.NON_NULL) private final LinkWithMetadata blockingTask; - @JsonSerialize(include = JsonSerialize.Inclusion.NON_NULL) + @JsonInclude(Include.NON_NULL) private final String blockingDetails; private final String detailedStatus; - @JsonSerialize(include = JsonSerialize.Inclusion.NON_NULL) + @JsonInclude(Include.NON_NULL) private final Map streams; private final Map links; diff --git a/rest/rest-api/src/main/java/org/apache/brooklyn/rest/domain/TypeDetail.java b/rest/rest-api/src/main/java/org/apache/brooklyn/rest/domain/TypeDetail.java new file mode 100644 index 0000000000..0fafcbeb03 --- /dev/null +++ b/rest/rest-api/src/main/java/org/apache/brooklyn/rest/domain/TypeDetail.java @@ -0,0 +1,87 @@ +/* + * 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.rest.domain; + +import org.apache.brooklyn.api.typereg.RegisteredType; +import org.apache.brooklyn.api.typereg.RegisteredType.TypeImplementationPlan; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; + +/** As {@link TypeSummary} but including plan information. */ +public class TypeDetail extends TypeSummary { + + public static class TypeImplementationPlanSummary { + @JsonInclude(value=Include.NON_EMPTY) + private String format; + private Object data; + private TypeImplementationPlanSummary() {} + private TypeImplementationPlanSummary(TypeImplementationPlan p) { + format = p.getPlanFormat(); + data = p.getPlanData(); + } + public String getFormat() { + return format; + } + public Object getData() { + return data; + } + } + private TypeImplementationPlanSummary plan; + + /** Constructor for JSON deserialization use only. */ + TypeDetail() { + plan = null; + } + + public TypeDetail(RegisteredType t) { + super(t); + plan = new TypeImplementationPlanSummary(t.getPlan()); + } + + public TypeImplementationPlanSummary getPlan() { + return plan; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((plan == null) ? 0 : plan.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!super.equals(obj)) + return false; + if (getClass() != obj.getClass()) + return false; + TypeDetail other = (TypeDetail) obj; + if (plan == null) { + if (other.plan != null) + return false; + } else if (!plan.equals(other.plan)) + return false; + return true; + } + +} diff --git a/rest/rest-api/src/main/java/org/apache/brooklyn/rest/domain/TypeSummary.java b/rest/rest-api/src/main/java/org/apache/brooklyn/rest/domain/TypeSummary.java new file mode 100644 index 0000000000..70942cfb57 --- /dev/null +++ b/rest/rest-api/src/main/java/org/apache/brooklyn/rest/domain/TypeSummary.java @@ -0,0 +1,293 @@ +/* + * 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.rest.domain; + +import java.util.Map; +import java.util.Set; + +import org.apache.brooklyn.api.typereg.BrooklynTypeRegistry.RegisteredTypeKind; +import org.apache.brooklyn.api.typereg.RegisteredType; +import org.apache.brooklyn.util.collections.MutableMap; +import org.apache.brooklyn.util.javalang.JavaClassNames; +import org.apache.brooklyn.util.text.NaturalOrderComparator; +import org.apache.brooklyn.util.text.VersionComparator; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.google.common.collect.ComparisonChain; +import com.google.common.collect.ImmutableSet; + +/** Summary info of {@link RegisteredType} items in the catalog. + * See {@link TypeDetail} for further information. */ +public class TypeSummary implements Comparable { + + private final String symbolicName; + private final String version; + private final String containingBundle; + private final RegisteredTypeKind kind; + + @JsonInclude(value=Include.NON_EMPTY) + private final String displayName; + @JsonInclude(value=Include.NON_EMPTY) + private final String description; + @JsonInclude(value=Include.NON_EMPTY) + private String iconUrl; + + @JsonInclude(value=Include.NON_EMPTY) + private Set aliases; + @JsonInclude(value=Include.NON_EMPTY) + private Set supertypes; + @JsonInclude(value=Include.NON_EMPTY) + private Set tags; + + @JsonInclude(value=Include.NON_DEFAULT) + private boolean disabled = false; + @JsonInclude(value=Include.NON_DEFAULT) + private boolean deprecated = false; + + // not exported directly, but used to provide other top-level json fields + // for specific types + @JsonIgnore + private final Map others = MutableMap.of(); + + /** Constructor for JSON deserialization use only. */ + TypeSummary() { + symbolicName = null; + version = null; + containingBundle = null; + kind = null; + + displayName = null; + description = null; + } + + public TypeSummary(RegisteredType t) { + symbolicName = t.getSymbolicName(); + version = t.getVersion(); + containingBundle = t.getContainingBundle(); + kind = t.getKind(); + + displayName = t.getDisplayName(); + description = t.getDescription(); + iconUrl = t.getIconUrl(); + + aliases = t.getAliases(); + supertypes = ImmutableSet.copyOf(t.getSuperTypes().stream().map(s -> + s instanceof Class ? ((Class)s).getName() : + s instanceof RegisteredType ? ((RegisteredType)s).getId() : + s.toString()).iterator()); + tags = t.getTags(); + + deprecated = t.isDeprecated(); + disabled = t.isDisabled(); + } + + public TypeSummary(TypeSummary t) { + symbolicName = t.getSymbolicName(); + version = t.getVersion(); + containingBundle = t.getContainingBundle(); + kind = t.getKind(); + + displayName = t.getDisplayName(); + description = t.getDescription(); + iconUrl = t.getIconUrl(); + + aliases = t.getAliases(); + supertypes = t.getSupertypes(); + tags = t.getTags(); + + deprecated = t.isDeprecated(); + disabled = t.isDisabled(); + + others.putAll(t.getExtraFields()); + } + + /** Mutable map of other top-level metadata included on this DTO (eg listing config keys or effectors) */ + @JsonAnyGetter + public Map getExtraFields() { + return others; + } + @JsonAnySetter + public void setExtraField(String name, Object value) { + others.put(name, value); + } + + public void setIconUrl(String iconUrl) { + this.iconUrl = iconUrl; + } + + @Override + public int compareTo(TypeSummary o2) { + TypeSummary o1 = this; + return ComparisonChain.start() + .compare(o1.symbolicName, o2.symbolicName, NaturalOrderComparator.INSTANCE) + .compareFalseFirst(o1.disabled, o2.disabled) + .compareFalseFirst(o1.deprecated, o2.deprecated) + .compare(o2.version, o1.version, VersionComparator.INSTANCE) + .result(); + } + + public String getSymbolicName() { + return symbolicName; + } + + public String getVersion() { + return version; + } + + public String getContainingBundle() { + return containingBundle; + } + + public RegisteredTypeKind getKind() { + return kind; + } + + public String getDisplayName() { + return displayName; + } + + public String getDescription() { + return description; + } + + public String getIconUrl() { + return iconUrl; + } + + public Set getAliases() { + return aliases; + } + + public Set getSupertypes() { + return supertypes; + } + + public Set getTags() { + return tags; + } + + public boolean isDisabled() { + return disabled; + } + + public boolean isDeprecated() { + return deprecated; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((aliases == null) ? 0 : aliases.hashCode()); + result = prime * result + ((containingBundle == null) ? 0 : containingBundle.hashCode()); + result = prime * result + (deprecated ? 1231 : 1237); + result = prime * result + ((description == null) ? 0 : description.hashCode()); + result = prime * result + (disabled ? 1231 : 1237); + result = prime * result + ((displayName == null) ? 0 : displayName.hashCode()); + result = prime * result + ((iconUrl == null) ? 0 : iconUrl.hashCode()); + result = prime * result + ((kind == null) ? 0 : kind.hashCode()); + // don't use 'others' - see equals comment + // result = prime * result + ((others == null) ? 0 : others.hashCode()); + result = prime * result + ((supertypes == null) ? 0 : supertypes.hashCode()); + result = prime * result + ((symbolicName == null) ? 0 : symbolicName.hashCode()); + result = prime * result + ((tags == null) ? 0 : tags.hashCode()); + result = prime * result + ((version == null) ? 0 : version.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + TypeSummary other = (TypeSummary) obj; + if (aliases == null) { + if (other.aliases != null) + return false; + } else if (!aliases.equals(other.aliases)) + return false; + if (containingBundle == null) { + if (other.containingBundle != null) + return false; + } else if (!containingBundle.equals(other.containingBundle)) + return false; + if (deprecated != other.deprecated) + return false; + if (description == null) { + if (other.description != null) + return false; + } else if (!description.equals(other.description)) + return false; + if (disabled != other.disabled) + return false; + if (displayName == null) { + if (other.displayName != null) + return false; + } else if (!displayName.equals(other.displayName)) + return false; + if (iconUrl == null) { + if (other.iconUrl != null) + return false; + } else if (!iconUrl.equals(other.iconUrl)) + return false; + if (kind != other.kind) + return false; + // don't compare "others" -- eg if "links" are set, we don't care +// if (others == null) { +// if (other.others != null) +// return false; +// } else if (!others.equals(other.others)) +// return false; + if (supertypes == null) { + if (other.supertypes != null) + return false; + } else if (!supertypes.equals(other.supertypes)) + return false; + if (symbolicName == null) { + if (other.symbolicName != null) + return false; + } else if (!symbolicName.equals(other.symbolicName)) + return false; + if (tags == null) { + if (other.tags != null) + return false; + } else if (!tags.equals(other.tags)) + return false; + if (version == null) { + if (other.version != null) + return false; + } else if (!version.equals(other.version)) + return false; + return true; + } + + @Override + public String toString() { + return JavaClassNames.cleanSimpleClassName(this)+"["+symbolicName+":"+version+ + ", containingBundle=" + containingBundle + ", kind=" + kind + ", displayName=" + displayName + "]"; + } + +} diff --git a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/BrooklynRestApi.java b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/BrooklynRestApi.java index 01e8ad5d4d..e172127d33 100644 --- a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/BrooklynRestApi.java +++ b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/BrooklynRestApi.java @@ -26,6 +26,7 @@ import org.apache.brooklyn.rest.resources.ActivityResource; import org.apache.brooklyn.rest.resources.ApidocResource; import org.apache.brooklyn.rest.resources.ApplicationResource; +import org.apache.brooklyn.rest.resources.BundleResource; import org.apache.brooklyn.rest.resources.CatalogResource; import org.apache.brooklyn.rest.resources.EffectorResource; import org.apache.brooklyn.rest.resources.EntityConfigResource; @@ -37,6 +38,7 @@ import org.apache.brooklyn.rest.resources.ScriptResource; import org.apache.brooklyn.rest.resources.SensorResource; import org.apache.brooklyn.rest.resources.ServerResource; +import org.apache.brooklyn.rest.resources.TypeResource; import org.apache.brooklyn.rest.resources.UsageResource; import org.apache.brooklyn.rest.util.DefaultExceptionMapper; import org.apache.brooklyn.rest.util.FormMapProvider; @@ -46,14 +48,14 @@ import io.swagger.jaxrs.listing.SwaggerSerializers; - -@SuppressWarnings("deprecation") public class BrooklynRestApi { public static Iterable getBrooklynRestResources() { List resources = new ArrayList<>(); resources.add(new LocationResource()); resources.add(new CatalogResource()); + resources.add(new TypeResource()); + resources.add(new BundleResource()); resources.add(new ApplicationResource()); resources.add(new EntityResource()); resources.add(new EntityConfigResource()); diff --git a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/resources/BundleResource.java b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/resources/BundleResource.java new file mode 100644 index 0000000000..24fe3ca0fc --- /dev/null +++ b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/resources/BundleResource.java @@ -0,0 +1,169 @@ +/* + * 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.rest.resources; + +import java.io.ByteArrayInputStream; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; + +import org.apache.brooklyn.api.typereg.ManagedBundle; +import org.apache.brooklyn.core.catalog.internal.BasicBrooklynCatalog; +import org.apache.brooklyn.core.mgmt.entitlement.Entitlements; +import org.apache.brooklyn.core.mgmt.ha.OsgiBundleInstallationResult; +import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal; +import org.apache.brooklyn.rest.api.BundleApi; +import org.apache.brooklyn.rest.domain.ApiError; +import org.apache.brooklyn.rest.domain.BundleInstallationRestResult; +import org.apache.brooklyn.rest.domain.BundleSummary; +import org.apache.brooklyn.rest.filter.HaHotStateRequired; +import org.apache.brooklyn.rest.transform.TypeTransformer; +import org.apache.brooklyn.rest.util.WebResourceUtils; +import org.apache.brooklyn.util.collections.MutableList; +import org.apache.brooklyn.util.exceptions.Exceptions; +import org.apache.brooklyn.util.exceptions.ReferenceWithError; +import org.apache.brooklyn.util.osgi.VersionedName; +import org.apache.brooklyn.util.osgi.VersionedName.VersionedNameComparator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.base.Predicate; +import com.google.common.base.Predicates; + +@HaHotStateRequired +public class BundleResource extends AbstractBrooklynRestResource implements BundleApi { + + private static final Logger log = LoggerFactory.getLogger(BundleResource.class); + private static final String LATEST = "latest"; + + @Override + public List list(String versions, boolean detail) { + return list(Predicates.alwaysTrue(), TypeResource.isLatestOnly(versions, true), detail); + } + + private List list(Predicate symbolicNameFilter, boolean onlyLatest, boolean detail) { + + Map bundles = new TreeMap<>(VersionedNameComparator.INSTANCE); + for (ManagedBundle b: ((ManagementContextInternal)mgmt()).getOsgiManager().get().getManagedBundles().values()) { + if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_CATALOG_ITEM, b.getId())) { + continue; + } + if (symbolicNameFilter.apply(b.getSymbolicName())) { + VersionedName key = onlyLatest ? new VersionedName(b.getSymbolicName(), LATEST) : b.getVersionedName(); + ManagedBundle oldBundle = bundles.get(key); + if (oldBundle==null || oldBundle.getVersionedName().compareTo(b.getVersionedName()) > 0) { + bundles.put(key, b); + } + } + } + return toBundleSummary(bundles.values(), detail); + } + + private List toBundleSummary(Iterable sortedItems, boolean detail) { + List result = MutableList.of(); + for (ManagedBundle t: sortedItems) { + result.add(TypeTransformer.bundleSummary(brooklyn(), t, ui.getBaseUriBuilder(), mgmt(), detail)); + } + return result; + } + + @Override + public List listVersions(String symbolicName, boolean detail) { + return list(Predicates.equalTo(symbolicName), false, detail); + } + + @Override + public BundleSummary detail(String symbolicName, String version) { + ManagedBundle b = lookup(symbolicName, version); + return TypeTransformer.bundleDetails(brooklyn(), b, ui.getBaseUriBuilder(), mgmt()); + } + + protected ManagedBundle lookup(String symbolicName, String version) { + ManagedBundle b = ((ManagementContextInternal)mgmt()).getOsgiManager().get().getManagedBundle(new VersionedName(symbolicName, version)); + if (b==null) { + throw WebResourceUtils.notFound("Bundle with id '%s:%s' not found", symbolicName, version); + } + if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_CATALOG_ITEM, b.getId())) { + throw WebResourceUtils.notFound("Bundle with id '%s:%s' not found", symbolicName, version); + } + return b; + } + + @Override + public BundleInstallationRestResult remove(String symbolicName, String version, Boolean force) { + ManagedBundle b = lookup(symbolicName, version); + log.info("REST removing "+symbolicName+":"+version); + if (force==null) force = false; + ReferenceWithError r = ((ManagementContextInternal)mgmt()).getOsgiManager().get().uninstallUploadedBundle(b, force); + return TypeTransformer.bundleInstallationResult(r.getWithoutError(), mgmt(), brooklyn(), ui); + } + + + @Override + public Response createFromYaml(String yaml, Boolean force) { + if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.ADD_CATALOG_ITEM, yaml)) { + throw WebResourceUtils.forbidden("User '%s' is not authorized to add catalog items", + Entitlements.getEntitlementContext().user()); + } + if (force==null) force = false; + + try { + return Response.status(Status.CREATED).entity( + TypeTransformer.bundleInstallationResult( + ((BasicBrooklynCatalog)brooklyn().getCatalog()).addItemsBundleResult(yaml, force), mgmt(), brooklyn(), ui)).build(); + } catch (Exception e) { + Exceptions.propagateIfFatal(e); + return badRequest(e); + } + } + + @Override + public Response createFromArchive(byte[] zipInput, Boolean force) { + if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.ROOT, null)) { + throw WebResourceUtils.forbidden("User '%s' is not authorized to add catalog items", + Entitlements.getEntitlementContext().user()); + } + if (force==null) force = false; + + ReferenceWithError result = ((ManagementContextInternal)mgmt()).getOsgiManager().get() + .install(null, new ByteArrayInputStream(zipInput), true, true, force); + + if (OsgiBundleInstallationResult.ResultCode.IGNORING_BUNDLE_AREADY_INSTALLED.equals(result.getWithoutError().getCode())) { + result = ReferenceWithError.newInstanceThrowingError(result.getWithoutError(), new IllegalStateException( + "Cannot add bundle" + result.getWithoutError().getMetadata().getVersionedName() + + "; different bundle with same name already installed")); + } + + if (result.hasError()) { + // (rollback already done as part of install, if necessary) + if (log.isTraceEnabled()) { + log.trace("Unable to create from archive, returning 400: "+result.getError().getMessage(), result.getError()); + } + return ApiError.builder().errorCode(Status.BAD_REQUEST).message(result.getWithoutError().getMessage()) + .data(TypeTransformer.bundleInstallationResult(result.getWithoutError(), mgmt(), brooklyn(), ui)).build().asJsonResponse(); + } + + BundleInstallationRestResult resultR = TypeTransformer.bundleInstallationResult(result.get(), mgmt(), brooklyn(), ui); + return Response.status(Status.CREATED).entity( resultR ).build(); + } + +} diff --git a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/resources/CatalogResource.java b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/resources/CatalogResource.java index d327e4046b..65e695675c 100644 --- a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/resources/CatalogResource.java +++ b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/resources/CatalogResource.java @@ -35,7 +35,6 @@ import javax.ws.rs.core.UriInfo; import org.apache.brooklyn.api.catalog.CatalogItem; -import org.apache.brooklyn.api.mgmt.ManagementContext; import org.apache.brooklyn.api.typereg.RegisteredType; import org.apache.brooklyn.core.catalog.internal.CatalogUtils; import org.apache.brooklyn.core.mgmt.entitlement.Entitlements; @@ -46,6 +45,7 @@ import org.apache.brooklyn.core.typereg.RegisteredTypes; import org.apache.brooklyn.rest.api.CatalogApi; import org.apache.brooklyn.rest.domain.ApiError; +import org.apache.brooklyn.rest.domain.BundleInstallationRestResult; import org.apache.brooklyn.rest.domain.CatalogEnricherSummary; import org.apache.brooklyn.rest.domain.CatalogEntitySummary; import org.apache.brooklyn.rest.domain.CatalogItemSummary; @@ -53,7 +53,7 @@ import org.apache.brooklyn.rest.domain.CatalogPolicySummary; import org.apache.brooklyn.rest.filter.HaHotStateRequired; import org.apache.brooklyn.rest.transform.CatalogTransformer; -import org.apache.brooklyn.rest.util.BrooklynRestResourceUtils; +import org.apache.brooklyn.rest.transform.TypeTransformer; import org.apache.brooklyn.rest.util.WebResourceUtils; import org.apache.brooklyn.util.collections.MutableList; import org.apache.brooklyn.util.collections.MutableMap; @@ -149,36 +149,6 @@ public Response createFromYaml(String yaml, boolean forceUpdate) { } } - public static class BundleInstallationRestResult { - // as Osgi result, but without bundle, and with maps of catalog items installed - - String message; - String bundle; - OsgiBundleInstallationResult.ResultCode code; - - Map types; - - public String getMessage() { - return message; - } - - public static BundleInstallationRestResult of(OsgiBundleInstallationResult in, ManagementContext mgmt, BrooklynRestResourceUtils brooklynU, UriInfo ui) { - BundleInstallationRestResult result = new BundleInstallationRestResult(); - result.message = in.getMessage(); - result.bundle = in.getVersionedName() != null ? in.getVersionedName().toString() : ""; - result.code = in.getCode(); - if (in.getCatalogItemsInstalled()!=null) { - result.types = MutableMap.of(); - for (String id: in.getCatalogItemsInstalled()) { - RegisteredType ci = mgmt.getTypeRegistry().get(id); - CatalogItemSummary summary = CatalogTransformer.catalogItemSummary(brooklynU, ci, ui.getBaseUriBuilder()); - result.types.put(id, summary); - } - } - return result; - } - } - @Override @Beta public Response createFromArchive(byte[] zipInput, boolean detail, boolean forceUpdate) { @@ -202,11 +172,11 @@ public Response createFromArchive(byte[] zipInput, boolean detail, boolean force log.trace("Unable to create from archive, returning 400: "+result.getError().getMessage(), result.getError()); } return ApiError.builder().errorCode(Status.BAD_REQUEST).message(result.getWithoutError().getMessage()) - .data(BundleInstallationRestResult.of(result.getWithoutError(), mgmt(), brooklyn(), ui)).build().asJsonResponse(); + .data(TypeTransformer.bundleInstallationResult(result.getWithoutError(), mgmt(), brooklyn(), ui)).build().asJsonResponse(); } - BundleInstallationRestResult resultR = BundleInstallationRestResult.of(result.get(), mgmt(), brooklyn(), ui); - return Response.status(Status.CREATED).entity( detail ? resultR : resultR.types ).build(); + BundleInstallationRestResult resultR = TypeTransformer.bundleInstallationResult(result.get(), mgmt(), brooklyn(), ui); + return Response.status(Status.CREATED).entity( detail ? resultR : resultR.getTypes() ).build(); } private Response buildCreateResponse(Iterable catalogItems) { diff --git a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/resources/TypeResource.java b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/resources/TypeResource.java new file mode 100644 index 0000000000..cf9f40f724 --- /dev/null +++ b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/resources/TypeResource.java @@ -0,0 +1,207 @@ +/* + * 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.rest.resources; + +import java.net.URI; +import java.util.List; +import java.util.Set; + +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import javax.ws.rs.core.UriBuilder; + +import org.apache.brooklyn.api.entity.Application; +import org.apache.brooklyn.api.entity.Entity; +import org.apache.brooklyn.api.location.Location; +import org.apache.brooklyn.api.policy.Policy; +import org.apache.brooklyn.api.sensor.Enricher; +import org.apache.brooklyn.api.typereg.RegisteredType; +import org.apache.brooklyn.core.catalog.internal.CatalogUtils; +import org.apache.brooklyn.core.mgmt.entitlement.Entitlements; +import org.apache.brooklyn.core.typereg.RegisteredTypePredicates; +import org.apache.brooklyn.core.typereg.RegisteredTypes; +import org.apache.brooklyn.rest.api.TypeApi; +import org.apache.brooklyn.rest.domain.TypeDetail; +import org.apache.brooklyn.rest.domain.TypeSummary; +import org.apache.brooklyn.rest.filter.HaHotStateRequired; +import org.apache.brooklyn.rest.transform.TypeTransformer; +import org.apache.brooklyn.rest.util.BrooklynRestResourceUtils; +import org.apache.brooklyn.rest.util.WebResourceUtils; +import org.apache.brooklyn.util.collections.MutableList; +import org.apache.brooklyn.util.collections.MutableSet; +import org.apache.brooklyn.util.core.ResourceUtils; +import org.apache.brooklyn.util.exceptions.Exceptions; +import org.apache.brooklyn.util.text.StringPredicates; +import org.apache.brooklyn.util.text.Strings; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.annotations.Beta; +import com.google.common.base.Predicate; +import com.google.common.base.Predicates; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.ImmutableList; +import com.google.common.io.Files; + +@HaHotStateRequired +@Beta +public class TypeResource extends AbstractBrooklynRestResource implements TypeApi { + + private static final Logger log = LoggerFactory.getLogger(TypeResource.class); + private static final String LATEST = "latest"; + private static final String ALL = "all"; + + private static Set missingIcons = MutableSet.of(); + + static boolean isLatestOnly(String versions, boolean defaultValue) { + if (ALL.equalsIgnoreCase(versions)) return false; + if (LATEST.equalsIgnoreCase(versions)) return true; + if (Strings.isNonBlank(versions)) { + log.warn("Invalid 'versions' argument '"+versions+"' when listing types; should be 'all' or 'latest'"); + } + return defaultValue; + } + + @Override + public List list(String supertype, String versions, String regex, String fragment) { + List> filters = MutableList.>of() + .append(RegisteredTypePredicates.entitledToSee(mgmt())); + if (Strings.isNonBlank(supertype)) { + // rewrite certain well known ones + // (in future this should happen automatically as Entity.class should be known as user-friendly name 'entity') + if ("entity".equals(supertype)) supertype = Entity.class.getName(); + else if ("enricher".equals(supertype)) supertype = Enricher.class.getName(); + else if ("policy".equals(supertype)) supertype = Policy.class.getName(); + else if ("location".equals(supertype)) supertype = Location.class.getName(); + // TODO application probably isn't at all interesting; keep it for backward compatibility, + // and meanwhile sort out things like "template" vs "quick launch" + // (probably adding tags on the API) + else if ("application".equals(supertype)) supertype = Application.class.getName(); + + filters.add(RegisteredTypePredicates.subtypeOf(supertype)); + } + if (TypeResource.isLatestOnly(versions, true)) { + // TODO inefficient - does n^2 comparisons where n is sufficient + // create RegisteredTypes.filterBestVersions to do a list after the initial parse + // (and javadoc in predicate method below) + filters.add(RegisteredTypePredicates.isBestVersion(mgmt())); + } + if (Strings.isNonEmpty(regex)) { + filters.add(RegisteredTypePredicates.nameOrAlias(StringPredicates.containsRegex(regex))); + } + if (Strings.isNonEmpty(fragment)) { + filters.add(RegisteredTypePredicates.nameOrAlias(StringPredicates.containsLiteralIgnoreCase(fragment))); + } + Predicate filter = Predicates.and(filters); + + ImmutableList sortedItems = + FluentIterable.from(brooklyn().getTypeRegistry().getMatching(filter)) + .toSortedList(RegisteredTypes.RegisteredTypeNameThenBestFirstComparator.INSTANCE); + return toTypeSummary(brooklyn(), sortedItems, ui.getBaseUriBuilder()); + } + + static List toTypeSummary(BrooklynRestResourceUtils brooklyn, Iterable sortedItems, UriBuilder uriBuilder) { + List result = MutableList.of(); + for (RegisteredType t: sortedItems) { + result.add(TypeTransformer.summary(brooklyn, t, uriBuilder)); + } + return result; + } + + @Override + public List listVersions(String nameOrAlias) { + Predicate filter = Predicates.and(RegisteredTypePredicates.entitledToSee(mgmt()), + RegisteredTypePredicates.nameOrAlias(nameOrAlias)); + ImmutableList sortedItems = + FluentIterable.from(brooklyn().getTypeRegistry().getMatching(filter)) + .toSortedList(RegisteredTypes.RegisteredTypeNameThenBestFirstComparator.INSTANCE); + return toTypeSummary(brooklyn(), sortedItems, ui.getBaseUriBuilder()); + } + + @Override + public TypeDetail detail(String symbolicName, String version) { + RegisteredType item = lookup(symbolicName, version); + return TypeTransformer.detail(brooklyn(), item, ui.getBaseUriBuilder()); + } + + protected RegisteredType lookup(String symbolicName, String version) { + if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_CATALOG_ITEM, symbolicName+":"+version)) { + // TODO best to default to "not found" - unless maybe they have permission to "see null" + throw WebResourceUtils.forbidden("User '%s' not permitted to see info on this type (including whether or not installed)", + Entitlements.getEntitlementContext().user()); + } + RegisteredType item; + if (LATEST.equalsIgnoreCase(version)) { + item = brooklyn().getTypeRegistry().get(symbolicName); + } else { + item = brooklyn().getTypeRegistry().get(symbolicName, version); + } + if (item==null) { + throw WebResourceUtils.notFound("Entity with id '%s:%s' not found", symbolicName, version); + } + return item; + } + + @Override + public Response icon(String symbolicName, String version) { + RegisteredType item = lookup(symbolicName, version); + return produceIcon(item); + } + + private Response produceIcon(RegisteredType result) { + String url = result.getIconUrl(); + if (url==null) { + log.debug("No icon available for "+result+"; returning "+Status.NO_CONTENT); + return Response.status(Status.NO_CONTENT).build(); + } + + if (brooklyn().isUrlServerSideAndSafe(url)) { + // classpath URL's we will serve IF they end with a recognised image format; + // paths (ie non-protocol) and + // NB, for security, file URL's are NOT served + log.debug("Loading and returning "+url+" as icon for "+result); + + MediaType mime = WebResourceUtils.getImageMediaTypeFromExtension(Files.getFileExtension(url)); + try { + Object content = ResourceUtils.create(CatalogUtils.newClassLoadingContext(mgmt(), result)).getResourceFromUrl(url); + return Response.ok(content, mime).build(); + } catch (Exception e) { + Exceptions.propagateIfFatal(e); + synchronized (missingIcons) { + if (missingIcons.add(url)) { + // note: this can be quite common when running from an IDE, as resources may not be copied; + // a mvn build should sort it out (the IDE will then find the resources, until you clean or maybe refresh...) + log.warn("Missing icon data for "+result.getId()+", expected at: "+url+" (subsequent messages will log debug only)"); + log.debug("Trace for missing icon data at "+url+": "+e, e); + } else { + log.debug("Missing icon data for "+result.getId()+", expected at: "+url+" (already logged WARN and error details)"); + } + } + throw WebResourceUtils.notFound("Icon unavailable for %s", result.getId()); + } + } + + log.debug("Returning redirect to "+url+" as icon for "+result); + + // for anything else we do a redirect (e.g. http / https; perhaps ftp) + return Response.temporaryRedirect(URI.create(url)).build(); + } + +} diff --git a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/transform/EntityTransformer.java b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/transform/EntityTransformer.java index 8a15020d36..73b2831eef 100644 --- a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/transform/EntityTransformer.java +++ b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/transform/EntityTransformer.java @@ -19,6 +19,7 @@ package org.apache.brooklyn.rest.transform; import static com.google.common.collect.Iterables.transform; +import static org.apache.brooklyn.rest.util.WebResourceUtils.serviceUriBuilder; import java.lang.reflect.Field; import java.net.URI; @@ -26,13 +27,21 @@ import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; +import javax.ws.rs.core.UriBuilder; + import org.apache.brooklyn.api.catalog.CatalogConfig; import org.apache.brooklyn.api.entity.Application; import org.apache.brooklyn.api.entity.Entity; import org.apache.brooklyn.api.objs.SpecParameter; import org.apache.brooklyn.config.ConfigKey; +import org.apache.brooklyn.core.catalog.internal.CatalogUtils; import org.apache.brooklyn.core.config.render.RendererHints; import org.apache.brooklyn.core.typereg.RegisteredTypes; +import org.apache.brooklyn.rest.api.ApplicationApi; +import org.apache.brooklyn.rest.api.CatalogApi; +import org.apache.brooklyn.rest.api.EntityApi; +import org.apache.brooklyn.rest.api.EntityConfigApi; +import org.apache.brooklyn.rest.domain.ConfigSummary; import org.apache.brooklyn.rest.domain.EnricherConfigSummary; import org.apache.brooklyn.rest.domain.EntityConfigSummary; import org.apache.brooklyn.rest.domain.EntitySummary; @@ -43,13 +52,6 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; -import javax.ws.rs.core.UriBuilder; -import org.apache.brooklyn.core.catalog.internal.CatalogUtils; -import org.apache.brooklyn.rest.api.ApplicationApi; -import org.apache.brooklyn.rest.api.CatalogApi; -import org.apache.brooklyn.rest.api.EntityApi; -import org.apache.brooklyn.rest.api.EntityConfigApi; -import static org.apache.brooklyn.rest.util.WebResourceUtils.serviceUriBuilder; /** * @author Adam Lowe @@ -123,6 +125,10 @@ public static EntityConfigSummary entityConfigSummary(ConfigKey config, Strin return new EntityConfigSummary(config, label, priority, pinned, mapOfLinks); } + public static ConfigSummary configSummary(ConfigKey config, String label, Double priority, Boolean pinned, Map links) { + return new ConfigSummary(config, label, priority, pinned, links); + } + public static PolicyConfigSummary policyConfigSummary(ConfigKey config, String label, Double priority, Map links) { return new PolicyConfigSummary(config, label, priority, links); } @@ -192,6 +198,12 @@ public static EntityConfigSummary entityConfigSummary(SpecParameter input, At return entityConfigSummary(input.getConfigKey(), input.getLabel(), priority, input.isPinned(), null); } + public static ConfigSummary configSummary(SpecParameter input) { + // could increment priority, or take from annotation, or introduce new field + Double priority = input.isPinned() ? Double.valueOf(1d) : null; + return configSummary(input.getConfigKey(), input.getLabel(), priority, input.isPinned(), null); + } + public static PolicyConfigSummary policyConfigSummary(SpecParameter input) { Double priority = input.isPinned() ? Double.valueOf(1d) : null; return policyConfigSummary(input.getConfigKey(), input.getLabel(), priority, null); diff --git a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/transform/PolicyTransformer.java b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/transform/PolicyTransformer.java index 8dad949188..d6ba1d33fe 100644 --- a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/transform/PolicyTransformer.java +++ b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/transform/PolicyTransformer.java @@ -96,7 +96,8 @@ public static PolicyConfigSummary policyConfigSummary(BrooklynRestResourceUtils return new PolicyConfigSummary(config.getName(), config.getTypeName(), config.getDescription(), PolicyConfigResource.getStringValueForDisplay(utils, policy, config.getDefaultValue()), - config.isReconfigurable(), + config.isReconfigurable(), + null, null, null, null, null, links); } } diff --git a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/transform/TypeTransformer.java b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/transform/TypeTransformer.java new file mode 100644 index 0000000000..e37ee6abc1 --- /dev/null +++ b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/transform/TypeTransformer.java @@ -0,0 +1,197 @@ +/* + * 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.rest.transform; + +import static org.apache.brooklyn.rest.util.WebResourceUtils.serviceUriBuilder; + +import java.net.URI; +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.ws.rs.core.UriBuilder; +import javax.ws.rs.core.UriInfo; + +import org.apache.brooklyn.api.effector.Effector; +import org.apache.brooklyn.api.entity.Entity; +import org.apache.brooklyn.api.entity.EntitySpec; +import org.apache.brooklyn.api.entity.EntityType; +import org.apache.brooklyn.api.internal.AbstractBrooklynObjectSpec; +import org.apache.brooklyn.api.location.Location; +import org.apache.brooklyn.api.mgmt.ManagementContext; +import org.apache.brooklyn.api.objs.EntityAdjunct; +import org.apache.brooklyn.api.objs.SpecParameter; +import org.apache.brooklyn.api.policy.Policy; +import org.apache.brooklyn.api.sensor.Enricher; +import org.apache.brooklyn.api.sensor.Feed; +import org.apache.brooklyn.api.sensor.Sensor; +import org.apache.brooklyn.api.typereg.ManagedBundle; +import org.apache.brooklyn.api.typereg.RegisteredType; +import org.apache.brooklyn.core.entity.EntityDynamicType; +import org.apache.brooklyn.core.mgmt.ha.OsgiBundleInstallationResult; +import org.apache.brooklyn.core.objs.BrooklynTypes; +import org.apache.brooklyn.core.typereg.RegisteredTypePredicates; +import org.apache.brooklyn.core.typereg.RegisteredTypes; +import org.apache.brooklyn.rest.api.TypeApi; +import org.apache.brooklyn.rest.domain.BundleInstallationRestResult; +import org.apache.brooklyn.rest.domain.BundleSummary; +import org.apache.brooklyn.rest.domain.ConfigSummary; +import org.apache.brooklyn.rest.domain.EffectorSummary; +import org.apache.brooklyn.rest.domain.EntityConfigSummary; +import org.apache.brooklyn.rest.domain.SensorSummary; +import org.apache.brooklyn.rest.domain.SummaryComparators; +import org.apache.brooklyn.rest.domain.TypeDetail; +import org.apache.brooklyn.rest.domain.TypeSummary; +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.Sets; + +public class TypeTransformer { + + private static final org.slf4j.Logger log = LoggerFactory.getLogger(TypeTransformer.class); + + public static TypeSummary summary(BrooklynRestResourceUtils b, RegisteredType item, UriBuilder ub) { + return embellish(new TypeSummary(item), item, false, b, ub); + } + + public static TypeDetail detail(BrooklynRestResourceUtils b, RegisteredType item, UriBuilder ub) { + return embellish(new TypeDetail(item), item, true, b, ub); + } + + private static T embellish(T result, RegisteredType item, boolean detail, BrooklynRestResourceUtils b, UriBuilder ub) { + result.setExtraField("links", makeLinks(item, ub)); + + if (RegisteredTypes.isTemplate(item)) { + result.setExtraField("template", true); + } + if (item.getIconUrl()!=null) { + result.setIconUrl(tidyIconLink(b, item, item.getIconUrl(), ub)); + } + + if (detail) { + if (RegisteredTypes.isSubtypeOf(item, Entity.class)) { + embellishEntity(result, item, b); + } else if (RegisteredTypes.isSubtypeOf(item, EntityAdjunct.class) || + // when implied supertypes are used we won't need the code below + RegisteredTypes.isSubtypeOf(item, Policy.class) || RegisteredTypes.isSubtypeOf(item, Enricher.class) || RegisteredTypes.isSubtypeOf(item, Feed.class) + ) { + try { + Set config = Sets.newLinkedHashSet(); + + AbstractBrooklynObjectSpec spec = b.getTypeRegistry().createSpec(item, null, null); + for (final SpecParameter input : spec.getParameters()){ + config.add(EntityTransformer.configSummary(input)); + } + + result.setExtraField("config", config); + } catch (Exception e) { + Exceptions.propagateIfFatal(e); + log.trace("Unable to create spec for "+item+": "+e, e); + } + + } else if (RegisteredTypes.isSubtypeOf(item, Location.class)) { + // TODO include config on location specs? (wasn't done previously so not needed, but good for completeness) + result.setExtraField("config", Collections.emptyMap()); + } + } + return result; + } + + protected static void embellishEntity(T result, RegisteredType item, BrooklynRestResourceUtils b) { + try { + Set config = Sets.newLinkedHashSet(); + Set sensors = Sets.newTreeSet(SummaryComparators.nameComparator()); + Set effectors = Sets.newTreeSet(SummaryComparators.nameComparator()); + + EntitySpec spec = b.getTypeRegistry().createSpec(item, null, EntitySpec.class); + EntityDynamicType typeMap = BrooklynTypes.getDefinedEntityType(spec.getType()); + EntityType type = typeMap.getSnapshot(); + + AtomicInteger paramPriorityCnt = new AtomicInteger(); + for (SpecParameter input: spec.getParameters()) + config.add(EntityTransformer.entityConfigSummary(input, paramPriorityCnt)); + for (Sensor x: type.getSensors()) + sensors.add(SensorTransformer.sensorSummaryForCatalog(x)); + for (Effector x: type.getEffectors()) + effectors.add(EffectorTransformer.effectorSummaryForCatalog(x)); + + result.setExtraField("config", config); + result.setExtraField("sensors", sensors); + result.setExtraField("effectors", effectors); + + } catch (Exception e) { + Exceptions.propagateIfFatal(e); + + // templates with multiple entities can't have spec created in the manner above; just ignore + if (item.getSuperTypes().contains(Entity.class)) { + log.warn("Unable to create spec for "+item+": "+e, e); + } + if (log.isTraceEnabled()) { + log.trace("Unable to create spec for "+item+": "+e, e); + } + } + } + + public static BundleSummary bundleSummary(BrooklynRestResourceUtils brooklyn, ManagedBundle b, UriBuilder baseUriBuilder, ManagementContext mgmt, boolean detail) { + BundleSummary result = new BundleSummary(b); + if (detail) { + result.setExtraField("osgiVersion", b.getOsgiVersionString()); + result.setExtraField("checksum", b.getChecksum()); + } + if (detail) { + for (RegisteredType t: mgmt.getTypeRegistry().getMatching(RegisteredTypePredicates.containingBundle(b))) { + result.addType(summary(brooklyn, t, baseUriBuilder)); + } + } + return result; + } + + public static BundleSummary bundleDetails(BrooklynRestResourceUtils brooklyn, ManagedBundle b, UriBuilder baseUriBuilder, ManagementContext mgmt) { + return bundleSummary(brooklyn, b, baseUriBuilder, mgmt, true); + } + + public static BundleInstallationRestResult bundleInstallationResult(OsgiBundleInstallationResult in, ManagementContext mgmt, BrooklynRestResourceUtils brooklynU, UriInfo ui) { + BundleInstallationRestResult result = new BundleInstallationRestResult( + in.getMessage(), in.getVersionedName() != null ? in.getVersionedName().toString() : "", in.getCode()); + for (RegisteredType t: in.getTypesInstalled()) { + TypeSummary summary = TypeTransformer.summary(brooklynU, t, ui.getBaseUriBuilder()); + result.getTypes().put(t.getId(), summary); + } + return result; + } + + protected static Map makeLinks(RegisteredType item, UriBuilder ub) { + return MutableMap.of().addIfNotNull("self", getSelfLink(item, ub)); + } + + private static URI getSelfLink(RegisteredType item, UriBuilder ub) { + return serviceUriBuilder(ub, TypeApi.class, "detail").build(item.getSymbolicName(), item.getVersion()); + } + private static String tidyIconLink(BrooklynRestResourceUtils b, RegisteredType item, String iconUrl, UriBuilder ub) { + if (b.isUrlServerSideAndSafe(iconUrl)) { + return serviceUriBuilder(ub, TypeApi.class, "icon").build(item.getSymbolicName(), item.getVersion()).toString(); + } + return iconUrl; + } + +} diff --git a/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/resources/BundleAndTypeResourcesTest.java b/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/resources/BundleAndTypeResourcesTest.java new file mode 100644 index 0000000000..87fa3782b1 --- /dev/null +++ b/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/resources/BundleAndTypeResourcesTest.java @@ -0,0 +1,1098 @@ +/* + * 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.rest.resources; + +import static com.google.common.base.Preconditions.checkNotNull; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +import java.awt.Image; +import java.awt.Toolkit; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import javax.ws.rs.core.GenericType; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.apache.brooklyn.api.entity.Entity; +import org.apache.brooklyn.api.objs.BrooklynObject; +import org.apache.brooklyn.api.objs.Configurable; +import org.apache.brooklyn.api.objs.Identifiable; +import org.apache.brooklyn.api.policy.Policy; +import org.apache.brooklyn.api.typereg.ManagedBundle; +import org.apache.brooklyn.api.typereg.OsgiBundleWithUrl; +import org.apache.brooklyn.api.typereg.RegisteredType; +import org.apache.brooklyn.core.entity.EntityPredicates; +import org.apache.brooklyn.core.mgmt.ha.OsgiManager; +import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal; +import org.apache.brooklyn.core.mgmt.osgi.OsgiStandaloneTest; +import org.apache.brooklyn.core.test.entity.TestEntity; +import org.apache.brooklyn.enricher.stock.Aggregator; +import org.apache.brooklyn.policy.autoscaling.AutoScalerPolicy; +import org.apache.brooklyn.rest.domain.BundleInstallationRestResult; +import org.apache.brooklyn.rest.domain.BundleSummary; +import org.apache.brooklyn.rest.domain.TypeDetail; +import org.apache.brooklyn.rest.domain.TypeSummary; +import org.apache.brooklyn.rest.testing.BrooklynRestResourceTest; +import org.apache.brooklyn.test.Asserts; +import org.apache.brooklyn.test.support.TestResourceUnavailableException; +import org.apache.brooklyn.util.collections.MutableList; +import org.apache.brooklyn.util.collections.MutableMap; +import org.apache.brooklyn.util.collections.MutableSet; +import org.apache.brooklyn.util.core.ResourceUtils; +import org.apache.brooklyn.util.core.osgi.BundleMaker; +import org.apache.brooklyn.util.javalang.JavaClassNames; +import org.apache.brooklyn.util.javalang.Reflections; +import org.apache.brooklyn.util.os.Os; +import org.apache.brooklyn.util.osgi.OsgiTestResources; +import org.apache.brooklyn.util.stream.Streams; +import org.apache.http.HttpHeaders; +import org.eclipse.jetty.http.HttpStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testng.Assert; +import org.testng.annotations.Test; +import org.testng.reporters.Files; + +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; + +public class BundleAndTypeResourcesTest extends BrooklynRestResourceTest { + + private static final Logger log = LoggerFactory.getLogger(BundleAndTypeResourcesTest.class); + + private static String TEST_VERSION = "0.1.2"; + private static String TEST_LASTEST_VERSION = "0.1.3"; + + private Collection initialBundles; + + @Override + protected boolean useLocalScannedCatalog() { + return true; + } + + @Override + protected void initClass() throws Exception { + super.initClass(); + // cache initially installed bundles + OsgiManager osgi = ((ManagementContextInternal)getManagementContext()).getOsgiManager().get(); + initialBundles = osgi.getManagedBundles().values(); + } + + protected void initMethod() throws Exception { + super.initMethod(); + + // and reset OSGi container + OsgiManager osgi = ((ManagementContextInternal)getManagementContext()).getOsgiManager().get(); + for (ManagedBundle b: osgi.getManagedBundles().values()) { + if (!initialBundles.contains(b)) { + osgi.uninstallUploadedBundle(b); + } + } + } + + @Test + /** based on CampYamlLiteTest */ + public void testRegisterCustomEntityTopLevelSyntaxWithBundleWhereEntityIsFromCoreAndIconFromBundle() { + TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_PATH); + + String symbolicName = "my.catalog.entity.id"; + String bundleUrl = OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_URL; + String yaml = Joiner.on("\n").join( + "brooklyn.catalog:", + " id: " + symbolicName, + " version: " + TEST_VERSION, + " itemType: entity", + " name: My Catalog App", + " description: My description", + " icon_url: classpath:/org/apache/brooklyn/test/osgi/entities/icon.gif", + " libraries:", + " - url: " + bundleUrl, + " item:", + " type: org.apache.brooklyn.core.test.entity.TestEntity"); + + Response response = client().path("/catalog/bundles") + .header(HttpHeaders.CONTENT_TYPE, "application/yaml") + .post(yaml); + + assertEquals(response.getStatus(), Response.Status.CREATED.getStatusCode()); + BundleInstallationRestResult installed = response.readEntity(BundleInstallationRestResult.class); + Asserts.assertSize(installed.getTypes().values(), 1); + TypeSummary installedItem = installed.getTypes().get(symbolicName+":"+TEST_VERSION); + Assert.assertNotNull(installedItem, ""+installed.getTypes()); + + TypeDetail entityItem = client().path("/catalog/types/"+symbolicName + "/" + TEST_VERSION) + .get(TypeDetail.class); + + Assert.assertEquals(new TypeSummary(entityItem), installedItem); + Assert.assertNotNull(entityItem.getPlan()); + Assert.assertTrue(((String)entityItem.getPlan().getData()).contains("org.apache.brooklyn.core.test.entity.TestEntity")); + + assertEquals(entityItem.getSymbolicName(), symbolicName); + assertEquals(entityItem.getVersion(), TEST_VERSION); + + // also check it's included in various lists + List list1 = client().path("/catalog/types/"+symbolicName).get(new GenericType>() {}); + assertEquals(list1, MutableList.of(installedItem)); + List list2 = client().path("/catalog/types").get(new GenericType>() {}); + Assert.assertTrue(list2.contains(installedItem), ""+list2); + List list3 = client().path("/catalog/types").query("supertype", "entity").get(new GenericType>() {}); + Assert.assertTrue(list3.contains(installedItem), ""+list3); + + // and internally let's check we have libraries + RegisteredType item = getManagementContext().getTypeRegistry().get(symbolicName, TEST_VERSION); + Assert.assertNotNull(item); + Collection libs = item.getLibraries(); + assertEquals(libs.size(), 1); + assertEquals(Iterables.getOnlyElement(libs).getUrl(), bundleUrl); + + // now let's check other things on the item + URI expectedIconUrl = URI.create(getEndpointAddress() + "/catalog/types/" + symbolicName + "/" + entityItem.getVersion()+"/icon").normalize(); + assertEquals(entityItem.getDisplayName(), "My Catalog App"); + assertEquals(entityItem.getDescription(), "My description"); + assertEquals(entityItem.getIconUrl(), expectedIconUrl.getPath()); + assertEquals(item.getIconUrl(), "classpath:/org/apache/brooklyn/test/osgi/entities/icon.gif"); + + // an InterfacesTag should be created for every catalog item + if (checkTraits(false)) { + @SuppressWarnings("unchecked") + Map> traitsMapTag = Iterables.getOnlyElement(Iterables.filter(entityItem.getTags(), Map.class)); + List actualInterfaces = traitsMapTag.get("traits"); + List> expectedInterfaces = Reflections.getAllInterfaces(TestEntity.class); + assertEquals(actualInterfaces.size(), expectedInterfaces.size()); + for (Class expectedInterface : expectedInterfaces) { + assertTrue(actualInterfaces.contains(expectedInterface.getName())); + } + } + + byte[] iconData = client().path("/catalog/types/" + symbolicName + "/" + TEST_VERSION+"/icon").get(byte[].class); + assertEquals(iconData.length, 43); + } + + @Test + public void testRegisterOsgiPolicyTopLevelSyntax() { + TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_PATH); + + String symbolicName = "my.catalog.entity.id."+JavaClassNames.niceClassAndMethod(); + String policyType = "org.apache.brooklyn.test.osgi.entities.SimplePolicy"; + String bundleUrl = OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_URL; + + String yaml = Joiner.on("\n").join( + "brooklyn.catalog:", + " id: " + symbolicName, + " version: " + TEST_VERSION, + " itemType: policy", + " name: My Catalog App", + " description: My description", + " libraries:", + " - url: " + bundleUrl, + " item:", + " type: " + policyType); + + TypeSummary installedItem = Iterables.getOnlyElement( client().path("/catalog/bundles") + .header(HttpHeaders.CONTENT_TYPE, "application/yaml") + .post(yaml, BundleInstallationRestResult.class).getTypes().values() ); + + assertEquals(installedItem.getSymbolicName(), symbolicName); + assertEquals(installedItem.getVersion(), TEST_VERSION); + Assert.assertTrue(installedItem.getSupertypes().contains(Policy.class.getName()), ""+installedItem.getSupertypes()); + } + + @Test + public void testFilterListOfEntitiesByName() { + List entities = client().path("/catalog/types") + .query("fragment", "vaNIllasOFTWAREpROCESS").get(new GenericType>() {}); + log.info("Matching entities: " + entities); + assertEquals(entities.size(), 1); + + entities = client().path("/catalog/types").query("supertype", "entity") + .query("fragment", "vaNIllasOFTWAREpROCESS").get(new GenericType>() {}); + log.info("Matching entities: " + entities); + assertEquals(entities.size(), 1); + + List entities2 = client().path("/catalog/types") + .query("regex", "[Vv]an.[alS]+oftware\\w+").get(new GenericType>() {}); + assertEquals(entities2.size(), 1); + + assertEquals(entities, entities2); + + entities = client().path("/catalog/types").query("supertype", "entity") + .query("fragment", "bweqQzZ").get(new GenericType>() {}); + Asserts.assertSize(entities, 0); + + entities = client().path("/catalog/types").query("supertype", "entity") + .query("regex", "bweq+z+").get(new GenericType>() {}); + Asserts.assertSize(entities, 0); + } + + @Test + public void testGetCatalogEntityIconDetails() throws IOException { + String catalogItemId = "testGetCatalogEntityIconDetails"; + addTestCatalogItemAsEntity(catalogItemId); + Response response = client().path(URI.create("/catalog/types/" + catalogItemId + "/" + TEST_VERSION + "/icon")) + .get(); + response.bufferEntity(); + Assert.assertEquals(response.getStatus(), 200); + Assert.assertEquals(response.getMediaType(), MediaType.valueOf("image/png")); + Image image = Toolkit.getDefaultToolkit().createImage(Files.readFile(response.readEntity(InputStream.class))); + Assert.assertNotNull(image); + } + + private void addTestCatalogItemAsEntity(String catalogItemId) { + addTestCatalogItem(catalogItemId, "entity", TEST_VERSION, "org.apache.brooklyn.rest.resources.DummyIconEntity"); + } + + private void addTestCatalogItem(String catalogItemId, String itemType, String version, String service) { + String yaml = Joiner.on("\n").join( + "brooklyn.catalog:", + " id: " + catalogItemId, + " version: " + TEST_VERSION, + " itemType: " + checkNotNull(itemType), + " name: My Catalog App", + " description: My description", + " icon_url: classpath:///bridge-small.png", + " version: " + version, + " item:", + " type: " + service); + + client().path("/catalog/bundles").header(HttpHeaders.CONTENT_TYPE, "application/yaml").post(yaml); + } + + @Test + public void testListPolicies() { + Set policies = client().path("/catalog/types").query("supertype", "policy") + .get(new GenericType>() {}); + + assertTrue(policies.size() > 0); + TypeSummary asp = null; + for (TypeSummary p : policies) { + if (AutoScalerPolicy.class.getName().equals(p.getSymbolicName())) + asp = p; + } + Assert.assertNotNull(asp, "didn't find AutoScalerPolicy"); + } + + @Test + public void testLocationAddGetAndRemove() { + String symbolicName = "my.catalog.location.id"; + String locationType = "localhost"; + String yaml = Joiner.on("\n").join( + "brooklyn.catalog:", + " id: " + symbolicName, + " version: " + TEST_VERSION, + " itemType: location", + " name: My Catalog Location", + " description: My description", + " item:", + " type: " + locationType); + + // Create location item + Map items = client().path("/catalog/bundles") + .header(HttpHeaders.CONTENT_TYPE, "application/yaml") + .post(yaml, BundleInstallationRestResult.class).getTypes(); + TypeSummary locationItem = Iterables.getOnlyElement(items.values()); + + assertEquals(locationItem.getSymbolicName(), symbolicName); + assertEquals(locationItem.getVersion(), TEST_VERSION); + + // Retrieve location item + TypeDetail location = client().path("/catalog/types/"+symbolicName+"/"+TEST_VERSION).get(TypeDetail.class); + assertEquals(location.getSymbolicName(), symbolicName); + + // Retrieve all locations + Set locations = client().path("/catalog/types").query("supertype", "location") + .get(new GenericType>() {}); + boolean found = false; + for (TypeSummary contender : locations) { + if (contender.getSymbolicName().equals(symbolicName)) { + found = true; + break; + } + } + Assert.assertTrue(found, "contenders="+locations); + + // Delete + Response deleteResponse = client().path("/catalog/bundles/"+locationItem.getContainingBundle().replaceAll(":", "/")) + .delete(); + assertEquals(deleteResponse.getStatus(), Response.Status.OK.getStatusCode()); + BundleInstallationRestResult deletionResponse = deleteResponse.readEntity(BundleInstallationRestResult.class); + Assert.assertEquals(deletionResponse.getBundle(), symbolicName+":"+TEST_VERSION); + Assert.assertEquals(deletionResponse.getTypes().keySet(), MutableSet.of(symbolicName+":"+TEST_VERSION)); + + Response getPostDeleteResponse = client().path("/catalog/types/"+symbolicName+"/"+TEST_VERSION) + .get(); + assertEquals(getPostDeleteResponse.getStatus(), Response.Status.NOT_FOUND.getStatusCode()); + } + + @Test + public void testListEnrichers() { + Set enrichers = client().path("/catalog/types").query("supertype", "enricher") + .get(new GenericType>() {}); + + assertTrue(enrichers.size() > 0); + TypeSummary asp = null; + for (TypeSummary p : enrichers) { + if (Aggregator.class.getName().equals(p.getSymbolicName())) + asp = p; + } + Assert.assertNotNull(asp, "didn't find Aggregator"); + } + + @Test + public void testEnricherAddGet() { + String symbolicName = "my.catalog.enricher.id"; + String enricherType = "org.apache.brooklyn.enricher.stock.Aggregator"; + String yaml = Joiner.on("\n").join( + "brooklyn.catalog:", + " id: " + symbolicName, + " version: " + TEST_VERSION, + " itemType: enricher", + " name: My Catalog Enricher", + " description: My description", + " item:", + " type: " + enricherType); + + // Create location item + Map items = client().path("/catalog/bundles") + .header(HttpHeaders.CONTENT_TYPE, "application/yaml") + .post(yaml, BundleInstallationRestResult.class).getTypes(); + TypeSummary enricherItem = Iterables.getOnlyElement(items.values()); + + assertEquals(enricherItem.getSymbolicName(), symbolicName); + assertEquals(enricherItem.getVersion(), TEST_VERSION); + + // Retrieve location item + TypeSummary enricher = client().path("/catalog/types/"+symbolicName+"/"+TEST_VERSION) + .get(TypeSummary.class); + assertEquals(enricher.getSymbolicName(), symbolicName); + + // Retrieve all locations + Set enrichers = client().path("/catalog/types").query("supertype", "enricher") + .get(new GenericType>() {}); + boolean found = false; + for (TypeSummary contender : enrichers) { + if (contender.getSymbolicName().equals(symbolicName)) { + found = true; + break; + } + } + Assert.assertTrue(found, "contenders="+enrichers); + } + + @Test + // osgi may fail in IDE, typically works on mvn CLI though + public void testRegisterOsgiEnricherTopLevelSyntax() { + TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_PATH); + + String symbolicName = "my.catalog.enricher.id"; + String enricherType = OsgiTestResources.BROOKLYN_TEST_OSGI_ENTITIES_SIMPLE_ENRICHER; + String bundleUrl = OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_URL; + + String yaml = Joiner.on("\n").join( + "brooklyn.catalog:", + " id: " + symbolicName, + " version: " + TEST_VERSION, + " itemType: enricher", + " name: My Catalog Enricher", + " description: My description", + " libraries:", + " - url: " + bundleUrl, + " item:", + " type: " + enricherType); + + TypeSummary installedItem = Iterables.getOnlyElement( client().path("/catalog/bundles") + .header(HttpHeaders.CONTENT_TYPE, "application/yaml") + .post(yaml, BundleInstallationRestResult.class).getTypes().values() ); + + assertEquals(installedItem.getSymbolicName(), symbolicName); + assertEquals(installedItem.getVersion(), TEST_VERSION); + } + + @Test + public void testDeleteCustomEntityFromCatalog() { + String symbolicName = "my.catalog.app.id.to.subsequently.delete"; + String yaml = Joiner.on("\n").join( + "brooklyn.catalog:", + " id: " + symbolicName, + " version: " + TEST_VERSION, + " itemType: entity", + " name: My Catalog App To Be Deleted", + " description: My description", + " item:", + " type: org.apache.brooklyn.core.test.entity.TestEntity"); + + client().path("/catalog/bundles") + .header(HttpHeaders.CONTENT_TYPE, "application/yaml") + .post(yaml); + + BundleSummary getInstalledBundle = client().path("/catalog/bundles/"+symbolicName+"/"+TEST_VERSION) + .get(BundleSummary.class); + assertEquals(getInstalledBundle.getSymbolicName(), symbolicName); + assertEquals(getInstalledBundle.getVersion(), TEST_VERSION); + Asserts.assertNotNull(getInstalledBundle.getTypes(), "expected 'types' in: "+getInstalledBundle.getExtraFields()); + Asserts.assertStringContains(""+getInstalledBundle.getTypes(), "My Catalog App"); + + Response deleteResponse = client().path("/catalog/bundles/"+symbolicName+"/"+TEST_VERSION) + .delete(); + + assertEquals(deleteResponse.getStatus(), Response.Status.OK.getStatusCode()); + // contents of delete tested in delete location method + + Response getPostDeleteResponse = client().path("/catalog/bundles/"+symbolicName+"/"+TEST_VERSION) + .get(); + assertEquals(getPostDeleteResponse.getStatus(), Response.Status.NOT_FOUND.getStatusCode()); + } + + private void addCatalogItemWithInvalidBundleUrl(String bundleUrl) { + String symbolicName = "my.catalog.entity.id"; + String yaml = Joiner.on("\n").join( + "brooklyn.catalog:", + " id: " + symbolicName, + " version: " + TEST_VERSION, + " itemType: entity", + " name: My Catalog App", + " description: My description", + " icon_url: classpath:/org/apache/brooklyn/test/osgi/entities/icon.gif", + " libraries:", + " - url: " + bundleUrl, + " item:", + " type: org.apache.brooklyn.core.test.entity.TestEntity"); + + Response response = client().path("/catalog/bundles") + .header(HttpHeaders.CONTENT_TYPE, "application/x-yaml") + .post(yaml); + + assertEquals(response.getStatus(), HttpStatus.BAD_REQUEST_400); + } + + @Test + public void testAddUnreachableItem() { + addCatalogItemWithInvalidBundleUrl("http://0.0.0.0/can-not-connect"); + } + + @Test + public void testAddInvalidItem() { + //equivalent to HTTP response 200 text/html + addCatalogItemWithInvalidBundleUrl("classpath://not-a-jar-file.txt"); + } + + @Test + public void testAddMissingItem() { + //equivalent to HTTP response 404 text/html + addCatalogItemWithInvalidBundleUrl("classpath://missing-jar-file.txt"); + } + + @Test + public void testInvalidArchive() throws Exception { + File f = Os.newTempFile("osgi", "zip"); + + Response response = client().path("/catalog/bundles") + .header(HttpHeaders.CONTENT_TYPE, "application/x-zip") + .post(Streams.readFully(new FileInputStream(f))); + + assertEquals(response.getStatus(), Response.Status.BAD_REQUEST.getStatusCode()); + Asserts.assertStringContainsIgnoreCase(response.readEntity(String.class), "zip file is empty"); + } + + @Test + public void testArchiveWithoutBom() throws Exception { + File f = createZip(ImmutableMap.of()); + + Response response = client().path("/catalog/bundles") + .header(HttpHeaders.CONTENT_TYPE, "application/x-zip") + .post(Streams.readFully(new FileInputStream(f))); + + assertEquals(response.getStatus(), Response.Status.BAD_REQUEST.getStatusCode()); + Asserts.assertStringContainsIgnoreCase(response.readEntity(String.class), "Missing bundle symbolic name in BOM or MANIFEST"); + } + + @Test + public void testArchiveWithoutBundleAndVersion() throws Exception { + File f = createZip(ImmutableMap.of("catalog.bom", Joiner.on("\n").join( + "brooklyn.catalog:", + " itemType: entity", + " name: My Catalog App", + " description: My description", + " icon_url: classpath:/org/apache/brooklyn/test/osgi/entities/icon.gif", + " item:", + " type: org.apache.brooklyn.core.test.entity.TestEntity"))); + + Response response = client().path("/catalog/bundles") + .header(HttpHeaders.CONTENT_TYPE, "application/x-zip") + .post(Streams.readFully(new FileInputStream(f))); + + assertEquals(response.getStatus(), Response.Status.BAD_REQUEST.getStatusCode()); + Asserts.assertStringContainsIgnoreCase(response.readEntity(String.class), "Missing bundle symbolic name in BOM or MANIFEST"); + } + + @Test + public void testArchiveWithoutBundle() throws Exception { + File f = createZip(ImmutableMap.of("catalog.bom", Joiner.on("\n").join( + "brooklyn.catalog:", + " version: 0.1.0", + " itemType: entity", + " name: My Catalog App", + " description: My description", + " icon_url: classpath:/org/apache/brooklyn/test/osgi/entities/icon.gif", + " item:", + " type: org.apache.brooklyn.core.test.entity.TestEntity"))); + + Response response = client().path("/catalog/bundles") + .header(HttpHeaders.CONTENT_TYPE, "application/x-zip") + .post(Streams.readFully(new FileInputStream(f))); + + assertEquals(response.getStatus(), Response.Status.BAD_REQUEST.getStatusCode()); + Asserts.assertStringContainsIgnoreCase(response.readEntity(String.class), + "Missing bundle symbolic name in BOM or MANIFEST"); + } + + @Test + public void testArchiveWithoutVersion() throws Exception { + File f = createZip(ImmutableMap.of("catalog.bom", Joiner.on("\n").join( + "brooklyn.catalog:", + " bundle: org.apache.brooklyn.test", + " itemType: entity", + " name: My Catalog App", + " description: My description", + " icon_url: classpath:/org/apache/brooklyn/test/osgi/entities/icon.gif", + " item:", + " type: org.apache.brooklyn.core.test.entity.TestEntity"))); + + Response response = client().path("/catalog/bundles") + .header(HttpHeaders.CONTENT_TYPE, "application/x-zip") + .post(Streams.readFully(new FileInputStream(f))); + + assertEquals(response.getStatus(), Response.Status.BAD_REQUEST.getStatusCode()); + Asserts.assertStringContainsIgnoreCase(response.readEntity(String.class), "Catalog BOM must define version"); + } + + @Test + public void testJarWithoutMatchingBundle() throws Exception { + String name = "My Catalog App"; + String bundle = "org.apache.brooklyn.test"; + String version = "0.1.0"; + String wrongBundleName = "org.apache.brooklyn.test2"; + File f = createJar(ImmutableMap.of( + "catalog.bom", Joiner.on("\n").join( + "brooklyn.catalog:", + " bundle: " + bundle, + " version: " + version, + " itemType: entity", + " name: " + name, + " description: My description", + " icon_url: classpath:/org/apache/brooklyn/test/osgi/entities/icon.gif", + " item:", + " type: org.apache.brooklyn.core.test.entity.TestEntity"), + "META-INF/MANIFEST.MF", Joiner.on("\n").join( + "Manifest-Version: 1.0", + "Bundle-Name: " + name, + "Bundle-SymbolicName: "+wrongBundleName, + "Bundle-Version: " + version, + "Bundle-ManifestVersion: " + version))); + + Response response = client().path("/catalog/bundles") + .header(HttpHeaders.CONTENT_TYPE, "application/x-jar") + .post(Streams.readFully(new FileInputStream(f))); + + assertEquals(response.getStatus(), Response.Status.BAD_REQUEST.getStatusCode()); + Asserts.assertStringContainsIgnoreCase(response.readEntity(String.class), + "symbolic name mismatch", + wrongBundleName, bundle); + } + + @Test + public void testJarWithoutMatchingVersion() throws Exception { + String name = "My Catalog App"; + String bundle = "org.apache.brooklyn.test"; + String version = "0.1.0"; + String wrongVersion = "0.3.0"; + File f = createJar(ImmutableMap.of( + "catalog.bom", Joiner.on("\n").join( + "brooklyn.catalog:", + " bundle: " + bundle, + " version: " + version, + " itemType: entity", + " name: " + name, + " description: My description", + " icon_url: classpath:/org/apache/brooklyn/test/osgi/entities/icon.gif", + " item:", + " type: org.apache.brooklyn.core.test.entity.TestEntity"), + "META-INF/MANIFEST.MF", Joiner.on("\n").join( + "Manifest-Version: 1.0", + "Bundle-Name: " + name, + "Bundle-SymbolicName: " + bundle, + "Bundle-Version: " + wrongVersion, + "Bundle-ManifestVersion: " + wrongVersion))); + + Response response = client().path("/catalog/bundles") + .header(HttpHeaders.CONTENT_TYPE, "application/x-jar") + .post(Streams.readFully(new FileInputStream(f))); + + assertEquals(response.getStatus(), Response.Status.BAD_REQUEST.getStatusCode()); + Asserts.assertStringContainsIgnoreCase(response.readEntity(String.class), + "version mismatch", + wrongVersion, version); + } + + @Test + public void testOsgiBundleWithBom() throws Exception { + TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_PATH); + final String symbolicName = OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_SYMBOLIC_NAME_FULL; + final String version = OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_VERSION; + final String bundleUrl = OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_URL; + BundleMaker bm = new BundleMaker(manager); + File f = Os.newTempFile("osgi", "jar"); + Files.copyFile(ResourceUtils.create(this).getResourceFromUrl(bundleUrl), f); + + String bom = Joiner.on("\n").join( + "brooklyn.catalog:", + " bundle: " + symbolicName, + " version: " + version, + " id: " + symbolicName, + " itemType: entity", + " name: My Catalog App", + " description: My description", + " icon_url: classpath:/org/apache/brooklyn/test/osgi/entities/icon.gif", + " item:", + " type: org.apache.brooklyn.core.test.entity.TestEntity"); + + f = bm.copyAdding(f, MutableMap.of(new ZipEntry("catalog.bom"), (InputStream) new ByteArrayInputStream(bom.getBytes()))); + + Response response = client().path("/catalog/bundles") + .header(HttpHeaders.CONTENT_TYPE, "application/x-jar") + .post(Streams.readFully(new FileInputStream(f))); + + assertEquals(response.getStatus(), Response.Status.CREATED.getStatusCode()); + + TypeSummary entityItem = client().path("/catalog/types/"+symbolicName + "/" + version) + .get(TypeSummary.class); + + assertEquals(entityItem.getSymbolicName(), symbolicName); + assertEquals(entityItem.getVersion(), version); + + // and internally let's check we have libraries + RegisteredType item = getManagementContext().getTypeRegistry().get(symbolicName, version); + Assert.assertNotNull(item); + Collection libs = item.getLibraries(); + assertEquals(libs.size(), 1); + OsgiBundleWithUrl lib = Iterables.getOnlyElement(libs); + Assert.assertNull(lib.getUrl()); + + assertEquals(lib.getSymbolicName(), "org.apache.brooklyn.test.resources.osgi.brooklyn-test-osgi-entities"); + assertEquals(lib.getSuppliedVersionString(), version); + + // now let's check other things on the item + URI expectedIconUrl = URI.create(getEndpointAddress() + "/catalog/types/" + symbolicName + "/" + entityItem.getVersion()+"/icon").normalize(); + assertEquals(entityItem.getDisplayName(), "My Catalog App"); + assertEquals(entityItem.getDescription(), "My description"); + assertEquals(entityItem.getIconUrl(), expectedIconUrl.getPath()); + assertEquals(item.getIconUrl(), "classpath:/org/apache/brooklyn/test/osgi/entities/icon.gif"); + + if (checkTraits(false)) { + // an InterfacesTag should be created for every catalog item + @SuppressWarnings("unchecked") + Map> traitsMapTag = Iterables.getOnlyElement(Iterables.filter(entityItem.getTags(), Map.class)); + List actualInterfaces = traitsMapTag.get("traits"); + List> expectedInterfaces = Reflections.getAllInterfaces(TestEntity.class); + assertEquals(actualInterfaces.size(), expectedInterfaces.size()); + for (Class expectedInterface : expectedInterfaces) { + assertTrue(actualInterfaces.contains(expectedInterface.getName())); + } + } + + byte[] iconData = client().path("/catalog/types/" + symbolicName + "/" + version + "/icon").get(byte[].class); + assertEquals(iconData.length, 43); + } + + @Test + public void testOsgiBundleWithBomNotInBrooklynNamespace() throws Exception { + TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), OsgiTestResources.BROOKLYN_TEST_OSGI_ENTITIES_COM_EXAMPLE_PATH); + final String symbolicName = OsgiTestResources.BROOKLYN_TEST_OSGI_ENTITIES_COM_EXAMPLE_SYMBOLIC_NAME_FULL; + final String version = OsgiTestResources.BROOKLYN_TEST_OSGI_ENTITIES_COM_EXAMPLE_VERSION; + final String bundleUrl = OsgiTestResources.BROOKLYN_TEST_OSGI_ENTITIES_COM_EXAMPLE_URL; + final String entityType = OsgiTestResources.BROOKLYN_TEST_OSGI_ENTITIES_COM_EXAMPLE_ENTITY; + final String iconPath = OsgiTestResources.BROOKLYN_TEST_OSGI_ENTITIES_COM_EXAMPLE_ICON_PATH; + BundleMaker bm = new BundleMaker(manager); + File f = Os.newTempFile("osgi", "jar"); + Files.copyFile(ResourceUtils.create(this).getResourceFromUrl(bundleUrl), f); + + String bom = Joiner.on("\n").join( + "brooklyn.catalog:", + " bundle: " + symbolicName, + " version: " + version, + " id: " + symbolicName, + " itemType: entity", + " name: My Catalog App", + " description: My description", + " icon_url: classpath:" + iconPath, + " item:", + " type: " + entityType); + + f = bm.copyAdding(f, MutableMap.of(new ZipEntry("catalog.bom"), (InputStream) new ByteArrayInputStream(bom.getBytes()))); + + Response response = client().path("/catalog/bundles") + .header(HttpHeaders.CONTENT_TYPE, "application/x-zip") + .post(Streams.readFully(new FileInputStream(f))); + + + assertEquals(response.getStatus(), Response.Status.CREATED.getStatusCode()); + + TypeDetail entityItem = client().path("/catalog/types/"+symbolicName + "/" + version) + .get(TypeDetail.class); + + Assert.assertNotNull(entityItem.getPlan().getData()); + Assert.assertTrue(entityItem.getPlan().getData().toString().contains(entityType)); + + assertEquals(entityItem.getSymbolicName(), symbolicName); + assertEquals(entityItem.getVersion(), version); + + // and internally let's check we have libraries + RegisteredType item = getManagementContext().getTypeRegistry().get(symbolicName, version); + Assert.assertNotNull(item); + Collection libs = item.getLibraries(); + assertEquals(libs.size(), 1); + OsgiBundleWithUrl lib = Iterables.getOnlyElement(libs); + Assert.assertNull(lib.getUrl()); + + assertEquals(lib.getSymbolicName(), symbolicName); + assertEquals(lib.getSuppliedVersionString(), version); + + // now let's check other things on the item + assertEquals(entityItem.getDescription(), "My description"); + URI expectedIconUrl = URI.create(getEndpointAddress() + "/catalog/types/" + symbolicName + "/" + entityItem.getVersion() + "/icon").normalize(); + assertEquals(entityItem.getIconUrl(), expectedIconUrl.getPath()); + assertEquals(item.getIconUrl(), "classpath:" + iconPath); + + if (checkTraits(false)) { + // an InterfacesTag should be created for every catalog item + @SuppressWarnings("unchecked") + Map> traitsMapTag = Iterables.getOnlyElement(Iterables.filter(entityItem.getTags(), Map.class)); + List actualInterfaces = traitsMapTag.get("traits"); + List expectedInterfaces = ImmutableList.of(Entity.class.getName(), BrooklynObject.class.getName(), Identifiable.class.getName(), Configurable.class.getName()); + assertTrue(actualInterfaces.containsAll(expectedInterfaces), "actual="+actualInterfaces); + } + + byte[] iconData = client().path("/catalog/types/" + symbolicName + "/" + version + "/icon").get(byte[].class); + assertEquals(iconData.length, 43); + + // Check that the catalog item is useable (i.e. can deploy the entity) + String appYaml = Joiner.on("\n").join( + "services:", + "- type: " + symbolicName + ":" + version, + " name: myEntityName"); + + Response appResponse = client().path("/applications") + .header(HttpHeaders.CONTENT_TYPE, "application/x-yaml") + .post(appYaml); + + assertEquals(appResponse.getStatus(), Response.Status.CREATED.getStatusCode()); + + Entity entity = Iterables.tryFind(getManagementContext().getEntityManager().getEntities(), EntityPredicates.displayNameEqualTo("myEntityName")).get(); + assertEquals(entity.getEntityType().getName(), entityType); + } + + private static File createZip(Map files) throws Exception { + File f = Os.newTempFile("osgi", "zip"); + + ZipOutputStream zip = new ZipOutputStream(new FileOutputStream(f)); + + for (Map.Entry entry : files.entrySet()) { + ZipEntry ze = new ZipEntry(entry.getKey()); + zip.putNextEntry(ze); + zip.write(entry.getValue().getBytes()); + } + + zip.closeEntry(); + zip.flush(); + zip.close(); + + return f; + } + + private static File createJar(Map files) throws Exception { + File f = Os.newTempFile("osgi", "jar"); + + JarOutputStream zip = new JarOutputStream(new FileOutputStream(f)); + + for (Map.Entry entry : files.entrySet()) { + JarEntry ze = new JarEntry(entry.getKey()); + zip.putNextEntry(ze); + zip.write(entry.getValue().getBytes()); + } + + zip.closeEntry(); + zip.flush(); + zip.close(); + + return f; + } + + @Test + public void testGetOnlyLatestApplication() { + String symbolicName = "latest.catalog.application.id"; + String itemType = "template"; + String serviceType = "org.apache.brooklyn.core.test.entity.TestEntity"; + + addTestCatalogItem(symbolicName, itemType, TEST_VERSION, serviceType); + addTestCatalogItem(symbolicName, itemType, TEST_LASTEST_VERSION, serviceType); + + TypeSummary application = client().path("/catalog/types/" + symbolicName + "/latest") + .get(TypeSummary.class); + assertEquals(application.getVersion(), TEST_LASTEST_VERSION); + } + + @Test + public void testGetOnlyLatestDifferentCases() { + // depends on installation of this + testGetOnlyLatestApplication(); + + String symbolicName = "latest.catalog.application.id"; + + TypeSummary application = client().path("/catalog/types/" + symbolicName + "/LaTeSt") + .get(TypeSummary.class); + assertEquals(application.getVersion(), TEST_LASTEST_VERSION); + + application = client().path("/catalog/types/" + symbolicName + "/LATEST") + .get(TypeSummary.class); + assertEquals(application.getVersion(), TEST_LASTEST_VERSION); + } + + @Test + public void testGetOnlyLatestEntity() { + String symbolicName = "latest.catalog.entity.id"; + String itemType = "entity"; + String serviceType = "org.apache.brooklyn.core.test.entity.TestEntity"; + + addTestCatalogItem(symbolicName, itemType, TEST_VERSION, serviceType); + addTestCatalogItem(symbolicName, itemType, TEST_LASTEST_VERSION, serviceType); + + TypeSummary application = client().path("/catalog/types/" + symbolicName + "/latest") + .get(TypeSummary.class); + assertEquals(application.getVersion(), TEST_LASTEST_VERSION); + } + + @Test + public void testGetOnlyLatestLocation() { + String symbolicName = "latest.catalog.location.id"; + String itemType = "location"; + String serviceType = "localhost"; + + addTestCatalogItem(symbolicName, itemType, TEST_VERSION, serviceType); + addTestCatalogItem(symbolicName, itemType, TEST_LASTEST_VERSION, serviceType); + + TypeSummary application = client().path("/catalog/types/" + symbolicName + "/latest") + .get(TypeSummary.class); + assertEquals(application.getVersion(), TEST_LASTEST_VERSION); + } + + + @Test + public void testForceUpdateForYAML() { + String symbolicName = "force.update.catalog.application.id"; + String itemType = "template"; + String initialName = "My Catalog App"; + String initialDescription = "My description"; + String updatedName = initialName + " 2"; + String updatedDescription = initialDescription + " 2"; + + String initialYaml = Joiner.on("\n").join( + "brooklyn.catalog:", + " id: " + symbolicName, + " version: " + TEST_VERSION, + " itemType: " + itemType, + " name: " + initialName, + " description: " + initialDescription, + " icon_url: classpath:///bridge-small.png", + " version: " + TEST_VERSION, + " item:", + " type: org.apache.brooklyn.core.test.entity.TestEntity"); + String updatedYaml = Joiner.on("\n").join( + "brooklyn.catalog:", + " id: " + symbolicName, + " version: " + TEST_VERSION, + " itemType: " + itemType, + " name: " + updatedName, + " description: " + updatedDescription, + " icon_url: classpath:///bridge-small.png", + " version: " + TEST_VERSION, + " item:", + " type: org.apache.brooklyn.core.test.entity.TestEntity"); + + client().path("/catalog/bundles").header(HttpHeaders.CONTENT_TYPE, "application/yaml").post(initialYaml); + + TypeDetail initialApplication = client().path("/catalog/types/" + symbolicName + "/" + TEST_VERSION) + .get(TypeDetail.class); + assertEquals(initialApplication.getDisplayName(), initialName); + assertEquals(initialApplication.getDescription(), initialDescription); + + Response invalidResponse = client().path("/catalog/bundles").header(HttpHeaders.CONTENT_TYPE, "application/yaml").post(updatedYaml); + + assertEquals(invalidResponse.getStatus(), Response.Status.BAD_REQUEST.getStatusCode()); + + Response validResponse = client().path("/catalog/bundles").query("force", true).header(HttpHeaders.CONTENT_TYPE, "application/yaml").post(updatedYaml); + + assertEquals(validResponse.getStatus(), Response.Status.CREATED.getStatusCode()); + + TypeSummary application = client().path("/catalog/types/" + symbolicName + "/" + TEST_VERSION) + .get(TypeSummary.class); + assertEquals(application.getDisplayName(), updatedName); + assertEquals(application.getDescription(), updatedDescription); + } + + @Test + public void testForceUpdateForZip() throws Exception { + final String symbolicName = "force.update.zip.catalog.application.id"; + final String initialName = "My Catalog App"; + final String initialDescription = "My Description"; + final String updatedName = initialName + " 2"; + final String updatedDescription = initialDescription +" 2"; + + File initialZip = createZip(ImmutableMap.of("catalog.bom", Joiner.on("\n").join( + "brooklyn.catalog:", + " bundle: " + symbolicName, + " version: " + TEST_VERSION, + " id: " + symbolicName, + " itemType: entity", + " name: " + initialName, + " description: " + initialDescription, + " icon_url: classpath:/org/apache/brooklyn/test/osgi/entities/icon.gif", + " item:", + " type: org.apache.brooklyn.core.test.entity.TestEntity"))); + File updatedZip = createZip(ImmutableMap.of("catalog.bom", Joiner.on("\n").join( + "brooklyn.catalog:", + " bundle: " + symbolicName, + " version: " + TEST_VERSION, + " id: " + symbolicName, + " itemType: entity", + " name: " + updatedName, + " description: " + updatedDescription, + " icon_url: classpath:/org/apache/brooklyn/test/osgi/entities/icon.gif", + " item:", + " type: org.apache.brooklyn.core.test.entity.TestEntity"))); + + client().path("/catalog/bundles") + .header(HttpHeaders.CONTENT_TYPE, "application/x-zip") + .post(Streams.readFully(new FileInputStream(initialZip))); + + TypeSummary initialEntity = client().path("/catalog/types/" + symbolicName + "/" + TEST_VERSION) + .get(TypeSummary.class); + assertEquals(initialEntity.getDisplayName(), initialName); + assertEquals(initialEntity.getDescription(), initialDescription); + + Response invalidResponse = client().path("/catalog/bundles") + .header(HttpHeaders.CONTENT_TYPE, "application/x-zip") + .post(Streams.readFully(new FileInputStream(updatedZip))); + + assertEquals(invalidResponse.getStatus(), Response.Status.BAD_REQUEST.getStatusCode()); + + Response validResponse = client().path("/catalog/bundles") + .header(HttpHeaders.CONTENT_TYPE, "application/x-zip") + .query("force", true) + .post(Streams.readFully(new FileInputStream(updatedZip))); + + assertEquals(validResponse.getStatus(), Response.Status.CREATED.getStatusCode()); + + TypeSummary entity = client().path("/catalog/types/" + symbolicName + "/" + TEST_VERSION) + .get(TypeSummary.class); + assertEquals(entity.getDisplayName(), updatedName); + assertEquals(entity.getDescription(), updatedDescription); + } + + @Test + public void testForceUpdateForJar() throws Exception { + final String symbolicName = "force.update.jar.catalog.application.id"; + final String initialName = "My Catalog App"; + final String initialDescription = "My Description"; + final String updatedName = initialName + " 2"; + final String updatedDescription = initialDescription +" 2"; + + File initialJar = createJar(ImmutableMap.of("catalog.bom", Joiner.on("\n").join( + "brooklyn.catalog:", + " bundle: " + symbolicName, + " version: " + TEST_VERSION, + " id: " + symbolicName, + " itemType: entity", + " name: " + initialName, + " description: " + initialDescription, + " icon_url: classpath:/org/apache/brooklyn/test/osgi/entities/icon.gif", + " item:", + " type: org.apache.brooklyn.core.test.entity.TestEntity"))); + File updatedJar = createJar(ImmutableMap.of("catalog.bom", Joiner.on("\n").join( + "brooklyn.catalog:", + " bundle: " + symbolicName, + " version: " + TEST_VERSION, + " id: " + symbolicName, + " itemType: entity", + " name: " + updatedName, + " description: " + updatedDescription, + " icon_url: classpath:/org/apache/brooklyn/test/osgi/entities/icon.gif", + " item:", + " type: org.apache.brooklyn.core.test.entity.TestEntity"))); + + client().path("/catalog/bundles") + .header(HttpHeaders.CONTENT_TYPE, "application/x-jar") + .post(Streams.readFully(new FileInputStream(initialJar))); + + TypeSummary initialEntity = client().path("/catalog/types/" + symbolicName + "/" + TEST_VERSION) + .get(TypeSummary.class); + assertEquals(initialEntity.getDisplayName(), initialName); + assertEquals(initialEntity.getDescription(), initialDescription); + + Response invalidResponse = client().path("/catalog/bundles") + .header(HttpHeaders.CONTENT_TYPE, "application/x-jar") + .post(Streams.readFully(new FileInputStream(updatedJar))); + + assertEquals(invalidResponse.getStatus(), Response.Status.BAD_REQUEST.getStatusCode()); + + Response validResponse = client().path("/catalog/bundles") + .header(HttpHeaders.CONTENT_TYPE, "application/x-jar") + .query("force", true) + .post(Streams.readFully(new FileInputStream(updatedJar))); + + assertEquals(validResponse.getStatus(), Response.Status.CREATED.getStatusCode()); + + TypeSummary entity = client().path("/catalog/types/" + symbolicName + "/" + TEST_VERSION) + .get(TypeSummary.class); + assertEquals(entity.getDisplayName(), updatedName); + assertEquals(entity.getDescription(), updatedDescription); + } + + // TODO traits no longer always set - we have supertypes so not needed, we should investigate when they are and when they aren't + // and switch those to setting and using the supertypes + private boolean checkTraits(boolean currentExpectedToBeWorking) { + return currentExpectedToBeWorking; + } + +} diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/osgi/VersionedName.java b/utils/common/src/main/java/org/apache/brooklyn/util/osgi/VersionedName.java index 9fec03c03c..9734153068 100644 --- a/utils/common/src/main/java/org/apache/brooklyn/util/osgi/VersionedName.java +++ b/utils/common/src/main/java/org/apache/brooklyn/util/osgi/VersionedName.java @@ -17,19 +17,24 @@ import static com.google.common.base.Preconditions.checkNotNull; +import java.util.Comparator; + import javax.annotation.Nullable; import org.apache.brooklyn.util.guava.Maybe; import org.apache.brooklyn.util.text.BrooklynVersionSyntax; +import org.apache.brooklyn.util.text.NaturalOrderComparator; import org.apache.brooklyn.util.text.Strings; +import org.apache.brooklyn.util.text.VersionComparator; import org.osgi.framework.Bundle; import org.osgi.framework.Version; import com.google.common.base.Objects; +import com.google.common.collect.ComparisonChain; /** Records a name (string) and version (string), * with conveniences for pretty-printing and converting to OSGi format. */ -public class VersionedName { +public class VersionedName implements Comparable { private final String name; private final String v; @@ -172,4 +177,21 @@ public static Maybe parseMaybe(String symbolicNameWithVersion, bo return Maybe.of(new VersionedName(parts[0], parts.length == 2 ? parts[1] : null)); } + @Override + public int compareTo(VersionedName other) { + return VersionedNameComparator.INSTANCE.compare(this, other); + } + + public static class VersionedNameComparator implements Comparator { + public static final VersionedNameComparator INSTANCE = new VersionedNameComparator(); + + @Override + public int compare(VersionedName o1, VersionedName o2) { + return ComparisonChain.start() + .compare(o1.getSymbolicName(), o2.getSymbolicName(), NaturalOrderComparator.INSTANCE) + .compare(o2.getOsgiVersionString(), o1.getOsgiVersionString(), VersionComparator.INSTANCE) + .compare(o2.getVersionString(), o1.getVersionString(), VersionComparator.INSTANCE) + .result(); + } + } }