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

com.gwtplatform.dispatch.rest.rebind.ActionGenerator Maven / Gradle / Ivy

There is a newer version: 1.6
Show newest version
/**
 * Copyright 2013 ArcBees Inc.
 *
 * 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 com.gwtplatform.dispatch.rest.rebind;

import java.io.PrintWriter;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;

import javax.inject.Inject;
import javax.inject.Provider;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.HEAD;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.PathParam;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.HttpHeaders;

import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;

import com.google.common.collect.Lists;
import com.google.common.eventbus.EventBus;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.JParameter;
import com.google.gwt.core.ext.typeinfo.JParameterizedType;
import com.google.gwt.core.ext.typeinfo.JType;
import com.google.gwt.core.ext.typeinfo.NotFoundException;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.inject.assistedinject.Assisted;
import com.gwtplatform.dispatch.rest.client.DateFormat;
import com.gwtplatform.dispatch.rest.client.NoXsrfHeader;
import com.gwtplatform.dispatch.rest.rebind.event.RegisterMetadataEvent;
import com.gwtplatform.dispatch.rest.rebind.event.RegisterSerializableTypeEvent;
import com.gwtplatform.dispatch.rest.rebind.type.ActionBinding;
import com.gwtplatform.dispatch.rest.rebind.type.MethodCall;
import com.gwtplatform.dispatch.rest.rebind.type.ResourceBinding;
import com.gwtplatform.dispatch.rest.rebind.util.AnnotationValueResolver;
import com.gwtplatform.dispatch.rest.rebind.util.FormParamValueResolver;
import com.gwtplatform.dispatch.rest.rebind.util.GeneratorUtil;
import com.gwtplatform.dispatch.rest.rebind.util.HeaderParamValueResolver;
import com.gwtplatform.dispatch.rest.rebind.util.PathParamValueResolver;
import com.gwtplatform.dispatch.rest.rebind.util.QueryParamValueResolver;
import com.gwtplatform.dispatch.rest.shared.HttpMethod;
import com.gwtplatform.dispatch.rest.shared.MetadataType;
import com.gwtplatform.dispatch.rest.shared.RestAction;

import static com.gwtplatform.dispatch.rest.shared.MetadataType.BODY_TYPE;
import static com.gwtplatform.dispatch.rest.shared.MetadataType.RESPONSE_TYPE;

public class ActionGenerator extends AbstractVelocityGenerator {
    private static class AnnotatedMethodParameter {
        private JParameter parameter;
        private String fieldName;

        private AnnotatedMethodParameter(JParameter parameter, String fieldName) {
            this.parameter = parameter;
            this.fieldName = fieldName;
        }
    }

    private static final String TEMPLATE = "com/gwtplatform/dispatch/rest/rebind/RestAction.vm";

    private static final String MANY_REST_ANNOTATIONS = "'%s' parameter's '%s' is annotated with more than one REST " +
            "annotations.";
    private static final String DATE_FORMAT_NOT_DATE = "'%s' parameter's '%s' is annotated with @DateFormat but its " +
            "type is not Date";
    private static final String MANY_POTENTIAL_BODY = "%s has more than one potential body parameter.";
    private static final String FORM_AND_BODY_PARAM = "%s has both @FormParam and a body parameter. You must specify " +
            "one or the other.";
    private static final String ADD_HEADER_PARAM = "addHeaderParam";
    private static final String ADD_PATH_PARAM = "addPathParam";
    private static final String ADD_QUERY_PARAM = "addQueryParam";
    private static final String ADD_FORM_PARAM = "addFormParam";
    private static final String SET_BODY_PARAM = "setBodyParam";
    private static final String ESCAPED_STRING = "\"%s\"";

    @SuppressWarnings("unchecked")
    private static final List> PARAM_ANNOTATIONS =
            Arrays.asList(HeaderParam.class, QueryParam.class, PathParam.class, FormParam.class);

    private final EventBus eventBus;

    private final JMethod actionMethod;
    private final ResourceBinding parent;
    private final String path;
    private final boolean secured;
    private final List parameters;
    private final List pathParams = new ArrayList();
    private final List headerParams = new ArrayList();
    private final List queryParams = new ArrayList();
    private final List formParams = new ArrayList();
    private final List potentialBodyParams = new ArrayList();

    private HttpMethod httpMethod;
    private JParameter bodyParam;

    @Inject
    ActionGenerator(
            EventBus eventBus,
            TypeOracle typeOracle,
            Logger logger,
            Provider velocityContextProvider,
            VelocityEngine velocityEngine,
            GeneratorUtil generatorUtil,
            @Assisted JMethod actionMethod,
            @Assisted ResourceBinding parent) {
        super(typeOracle, logger, velocityContextProvider, velocityEngine, generatorUtil);

        this.eventBus = eventBus;
        this.actionMethod = actionMethod;
        this.parent = parent;

        path = concatenatePath(parent.getResourcePath(), extractPath(actionMethod));
        secured = parent.isSecured() && !actionMethod.isAnnotationPresent(NoXsrfHeader.class);
        parameters = Lists.newArrayList(parent.getCtorParameters());
    }

    public ActionBinding generate() throws UnableToCompleteException {
        String implName = getSuperTypeName() + SUFFIX;
        PrintWriter printWriter = getGeneratorUtil().tryCreatePrintWriter(getPackage(), implName);

        if (printWriter != null) {
            doGenerate(implName, printWriter);

            registerMetadata();
        } else {
            getLogger().debug("Action already generated. Returning.");
        }

        return createActionBinding(implName);
    }

    @Override
    protected void populateVelocityContext(VelocityContext velocityContext) throws UnableToCompleteException {
        velocityContext.put("resultClass", getResultType());
        velocityContext.put("httpMethod", httpMethod);
        velocityContext.put("methodCalls", getMethodCallsToAdd());
        velocityContext.put("restPath", path);
        velocityContext.put("ctorParams", parameters);
        velocityContext.put("secured", secured);
    }

    @Override
    protected String getPackage() {
        return parent.getImplPackage().replace(SHARED_PACKAGE, CLIENT_PACKAGE);
    }

    private String getQualifiedImplName() {
        return getPackage() + "." + getSuperTypeName() + SUFFIX;
    }

    private String getSuperTypeName() {
        // The service may define overloads, in which case the method name is not unique.
        // The method index will ensure the generated class uniqueness.
        int methodIndex = Arrays.asList(actionMethod.getEnclosingType().getMethods()).indexOf(actionMethod);

        return parent.getSuperTypeName() + "_" + methodIndex + "_" + actionMethod.getName();
    }

    private List getMethodCallsToAdd() {
        List methodCalls = new ArrayList();

        methodCalls.addAll(getMethodCallsToAdd(headerParams, ADD_HEADER_PARAM));
        methodCalls.addAll(getMethodCallsToAdd(pathParams, ADD_PATH_PARAM));
        methodCalls.addAll(getMethodCallsToAdd(queryParams, ADD_QUERY_PARAM));
        methodCalls.addAll(getMethodCallsToAdd(formParams, ADD_FORM_PARAM));

        addContentTypeHeaderMethodCall(methodCalls);

        if (bodyParam != null) {
            methodCalls.add(new MethodCall(SET_BODY_PARAM, bodyParam.getName()));
        }

        return methodCalls;
    }

    private List getMethodCallsToAdd(List methodParameters, String methodName) {
        List methodCalls = new ArrayList();
        for (AnnotatedMethodParameter methodParameter : methodParameters) {
            List arguments = Lists.newArrayList(String.format(ESCAPED_STRING, methodParameter.fieldName),
                    methodParameter.parameter.getName());

            maybeAddDateFormat(arguments, methodParameter);

            methodCalls.add(new MethodCall(methodName, arguments.toArray(new String[arguments.size()])));
        }

        return methodCalls;
    }

    private void maybeAddDateFormat(List args, AnnotatedMethodParameter methodParameter) {
        if (methodParameter.parameter.isAnnotationPresent(DateFormat.class)) {
            String dateFormat = methodParameter.parameter.getAnnotation(DateFormat.class).value();
            args.add(String.format(ESCAPED_STRING, dateFormat));
        }
    }

    private void addContentTypeHeaderMethodCall(List methodCalls) {
        Consumes consumes = actionMethod.getAnnotation(Consumes.class);

        if (consumes != null && consumes.value().length > 0) {
            MethodCall methodCall = new MethodCall(
                    ADD_HEADER_PARAM,
                    String.format(ESCAPED_STRING, HttpHeaders.CONTENT_TYPE),
                    String.format(ESCAPED_STRING, consumes.value()[0]));
            methodCalls.add(methodCall);
        }
    }

    private void doGenerate(String implName, PrintWriter printWriter) throws UnableToCompleteException {
        verifyIsAction();

        retrieveHttpMethod();
        retrieveParameterConfig();
        retrieveBodyConfig();

        mergeTemplate(printWriter, TEMPLATE, implName);
    }

    private void registerMetadata() throws UnableToCompleteException {
        if (bodyParam != null) {
            registerMetadatum(BODY_TYPE, bodyParam.getType());
        }

        registerMetadatum(RESPONSE_TYPE, getResultType());
    }

    private void registerMetadatum(MetadataType metadataType, JType type) {
        String typeLiteral = "\"" + type.getParameterizedQualifiedSourceName() + "\"";

        eventBus.post(new RegisterMetadataEvent(getQualifiedImplName(), metadataType, typeLiteral));

        if (!Void.class.getCanonicalName().equals(type.getQualifiedSourceName()) && type.isPrimitive() == null) {
            eventBus.post(new RegisterSerializableTypeEvent(type));
        }
    }

    private void verifyIsAction() throws UnableToCompleteException {
        JClassType actionClass = null;

        try {
            actionClass = getTypeOracle().getType(RestAction.class.getName());
        } catch (NotFoundException e) {
            getLogger().die("Unable to find interface Action.");
        }

        JClassType returnClass = getReturnType().isClassOrInterface();
        if (!returnClass.isAssignableTo(actionClass)) {
            String typeName = returnClass.getQualifiedSourceName();
            getLogger().die(typeName + " must implement RestAction.");
        }
    }

    private void retrieveHttpMethod() throws UnableToCompleteException {
        Boolean moreThanOneAnnotation = false;

        if (actionMethod.isAnnotationPresent(GET.class)) {
            httpMethod = HttpMethod.GET;
        }

        if (actionMethod.isAnnotationPresent(POST.class)) {
            moreThanOneAnnotation = httpMethod != null;
            httpMethod = HttpMethod.POST;
        }

        if (actionMethod.isAnnotationPresent(PUT.class)) {
            moreThanOneAnnotation = moreThanOneAnnotation || httpMethod != null;
            httpMethod = HttpMethod.PUT;
        }

        if (actionMethod.isAnnotationPresent(DELETE.class)) {
            moreThanOneAnnotation = moreThanOneAnnotation || httpMethod != null;
            httpMethod = HttpMethod.DELETE;
        }

        if (actionMethod.isAnnotationPresent(HEAD.class)) {
            moreThanOneAnnotation = moreThanOneAnnotation || httpMethod != null;
            httpMethod = HttpMethod.HEAD;
        }

        if (httpMethod == null) {
            getLogger().die(actionMethod.getName() + " has no http method annotations.");
        } else if (moreThanOneAnnotation) {
            getLogger().warn(actionMethod.getName() + " has more than one http method annotation.");
        }
    }

    private void retrieveParameterConfig() throws UnableToCompleteException {
        Collections.addAll(parameters, actionMethod.getParameters());

        buildParamList(parameters, HeaderParam.class, new HeaderParamValueResolver(), headerParams);
        buildParamList(parameters, PathParam.class, new PathParamValueResolver(), pathParams);
        buildParamList(parameters, QueryParam.class, new QueryParamValueResolver(), queryParams);
        buildParamList(parameters, FormParam.class, new FormParamValueResolver(), formParams);

        buildPotentialBodyParams();
    }

    private  void buildParamList(List parameters, Class annotationClass,
            AnnotationValueResolver annotationValueResolver, List destination)
            throws UnableToCompleteException {
        List> restrictedAnnotations = getRestrictedAnnotations(annotationClass);

        for (JParameter parameter : parameters) {
            T parameterAnnotation = parameter.getAnnotation(annotationClass);

            if (parameterAnnotation != null) {
                if (hasAnnotationFrom(parameter, restrictedAnnotations)) {
                    getLogger().die(String.format(MANY_REST_ANNOTATIONS, actionMethod.getName(), parameter.getName()));
                }
                if (parameter.isAnnotationPresent(DateFormat.class) && !isDate(parameter)) {
                    getLogger().die(String.format(DATE_FORMAT_NOT_DATE, actionMethod.getName(), parameter.getName()));
                }

                String value = annotationValueResolver.resolve(parameterAnnotation);
                destination.add(new AnnotatedMethodParameter(parameter, value));
            }
        }
    }

    private boolean isDate(JParameter parameter) {
        return Date.class.getCanonicalName().equals(parameter.getType().getQualifiedSourceName());
    }

    private List> getRestrictedAnnotations(Class allowedAnnotation) {
        List> restrictedAnnotations = new ArrayList>();
        restrictedAnnotations.addAll(PARAM_ANNOTATIONS);
        restrictedAnnotations.remove(allowedAnnotation);

        return restrictedAnnotations;
    }

    private Boolean hasAnnotationFrom(JParameter parameter, List> restrictedAnnotations) {
        for (Class restrictedAnnotation : restrictedAnnotations) {
            if (parameter.isAnnotationPresent(restrictedAnnotation)) {
                return true;
            }
        }

        return false;
    }

    private void buildPotentialBodyParams() {
        potentialBodyParams.addAll(parameters);

        List annotatedParameters = new ArrayList();
        annotatedParameters.addAll(headerParams);
        annotatedParameters.addAll(pathParams);
        annotatedParameters.addAll(queryParams);
        annotatedParameters.addAll(formParams);

        for (AnnotatedMethodParameter annotatedParameter : annotatedParameters) {
            potentialBodyParams.remove(annotatedParameter.parameter);
        }
    }

    private void retrieveBodyConfig() throws UnableToCompleteException {
        if (potentialBodyParams.isEmpty()) {
            return;
        }

        if (potentialBodyParams.size() > 1) {
            getLogger().die(String.format(MANY_POTENTIAL_BODY, actionMethod.getName()));
        }

        if (!formParams.isEmpty()) {
            getLogger().die(String.format(FORM_AND_BODY_PARAM, actionMethod.getName()));
        }

        bodyParam = potentialBodyParams.get(0);
    }

    private ActionBinding createActionBinding(String implName) throws UnableToCompleteException {
        String resultClass = getResultType().getParameterizedQualifiedSourceName();

        ActionBinding binding = new ActionBinding(path, getPackage(), implName, actionMethod.getName(), resultClass,
                parameters);
        binding.setSecured(secured);

        return binding;
    }

    @SuppressWarnings("ConstantConditions")
    private JClassType getResultType() throws UnableToCompleteException {
        JParameterizedType parameterized = getReturnType().isParameterized();
        if (parameterized == null || parameterized.getTypeArgs().length != 1) {
            getLogger().die("The action must specify a result type argument.");
        }
        return parameterized.getTypeArgs()[0];
    }

    private JType getReturnType() {
        return actionMethod.getReturnType();
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy