All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.fabric3.management.rest.runtime.RestfulManagementExtension Maven / Gradle / Ivy

The newest version!
/*
 * Fabric3
 * Copyright (c) 2009-2015 Metaform Systems
 *
 * Licensed 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.fabric3.management.rest.runtime;

import javax.servlet.http.HttpServletRequest;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
import java.util.stream.Collectors;

import org.fabric3.api.Role;
import org.fabric3.api.annotation.management.Management;
import org.fabric3.api.annotation.management.ManagementOperation;
import org.fabric3.api.host.Fabric3Exception;
import org.fabric3.api.model.type.java.ManagementInfo;
import org.fabric3.api.model.type.java.ManagementOperationInfo;
import org.fabric3.api.model.type.java.OperationType;
import org.fabric3.management.rest.framework.DynamicResourceService;
import org.fabric3.management.rest.spi.ResourceHost;
import org.fabric3.management.rest.spi.ResourceListener;
import org.fabric3.management.rest.spi.ResourceMapping;
import org.fabric3.management.rest.spi.Verb;
import org.fabric3.management.rest.transformer.TransformerPair;
import org.fabric3.management.rest.transformer.TransformerPairService;
import org.fabric3.spi.management.ManagementExtension;
import org.fabric3.spi.model.type.java.JavaType;
import org.oasisopen.sca.annotation.Init;
import org.oasisopen.sca.annotation.Property;
import org.oasisopen.sca.annotation.Reference;

/**
 * Responsible for exporting components and instances as management resources.  As part of this process, a fully-navigable management resource hierarchy
 * will be dynamically created. For example, if a component is exported to /runtime/foo/bar and a /runtime/foo resource is not configured, one will be created
 * dynamically with a link to runtime/foo/bar. If a configured resource is later exported, any previously generated dynamic resource will be overriden.
 */
public class RestfulManagementExtension implements ManagementExtension {
    private static final JavaType JSON_INPUT_TYPE = new JavaType(InputStream.class, "JSON");
    private static final JavaType JSON_OUTPUT_TYPE = new JavaType(byte[].class, "JSON");

    private static final String EMPTY_PATH = "";
    private static final String ROOT_PATH = "/";

    private TransformerPairService pairService;

    private Method rootResourceMethod;
    private Method dynamicGetResourceMethod;

    private ResourceHost resourceHost;
    private ManagementSecurity security = ManagementSecurity.DISABLED;

    private List listeners = new ArrayList<>();
    private Map dynamicResources = new ConcurrentHashMap<>();

    public RestfulManagementExtension(@Reference TransformerPairService pairService, @Reference Marshaller marshaller, @Reference ResourceHost resourceHost) {
        this.pairService = pairService;
        this.resourceHost = resourceHost;
    }

    @Property(required = false)
    public void setSecurity(String level) throws Fabric3Exception {
        try {
            security = ManagementSecurity.valueOf(level.toUpperCase());
        } catch (IllegalArgumentException e) {
            throw new Fabric3Exception("Invalid management security setting:" + level);
        }
    }

    /**
     * Setter to support re-injection of optional listeners.
     *
     * @param listeners the listeners
     */
    @Reference(required = false)
    public void setListeners(List listeners) {
        this.listeners = listeners;
    }

    @Init()
    public void init() throws NoSuchMethodException {
        rootResourceMethod = ResourceInvoker.class.getMethod("invoke", HttpServletRequest.class);
        dynamicGetResourceMethod = DynamicResourceService.class.getMethod("getResource", HttpServletRequest.class);
    }

    public String getType() {
        return "fabric3.rest";
    }

    public void export(URI componentUri, ManagementInfo info, Supplier supplier) throws Fabric3Exception {
        String root = info.getPath();
        if (root.length() == 0) {
            root = componentUri.getPath();
        }
        List getMappings = new ArrayList<>();

        String identifier = componentUri.toString();

        boolean rootResourcePathOverride = false;
        for (ManagementOperationInfo operationInfo : info.getOperations()) {
            // calculate if a root resource needs to be created
            String path = operationInfo.getPath();
            if (ROOT_PATH.equals(path)) {
                rootResourcePathOverride = true;
            }
        }
        for (ManagementOperationInfo operationInfo : info.getOperations()) {
            Method method = operationInfo.getMethod();
            String path = operationInfo.getPath();
            OperationType type = operationInfo.getOperationType();
            Verb verb = getVerb(method, type);

            Set roles = operationInfo.getRoles();
            if (roles.isEmpty()) {
                // No roles specified for operation. Default to read/write roles specified on the class.
                if (Verb.GET == verb) {
                    roles = info.getReadRoles();
                } else {
                    roles = info.getWriteRoles();
                }
            }
            TransformerPair pair = pairService.getTransformerPair(Collections.singletonList(method), JSON_INPUT_TYPE, JSON_OUTPUT_TYPE);
            ResourceMapping mapping = createMapping(identifier, root, path, method, verb, supplier, pair, roles);
            if (Verb.GET == mapping.getVerb()) {
                getMappings.add(mapping);
            }

            createDynamicResources(mapping, root, rootResourcePathOverride);

            if (dynamicResources.remove(mapping.getPath()) != null) {
                resourceHost.unregisterPath(mapping.getPath(), mapping.getVerb());
            }
            resourceHost.register(mapping);
            notifyExport(path, mapping);
        }
        if (!rootResourcePathOverride) {
            createRootResource(identifier, root, getMappings);
        }
    }

    public void export(String name, String group, String description, Object instance) throws Fabric3Exception {
        String root = "/runtime/" + name;

        Set readRoles = new HashSet<>();
        Set writeRoles = new HashSet<>();
        parseRoles(instance, readRoles, writeRoles);

        boolean rootResourcePathOverride = false;
        List getMappings = new ArrayList<>();

        List methods = Arrays.asList(instance.getClass().getMethods());
        for (Method method : methods) {
            ManagementOperation opAnnotation = method.getAnnotation(ManagementOperation.class);
            if (opAnnotation == null) {
                continue;
            }
            String path = opAnnotation.path();
            if (ROOT_PATH.equals(path)) {
                rootResourcePathOverride = true;
            }

        }
        for (Method method : methods) {
            Set roles;
            ManagementOperation opAnnotation = method.getAnnotation(ManagementOperation.class);
            if (opAnnotation != null) {
                OperationType type = OperationType.valueOf(opAnnotation.type().toString());
                Verb verb = getVerb(method, type);
                String[] rolesAllowed = opAnnotation.rolesAllowed();
                if (rolesAllowed.length == 0) {
                    if (Verb.GET == verb) {
                        roles = readRoles;
                    } else {
                        roles = writeRoles;
                    }
                } else {
                    roles = new HashSet<>();
                    for (String roleName : rolesAllowed) {
                        roles.add(new Role(roleName));
                    }
                }

                TransformerPair pair = pairService.getTransformerPair(Collections.singletonList(method), JSON_INPUT_TYPE, JSON_OUTPUT_TYPE);
                ResourceMapping mapping = createMapping(name, root, EMPTY_PATH, method, verb, instance, pair, roles);

                if (Verb.GET == mapping.getVerb()) {
                    getMappings.add(mapping);
                }

                createDynamicResources(mapping, root, rootResourcePathOverride);

                resourceHost.register(mapping);
                notifyExport(mapping.getRelativePath(), mapping);

            }
        }
        if (!rootResourcePathOverride) {
            createRootResource(name, root, getMappings);
        }
    }

    public void remove(URI componentUri, ManagementInfo info) throws Fabric3Exception {
        String identifier = componentUri.toString();
        resourceHost.unregister(identifier);
        for (ResourceListener listener : listeners) {
            listener.onRootResourceRemove(identifier);
            listener.onSubResourceRemove(identifier);
        }
    }

    public void remove(String name, String group) throws Fabric3Exception {
        resourceHost.unregister(name);
        for (ResourceListener listener : listeners) {
            listener.onRootResourceRemove(name);
            listener.onSubResourceRemove(name);
        }
    }

