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

com.gwtplatform.dispatch.rest.rebind.RestActionGenerator 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.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.Path;
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.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.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.FromParamMethodCall;
import com.gwtplatform.dispatch.rest.rebind.type.MethodCall;
import com.gwtplatform.dispatch.rest.rebind.type.NoParamMethodCall;
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 RestActionGenerator extends AbstractVelocityGenerator {
    private static class AnnotatedMethodParameter {
        private JParameter parameter;
        private String fieldName;

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

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

    private static final String TEMPLATE = "com/gwtplatform/dispatch/rest/rebind/RestAction.vm";
    private static final String PATH_PARAM = "{%s}";
    private static final String PATH_PARAM_MISSING = "@PathParam(\"%1$s\") declared, but '%1$s' not found in %2$s.";
    private static final String MANY_REST_ANNOTATIONS = "'%s' parameter's '%s' is annotated with more than one REST " +
                                                        "annotations.";
    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 final EventBus eventBus;

    private final JMethod actionMethod;
    private final JType returnType;
    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 String path = "";
    private JParameter bodyParam;

    @Inject
    public RestActionGenerator(
            EventBus eventBus,
            TypeOracle typeOracle,
            Logger logger,
            Provider velocityContextProvider,
            VelocityEngine velocityEngine,
            GeneratorUtil generatorUtil,
            @Assisted JMethod actionMethod) throws UnableToCompleteException {
        super(typeOracle, logger, velocityContextProvider, velocityEngine, generatorUtil);

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

        returnType = actionMethod.getReturnType();
    }

    public ActionBinding generate(String restServicePath) throws Exception {
        verifyIsAction();
        JClassType resultType = getResultType();

        path = restServicePath;
        retrieveConfigAnnotations();
        retrieveParameterConfig();
        retrieveBodyConfig();

        verifyPathParamsExist();

        String implName = getClassName();
        PrintWriter printWriter = getGeneratorUtil().tryCreatePrintWriter(getPackage(), implName);

        if (printWriter != null) {
            mergeTemplate(printWriter, TEMPLATE, implName);
        } else {
            getLogger().debug("Action already generated. Returning.");
        }

        registerMetadata();

        return new ActionBinding(implName, actionMethod.getName(), resultType.getParameterizedQualifiedSourceName(),
                actionMethod.getParameters());
    }

    @Override
    protected String getPackage() {
        return actionMethod.getEnclosingType().getPackage().getName().replace(SHARED_PACKAGE, CLIENT_PACKAGE);
    }

    @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", actionMethod.getParameters());
    }

    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 FromParamMethodCall(SET_BODY_PARAM, null, bodyParam));
        }

        return methodCalls;
    }

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

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

    private List getMethodCallsToAdd(List methodParameters,
                                                 String methodName) {
        List methodCalls = new ArrayList();
        for (AnnotatedMethodParameter methodParameter : methodParameters) {
            methodCalls.add(new FromParamMethodCall(methodName, methodParameter.fieldName, methodParameter.parameter));
        }
        return methodCalls;
    }

    private void registerMetadata() throws Exception {
        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(getQualifiedClassName(), metadataType, typeLiteral));

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

    private String getQualifiedClassName() {
        return getPackage() + "." + getClassName();
    }

    private String getClassName() {
        return getBaseName() + "_" + returnType.isClassOrInterface().getName() + SUFFIX;
    }

    private String getBaseName() {
        StringBuilder nameBuilder = new StringBuilder(actionMethod.getName());
        Character firstChar = Character.toUpperCase(nameBuilder.charAt(0));
        nameBuilder.setCharAt(0, firstChar);

        StringBuilder classNameBuilder = new StringBuilder();
        classNameBuilder.append(actionMethod.getEnclosingType().getName())
                        .append("_")
                        .append(nameBuilder);

        for (JType type : actionMethod.getErasedParameterTypes()) {
            classNameBuilder.append("_").append(type.getSimpleSourceName());
        }

        return classNameBuilder.toString();
    }

    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 = returnType.isClassOrInterface();
        if (!returnClass.isAssignableTo(actionClass)) {
            String typeName = returnClass.getQualifiedSourceName();
            getLogger().die(typeName + " must implement RestAction.");
        }
    }

    private void retrieveConfigAnnotations() throws UnableToCompleteException {
        retrieveHttpMethod();

        if (actionMethod.isAnnotationPresent(Path.class)) {
            path = concatenatePath(path, actionMethod.getAnnotation(Path.class).value());
        }
    }

    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 {
        JParameter[] 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(JParameter[] 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()));
                    throw new UnableToCompleteException();
                }

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

    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() {
        Collections.addAll(potentialBodyParams, actionMethod.getParameters());

        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 verifyPathParamsExist() throws UnableToCompleteException {
        for (AnnotatedMethodParameter param : pathParams) {
            verifyPathParamExists(param.fieldName);
        }
    }

    private void verifyPathParamExists(String param) throws UnableToCompleteException {
        if (!path.contains(String.format(PATH_PARAM, param))) {
            String warning = String.format(PATH_PARAM_MISSING, param, path);
            getLogger().die(warning);
        }
    }

    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 JClassType getResultType() throws UnableToCompleteException {
        JParameterizedType parameterized = returnType.isParameterized();
        if (parameterized == null || parameterized.getTypeArgs().length != 1) {
            getLogger().die("The action must specify a result type argument.");
        }
        return parameterized.getTypeArgs()[0];
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy