/* * 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.InputStream; import java.net.URI; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.Set; import javax.annotation.Nullable; import javax.ws.rs.Consumes; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; import org.apache.brooklyn.api.catalog.CatalogItem; import org.apache.brooklyn.api.entity.Application; import org.apache.brooklyn.api.entity.Entity; import org.apache.brooklyn.api.entity.EntitySpec; import org.apache.brooklyn.api.location.Location; import org.apache.brooklyn.api.location.LocationSpec; import org.apache.brooklyn.api.policy.Policy; import org.apache.brooklyn.api.policy.PolicySpec; import org.apache.brooklyn.api.typereg.RegisteredType; import org.apache.brooklyn.core.catalog.CatalogPredicates; import org.apache.brooklyn.core.catalog.internal.BasicBrooklynCatalog; import org.apache.brooklyn.core.catalog.internal.CatalogDto; import org.apache.brooklyn.core.catalog.internal.CatalogItemComparator; import org.apache.brooklyn.core.catalog.internal.CatalogUtils; import org.apache.brooklyn.core.mgmt.entitlement.Entitlements; import org.apache.brooklyn.core.mgmt.entitlement.Entitlements.StringAndArgument; import org.apache.brooklyn.core.typereg.RegisteredTypeLoadingContexts; import org.apache.brooklyn.core.typereg.RegisteredTypePredicates; 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.CatalogEntitySummary; import org.apache.brooklyn.rest.domain.CatalogItemSummary; import org.apache.brooklyn.rest.domain.CatalogLocationSummary; 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.WebResourceUtils; 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.exceptions.Exceptions; import org.apache.brooklyn.util.guava.Maybe; import org.apache.brooklyn.util.stream.Streams; 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.base.Function; import com.google.common.base.Predicate; import com.google.common.base.Predicates; import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.common.io.Files; import com.sun.jersey.core.header.FormDataContentDisposition; @HaHotStateRequired public class CatalogResource extends AbstractBrooklynRestResource implements CatalogApi { private static final Logger log = LoggerFactory.getLogger(CatalogResource.class); @SuppressWarnings("rawtypes") private final Function<CatalogItem, CatalogItemSummary> TO_CATALOG_ITEM_SUMMARY = new Function<CatalogItem, CatalogItemSummary>() { @Override public CatalogItemSummary apply(@Nullable CatalogItem input) { return CatalogTransformer.catalogItemSummary(brooklyn(), input); } }; @Override @Consumes(MediaType.MULTIPART_FORM_DATA) public Response createFromMultipart(InputStream uploadedInputStream, FormDataContentDisposition fileDetail) { return create(Streams.readFullyString(uploadedInputStream)); } static Set<String> missingIcons = MutableSet.of(); @Override public Response create(String yaml) { if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.ADD_CATALOG_ITEM, yaml)) { throw WebResourceUtils.unauthorized("User '%s' is not authorized to add catalog item", Entitlements.getEntitlementContext().user()); } Iterable<? extends CatalogItem<?, ?>> items; try { items = brooklyn().getCatalog().addItems(yaml); } catch (IllegalArgumentException e) { return Response.status(Status.BAD_REQUEST) .type(MediaType.APPLICATION_JSON) .entity(ApiError.of(e)) .build(); } log.info("REST created catalog items: "+items); Map<String,Object> result = MutableMap.of(); for (CatalogItem<?,?> item: items) { try { result.put(item.getId(), CatalogTransformer.catalogItemSummary(brooklyn(), item)); } catch (Throwable t) { log.warn("Error loading catalog item '"+item+"' (rethrowing): "+t); throw Exceptions.propagate(t); } } return Response.status(Status.CREATED).entity(result).build(); } @SuppressWarnings("deprecation") @Override public Response resetXml(String xml, boolean ignoreErrors) { if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.MODIFY_CATALOG_ITEM, null) || !Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.ADD_CATALOG_ITEM, null)) { throw WebResourceUtils.unauthorized("User '%s' is not authorized to modify catalog", Entitlements.getEntitlementContext().user()); } ((BasicBrooklynCatalog)mgmt().getCatalog()).reset(CatalogDto.newDtoFromXmlContents(xml, "REST reset"), !ignoreErrors); return Response.ok().build(); } @Override @Deprecated public void deleteEntity(String entityId) throws Exception { if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.MODIFY_CATALOG_ITEM, StringAndArgument.of(entityId, "delete"))) { throw WebResourceUtils.unauthorized("User '%s' is not authorized to modify catalog", Entitlements.getEntitlementContext().user()); } try { Maybe<RegisteredType> item = RegisteredTypes.tryValidate(mgmt().getTypeRegistry().get(entityId), RegisteredTypeLoadingContexts.spec(Entity.class)); if (item.isNull()) { throw WebResourceUtils.notFound("Entity with id '%s' not found", entityId); } if (item.isAbsent()) { throw WebResourceUtils.notFound("Item with id '%s' is not an entity", entityId); } brooklyn().getCatalog().deleteCatalogItem(item.get().getSymbolicName(), item.get().getVersion()); } catch (NoSuchElementException e) { // shouldn't come here throw WebResourceUtils.notFound("Entity with id '%s' could not be deleted", entityId); } } @Override public void deleteApplication(String symbolicName, String version) throws Exception { deleteEntity(symbolicName, version); } @Override public void deleteEntity(String symbolicName, String version) throws Exception { if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.MODIFY_CATALOG_ITEM, StringAndArgument.of(symbolicName+(Strings.isBlank(version) ? "" : ":"+version), "delete"))) { throw WebResourceUtils.unauthorized("User '%s' is not authorized to modify catalog", Entitlements.getEntitlementContext().user()); } RegisteredType item = mgmt().getTypeRegistry().get(symbolicName, version); if (item == null) { throw WebResourceUtils.notFound("Entity with id '%s:%s' not found", symbolicName, version); } else if (!RegisteredTypePredicates.IS_ENTITY.apply(item) && !RegisteredTypePredicates.IS_APPLICATION.apply(item)) { throw WebResourceUtils.preconditionFailed("Item with id '%s:%s' not an entity", symbolicName, version); } else { brooklyn().getCatalog().deleteCatalogItem(item.getSymbolicName(), item.getVersion()); } } @Override public void deletePolicy(String policyId, String version) throws Exception { if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.MODIFY_CATALOG_ITEM, StringAndArgument.of(policyId+(Strings.isBlank(version) ? "" : ":"+version), "delete"))) { throw WebResourceUtils.unauthorized("User '%s' is not authorized to modify catalog", Entitlements.getEntitlementContext().user()); } RegisteredType item = mgmt().getTypeRegistry().get(policyId, version); if (item == null) { throw WebResourceUtils.notFound("Policy with id '%s:%s' not found", policyId, version); } else if (!RegisteredTypePredicates.IS_POLICY.apply(item)) { throw WebResourceUtils.preconditionFailed("Item with id '%s:%s' not a policy", policyId, version); } else { brooklyn().getCatalog().deleteCatalogItem(item.getSymbolicName(), item.getVersion()); } } @Override public void deleteLocation(String locationId, String version) throws Exception { if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.MODIFY_CATALOG_ITEM, StringAndArgument.of(locationId+(Strings.isBlank(version) ? "" : ":"+version), "delete"))) { throw WebResourceUtils.unauthorized("User '%s' is not authorized to modify catalog", Entitlements.getEntitlementContext().user()); } RegisteredType item = mgmt().getTypeRegistry().get(locationId, version); if (item == null) { throw WebResourceUtils.notFound("Location with id '%s:%s' not found", locationId, version); } else if (!RegisteredTypePredicates.IS_LOCATION.apply(item)) { throw WebResourceUtils.preconditionFailed("Item with id '%s:%s' not a location", locationId, version); } else { brooklyn().getCatalog().deleteCatalogItem(item.getSymbolicName(), item.getVersion()); } } @Override public List<CatalogEntitySummary> listEntities(String regex, String fragment, boolean allVersions) { Predicate<CatalogItem<Entity, EntitySpec<?>>> filter = Predicates.and( CatalogPredicates.IS_ENTITY, CatalogPredicates.<Entity, EntitySpec<?>>disabled(false)); List<CatalogItemSummary> result = getCatalogItemSummariesMatchingRegexFragment(filter, regex, fragment, allVersions); return castList(result, CatalogEntitySummary.class); } @Override public List<CatalogItemSummary> listApplications(String regex, String fragment, boolean allVersions) { @SuppressWarnings("unchecked") Predicate<CatalogItem<Application, EntitySpec<? extends Application>>> filter = Predicates.and( CatalogPredicates.IS_TEMPLATE, CatalogPredicates.<Application,EntitySpec<? extends Application>>deprecated(false), CatalogPredicates.<Application,EntitySpec<? extends Application>>disabled(false)); return getCatalogItemSummariesMatchingRegexFragment(filter, regex, fragment, allVersions); } @Override @Deprecated public CatalogEntitySummary getEntity(String entityId) { if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_CATALOG_ITEM, entityId)) { throw WebResourceUtils.unauthorized("User '%s' is not authorized to see catalog entry", Entitlements.getEntitlementContext().user()); } CatalogItem<Entity,EntitySpec<?>> result = CatalogUtils.getCatalogItemOptionalVersion(mgmt(), Entity.class, entityId); if (result==null) { throw WebResourceUtils.notFound("Entity with id '%s' not found", entityId); } return CatalogTransformer.catalogEntitySummary(brooklyn(), result); } @Override public CatalogEntitySummary getEntity(String symbolicName, String version) { if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_CATALOG_ITEM, symbolicName+(Strings.isBlank(version)?"":":"+version))) { throw WebResourceUtils.unauthorized("User '%s' is not authorized to see catalog entry", Entitlements.getEntitlementContext().user()); } //TODO These casts are not pretty, we could just provide separate get methods for the different types? //Or we could provide asEntity/asPolicy cast methods on the CataloItem doing a safety check internally @SuppressWarnings("unchecked") CatalogItem<Entity, EntitySpec<?>> result = (CatalogItem<Entity, EntitySpec<?>>) brooklyn().getCatalog().getCatalogItem(symbolicName, version); if (result==null) { throw WebResourceUtils.notFound("Entity with id '%s:%s' not found", symbolicName, version); } return CatalogTransformer.catalogEntitySummary(brooklyn(), result); } @Override @Deprecated public CatalogEntitySummary getApplication(String applicationId) throws Exception { return getEntity(applicationId); } @Override public CatalogEntitySummary getApplication(String symbolicName, String version) { return getEntity(symbolicName, version); } @Override public List<CatalogPolicySummary> listPolicies(String regex, String fragment, boolean allVersions) { Predicate<CatalogItem<Policy, PolicySpec<?>>> filter = Predicates.and( CatalogPredicates.IS_POLICY, CatalogPredicates.<Policy, PolicySpec<?>>disabled(false)); List<CatalogItemSummary> result = getCatalogItemSummariesMatchingRegexFragment(filter, regex, fragment, allVersions); return castList(result, CatalogPolicySummary.class); } @Override @Deprecated public CatalogPolicySummary getPolicy(String policyId) { if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_CATALOG_ITEM, policyId)) { throw WebResourceUtils.unauthorized("User '%s' is not authorized to see catalog entry", Entitlements.getEntitlementContext().user()); } CatalogItem<? extends Policy, PolicySpec<?>> result = CatalogUtils.getCatalogItemOptionalVersion(mgmt(), Policy.class, policyId); if (result==null) { throw WebResourceUtils.notFound("Policy with id '%s' not found", policyId); } return CatalogTransformer.catalogPolicySummary(brooklyn(), result); } @Override public CatalogPolicySummary getPolicy(String policyId, String version) throws Exception { if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_CATALOG_ITEM, policyId+(Strings.isBlank(version)?"":":"+version))) { throw WebResourceUtils.unauthorized("User '%s' is not authorized to see catalog entry", Entitlements.getEntitlementContext().user()); } @SuppressWarnings("unchecked") CatalogItem<? extends Policy, PolicySpec<?>> result = (CatalogItem<? extends Policy, PolicySpec<?>>)brooklyn().getCatalog().getCatalogItem(policyId, version); if (result==null) { throw WebResourceUtils.notFound("Policy with id '%s:%s' not found", policyId, version); } return CatalogTransformer.catalogPolicySummary(brooklyn(), result); } @Override public List<CatalogLocationSummary> listLocations(String regex, String fragment, boolean allVersions) { Predicate<CatalogItem<Location, LocationSpec<?>>> filter = Predicates.and( CatalogPredicates.IS_LOCATION, CatalogPredicates.<Location, LocationSpec<?>>disabled(false)); List<CatalogItemSummary> result = getCatalogItemSummariesMatchingRegexFragment(filter, regex, fragment, allVersions); return castList(result, CatalogLocationSummary.class); } @Override @Deprecated public CatalogLocationSummary getLocation(String locationId) { if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_CATALOG_ITEM, locationId)) { throw WebResourceUtils.unauthorized("User '%s' is not authorized to see catalog entry", Entitlements.getEntitlementContext().user()); } CatalogItem<? extends Location, LocationSpec<?>> result = CatalogUtils.getCatalogItemOptionalVersion(mgmt(), Location.class, locationId); if (result==null) { throw WebResourceUtils.notFound("Location with id '%s' not found", locationId); } return CatalogTransformer.catalogLocationSummary(brooklyn(), result); } @Override public CatalogLocationSummary getLocation(String locationId, String version) throws Exception { if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_CATALOG_ITEM, locationId+(Strings.isBlank(version)?"":":"+version))) { throw WebResourceUtils.unauthorized("User '%s' is not authorized to see catalog entry", Entitlements.getEntitlementContext().user()); } @SuppressWarnings("unchecked") CatalogItem<? extends Location, LocationSpec<?>> result = (CatalogItem<? extends Location, LocationSpec<?>>)brooklyn().getCatalog().getCatalogItem(locationId, version); if (result==null) { throw WebResourceUtils.notFound("Location with id '%s:%s' not found", locationId, version); } return CatalogTransformer.catalogLocationSummary(brooklyn(), result); } @SuppressWarnings({ "unchecked", "rawtypes" }) private <T,SpecT> List<CatalogItemSummary> getCatalogItemSummariesMatchingRegexFragment(Predicate<CatalogItem<T,SpecT>> type, String regex, String fragment, boolean allVersions) { List filters = new ArrayList(); filters.add(type); if (Strings.isNonEmpty(regex)) filters.add(CatalogPredicates.xml(StringPredicates.containsRegex(regex))); if (Strings.isNonEmpty(fragment)) filters.add(CatalogPredicates.xml(StringPredicates.containsLiteralIgnoreCase(fragment))); if (!allVersions) filters.add(CatalogPredicates.isBestVersion(mgmt())); filters.add(CatalogPredicates.entitledToSee(mgmt())); ImmutableList<CatalogItem<Object, Object>> sortedItems = FluentIterable.from(brooklyn().getCatalog().getCatalogItems()) .filter(Predicates.and(filters)) .toSortedList(CatalogItemComparator.getInstance()); return Lists.transform(sortedItems, TO_CATALOG_ITEM_SUMMARY); } @Override @Deprecated public Response getIcon(String itemId) { if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_CATALOG_ITEM, itemId)) { throw WebResourceUtils.unauthorized("User '%s' is not authorized to see catalog entry", Entitlements.getEntitlementContext().user()); } return getCatalogItemIcon( mgmt().getTypeRegistry().get(itemId) ); } @Override public Response getIcon(String itemId, String version) { if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_CATALOG_ITEM, itemId+(Strings.isBlank(version)?"":":"+version))) { throw WebResourceUtils.unauthorized("User '%s' is not authorized to see catalog entry", Entitlements.getEntitlementContext().user()); } return getCatalogItemIcon(mgmt().getTypeRegistry().get(itemId, version)); } @Override public void setDeprecatedLegacy(String itemId, boolean deprecated) { log.warn("Use of deprecated \"/v1/catalog/entities/{itemId}/deprecated/{deprecated}\" for "+itemId +"; use \"/v1/catalog/entities/{itemId}/deprecated\" with request body"); setDeprecated(itemId, deprecated); } @SuppressWarnings("deprecation") @Override public void setDeprecated(String itemId, boolean deprecated) { if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.MODIFY_CATALOG_ITEM, StringAndArgument.of(itemId, "deprecated"))) { throw WebResourceUtils.unauthorized("User '%s' is not authorized to modify catalog", Entitlements.getEntitlementContext().user()); } CatalogUtils.setDeprecated(mgmt(), itemId, deprecated); } @SuppressWarnings("deprecation") @Override public void setDisabled(String itemId, boolean disabled) { if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.MODIFY_CATALOG_ITEM, StringAndArgument.of(itemId, "disabled"))) { throw WebResourceUtils.unauthorized("User '%s' is not authorized to modify catalog", Entitlements.getEntitlementContext().user()); } CatalogUtils.setDisabled(mgmt(), itemId, disabled); } private Response getCatalogItemIcon(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(); } // TODO Move to an appropriate utility class? @SuppressWarnings("unchecked") private static <T> List<T> castList(List<? super T> list, Class<T> elementType) { List<T> result = Lists.newArrayList(); Iterator<? super T> li = list.iterator(); while (li.hasNext()) { try { result.add((T) li.next()); } catch (Throwable throwable) { if (throwable instanceof NoClassDefFoundError) { // happens if class cannot be loaded for any reason during transformation - don't treat as fatal } else { Exceptions.propagateIfFatal(throwable); } // item cannot be transformed; we will have logged a warning earlier log.debug("Ignoring invalid catalog item: "+throwable); } } return result; } }