    /**
     * Returns the HTTP verb for the operation.
     *
     * @param method the method name
     * @param type   the operation type
     * @return the HTTP verb
     */
    private Verb getVerb(Method method, OperationType type) {
        String methodName = method.getName();
        if (OperationType.UNDEFINED == type) {
            return MethodHelper.convertToVerb(methodName);
        } else {
            return Verb.valueOf(type.toString());
        }
    }

    /**
     * Notifies listeners of a root or sub- resource export event
     *
     * @param path    the resource path
     * @param mapping the resource mapping
     */
    private void notifyExport(String path, ResourceMapping mapping) {
        if (ROOT_PATH.equals(path)) {
            for (ResourceListener listener : listeners) {
                listener.onRootResourceExport(mapping);
            }
        } else {
            for (ResourceListener listener : listeners) {
                listener.onSubResourceExport(mapping);
            }
        }
    }

    /**
     * Creates a managed artifact mapping.
     *
     * @param identifier the identifier used to group a set of mappings during deployment and undeployment
     * @param root       the root path for the artifact
     * @param path       the relative path of the operation. The path may be blank, in which case one will be calculated from the method name
     * @param method     the management operation
     * @param verb       the HTTP verb the operation uses
     * @param instance   the artifact
     * @param pair       the transformer pair for deserializing JSON requests and serializing responses
     * @param roles      the roles required to invoke the operation
     * @return the mapping
     */
    private ResourceMapping createMapping(String identifier,
                                          String root,
                                          String path,
                                          Method method,
                                          Verb verb,
                                          Object instance,
                                          TransformerPair pair,
                                          Set roles) {
        String methodName = method.getName();
        String rootPath;
        if (path.length() == 0) {
            path = MethodHelper.convertToPath(methodName);
        }
        path = path.toLowerCase();
        if (ROOT_PATH.equals(path)) {
            // if the path is for the root resource, there is no sub-path
            rootPath = root.toLowerCase();
        } else {
            rootPath = root.toLowerCase() + "/" + path;
        }
        return new ResourceMapping(identifier, rootPath, path, verb, method, instance, pair, roles);
    }

    /**
     * Creates a root resource that aggreggates information from sub-resources.
     *
     * @param identifier the identifier used to group a set of mappings during deployment and undeployment
     * @param root       the root path
     * @param mappings   the sub-resource mappings   @throws ManagementException if an error occurs creating the root resource
     * @throws Fabric3Exception if there is an error creating the mapping
     */
    private void createRootResource(String identifier, String root, List mappings) throws Fabric3Exception {
        ResourceInvoker invoker = new ResourceInvoker(mappings, security);
        List methods = mappings.stream().map(ResourceMapping::getMethod).collect(Collectors.toList());
        TransformerPair pair = pairService.getTransformerPair(methods, JSON_INPUT_TYPE, JSON_OUTPUT_TYPE);
        root = root.toLowerCase();
        Set roles = Collections.emptySet();
        ResourceMapping mapping = new ResourceMapping(identifier, root, root, Verb.GET, rootResourceMethod, invoker, pair, roles);
        ResourceMapping previous = dynamicResources.remove(root);
        if (previous != null) {
            resourceHost.unregisterPath(previous.getPath(), Verb.GET);
        }
        resourceHost.register(mapping);
        for (ResourceListener listener : listeners) {
            listener.onRootResourceExport(mapping);
        }
        createDynamicResources(mapping, root, false);
    }

