/*
 * 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 javax.ws.rs.core.Response.created;
import static javax.ws.rs.core.Response.status;
import static javax.ws.rs.core.Response.Status.ACCEPTED;
import static org.apache.brooklyn.rest.util.WebResourceUtils.serviceAbsoluteUriBuilder;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.stream.Collectors;

import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.ResponseBuilder;
import javax.ws.rs.core.UriInfo;

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.entity.Group;
import org.apache.brooklyn.api.location.Location;
import org.apache.brooklyn.api.mgmt.Task;
import org.apache.brooklyn.api.objs.BrooklynObject;
import org.apache.brooklyn.api.sensor.AttributeSensor;
import org.apache.brooklyn.api.sensor.Sensor;
import org.apache.brooklyn.api.typereg.RegisteredType;
import org.apache.brooklyn.config.ConfigKey;
import org.apache.brooklyn.core.config.ConfigPredicates;
import org.apache.brooklyn.core.config.ConstraintViolationException;
import org.apache.brooklyn.core.entity.Attributes;
import org.apache.brooklyn.core.entity.EntityInternal;
import org.apache.brooklyn.core.entity.EntityPredicates;
import org.apache.brooklyn.core.entity.lifecycle.Lifecycle;
import org.apache.brooklyn.core.entity.trait.Startable;
import org.apache.brooklyn.core.mgmt.EntityManagementUtils;
import org.apache.brooklyn.core.mgmt.EntityManagementUtils.CreationResult;
import org.apache.brooklyn.core.mgmt.entitlement.EntitlementPredicates;
import org.apache.brooklyn.core.mgmt.entitlement.Entitlements;
import org.apache.brooklyn.core.mgmt.entitlement.Entitlements.EntityAndItem;
import org.apache.brooklyn.core.mgmt.entitlement.Entitlements.StringAndArgument;
import org.apache.brooklyn.core.mgmt.internal.IdAlreadyExistsException;
import org.apache.brooklyn.core.sensor.Sensors;
import org.apache.brooklyn.core.typereg.RegisteredTypeLoadingContexts;
import org.apache.brooklyn.core.typereg.RegisteredTypes;
import org.apache.brooklyn.entity.group.AbstractGroup;
import org.apache.brooklyn.rest.api.ApplicationApi;
import org.apache.brooklyn.rest.domain.ApplicationSpec;
import org.apache.brooklyn.rest.domain.ApplicationSummary;
import org.apache.brooklyn.rest.domain.EntityDetail;
import org.apache.brooklyn.rest.domain.EntitySummary;
import org.apache.brooklyn.rest.domain.TaskSummary;
import org.apache.brooklyn.rest.filter.HaHotStateRequired;
import org.apache.brooklyn.rest.transform.ApplicationTransformer;
import org.apache.brooklyn.rest.transform.EntityTransformer;
import org.apache.brooklyn.rest.transform.TaskTransformer;
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.MutableMap;
import org.apache.brooklyn.util.core.ResourceUtils;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.exceptions.UserFacingException;
import org.apache.brooklyn.util.guava.Maybe;
import org.apache.brooklyn.util.javalang.JavaClassNames;
import org.apache.brooklyn.util.net.Urls;
import org.apache.brooklyn.util.repeat.Repeater;
import org.apache.brooklyn.util.text.StringEscapes.JavaStringEscapes;
import org.apache.brooklyn.util.text.Strings;
import org.apache.brooklyn.util.text.WildcardGlobs;
import org.apache.brooklyn.util.time.Duration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;

@HaHotStateRequired
public class ApplicationResource extends AbstractBrooklynRestResource implements ApplicationApi {

    private static final Logger log = LoggerFactory.getLogger(ApplicationResource.class);

    @Context
    private UriInfo uriInfo;

    /** depth 0 means no detail even at root; negative means infinite; positive means include details for that many levels 
     * (ie 1 means this entity but no details of descendants) */
    private EntitySummary fromEntity(Entity entity, boolean includeTags, int detailDepth, List<String> extraSensorGlobs, List<String> extraConfigGlobs) {
        if (detailDepth==0) {
            return new EntitySummary(
                entity.getId(), 
                entity.getDisplayName(),
                entity.getEntityType().getName(),
                entity.getCatalogItemId(),
                MutableMap.of("self", EntityTransformer.entityUri(entity, ui.getBaseUriBuilder())) );
        }

        Boolean serviceUp = entity.getAttribute(Attributes.SERVICE_UP);

        Lifecycle serviceState = entity.getAttribute(Attributes.SERVICE_STATE_ACTUAL);

        String iconUrl = RegisteredTypes.getIconUrl(entity);
        if (iconUrl!=null) {
            if (brooklyn().isUrlServerSideAndSafe(iconUrl))
                // route to server if it is a server-side url
                iconUrl = EntityTransformer.entityUri(entity, ui.getBaseUriBuilder())+"/icon";
        }

        List<EntitySummary> children = Lists.newArrayList();
        if (!entity.getChildren().isEmpty()) {
            for (Entity child : entity.getChildren()) {
                if (Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_ENTITY, child)) {
                    children.add(fromEntity(child, includeTags, detailDepth-1, extraSensorGlobs, extraConfigGlobs));
                }
            }
        }

        String parentId = null;
        if (entity.getParent()!= null) {
            parentId = entity.getParent().getId();
        }

        List<String> groupIds = Lists.newArrayList();
        if (!entity.groups().isEmpty()) {
            groupIds.addAll(entitiesIdAsArray(entity.groups()));
        }

        List<Map<String, String>> members = Lists.newArrayList();
        if (entity instanceof Group) {
            // use attribute instead of method in case it is read-only
            Collection<Entity> memberEntities = entity.getAttribute(AbstractGroup.GROUP_MEMBERS);
            if (memberEntities != null && !memberEntities.isEmpty())
                members.addAll(entitiesIdAndNameAsList(memberEntities));
        }

        EntityDetail result = new EntityDetail(
                entity.getApplicationId(),
                entity.getId(),
                parentId,
                entity.getDisplayName(),
                entity.getEntityType().getName(),
                serviceUp,
                serviceState,
                iconUrl,
                entity.getCatalogItemId(),
                children,
                groupIds,
                members,
                MutableMap.of("self", EntityTransformer.entityUri(entity, ui.getBaseUriBuilder())) );
        
        if (includeTags) {
            result.setExtraField("tags", resolving(MutableList.copyOf(entity.tags().getTags())).preferJson(true).resolve() );
        }
        result.setExtraField("creationTimeUtc", entity.getCreationTime());
        addSensorsByGlobs(result, entity, extraSensorGlobs);
        addConfigByGlobs(result, entity, extraConfigGlobs);
        
        return result;
    }

    private List<Map<String, String>> entitiesIdAndNameAsList(Collection<? extends Entity> entities) {
        List<Map<String, String>> members = Lists.newArrayList();
        for (Entity entity : entities) {
            if (Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_ENTITY, entity)) {
                members.add(ImmutableMap.of("id", entity.getId(), "name", entity.getDisplayName()));
            }
        }
        return members;
    }

    private List<String> entitiesIdAsArray(Iterable<? extends Entity> entities) {
        List<String> ids = Lists.newArrayList();
        for (Entity entity : entities) {
            if (Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_ENTITY, entity)) {
                ids.add(entity.getId());
            }
        }
        return ids;
    }

    @Override
    public List<EntityDetail> fetch(String entityIds, String extraSensorsS) {
        List<String> extraSensorNames = JavaStringEscapes.unwrapOptionallyQuotedJavaStringList(extraSensorsS);
        List<AttributeSensor<?>> extraSensors = extraSensorNames.stream().map((s) -> Sensors.newSensor(Object.class, s)).collect(Collectors.toList());
        
        List<EntityDetail> entitySummaries = Lists.newArrayList();
        for (Entity application : mgmt().getApplications()) {
            entitySummaries.add(addSensorsByName((EntityDetail)fromEntity(application, false, -1, null, null), application, extraSensors));
        }

        if (Strings.isNonBlank(entityIds)) {
            List<String> extraEntities = JavaStringEscapes.unwrapOptionallyQuotedJavaStringList(entityIds);
            for (String entityId: extraEntities) {
                Entity entity = mgmt().getEntityManager().getEntity(entityId.trim());
                while (entity != null && entity.getParent() != null) {
                    if (Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_ENTITY, entity)) {
                        entitySummaries.add(addSensorsByName((EntityDetail)fromEntity(entity, false, -1, null, null), entity, extraSensors));
                    }
                    entity = entity.getParent();
                }
            }
        }
        return entitySummaries;
    }
    
    @Override
    public List<EntitySummary> details(String entityIds, boolean includeAllApps, String extraSensorsGlobsS, String extraConfigGlobsS, int depth) {
        List<String> extraSensorGlobs = JavaStringEscapes.unwrapOptionallyQuotedJavaStringList(extraSensorsGlobsS);
        List<String> extraConfigGlobs = JavaStringEscapes.unwrapOptionallyQuotedJavaStringList(extraConfigGlobsS);

        Map<String, EntitySummary> entitySummaries = MutableMap.of();

        if (includeAllApps) {
            for (Entity application : mgmt().getApplications()) {
                if (Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_ENTITY, application)) {
                    entitySummaries.put(application.getId(), fromEntity(application, true, depth, extraSensorGlobs, extraConfigGlobs));
                }
            }
        }

        if (Strings.isNonBlank(entityIds)) {
            List<String> extraEntities = JavaStringEscapes.unwrapOptionallyQuotedJavaStringList(entityIds);
            for (String entityId: extraEntities) {
                Entity entity = mgmt().getEntityManager().getEntity(entityId.trim());
                while (entity != null && !entitySummaries.containsKey(entity.getId())) {
                    if (Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_ENTITY, entity)) {
                        entitySummaries.put(entity.getId(), fromEntity(entity, true, depth, extraSensorGlobs, extraConfigGlobs));
                    }
                    entity = entity.getParent();
                }
            }
        }
        return MutableList.copyOf(entitySummaries.values());
    }

    private EntityDetail addSensorsByName(EntityDetail result, Entity entity, List<AttributeSensor<?>> extraSensors) {
        if (extraSensors!=null && !extraSensors.isEmpty()) {
            Object sensorsO = result.getExtraFields().get("sensors");
            if (sensorsO==null) {
                sensorsO = MutableMap.of();
                result.setExtraField("sensors", sensorsO);
            }
            if (!(sensorsO instanceof Map)) {
                throw new IllegalStateException("sensors field in result for "+entity+" should be a Map; found: "+sensorsO);
            }
            @SuppressWarnings("unchecked")
            Map<String,Object> sensors = (Map<String, Object>) sensorsO;
            
            for (AttributeSensor<?> s: extraSensors) {
                if (Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_SENSOR, new EntityAndItem<String>(entity, s.getName()))) {
                    Object sv = entity.sensors().get(s);
                    if (sv!=null) {
                        sv = resolving(sv).preferJson(true).asJerseyOutermostReturnValue(false).raw(true).context(entity).immediately(true).timeout(Duration.ZERO).resolve();
                        sensors.put(s.getName(), sv);
                    }
                }
            }
        }
        return result;
    }
    
    private EntitySummary addSensorsByGlobs(EntitySummary result, Entity entity, List<String> extraSensorGlobs) {
        if (extraSensorGlobs!=null && !extraSensorGlobs.isEmpty()) {
            Object sensorsO = result.getExtraFields().get("sensors");
            if (sensorsO==null) {
                sensorsO = MutableMap.of();
                result.setExtraField("sensors", sensorsO);
            }
            if (!(sensorsO instanceof Map)) {
                throw new IllegalStateException("sensors field in result for "+entity+" should be a Map; found: "+sensorsO);
            }
            @SuppressWarnings("unchecked")
            Map<String,Object> sensors = (Map<String, Object>) sensorsO;
            
            Map<AttributeSensor<?>, Object> svs = entity.sensors().getAll();
            svs.entrySet().stream().forEach(kv -> {
                Object sv = kv.getValue();
                if (sv!=null) {
                    String name = kv.getKey().getName();
                    if (Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_SENSOR, new EntityAndItem<String>(entity, name))) {
                        if (extraSensorGlobs.stream().anyMatch(sn -> WildcardGlobs.isGlobMatched(sn, name))) {
                            sv = resolving(sv).preferJson(true).asJerseyOutermostReturnValue(false).raw(true).context(entity).immediately(true).timeout(Duration.ZERO).resolve();
                            sensors.put(name, sv);
                        }
                    }
                }
            });
        }
        return result;
    }

    private EntitySummary addConfigByGlobs(EntitySummary result, Entity entity, List<String> extraConfigGlobs) {
        if (extraConfigGlobs!=null && !extraConfigGlobs.isEmpty()) {
            Object configO = result.getExtraFields().get("config");
            if (configO==null) {
                configO = MutableMap.of();
                result.setExtraField("config", configO);
            }
            if (!(configO instanceof Map)) {
                throw new IllegalStateException("config field in result for "+entity+" should be a Map; found: "+configO);
            }
            @SuppressWarnings("unchecked")
            Map<String,Object> configs = (Map<String, Object>) configO;
            
            List<Predicate<ConfigKey<?>>> extraConfigPreds = MutableList.of();
            extraConfigGlobs.stream().forEach(g -> { extraConfigPreds.add( ConfigPredicates.nameMatchesGlob(g) ); });
            entity.config().findKeysDeclared(Predicates.or(extraConfigPreds)).forEach(key -> {
                if (Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_CONFIG, new EntityAndItem<String>(entity, key.getName()))) {
                    Maybe<Object> vRaw = ((EntityInternal)entity).config().getRaw(key);
                    Object v = vRaw.isPresent() ? vRaw.get() : entity.config().get(key);
                    if (v!=null) {
                        v = resolving(v, mgmt()).preferJson(true).asJerseyOutermostReturnValue(false).raw(true).context(entity).immediately(true).timeout(Duration.ZERO).resolve();
                        configs.put(key.getName(), v);
                    }
                }
            });
        }
        return result;
    }

    @Override
    public List<ApplicationSummary> list(String typeRegex) {
        if (Strings.isBlank(typeRegex)) {
            typeRegex = ".*";
        }
        return FluentIterable
                .from(mgmt().getApplications())
                .filter(EntitlementPredicates.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_ENTITY))
                .filter(EntityPredicates.hasInterfaceMatching(typeRegex))
                .transform(ApplicationTransformer.fromApplication(ui.getBaseUriBuilder()))
                .toList();
    }

    @Override
    public ApplicationSummary get(String application) {
        return ApplicationTransformer.summaryFromApplication(brooklyn().getApplication(application), ui.getBaseUriBuilder());
    }

    /** @deprecated since 0.7.0 see #create */ @Deprecated
    protected Response createFromAppSpec(ApplicationSpec applicationSpec) {
        if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.DEPLOY_APPLICATION, applicationSpec)) {
            throw WebResourceUtils.forbidden("User '%s' is not authorized to start application %s",
                Entitlements.getEntitlementContext().user(), applicationSpec);
        }

        checkApplicationTypesAreValid(applicationSpec);
        checkLocationsAreValid(applicationSpec);
        List<Location> locations = brooklyn().getLocationsManaged(applicationSpec);
        Application app = brooklyn().create(applicationSpec);
        Task<?> t = brooklyn().start(app, locations);
        waitForStart(app, Duration.millis(100));
        TaskSummary ts = TaskTransformer.fromTask(ui.getBaseUriBuilder()).apply(t);
        URI ref = serviceAbsoluteUriBuilder(uriInfo.getBaseUriBuilder(), ApplicationApi.class, "get")
                .build(app.getApplicationId());
        return created(ref).entity(ts).build();
    }

    @Override
    public Response createFromYaml(String yaml) {
        return createFromYaml(yaml, Optional.absent());
    }
    
    @Override
    public Response createFromYamlWithAppId(String yaml, String appId) {
        return createFromYaml(yaml, Optional.of(appId));
    }
    
    protected Response createFromYaml(String yaml, Optional<String> appId) {
        // First of all, see if it's a URL
        Preconditions.checkNotNull(yaml, "Blueprint must not be null");
        URI uri = null;
        try {
            String yamlUrl = yaml.trim();
            if (Urls.isUrlWithProtocol(yamlUrl)) {
                uri = new URI(yamlUrl);
            }
        } catch (URISyntaxException e) {
            // It's not a URI then...
            uri = null;
        }
        if (uri != null) {
            log.debug("Create app called with URI; retrieving contents: {}", uri);
            try {
                yaml = ResourceUtils.create(mgmt()).getResourceAsString(uri.toString());
            } catch (Exception e) {
                Exceptions.propagateIfFatal(e);
                throw new UserFacingException("Cannot resolve URL: "+uri, e);
            }
        }

        log.debug("Creating app from yaml:\n{}", yaml);

        EntitySpec<? extends Application> spec;
        try {
            spec = createEntitySpecForApplication(yaml);
        } catch (Exception e) {
            Exceptions.propagateIfFatal(e);
            log.warn("Failed REST deployment, could not create spec: "+e);
            UserFacingException userFacing = Exceptions.getFirstThrowableOfType(e, UserFacingException.class);
            if (userFacing!=null) {
                log.debug("Throwing "+userFacing+", wrapped in "+e);
                throw userFacing;
            }
            throw WebResourceUtils.badRequest(e, "Error in blueprint");
        }
        
        if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.DEPLOY_APPLICATION, spec)) {
            throw WebResourceUtils.forbidden("User '%s' is not authorized to start application %s",
                Entitlements.getEntitlementContext().user(), yaml);
        }

        try {
            return launch(yaml, spec, appId);
        } catch (IdAlreadyExistsException e) {
            log.warn("Failed REST deployment launching "+spec+": "+e);
            throw WebResourceUtils.throwWebApplicationException(Response.Status.CONFLICT, e, "Error launching blueprint, id already exists");
        } catch (Exception e) {
            Exceptions.propagateIfFatal(e);
            log.warn("Failed REST deployment launching "+spec+": "+e);
            throw WebResourceUtils.badRequest(e, "Error launching blueprint");
        }
    }

    private Response launch(String yaml, EntitySpec<? extends Application> spec, Optional<String> entityId) {
        try {
            Application app = EntityManagementUtils.createUnstarted(mgmt(), spec, entityId);

            boolean isEntitled = Entitlements.isEntitled(
                    mgmt().getEntitlementManager(),
                    Entitlements.INVOKE_EFFECTOR,
                    EntityAndItem.of(app, StringAndArgument.of(Startable.START.getName(), null)));

            if (!isEntitled) {
                throw WebResourceUtils.forbidden("User '%s' is not authorized to start application %s",
                    Entitlements.getEntitlementContext().user(), spec.getType());
            }

            CreationResult<Application,Void> result = EntityManagementUtils.start(app);
            waitForStart(app, Duration.millis(100));

            log.info("Launched from YAML: " + yaml + " -> " + app + " (" + result.task() + ")");

            URI ref = serviceAbsoluteUriBuilder(ui.getBaseUriBuilder(), ApplicationApi.class, "get").build(app.getApplicationId());
            ResponseBuilder response = created(ref);
            if (result.task() != null)
                response.entity(TaskTransformer.fromTask(ui.getBaseUriBuilder()).apply(result.task()));
            return response.build();
        } catch (ConstraintViolationException e) {
            throw new UserFacingException(e);
        } catch (Exception e) {
            throw Exceptions.propagate(e);
        }
    }

    private void waitForStart(final Application app, Duration timeout) {
        // without this UI shows "stopped" sometimes esp if brooklyn has just started,
        // because app goes to stopped state if it sees unstarted children,
        // and that gets returned too quickly, before the start has taken effect
        Repeater.create("wait a bit for app start").every(Duration.millis(10)).limitTimeTo(timeout).until(
            new Callable<Boolean>() {
                @Override
                public Boolean call() throws Exception {
                    Lifecycle state = app.getAttribute(Attributes.SERVICE_STATE_ACTUAL);
                    if (state==Lifecycle.CREATED || state==Lifecycle.STOPPED) return false;
                    return true;
                }
            }).run();
    }

    @Override
    public Response createPoly(byte[] inputToAutodetectType) {
        log.debug("Creating app from autodetecting input");

        boolean looksLikeLegacy = false;
        Exception legacyFormatException = null;
        // attempt legacy format
        try {
            ApplicationSpec appSpec = mapper().readValue(inputToAutodetectType, ApplicationSpec.class);
            if (appSpec.getType() != null || appSpec.getEntities() != null) {
                looksLikeLegacy = true;
            }
            return createFromAppSpec(appSpec);
        } catch (Exception e) {
            Exceptions.propagateIfFatal(e);
            legacyFormatException = e;
            log.debug("Input is not legacy ApplicationSpec JSON (will try others)");
        }

        //TODO infer encoding from request
        String potentialYaml = new String(inputToAutodetectType);
        EntitySpec<? extends Application> spec;
        try {
            spec = createEntitySpecForApplication(potentialYaml);
        } catch (Exception e) {
            Exceptions.propagateIfFatal(e);
            log.warn("Failed REST deployment, could not create spec (autodetecting): "+e);
            
            // TODO if not yaml/json - try ZIP, etc
            
            throw WebResourceUtils.badRequest(e, "Error in blueprint");
        }


        if (spec != null) {
            try {
                return launch(potentialYaml, spec, Optional.absent());
            } catch (Exception e) {
                Exceptions.propagateIfFatal(e);
                log.warn("Failed REST deployment launching "+spec+": "+e);
                throw WebResourceUtils.badRequest(e, "Error launching blueprint (autodetecting)");
            }
        } else if (looksLikeLegacy) {
            throw Exceptions.propagate(legacyFormatException);
        } else {
            return Response.serverError().entity("Unsupported format; not able to autodetect.").build();
        }
    }

    @Override
    public Response createFromForm(String contents) {
        log.debug("Creating app from form");
        return createPoly(contents.getBytes());
    }

    @Override
    public Response delete(String application) {
        Application app = brooklyn().getApplication(application);
        if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.INVOKE_EFFECTOR, Entitlements.EntityAndItem.of(app,
            StringAndArgument.of(Entitlements.LifecycleEffectors.DELETE, null)))) {
            throw WebResourceUtils.forbidden("User '%s' is not authorized to delete application %s",
                Entitlements.getEntitlementContext().user(), app);
        }
        Task<?> t = brooklyn().destroy(app);
        TaskSummary ts = TaskTransformer.fromTask(ui.getBaseUriBuilder()).apply(t);
        return status(ACCEPTED).entity(ts).build();
    }

    private EntitySpec<? extends Application> createEntitySpecForApplication(String potentialYaml) {
        return EntityManagementUtils.createEntitySpecForApplication(mgmt(), potentialYaml);
    }
    
    private void checkApplicationTypesAreValid(ApplicationSpec applicationSpec) {
        String appType = applicationSpec.getType();
        if (appType != null) {
            checkEntityTypeIsValid(appType);

            if (applicationSpec.getEntities() != null) {
                throw WebResourceUtils.preconditionFailed("Application given explicit type '%s' must not define entities", appType);
            }
            return;
        }

        for (org.apache.brooklyn.rest.domain.EntitySpec entitySpec : applicationSpec.getEntities()) {
            String entityType = entitySpec.getType();
            checkEntityTypeIsValid(checkNotNull(entityType, "entityType"));
        }
    }

    private void checkSpecTypeIsValid(String type, Class<? extends BrooklynObject> subType) {
        Maybe<RegisteredType> typeV = RegisteredTypes.tryValidate(mgmt().getTypeRegistry().get(type), RegisteredTypeLoadingContexts.spec(subType));
        if (!typeV.isNull()) {
            // found, throw if any problem
            typeV.get();
            return;
        }

        // not found, try classloading
        try {
            brooklyn().getCatalogClassLoader().loadClass(type);
        } catch (ClassNotFoundException e) {
            log.debug("Class not found for type '" + type + "'; reporting 404", e);
            throw WebResourceUtils.notFound("Undefined type '%s'", type);
        }
        log.info(JavaClassNames.simpleClassName(subType)+" type '{}' not defined in catalog but is on classpath; continuing", type);
    }

    private void checkEntityTypeIsValid(String type) {
        checkSpecTypeIsValid(type, Entity.class);
    }

    @SuppressWarnings("deprecation")
    private void checkLocationsAreValid(ApplicationSpec applicationSpec) {
        for (String locationId : applicationSpec.getLocations()) {
            locationId = BrooklynRestResourceUtils.fixLocation(locationId);
            if (brooklyn().getLocationRegistry().getLocationSpec(locationId).isAbsent() && brooklyn().getLocationRegistry().getDefinedLocationById(locationId)==null) {
                throw WebResourceUtils.notFound("Undefined location '%s'", locationId);
            }
        }
    }

    @Override
    public List<EntitySummary> getDescendants(String application, String typeRegex) {
        Entity entity =brooklyn().getApplication(application);
        if (entity != null && !Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_ENTITY, entity)) {
            throw WebResourceUtils.forbidden("User '%s' is not authorized to see the descendants of entity '%s'",
                    Entitlements.getEntitlementContext().user(), entity);
        }
        return EntityTransformer.entitySummaries(brooklyn().descendantsOfType(application, application, typeRegex), ui.getBaseUriBuilder());
    }

    @Override
    public Map<String, Object> getDescendantsSensor(String application, String sensor, String typeRegex) {
        Entity entity =brooklyn().getApplication(application);
        if (entity != null && !Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_ENTITY, entity)) {
            throw WebResourceUtils.forbidden("User '%s' is not authorized to see the value of sensor '%s' for descendants of entity '%s'",
                    Entitlements.getEntitlementContext().user(), sensor, entity);
        }
        Iterable<Entity> descs = brooklyn().descendantsOfType(application, application, typeRegex);
        return getSensorMap(sensor, descs);
    }

    public static Map<String, Object> getSensorMap(String sensor, Iterable<Entity> descs) {
        if (Iterables.isEmpty(descs))
            return Collections.emptyMap();
        Map<String, Object> result = MutableMap.of();
        Iterator<Entity> di = descs.iterator();
        Sensor<?> s = null;
        while (di.hasNext()) {
            Entity potentialSource = di.next();
            s = potentialSource.getEntityType().getSensor(sensor);
            if (s!=null) break;
        }
        if (s==null)
            s = Sensors.newSensor(Object.class, sensor);
        if (!(s instanceof AttributeSensor<?>)) {
            log.warn("Cannot retrieve non-attribute sensor "+s+" for entities; returning empty map");
            return result;
        }
        for (Entity e: descs) {
            Object v = null;
            try {
                v = e.getAttribute((AttributeSensor<?>)s);
            } catch (Exception exc) {
                Exceptions.propagateIfFatal(exc);
                log.warn("Error retrieving sensor "+s+" for "+e+" (ignoring): "+exc);
            }
            if (v!=null)
                result.put(e.getId(), v);
        }
        return result;
    }

}
