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

org.glassfish.admin.rest.composite.CompositeResource Maven / Gradle / Ivy

/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 2012-2013 Oracle and/or its affiliates. All rights reserved.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License.  You can
 * obtain a copy of the License at
 * https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html
 * or packager/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 *
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at packager/legal/LICENSE.txt.
 *
 * GPL Classpath Exception:
 * Oracle designates this particular file as subject to the "Classpath"
 * exception as provided by Oracle in the GPL Version 2 section of the License
 * file that accompanied this code.
 *
 * Modifications:
 * If applicable, add the following below the License Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyright [year] [name of copyright owner]"
 *
 * Contributor(s):
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 *
 * Portions Copyright [2017-2021] [Payara Foundation and/or its affiliates]
 */
package org.glassfish.admin.rest.composite;

import com.sun.enterprise.admin.report.ActionReporter;
import java.net.URI;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.Properties;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import jakarta.json.JsonObject;
import javax.security.auth.Subject;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.PathSegment;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.ResponseBuilder;
import jakarta.ws.rs.core.Response.Status;
import jakarta.ws.rs.core.UriBuilder;
import org.glassfish.admin.rest.Constants;
import org.glassfish.admin.rest.RestResource;
import org.glassfish.admin.rest.model.ResponseBody;
import org.glassfish.admin.rest.model.RestCollectionResponseBody;
import org.glassfish.admin.rest.model.RestModelResponseBody;
import org.glassfish.admin.rest.model.SseResponseBody;
import org.glassfish.admin.rest.resources.AbstractResource;
import org.glassfish.admin.rest.utils.DetachedCommandHelper;
import org.glassfish.admin.rest.utils.JsonFilter;
import org.glassfish.admin.rest.utils.JsonUtil;
import org.glassfish.admin.rest.utils.SseCommandHelper;
import org.glassfish.admin.rest.utils.StringUtil;
import org.glassfish.admin.rest.utils.Util;
import org.glassfish.admin.rest.utils.xml.RestActionReporter;
import org.glassfish.api.ActionReport;
import org.glassfish.api.admin.CommandRunner;
import org.glassfish.api.admin.ParameterMap;
import org.glassfish.internal.api.Globals;
import org.glassfish.jersey.internal.util.collection.Ref;
import org.glassfish.jersey.media.sse.EventOutput;


/**
 * This is the base class for all composite resources. It provides all of the basic configuration and utilities needed
 * by composites.  For top-level resources, the @Path and @Service annotations are still
 * required, though, in order for the resource to be located and configured properly.
 * @author jdlee
 */
@Produces(Constants.MEDIA_TYPE_JSON)
public abstract class CompositeResource extends AbstractResource implements RestResource {
    // All methods that expect a request body should include the annotation:
    // @Consumes(CONSUMES_TYPE)
    public static final String CONSUMES_TYPE = Constants.MEDIA_TYPE_JSON;

    final protected static String DETACHED = "__detached";
    final protected static String DETACHED_DEFAULT = "false";

    final protected static String INCLUDE = "__includeFields";
    final protected static String EXCLUDE = "__excludeFields";

    // TODO: These should be configurable
    protected static final int THREAD_POOL_CORE = 5;
    protected static final int THREAD_POOL_MAX = 10;

    protected CompositeUtil compositeUtil = CompositeUtil.instance();

    public void setSubjectRef(Ref subjectRef) {
        this.subjectRef = subjectRef;
    }

    public CompositeUtil getCompositeUtil() {
        return compositeUtil;
    }

    /**
     * This method creates a sub-resource of the specified type. Since the JAX-RS does not allow for injection into
     * sub-resources (as it doesn't know or control the lifecycle of the object), this method performs a manual
     * "injection" of the various system objects the resource might need. If the requested Class can not be instantiated
     * (e.g., it does not have a no-arg public constructor), the system will throw a WebApplicationException
     * with an HTTP status code of 500 (internal server error).
     *
     * @param clazz The Class of the desired sub-resource
     * @return
     */
    public  T getSubResource(Class clazz) {
        try {
            T resource = clazz.newInstance();
            CompositeResource cr = (CompositeResource)resource;
            cr.locatorBridge = locatorBridge;
            cr.subjectRef = subjectRef;
            cr.uriInfo = uriInfo;
            cr.securityContext = securityContext;
            cr.requestHeaders = requestHeaders;
            cr.serviceLocator = serviceLocator;

            return resource;
        } catch (Exception ex) {
            throw new WebApplicationException(ex, Status.INTERNAL_SERVER_ERROR);
        }
    }

    // Convenience methods for creating models
    protected  T newModel(Class modelIface) {
        return getCompositeUtil().getModel(modelIface);
    }
    protected  T newTemplate(Class modelIface) {
        // We don't want any model trimming to happen on templates since the caller is supposed to
        // get the template, modify it, then POST it back and since POST should be getting full entities.
        T template = newModel(modelIface);
        template.allFieldsSet();
        return template;
    }
    protected  T getTypedModel(Class modelIface, JsonObject jsonModel) throws Exception {
        if (jsonModel == null) {
          return null;
        }
        return CompositeUtil.instance().unmarshallClass(getLocale(), modelIface, jsonModel);
    }
    protected JsonObject getJsonModel(RestModel typedModel) throws Exception {
        return (JsonObject)JsonUtil.getJsonValue(typedModel, false); // include confidential properties
    }

    // Convenience methods for constructing URIs
    /**
     * Every resource that returns a collection will need to return the URI for each item in the collection. This method
     * handles the creation of that URI, ensuring a correct and consistent URI pattern.
     * @param name
     * @return
     */
    protected URI getChildItemUri(String name) {
        return getSubUri("id/" + name);
    }
    protected URI getUri(String path) {
        return this.uriInfo.getBaseUriBuilder().path(path).build();
    }
    protected URI getSubUri(String name) {
        return this.uriInfo.getAbsolutePathBuilder().path(name).build();
    }

    // Convenience methods for adding links
    protected void addResourceLink(ResponseBody rb, String rel) throws Exception {
        rb.addResourceLink(rel, getSubUri(rel));
    }
    protected void addActionResourceLink(ResponseBody rb, String action) throws Exception {
        rb.addActionResourceLink(action, getSubUri(action));
    }

    protected boolean includeResourceLinks() {
        final String hdr = requestHeaders.getRequestHeaders().getFirst("X-Skip-Metadata"); // X-Skip-Resource-Links
        boolean skip = "true".equalsIgnoreCase(hdr);
        return !skip;
    }

    // Convenience methods for computing a resource's parent uri
    protected URI getParentUri() throws Exception {
        return getParentUri(false);
    }
    protected URI getCollectionChildParentUri() throws Exception {
        return getParentUri(true);
    }
    private URI getParentUri(boolean isCollectionChild) throws Exception {
        List pathSegments = this.uriInfo.getPathSegments();
        int count = pathSegments.size() - 1; // go up a level to get to the parent
        if (isCollectionChild) {
            count--; // collection children have the url pattern .../foos/id/myfoo. need to go up another level
        }
        // [0] = 'javaservice', which is a resource
        if (count <= 0) {
            return null; // top level resource
        }
        UriBuilder bldr = this.uriInfo.getBaseUriBuilder();
        for (int i = 0; i < count; i++) {
            bldr.path(pathSegments.get(i).getPath());
        }
        return bldr.build();
    }

    /**
     * Execute a delete AdminCommand with no parameters.
     * @param command
     * @return
     */
    protected ActionReporter executeDeleteCommand(String command) {
        return getCompositeUtil().executeDeleteCommand(getSubject(), command);
    }

    /**
     * Execute a delete AdminCommand with the specified parameters.
     * @param command
     * @param parameters
     * @return
     */
    protected ActionReporter executeDeleteCommand(String command, ParameterMap parameters) {
        return getCompositeUtil().executeDeleteCommand(getSubject(), command, parameters);
    }

    /**
     * Execute a delete AdminCommand with the specified parameters.
     * @param command
     * @param parameters
     * @return
     */
    protected ActionReporter executeDeleteCommandManaged(String command, ParameterMap parameters) {
        return getCompositeUtil().executeDeleteCommandManaged(getSubject(), command, parameters);
    }

    /**
     * Execute a writing AdminCommand with no parameters.
     * @param command
     * @return
     */
    protected ActionReporter executeWriteCommand(String command) {
        return getCompositeUtil().executeWriteCommand(getSubject(), command);
    }

    /**
     * Execute a writing AdminCommand with the specified parameters.
     * @param command
     * @param parameters
     * @return
     */
    protected ActionReporter executeWriteCommand(String command, ParameterMap parameters) {
        return getCompositeUtil().executeWriteCommand(getSubject(), command, parameters);
    }

    /**
     * Execute a writing AdminCommand with the specified parameters.
     * @param command
     * @param parameters
     * @return
     */
    protected ActionReporter executeWriteCommandManaged(String command, ParameterMap parameters) {
        return getCompositeUtil().executeWriteCommandManaged(getSubject(), command, parameters);
    }

    /**
     * Execute a read-only AdminCommand with the specified parameters.
     * @param command
     * @param parameters
     * @return
     */
    protected ActionReporter executeReadCommand(String command) {
        return getCompositeUtil().executeReadCommand(getSubject(), command);
    }

    /**
     * Execute a read-only AdminCommand with no parameters.
     * @param command
     * @param parameters
     * @return
     */
    protected ActionReporter executeReadCommand(String command, ParameterMap parameters) {
        return getCompositeUtil().executeReadCommand(getSubject(), command, parameters);
    }

    /**
     * Execute an AdminCommand with the specified parameters.
     * @param command
     * @param parameters
     * @param status
     * @param includeFailureMessage
     * @param throwOnWarning (vs.ignore warning)
     * @return
     */
    protected ActionReporter executeCommand(String command, ParameterMap parameters, Status status, boolean includeFailureMessage, boolean throwOnWarning) {
        return getCompositeUtil().executeCommand(getSubject(), command, parameters, status, includeFailureMessage, throwOnWarning, false);
    }

    /**
     * Execute an AdminCommand via SSE, but provide an ActionReportProcessor that allows
     * the calling resource, via an EntityBuilder instance, to return a ResponseBody that
     * extra information such as the newly create entity, as well as any messages returned by the subsystem.
     */
    protected EventOutput executeSseCommand(final Subject subject, final String command,
                                        final ParameterMap parameters,
                                        final ResponseBodyBuilder builder) {
        return getCompositeUtil().executeSseCommand(subject, command, parameters, new SseCommandHelper.ActionReportProcessor() {
            @Override
            public ActionReport process(ActionReport report, EventOutput ec) {
                if (report != null) {
                    ResponseBody rb = builder.build(report);
                    Properties props = new Properties();
                    props.put("response", rb);
                    report.setExtraProperties(props);
                }

                return report;
            }

        });
    }

    /** Execute an AdminCommand with the specified parameters and
     * return EventOutput suitable for SSE.
     */
    protected EventOutput executeSseCommand(final Subject subject, final String command,
                                        final ParameterMap parameters,
                                        final SseCommandHelper.ActionReportProcessor processor) {
        return getCompositeUtil().executeSseCommand(subject, command, parameters, processor);
    }

    /** Execute an AdminCommand with the specified parameters and
     * return EventOutput suitable for SSE.
     */
    protected EventOutput executeSseCommand(final Subject subject, final String command,
                                        final ParameterMap parameters) {
        return getCompositeUtil().executeSseCommand(subject, command, parameters);
    }