    /**
     * Parses read and write roles specified on an {@link Management} annotation.
     *
     * @param instance   the instance containing the annotation
     * @param readRoles  the collection of read roles to populate
     * @param writeRoles the collection of write roles to populate
     */
    private void parseRoles(Object instance, Set readRoles, Set writeRoles) {
        Management annotation = instance.getClass().getAnnotation(Management.class);
        if (annotation != null) {
            String[] readRoleNames = annotation.readRoles();
            for (String roleName : readRoleNames) {
                readRoles.add(new Role(roleName));
            }
            String[] writeRoleNames = annotation.writeRoles();
            for (String roleName : writeRoleNames) {
                writeRoles.add(new Role(roleName));
            }
        }

        // set default roles if none specified
        if (readRoles.isEmpty()) {
            readRoles.add(new Role(Management.FABRIC3_ADMIN_ROLE));
            readRoles.add(new Role(Management.FABRIC3_OBSERVER_ROLE));
        }
        if (writeRoles.isEmpty()) {
            writeRoles.add(new Role(Management.FABRIC3_ADMIN_ROLE));
        }
    }

    /**
     * Creates parent resources dynamically for the given mapping if they do not already exist.
     *
     * @param mapping            the mapping
     * @param rootResourcePath   the root resource path for this hierarchy
     * @param createRootResource true if a dynamic root resource should be dynamically created
     * @throws Fabric3Exception if there was an error creating parent resources
     */
    private void createDynamicResources(ResourceMapping mapping, String rootResourcePath, boolean createRootResource) throws Fabric3Exception {
        ResourceMapping previous = dynamicResources.remove(mapping.getPath());
        if (previous != null) {
            // A dynamic resource service was already registered. Remove it since it is being replaced by a configured resource service.
            resourceHost.unregisterPath(previous.getPath(), previous.getVerb());
        } else {
            List dynamicMappings = createDynamicResourceMappings(mapping, rootResourcePath, createRootResource);
            // add the resources as listeners first as parents need to be notified of children in order to generate links during registration
            listeners.addAll(dynamicMappings.stream().map(dynamicMapping -> (ResourceListener) dynamicMapping.getInstance()).collect(Collectors.toList()));
            dynamicMappings.stream().filter(dynamicMapping -> !resourceHost.isPathRegistered(dynamicMapping.getPath(), dynamicMapping.getVerb())).forEach(
                    dynamicMapping -> {
                        resourceHost.register(dynamicMapping);
                        notifyExport(dynamicMapping.getRelativePath(), dynamicMapping);
                    });
        }
    }

    /**
     * Creates a collection of parent resources dynamically for the given mapping if they do not already exist.
     *
     * @param mapping            the mapping
     * @param rootResourcePath   the root resource path for this hierarchy
     * @param createRootResource true if a dynamic root resource should be dynamically created
     * @return the an ordered collection of resources starting with the top-most resource in the hierarchy
     */
    private List createDynamicResourceMappings(ResourceMapping mapping, String rootResourcePath, boolean createRootResource) {
        String path = mapping.getPath();
        List mappings = new ArrayList<>();
        while (path != null) {
            String current = PathHelper.getParentPath(path);
            if (path.equals(current)) {
                // reached the path hierarchy root or the top
                break;
            } else if (dynamicResources.containsKey(current)) {
                break;
            } else if (resourceHost.isPathRegistered(current, Verb.GET)) {
                break;
            }
            path = current;
            try {
                if (!createRootResource && current.equals(rootResourcePath)) {
                    continue; // skip creating the root resource since one is provided
                }
                DynamicResourceService resourceService = new DynamicResourceService(current);
                List list = Collections.singletonList(dynamicGetResourceMethod);
                TransformerPair pair = pairService.getTransformerPair(list, JSON_INPUT_TYPE, JSON_OUTPUT_TYPE);
                ResourceMapping dynamicMapping = new ResourceMapping(current,
                                                                     current,
                                                                     ROOT_PATH,
                                                                     Verb.GET,
                                                                     dynamicGetResourceMethod,
                                                                     resourceService,
                                                                     pair,
                                                                     mapping.getRoles());
                mappings.add(dynamicMapping);
                dynamicResources.put(dynamicMapping.getPath(), dynamicMapping);
            } catch (Fabric3Exception e) {
                throw new AssertionError(e);
            }
        }
        // reverse the collection to register the top-most parent first
        Collections.reverse(mappings);
        return mappings;
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy