blob: 2e30ee27917ae754290fcfd1542ecc6aac66ba98 [file] [log] [blame]
/*
* 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.util;
import static com.google.common.collect.Iterables.transform;
import static org.apache.brooklyn.rest.util.WebResourceUtils.notFound;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.concurrent.Future;
import javax.ws.rs.core.MediaType;
import org.apache.brooklyn.api.catalog.BrooklynCatalog;
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.location.LocationRegistry;
import org.apache.brooklyn.api.mgmt.ManagementContext;
import org.apache.brooklyn.api.mgmt.Task;
import org.apache.brooklyn.api.objs.EntityAdjunct;
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.typereg.BrooklynTypeRegistry;
import org.apache.brooklyn.api.typereg.RegisteredType;
import org.apache.brooklyn.camp.brooklyn.BrooklynCampConstants;
import org.apache.brooklyn.camp.brooklyn.spi.dsl.methods.DslComponent;
import org.apache.brooklyn.camp.brooklyn.spi.dsl.methods.DslComponent.Scope;
import org.apache.brooklyn.config.ConfigKey;
import org.apache.brooklyn.core.entity.Attributes;
import org.apache.brooklyn.core.entity.Entities;
import org.apache.brooklyn.core.entity.EntityInternal;
import org.apache.brooklyn.core.entity.trait.Startable;
import org.apache.brooklyn.core.mgmt.BrooklynTaskTags;
import org.apache.brooklyn.core.mgmt.entitlement.Entitlements;
import org.apache.brooklyn.core.mgmt.entitlement.Entitlements.StringAndArgument;
import org.apache.brooklyn.core.objs.BrooklynTypes;
import org.apache.brooklyn.enricher.stock.Enrichers;
import org.apache.brooklyn.entity.stock.BasicApplication;
import org.apache.brooklyn.rest.domain.ApplicationSpec;
import org.apache.brooklyn.rest.domain.EntitySpec;
import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.collections.MutableSet;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.javalang.Reflections;
import org.apache.brooklyn.util.javalang.coerce.PrimitiveStringTypeCoercions;
import org.apache.brooklyn.util.net.Urls;
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.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.io.Files;
public class BrooklynRestResourceUtils {
private static final Logger log = LoggerFactory.getLogger(BrooklynRestResourceUtils.class);
private final ManagementContext mgmt;
public BrooklynRestResourceUtils(ManagementContext mgmt) {
Preconditions.checkNotNull(mgmt, "mgmt");
this.mgmt = mgmt;
}
public BrooklynTypeRegistry getTypeRegistry() {
return mgmt.getTypeRegistry();
}
public BrooklynCatalog getCatalog() {
return mgmt.getCatalog();
}
public ClassLoader getCatalogClassLoader() {
return mgmt.getCatalogClassLoader();
}
public LocationRegistry getLocationRegistry() {
return mgmt.getLocationRegistry();
}
/** finds the policy indicated by the given ID or name.
* @see {@link #getEntity(String,String)}; it then searches the policies of that
* entity for one whose ID or name matches that given.
* <p>
*
* @throws 404 or 412 (unless input is null in which case output is null) */
public Policy getPolicy(String application, String entity, String policy) {
return getPolicy(getEntity(application, entity), policy);
}
/** finds the policy indicated by the given ID or name.
* @see {@link #getEntity(String,String)}; it then searches the policies of that
* entity for one whose ID or name matches that given.
* <p>
*
* @throws 404 or 412 (unless input is null in which case output is null) */
public EntityAdjunct getAdjunct(String application, String entity, String adjunct) {
return getAdjunct(getEntity(application, entity), adjunct);
}
/** finds the policy indicated by the given ID or name.
* @see {@link #getPolicy(String,String,String)}.
* <p>
*
* @throws 404 or 412 (unless input is null in which case output is null) */
public Policy getPolicy(Entity entity, String policy) {
if (policy==null) return null;
for (Policy p: entity.policies()) {
if (policy.equals(p.getId())) return p;
}
for (Policy p: entity.policies()) {
if (policy.equals(p.getDisplayName())) return p;
}
throw WebResourceUtils.notFound("Cannot find policy '%s' in entity '%s'", policy, entity);
}
/** finds the policy indicated by the given ID or name.
* @see {@link #getAdjunct(String,String,String)}.
* <p>
*
* @throws 404 or 412 (unless input is null in which case output is null) */
public EntityAdjunct getAdjunct(Entity entity, String adjunct) {
if (adjunct==null) return null;
for (Policy p: entity.policies()) {
if (adjunct.equals(p.getId())) return p;
}
for (Enricher p: entity.enrichers()) {
if (adjunct.equals(p.getId())) return p;
}
for (Feed p: ((EntityInternal)entity).feeds()) {
if (adjunct.equals(p.getId())) return p;
}
for (Policy p: entity.policies()) {
if (adjunct.equals(p.getDisplayName())) return p;
}
for (Enricher p: entity.enrichers()) {
if (adjunct.equals(p.getDisplayName())) return p;
}
for (Feed p: ((EntityInternal)entity).feeds()) {
if (adjunct.equals(p.getDisplayName())) return p;
}
throw WebResourceUtils.notFound("Cannot find adjunct '%s' in entity '%s'", adjunct, entity);
}
/** finds the entity indicated by the given ID or name
* <p>
* prefers ID based lookup in which case appId is optional, and if supplied will be enforced.
* optionally the name can be supplied, for cases when paths should work across versions,
* in which case names will be searched recursively (and the application is required).
*
* @throws 404 or 412 (unless input is null in which case output is null) */
public Entity getEntity(String application, String entity) {
if (entity==null) return null;
Application app = application!=null ? getApplication(application) : null;
Entity e = mgmt.getEntityManager().getEntity(entity);
if (e!=null) {
if (!Entitlements.isEntitled(mgmt.getEntitlementManager(), Entitlements.SEE_ENTITY, e)) {
throw WebResourceUtils.notFound("Cannot find entity '%s': no known ID and application not supplied for searching", entity);
}
if (app==null || app.equals(findTopLevelApplication(e))) return e;
throw WebResourceUtils.preconditionFailed("Application '%s' specified does not match application '%s' to which entity '%s' (%s) is associated",
application, e.getApplication()==null ? null : e.getApplication().getId(), entity, e);
}
if (application==null)
throw WebResourceUtils.notFound("Cannot find entity '%s': no known ID and application not supplied for searching", entity);
assert app!=null : "null app should not be returned from getApplication";
e = searchForEntityNamed(app, entity);
if (e!=null) return e;
throw WebResourceUtils.notFound("Cannot find entity '%s' in application '%s' (%s)", entity, application, app);
}
private Application findTopLevelApplication(Entity e) {
// For nested apps, e.getApplication() can return its direct parent-app rather than the root app
// (particularly if e.getApplication() was called before the parent-app was wired up to its parent,
// because that call causes the application to be cached).
// Therefore we continue to walk the hierarchy until we find an "orphaned" application at the top.
Application app = e.getApplication();
while (app != null && !app.equals(app.getApplication())) {
app = app.getApplication();
}
return app;
}
/** looks for the given application instance, first by ID then by name
*
* @throws 404 if not found, or not entitled
*/
public Application getApplication(String application) {
Entity e = mgmt.getEntityManager().getEntity(application);
if (!Entitlements.isEntitled(mgmt.getEntitlementManager(), Entitlements.SEE_ENTITY, e)) {
throw notFound("Application '%s' not found", application);
}
if (e != null && e instanceof Application) return (Application) e;
for (Application app : mgmt.getApplications()) {
if (app.getId().equals(application)) return app;
if (application.equalsIgnoreCase(app.getDisplayName())) return app;
}
throw notFound("Application '%s' not found", application);
}
/** walks the hierarchy (depth-first) at root (often an Application) looking for
* an entity matching the given ID or name; returns the first such entity, or null if none found
**/
public Entity searchForEntityNamed(Entity root, String entity) {
if (root.getId().equals(entity) || entity.equals(root.getDisplayName())) return root;
for (Entity child: root.getChildren()) {
Entity result = searchForEntityNamed(child, entity);
if (result!=null) return result;
}
return null;
}
private class FindItemAndClass {
String catalogItemId;
Class<? extends Entity> clazz;
@SuppressWarnings({ "unchecked" })
private FindItemAndClass inferFrom(String type) {
RegisteredType item = mgmt.getTypeRegistry().get(type);
if (item != null) {
return setAs(
mgmt.getTypeRegistry().createSpec(item, null, org.apache.brooklyn.api.entity.EntitySpec.class).getType(),
item.getId());
} else {
try {
setAs(
(Class<? extends Entity>) getCatalog().getRootClassLoader().loadClass(type),
null);
log.info("Catalog does not contain item for type {}; loaded class directly instead", type);
return this;
} catch (ClassNotFoundException e2) {
log.warn("No catalog item for type {}, and could not load class directly; rethrowing", type);
throw new NoSuchElementException("Unable to find catalog item for type "+type);
}
}
}
private FindItemAndClass setAs(Class<? extends Entity> clazz, String catalogItemId) {
this.clazz = clazz;
this.catalogItemId = catalogItemId;
return this;
}
}
public Application create(ApplicationSpec spec) {
log.warn("Using deprecated functionality (as of 0.9.0), ApplicationSpec style (pre CAMP plans). " +
"Transition to actively supported spec plans.");
log.debug("REST creating application instance for {}", spec);
if (!Entitlements.isEntitled(mgmt.getEntitlementManager(), Entitlements.DEPLOY_APPLICATION, spec)) {
throw WebResourceUtils.forbidden("User '%s' is not authorized to deploy application %s",
Entitlements.getEntitlementContext().user(), spec);
}
final String type = spec.getType();
final String name = spec.getName();
final Map<String,String> configO = spec.getConfig();
final Set<EntitySpec> entities = (spec.getEntities() == null) ? ImmutableSet.<EntitySpec>of() : spec.getEntities();
final Application instance;
// Load the class; first try to use the appropriate catalog item; but then allow anything that is on the classpath
FindItemAndClass itemAndClass;
if (Strings.isEmpty(type)) {
itemAndClass = new FindItemAndClass().setAs(BasicApplication.class, null);
} else {
itemAndClass = new FindItemAndClass().inferFrom(type);
}
if (!Entitlements.isEntitled(mgmt.getEntitlementManager(), Entitlements.INVOKE_EFFECTOR, null)) {
throw WebResourceUtils.forbidden("User '%s' is not authorized to create application from applicationSpec %s",
Entitlements.getEntitlementContext().user(), spec);
}
try {
if (Application.class.isAssignableFrom(itemAndClass.clazz)) {
org.apache.brooklyn.api.entity.EntitySpec<?> coreSpec = toCoreEntitySpec(itemAndClass.clazz, name, configO, itemAndClass.catalogItemId);
configureRenderingMetadata(spec, coreSpec);
for (EntitySpec entitySpec : entities) {
log.info("REST creating instance for entity {}", entitySpec.getType());
coreSpec.child(toCoreEntitySpec(entitySpec));
}
log.info("REST placing '{}' under management", spec.getName() != null ? spec.getName() : spec);
instance = (Application) mgmt.getEntityManager().createEntity(coreSpec);
} else if (Entity.class.isAssignableFrom(itemAndClass.clazz)) {
if (entities.size() > 0)
log.warn("Cannot supply additional entities when using a non-application entity; ignoring in spec {}", spec);
org.apache.brooklyn.api.entity.EntitySpec<?> coreSpec = toCoreEntitySpec(BasicApplication.class, name, configO, itemAndClass.catalogItemId);
configureRenderingMetadata(spec, coreSpec);
coreSpec.child(toCoreEntitySpec(itemAndClass.clazz, name, configO, itemAndClass.catalogItemId)
.configure(BrooklynCampConstants.PLAN_ID, "soleChildId"));
coreSpec.enricher(Enrichers.builder()
.propagatingAllBut(Attributes.SERVICE_UP, Attributes.SERVICE_NOT_UP_INDICATORS,
Attributes.SERVICE_STATE_ACTUAL, Attributes.SERVICE_STATE_EXPECTED,
Attributes.SERVICE_PROBLEMS)
.from(new DslComponent(Scope.CHILD, "soleChildId").newTask())
.build());
log.info("REST placing '{}' under management", spec.getName());
instance = (Application) mgmt.getEntityManager().createEntity(coreSpec);
} else {
throw new IllegalArgumentException("Class " + itemAndClass.clazz + " must extend one of ApplicationBuilder, Application or Entity");
}
return instance;
} catch (Exception e) {
log.error("REST failed to create application: " + e, e);
throw Exceptions.propagate(e);
}
}
public Task<?> start(Application app, ApplicationSpec spec) {
return start(app, getLocationsManaged(spec));
}
public Task<?> start(Application app, List<? extends Location> locations) {
return Entities.invokeEffector(app, app, Startable.START,
MutableMap.of("locations", locations));
}
public List<Location> getLocationsManaged(ApplicationSpec spec) {
// Start all the managed entities by asking the app instance to start in background
Function<String, Location> buildLocationFromId = new Function<String, Location>() {
@Override
public Location apply(String id) {
id = fixLocation(id);
return getLocationRegistry().getLocationManaged(id);
}
};
ArrayList<Location> locations = Lists.newArrayList(transform(spec.getLocations(), buildLocationFromId));
return locations;
}
private org.apache.brooklyn.api.entity.EntitySpec<? extends Entity> toCoreEntitySpec(org.apache.brooklyn.rest.domain.EntitySpec spec) {
String type = spec.getType();
String name = spec.getName();
Map<String, String> config = (spec.getConfig() == null) ? Maps.<String,String>newLinkedHashMap() : Maps.newLinkedHashMap(spec.getConfig());
FindItemAndClass itemAndClass = new FindItemAndClass().inferFrom(type);
final Class<? extends Entity> clazz = itemAndClass.clazz;
org.apache.brooklyn.api.entity.EntitySpec<? extends Entity> result;
if (clazz.isInterface()) {
result = org.apache.brooklyn.api.entity.EntitySpec.create(clazz);
} else {
result = org.apache.brooklyn.api.entity.EntitySpec.create(Entity.class).impl(clazz).additionalInterfaces(Reflections.getAllInterfaces(clazz));
}
result.catalogItemId(itemAndClass.catalogItemId);
if (!Strings.isEmpty(name)) result.displayName(name);
result.configure( convertFlagsToKeys(result.getType(), config) );
configureRenderingMetadata(spec, result);
return result;
}
protected void configureRenderingMetadata(ApplicationSpec input, org.apache.brooklyn.api.entity.EntitySpec<?> entity) {
entity.configure(getRenderingConfigurationFor(input.getType()));
}
protected void configureRenderingMetadata(EntitySpec input, org.apache.brooklyn.api.entity.EntitySpec<?> entity) {
entity.configure(getRenderingConfigurationFor(input.getType()));
}
protected Map<?, ?> getRenderingConfigurationFor(String catalogId) {
MutableMap<Object, Object> result = MutableMap.of();
RegisteredType item = mgmt.getTypeRegistry().get(catalogId);
if (item==null) return result;
result.addIfNotNull("iconUrl", item.getIconUrl());
return result;
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private <T extends Entity> org.apache.brooklyn.api.entity.EntitySpec<?> toCoreEntitySpec(Class<T> clazz, String name, Map<String,String> configO, String catalogItemId) {
Map<String, String> config = (configO == null) ? Maps.<String,String>newLinkedHashMap() : Maps.newLinkedHashMap(configO);
org.apache.brooklyn.api.entity.EntitySpec<? extends Entity> result;
if (clazz.isInterface()) {
result = org.apache.brooklyn.api.entity.EntitySpec.create(clazz);
} else {
// If this is a concrete class, particularly for an Application class, we want the proxy
// to expose all interfaces it implements.
Class interfaceclazz = (Application.class.isAssignableFrom(clazz)) ? Application.class : Entity.class;
Set<Class<?>> additionalInterfaceClazzes = Reflections.getInterfacesIncludingClassAncestors(clazz);
result = org.apache.brooklyn.api.entity.EntitySpec.create(interfaceclazz).impl(clazz).additionalInterfaces(additionalInterfaceClazzes);
}
result.catalogItemId(catalogItemId);
if (!Strings.isEmpty(name)) result.displayName(name);
result.configure( convertFlagsToKeys(result.getImplementation(), config) );
return result;
}
private Map<?,?> convertFlagsToKeys(Class<? extends Entity> javaType, Map<?, ?> config) {
if (config==null || config.isEmpty() || javaType==null) return config;
Map<String, ConfigKey<?>> configKeys = BrooklynTypes.getDefinedConfigKeys(javaType);
Map<Object,Object> result = new LinkedHashMap<Object,Object>();
for (Map.Entry<?,?> entry: config.entrySet()) {
log.debug("Setting key {} to {} for REST creation of {}", new Object[] { entry.getKey(), entry.getValue(), javaType});
Object key = configKeys.get(entry.getKey());
if (key==null) {
log.warn("Unrecognised config key {} passed to {}; will be treated as flag (and likely ignored)", entry.getKey(), javaType);
key = entry.getKey();
}
result.put(key, entry.getValue());
}
return result;
}
public Task<?> destroy(final Application application) {
return mgmt.getExecutionManager().submit(
MutableMap.of("displayName", "destroying "+application,
"description", "REST call to destroy application "+application.getDisplayName()+" ("+application+")"),
new Runnable() {
@Override
public void run() {
((EntityInternal)application).destroy();
mgmt.getEntityManager().unmanage(application);
}
});
}
public Task<?> expunge(final Entity entity, final boolean release) {
if (mgmt.getEntitlementManager().isEntitled(Entitlements.getEntitlementContext(),
Entitlements.INVOKE_EFFECTOR, Entitlements.EntityAndItem.of(entity,
StringAndArgument.of("expunge", MutableMap.of("release", release))))) {
Map<String, Object> flags = MutableMap.<String, Object>of("displayName", "expunging " + entity, "description", "REST call to expunge entity "
+ entity.getDisplayName() + " (" + entity + ")");
if (Entitlements.getEntitlementContext() != null) {
flags.put("tags", MutableSet.of(BrooklynTaskTags.tagForEntitlement(Entitlements.getEntitlementContext())));
}
return mgmt.getExecutionManager().submit(
flags, new Runnable() {
@Override
public void run() {
if (release)
Entities.destroy(entity, false);
else
mgmt.getEntityManager().unmanage(entity);
}
});
}
throw WebResourceUtils.forbidden("User '%s' is not authorized to expunge entity %s",
Entitlements.getEntitlementContext().user(), entity);
}
@Deprecated
public static String fixLocation(String locationId) {
if (locationId.startsWith("/locations/") || locationId.startsWith("/v1/locations/")) {
log.warn("REST API using legacy URI syntax for location: "+locationId);
locationId = Strings.removeFromStart(locationId, "/v1/locations/");
locationId = Strings.removeFromStart(locationId, "/locations/");
}
return locationId;
}
public Object getObjectValueForDisplay(Object value) {
if (value==null) return null;
// currently everything converted to string, expanded if it is a "done" future
if (value instanceof Future) {
if (((Future<?>)value).isDone()) {
try {
value = ((Future<?>)value).get();
} catch (Exception e) {
value = ""+value+" (error evaluating: "+e+")";
}
}
}
if (PrimitiveStringTypeCoercions.isPrimitiveOrBoxer(value.getClass())) return value;
return value.toString();
}
// currently everything converted to string, expanded if it is a "done" future
public String getStringValueForDisplay(Object value) {
if (value==null) return null;
return ""+getObjectValueForDisplay(value);
}
/** true if the URL points to content which must be resolved on the server-side (i.e. classpath)
* and which is safe to do so (currently just images, though in future perhaps also javascript and html plugins)
* <p>
* note we do not let caller access classpath through this mechanism,
* just those which are supplied by the platform administrator e.g. as an icon url */
public boolean isUrlServerSideAndSafe(String url) {
if (Strings.isEmpty(url)) return false;
String ext = Files.getFileExtension(url);
if (Strings.isEmpty(ext)) return false;
MediaType mime = WebResourceUtils.getImageMediaTypeFromExtension(ext);
if (mime==null) return false;
return !Urls.isUrlWithProtocol(url) || url.startsWith("classpath:");
}
public Iterable<Entity> descendantsOfAnyType(String application, String entity) {
List<Entity> result = Lists.newArrayList();
Entity e = getEntity(application, entity);
gatherAllDescendants(e, result);
return result;
}
private static void gatherAllDescendants(Entity e, List<Entity> result) {
if (result.add(e)) {
for (Entity ee: e.getChildren())
gatherAllDescendants(ee, result);
}
}
public Iterable<Entity> descendantsOfType(String application, String entity, final String typeRegex) {
Iterable<Entity> result = descendantsOfAnyType(application, entity);
return Iterables.filter(result, new Predicate<Entity>() {
@Override
public boolean apply(Entity entity) {
if (entity==null) return false;
return (entity.getEntityType().getName().matches(typeRegex));
}
});
}
public void reloadBrooklynProperties() {
mgmt.reloadBrooklynProperties();
}
}