    /**
     * TBD - Jason Lee wants to move this into the defaults generators.
     *
     * Finds an unused name given the list of currently used names and a name prefix.
     *
     * @param namePrefix
     * @param usedNames
     * @return a String containing an unused dname, or an empty string if all candidate names are currently in use.
     */
    protected String generateDefaultName(String namePrefix, Collection usedNames) {
        for (int i = 1; i <= 100; i++) {
            String name = namePrefix + "-" + i;
            if (!usedNames.contains(name)) {
                return name;
            }
        }
        // All the candidate names are in use.  Return an empty name.
        return "";
    }

    // Convenience methods for 'create' method responses
    protected Response created(String name, String message) throws Exception {
        return created(responseBody(), name, message);
    }
    protected Response created(ResponseBody rb, String name, String message) throws Exception {
        rb.addSuccess(message);
        return created(rb, name);
    }
    protected Response created(ResponseBody rb, String name) throws Exception {
        return created(rb, getChildItemUri(name));
    }
    protected Response created(ResponseBody rb, URI uri) throws Exception {
        return Response.created(uri).entity(rb).build();
    }

    // Convenience methods for 'update' method responses
    protected Response updated(String message) {
        return updated(responseBody(), message);
    }
    protected Response updated(ResponseBody rb, String message) {
        rb.addSuccess(message);
        return updated(rb);
    }
    protected Response updated(ResponseBody rb) {
        return ok(rb);
    }

    // Convenience methods for 'delete' method responses
    protected Response deleted(String message) {
        return deleted(responseBody(), message);
    }
    protected Response deleted(ResponseBody rb, String message) {
        rb.addSuccess(message);
        return deleted(rb);
    }
    protected Response deleted(ResponseBody rb) {
        return ok(rb);
    }

    // Convenience methods for 'action' method responses
    protected Response acted(String message) {
        return acted(responseBody(), message);
    }
    protected Response acted(ResponseBody rb, String message) {
        rb.addSuccess(message);
        return acted(rb);
    }
    protected Response acted(ResponseBody rb) {
        return ok(rb);
    }

    // Convenience methods for detached method responses
    protected Response accepted(String message, URI jobUri, URI newItemUri) {
        return accepted(responseBody(), message, jobUri, newItemUri);
    }
    protected Response accepted(ResponseBody rb, String message, URI jobUri, URI newItemUri) {
        rb.addSuccess(message);
        return accepted(rb, jobUri, newItemUri);
    }
    protected Response accepted(ResponseBody rb, URI jobUri, URI newItemUri) {
        ResponseBuilder bldr = Response.status(Status.ACCEPTED).entity(rb);
        if (jobUri != null) {
            bldr.header("Location", jobUri);
        }
        if (newItemUri != null) {
            bldr.header("X-Location", newItemUri);
        }
        return bldr.build();
    }
    protected Response accepted(String command, ParameterMap parameters, URI childUri) {
        return accepted(responseBody(), launchDetachedCommand(command, parameters), childUri);
    }
    protected URI launchDetachedCommand(String command, ParameterMap parameters) {
        CommandRunner cr = Globals.getDefaultHabitat().getService(CommandRunner.class);
        final RestActionReporter ar = new RestActionReporter();
        final CommandRunner.CommandInvocation commandInvocation =
                cr.getCommandInvocation(command, ar, getSubject()).
                parameters(parameters);
        final String jobId = DetachedCommandHelper.invokeAsync(commandInvocation);
        return getUri("jobs/id/" + jobId);
    }

    protected Response ok(ResponseBody rb) {
        return Response.ok(rb).build();
    }

    // Convenience methods for throwing common webapp exceptions
    protected Response badRequest(ResponseBody rb, String message) {
        rb.addFailure(message);
        return badRequest(rb);
    }
    protected Response badRequest(ResponseBody rb) {
        return Response.status(Status.BAD_REQUEST).entity(rb).build();
    }
    protected WebApplicationException badRequest(Throwable cause) {
        return new WebApplicationException(cause, Status.BAD_REQUEST);
    }
    protected WebApplicationException badRequest(String message) {
        return new WebApplicationException(Response.status(Status.BAD_REQUEST).entity(message).build());
    }
    protected WebApplicationException notFound(String message) {
        return new WebApplicationException(Response.status(Status.NOT_FOUND).entity(message).build());
    }

/*
    protected void internalServerError(Exception e) {
        ExceptionUtils.log(e);
    }
*/

    // Convenience methods for creating response bodies
    protected  RestCollectionResponseBody restCollectionResponseBody(Class modelIface, String collectionName, URI parentUri) {
        RestCollectionResponseBody rb = restCollectionResponseBody(modelIface, collectionName);
        rb.addParentResourceLink(parentUri);
        return rb;
    }
    protected  RestCollectionResponseBody restCollectionResponseBody(Class modelIface, String collectionName) {
        return new RestCollectionResponseBody(includeResourceLinks(), this.uriInfo, collectionName);
    }
    protected  RestModelResponseBody restModelResponseBody(Class modelIface, URI parentUri, T entity) {
        RestModelResponseBody rb = restModelResponseBody(modelIface, parentUri);
        rb.setEntity(entity);
        return rb;
    }
    protected  RestModelResponseBody restModelResponseBody(Class modelIface, URI parentUri) {
        RestModelResponseBody rb = restModelResponseBody(modelIface);
        rb.addParentResourceLink(parentUri);
        return rb;
    }
    protected  RestModelResponseBody restModelResponseBody(Class modelIface) {
        return new RestModelResponseBody(includeResourceLinks());
    }
    protected ResponseBody responseBody() {
        return new ResponseBody(includeResourceLinks());
    }

    // Convenience methods for creating responses from response bodies
    protected Response getResponse(ResponseBody responseBody) {
        return getResponse(Status.OK, responseBody);
    }
    protected Response getResponse(Status status, ResponseBody responseBody) {
        return Response.status(status).entity(responseBody).build();
    }

    // Convenience methods to help filter returned data
    protected JsonFilter getFilter(String include, String exclude) throws Exception {
        return new JsonFilter(getLocale(), include, exclude);
    }
    protected JsonFilter getFilter(String include, String exclude, String identityAttr) throws Exception {
        return new JsonFilter(getLocale(), include, exclude, identityAttr);
    }
    protected  T filterModel(Class modelIface, T unfilteredModel, String include, String exclude) throws Exception {
        return filterModel(modelIface, unfilteredModel, getFilter(include, exclude));
    }
    protected  T filterModel(Class modelIface, T unfilteredModel, String include, String exclude, String identityAttr) throws Exception {
        return filterModel(modelIface, unfilteredModel, getFilter(include, exclude, identityAttr));
    }
    protected  T filterModel(Class modelIface, T unfilteredModel, JsonFilter filter) throws Exception {
        JsonObject unfilteredJson = (JsonObject)JsonUtil.getJsonObject(unfilteredModel, false); // don't hide confidential properties
        JsonObject filteredJson = filter.trim(unfilteredJson);
        T filteredModel = getTypedModel(modelIface, filteredJson);
        filteredModel.trimmed(); // TBD - remove once the conversion to the new REST style guide is completed
        return filteredModel;
    }

    protected Locale getLocale() {
        return CompositeUtil.instance().getLocale(requestHeaders);
    }

    /**
     * Convenience method for getting a path parameter.  Equivalent to uriInfo.getPathParameters().getFirst(name)
     * @param name
     * @return
     */
    protected String getPathParam(String name) {
        return this.uriInfo.getPathParameters().getFirst(name);
    }

    protected ParameterMap parameterMap() {
        return Util.parameterMap();
    }

    protected synchronized ExecutorService getExecutorService() {
        return ExecutorServiceHolder.INSTANCE;
    }

    private static class ExecutorServiceHolder {
        private static ExecutorService INSTANCE = new ThreadPoolExecutor(
                    THREAD_POOL_CORE, // core thread pool size
                    THREAD_POOL_MAX, // maximum thread pool size
                    1, // time to wait before resizing pool
                    TimeUnit.MINUTES,
                    new ArrayBlockingQueue(THREAD_POOL_MAX, true),
                    new ThreadPoolExecutor.CallerRunsPolicy());
    }

    protected Response act(final CommandInvoker invoker, boolean detached) {
        if (detached) {
            return accepted(invoker.getCommand(), invoker.getParams(), null);
        } else {
            invoker.setResult(executeWriteCommand(invoker.getCommand(), invoker.getParams()).getExtraProperties());
            return acted(invoker.getSuccessMessage());
        }
    }

    protected Response actSse(final CommandInvoker invoker) {
        final boolean includeResourceLinks = includeResourceLinks();
        EventOutput eo = executeSseCommand(getSubject(), invoker.getCommand(), invoker.getParams(), new ResponseBodyBuilderImpl() {
            @Override
            protected ResponseBody success(ActionReport report) {
                invoker.setResult(report.getExtraProperties());
                SseResponseBody responseBody = new SseResponseBody();
                responseBody.addSuccess(invoker.getSuccessMessage());
                return responseBody;
            }
            @Override
            protected boolean includeResourceLinks() {
                return includeResourceLinks;
            }
        });

        return Response.status(Status.ACCEPTED).entity(eo).build();
    }

    protected Response create(final CreateCommandInvoker invoker, boolean detached) throws Exception {
        if (detached) {
            final String newItemName = invoker.getNewItemName();
            final URI newItemUri = StringUtil.notEmpty(newItemName) ? getChildItemUri(newItemName) : null;
            return accepted(invoker.getCommand(), invoker.getParams(), newItemUri);
        } else {
            invoker.setResult(executeWriteCommand(invoker.getCommand(), invoker.getParams()).getExtraProperties());
            return created(invoker.getNewItemName(), invoker.getSuccessMessage());
        }
    }

    protected Response createSse(final CreateCommandInvoker invoker) throws Exception {
        final String collectionUri = uriInfo.getAbsolutePathBuilder().build().toString();
        final boolean includeResourceLinks = includeResourceLinks();
        EventOutput eo = executeSseCommand(getSubject(), invoker.getCommand(), invoker.getParams(), new ResponseBodyBuilderImpl() {
            @Override
            protected ResponseBody success(ActionReport report) {
                invoker.setResult(report.getExtraProperties());
                SseResponseBody responseBody = new SseResponseBody();
                responseBody.addHeader("Location", collectionUri + "/id/" + invoker.getNewItemName())
                        .addSuccess(invoker.getSuccessMessage());
                return responseBody;
            }
            @Override
            protected boolean includeResourceLinks() {
                return includeResourceLinks;
            }
        });

        return Response.status(Status.ACCEPTED).entity(eo).build();
    }

    public class CommandInvoker {

        public CommandInvoker() {}

        public CommandInvoker(String command, ParameterMap params, String successMessage) {
            setCommand(command);
            setParams(params);
            setSuccessMessage(successMessage);
        }

        private String command;
        public void setCommand(String val) { this.command = val; }
        public String getCommand() { return this.command; }

        private ParameterMap params;
        public void setParams(ParameterMap val) { this.params = val; }
        public ParameterMap getParams() { return this.params; }

        private String successMsg;
        public void setSuccessMessage(String val) { this.successMsg = val; }
        public String getSuccessMessage() { return this.successMsg; }

        public void setResult(Properties extraProperties) {}
    }

    public class CreateCommandInvoker extends CommandInvoker {

        public CreateCommandInvoker() { super(); }

        public CreateCommandInvoker(String command, ParameterMap params, String successMessage, String newItemName) {
            super(command, params, successMessage);
            setNewItemName(newItemName);
        }

        private String newItemName;
        public void setNewItemName(String val) { this.newItemName = val; }
        public String getNewItemName() { return this.newItemName; }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy