com.hileone.restretrofit.request.RequestFactoryParser.java Source code

Java tutorial

Introduction

Here is the source code for com.hileone.restretrofit.request.RequestFactoryParser.java

Source

/*
 * Copyright (C) 2013 Square, 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.hileone.restretrofit.request;

import com.hileone.restretrofit.utils.RestUtils;
import com.hileone.restretrofit.http.Body;
import com.hileone.restretrofit.http.DELETE;
import com.hileone.restretrofit.http.Field;
import com.hileone.restretrofit.http.FieldMap;
import com.hileone.restretrofit.http.FormUrlEncoded;
import com.hileone.restretrofit.http.GET;
import com.hileone.restretrofit.http.HEAD;
import com.hileone.restretrofit.http.HTTP;
import com.hileone.restretrofit.http.Header;
import com.hileone.restretrofit.http.Headers;
import com.hileone.restretrofit.http.Multipart;
import com.hileone.restretrofit.http.OPTIONS;
import com.hileone.restretrofit.http.PATCH;
import com.hileone.restretrofit.http.POST;
import com.hileone.restretrofit.http.PUT;
import com.hileone.restretrofit.http.Part;
import com.hileone.restretrofit.http.PartMap;
import com.hileone.restretrofit.http.Path;
import com.hileone.restretrofit.http.Query;
import com.hileone.restretrofit.http.QueryMap;
import com.hileone.restretrofit.http.Url;
import com.squareup.okhttp.MediaType;
import com.squareup.okhttp.RequestBody;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public final class RequestFactoryParser {
    // Upper and lower characters, digits, underscores, and hyphens, starting with a character.
    private static final String PARAM = "[a-zA-Z][a-zA-Z0-9_-]*";
    private static final Pattern PARAM_NAME_REGEX = Pattern.compile(PARAM);
    private static final Pattern PARAM_URL_REGEX = Pattern.compile("\\{(" + PARAM + ")\\}");
    private static final BuiltInConverters buildConverts = new BuiltInConverters();

    /**
     * parse
     * @param method method
     * @param responseType responseType
     * @param url url
     * @return RequestFactory
     */
    public static RequestFactory parse(Method method, Type responseType, String url) {
        RequestFactoryParser parser = new RequestFactoryParser(method);
        parser.parseMethodAnnotations(responseType);
        parser.parseParameters();
        return parser.toRequestFactory(RestUtils.baseUrl(url));
    }

    private final Method method;

    private String httpMethod;
    private boolean hasBody;
    private boolean isFormEncoded;
    private boolean isMultipart;
    private String relativeUrl;
    private com.squareup.okhttp.Headers headers;
    private MediaType contentType;
    private RequestAction[] requestActions;

    private Set<String> relativeUrlParamNames;

    private RequestFactoryParser(Method method) {
        this.method = method;
    }

    private RequestFactory toRequestFactory(BaseUrl baseUrl) {
        return new RequestFactory(httpMethod, baseUrl, relativeUrl, headers, contentType, hasBody, isFormEncoded,
                isMultipart, requestActions);
    }

    private RuntimeException parameterError(Throwable cause, int index, String message, Object... args) {
        return RestUtils.methodError(cause, method, message + " (parameter #" + (index + 1) + ")", args);
    }

    private RuntimeException parameterError(int index, String message, Object... args) {
        return RestUtils.methodError(method, message + " (parameter #" + (index + 1) + ")", args);
    }

    private void parseMethodAnnotations(Type responseType) {
        for (Annotation annotation : method.getAnnotations()) {
            if (annotation instanceof DELETE) {
                parseHttpMethodAndPath("DELETE", ((DELETE) annotation).value(), false);
            } else if (annotation instanceof GET) {
                parseHttpMethodAndPath("GET", ((GET) annotation).value(), false);
            } else if (annotation instanceof HEAD) {
                parseHttpMethodAndPath("HEAD", ((HEAD) annotation).value(), false);
                if (!Void.class.equals(responseType)) {
                    throw RestUtils.methodError(method, "HEAD method must use Void as response type.");
                }
            } else if (annotation instanceof PATCH) {
                parseHttpMethodAndPath("PATCH", ((PATCH) annotation).value(), true);
            } else if (annotation instanceof POST) {
                parseHttpMethodAndPath("POST", ((POST) annotation).value(), true);
            } else if (annotation instanceof PUT) {
                parseHttpMethodAndPath("PUT", ((PUT) annotation).value(), true);
            } else if (annotation instanceof OPTIONS) {
                parseHttpMethodAndPath("OPTIONS", ((OPTIONS) annotation).value(), false);
            } else if (annotation instanceof HTTP) {
                HTTP http = (HTTP) annotation;
                parseHttpMethodAndPath(http.method(), http.path(), http.hasBody());
            } else if (annotation instanceof Headers) {
                String[] headersToParse = ((Headers) annotation).value();
                if (headersToParse.length == 0) {
                    throw RestUtils.methodError(method, "@Headers annotation is empty.");
                }
                headers = parseHeaders(headersToParse);
            } else if (annotation instanceof Multipart) {
                if (isFormEncoded) {
                    throw RestUtils.methodError(method, "Only one encoding annotation is allowed.");
                }
                isMultipart = true;
            } else if (annotation instanceof FormUrlEncoded) {
                if (isMultipart) {
                    throw RestUtils.methodError(method, "Only one encoding annotation is allowed.");
                }
                isFormEncoded = true;
            }
        }

        if (httpMethod == null) {
            throw RestUtils.methodError(method, "HTTP method annotation is required (e.g., @GET, @POST, etc.).");
        }
        if (!hasBody) {
            if (isMultipart) {
                throw RestUtils.methodError(method,
                        "Multipart can only be specified on HTTP methods with request body (e.g., @POST).");
            }
            if (isFormEncoded) {
                throw RestUtils.methodError(method,
                        "FormUrlEncoded can only be specified on HTTP methods with request body "
                                + "(e.g., @POST).");
            }
        }
    }

    private void parseHttpMethodAndPath(String httpMethod, String value, boolean hasBody) {
        if (this.httpMethod != null) {
            throw RestUtils.methodError(method, "Only one HTTP method is allowed. Found: %s and %s.",
                    this.httpMethod, httpMethod);
        }
        this.httpMethod = httpMethod;
        this.hasBody = hasBody;

        if (value.isEmpty()) {
            return;
        }

        // Get the relative URL path and existing query string, if present.
        int question = value.indexOf('?');
        if (question != -1 && question < value.length() - 1) {
            // Ensure the query string does not have any named parameters.
            String queryParams = value.substring(question + 1);
            Matcher queryParamMatcher = PARAM_URL_REGEX.matcher(queryParams);
            if (queryParamMatcher.find()) {
                throw RestUtils.methodError(method, "URL query string \"%s\" must not have replace block. "
                        + "For dynamic query parameters use @Query.", queryParams);
            }
        }

        this.relativeUrl = value;
        this.relativeUrlParamNames = parsePathParameters(value);
    }

    private com.squareup.okhttp.Headers parseHeaders(String[] headers) {
        com.squareup.okhttp.Headers.Builder builder = new com.squareup.okhttp.Headers.Builder();
        for (String header : headers) {
            int colon = header.indexOf(':');
            if (colon == -1 || colon == 0 || colon == header.length() - 1) {
                throw RestUtils.methodError(method,
                        "@Headers value must be in the form \"Name: Value\". Found: \"%s\"", header);
            }
            String headerName = header.substring(0, colon);
            String headerValue = header.substring(colon + 1).trim();
            if ("Content-Type".equalsIgnoreCase(headerName)) {
                contentType = MediaType.parse(headerValue);
            } else {
                builder.add(headerName, headerValue);
            }
        }
        return builder.build();
    }

    private void parseParameters() {
        Type[] methodParameterTypes = method.getGenericParameterTypes();
        Annotation[][] methodParameterAnnotationArrays = method.getParameterAnnotations();

        boolean gotField = false;
        boolean gotPart = false;
        boolean gotBody = false;
        boolean gotPath = false;
        boolean gotQuery = false;
        boolean gotUrl = false;

        int count = methodParameterAnnotationArrays.length;
        RequestAction[] requestActions = new RequestAction[count];
        for (int i = 0; i < count; i++) {
            Type methodParameterType = methodParameterTypes[i];
            if (RestUtils.hasUnresolvableType(methodParameterType)) {
                throw parameterError(i, "Parameter type must not include a type variable or wildcard: %s",
                        methodParameterType);
            }

            Annotation[] methodParameterAnnotations = methodParameterAnnotationArrays[i];
            if (methodParameterAnnotations != null) {
                for (Annotation methodParameterAnnotation : methodParameterAnnotations) {
                    RequestAction action = null;
                    if (methodParameterAnnotation instanceof Url) {
                        if (gotUrl) {
                            throw parameterError(i, "Multiple @Url method annotations found.");
                        }
                        if (gotPath) {
                            throw parameterError(i, "@Path parameters may not be used with @Url.");
                        }
                        if (gotQuery) {
                            throw parameterError(i, "A @Url parameter must not come after a @Query");
                        }
                        if (methodParameterType != String.class) {
                            throw parameterError(i, "@Url must be String type.");
                        }
                        if (relativeUrl != null) {
                            throw parameterError(i, "@Url cannot be used with @%s URL", httpMethod);
                        }
                        gotUrl = true;
                        action = new RequestAction.Url();

                    } else if (methodParameterAnnotation instanceof Path) {
                        if (gotQuery) {
                            throw parameterError(i, "A @Path parameter must not come after a @Query.");
                        }
                        if (gotUrl) {
                            throw parameterError(i, "@Path parameters may not be used with @Url.");
                        }
                        if (relativeUrl == null) {
                            throw parameterError(i, "@Path can only be used with relative url on @%s", httpMethod);
                        }
                        gotPath = true;

                        Path path = (Path) methodParameterAnnotation;
                        String name = path.value();
                        validatePathName(i, name);

                        Converter<?, String> valueConverter = stringConverter(methodParameterType,
                                methodParameterAnnotations);
                        action = new RequestAction.Path<>(name, valueConverter, path.encoded());

                    } else if (methodParameterAnnotation instanceof Query) {
                        Query query = (Query) methodParameterAnnotation;
                        String name = query.value();
                        boolean encoded = query.encoded();

                        Class<?> rawParameterType = RestUtils.getRawType(methodParameterType);
                        if (Iterable.class.isAssignableFrom(rawParameterType)) {
                            if (!(methodParameterType instanceof ParameterizedType)) {
                                throw parameterError(i,
                                        rawParameterType.getSimpleName() + " must include generic type (e.g., "
                                                + rawParameterType.getSimpleName() + "<String>)");
                            }
                            ParameterizedType parameterizedType = (ParameterizedType) methodParameterType;
                            Type iterableType = RestUtils.getParameterUpperBound(0, parameterizedType);
                            Converter<?, String> valueConverter = stringConverter(iterableType,
                                    methodParameterAnnotations);
                            action = new RequestAction.Query<>(name, valueConverter, encoded).iterable();
                        } else if (rawParameterType.isArray()) {
                            Class<?> arrayComponentType = boxIfPrimitive(rawParameterType.getComponentType());
                            Converter<?, String> valueConverter = stringConverter(arrayComponentType,
                                    methodParameterAnnotations);
                            action = new RequestAction.Query<>(name, valueConverter, encoded).array();
                        } else {
                            Converter<?, String> valueConverter = stringConverter(methodParameterType,
                                    methodParameterAnnotations);
                            action = new RequestAction.Query<>(name, valueConverter, encoded);
                        }

                        gotQuery = true;

                    } else if (methodParameterAnnotation instanceof QueryMap) {
                        if (!Map.class.isAssignableFrom(RestUtils.getRawType(methodParameterType))) {
                            throw parameterError(i, "@QueryMap parameter type must be Map.");
                        }
                        if (!(methodParameterType instanceof ParameterizedType)) {
                            throw parameterError(i, "Map must include generic types (e.g., Map<String, String>)");
                        }
                        ParameterizedType parameterizedType = (ParameterizedType) methodParameterType;
                        Type keyType = RestUtils.getParameterUpperBound(0, parameterizedType);
                        if (String.class != keyType) {
                            throw parameterError(i, "@QueryMap keys must be of type String: " + keyType);
                        }
                        Type valueType = RestUtils.getParameterUpperBound(1, parameterizedType);
                        Converter<?, String> valueConverter = stringConverter(valueType,
                                methodParameterAnnotations);

                        QueryMap queryMap = (QueryMap) methodParameterAnnotation;
                        action = new RequestAction.QueryMap<>(valueConverter, queryMap.encoded());

                    } else if (methodParameterAnnotation instanceof Header) {
                        Header header = (Header) methodParameterAnnotation;
                        String name = header.value();

                        Class<?> rawParameterType = RestUtils.getRawType(methodParameterType);
                        if (Iterable.class.isAssignableFrom(rawParameterType)) {
                            if (!(methodParameterType instanceof ParameterizedType)) {
                                throw parameterError(i,
                                        rawParameterType.getSimpleName() + " must include generic type (e.g., "
                                                + rawParameterType.getSimpleName() + "<String>)");
                            }
                            ParameterizedType parameterizedType = (ParameterizedType) methodParameterType;
                            Type iterableType = RestUtils.getParameterUpperBound(0, parameterizedType);
                            Converter<?, String> valueConverter = stringConverter(iterableType,
                                    methodParameterAnnotations);
                            action = new RequestAction.Header<>(name, valueConverter).iterable();
                        } else if (rawParameterType.isArray()) {
                            Class<?> arrayComponentType = boxIfPrimitive(rawParameterType.getComponentType());
                            Converter<?, String> valueConverter = stringConverter(arrayComponentType,
                                    methodParameterAnnotations);
                            action = new RequestAction.Header<>(name, valueConverter).array();
                        } else {
                            Converter<?, String> valueConverter = stringConverter(methodParameterType,
                                    methodParameterAnnotations);
                            action = new RequestAction.Header<>(name, valueConverter);
                        }

                    } else if (methodParameterAnnotation instanceof Field) {
                        if (!isFormEncoded) {
                            throw parameterError(i, "@Field parameters can only be used with form encoding.");
                        }
                        Field field = (Field) methodParameterAnnotation;
                        String name = field.value();
                        boolean encoded = field.encoded();

                        Class<?> rawParameterType = RestUtils.getRawType(methodParameterType);
                        if (Iterable.class.isAssignableFrom(rawParameterType)) {
                            if (!(methodParameterType instanceof ParameterizedType)) {
                                throw parameterError(i,
                                        rawParameterType.getSimpleName() + " must include generic type (e.g., "
                                                + rawParameterType.getSimpleName() + "<String>)");
                            }
                            ParameterizedType parameterizedType = (ParameterizedType) methodParameterType;
                            Type iterableType = RestUtils.getParameterUpperBound(0, parameterizedType);
                            Converter<?, String> valueConverter = stringConverter(iterableType,
                                    methodParameterAnnotations);
                            action = new RequestAction.Field<>(name, valueConverter, encoded).iterable();
                        } else if (rawParameterType.isArray()) {
                            Class<?> arrayComponentType = boxIfPrimitive(rawParameterType.getComponentType());
                            Converter<?, String> valueConverter = stringConverter(arrayComponentType,
                                    methodParameterAnnotations);
                            action = new RequestAction.Field<>(name, valueConverter, encoded).array();
                        } else {
                            Converter<?, String> valueConverter = stringConverter(methodParameterType,
                                    methodParameterAnnotations);
                            action = new RequestAction.Field<>(name, valueConverter, encoded);
                        }

                        gotField = true;

                    } else if (methodParameterAnnotation instanceof FieldMap) {
                        if (!isFormEncoded) {
                            throw parameterError(i, "@FieldMap parameters can only be used with form encoding.");
                        }
                        if (!Map.class.isAssignableFrom(RestUtils.getRawType(methodParameterType))) {
                            throw parameterError(i, "@FieldMap parameter type must be Map.");
                        }
                        if (!(methodParameterType instanceof ParameterizedType)) {
                            throw parameterError(i, "Map must include generic types (e.g., Map<String, String>)");
                        }
                        ParameterizedType parameterizedType = (ParameterizedType) methodParameterType;
                        Type keyType = RestUtils.getParameterUpperBound(0, parameterizedType);
                        if (String.class != keyType) {
                            throw parameterError(i, "@FieldMap keys must be of type String: " + keyType);
                        }
                        Type valueType = RestUtils.getParameterUpperBound(1, parameterizedType);
                        Converter<?, String> valueConverter = stringConverter(valueType,
                                methodParameterAnnotations);

                        FieldMap fieldMap = (FieldMap) methodParameterAnnotation;
                        action = new RequestAction.FieldMap<>(valueConverter, fieldMap.encoded());
                        gotField = true;

                    } else if (methodParameterAnnotation instanceof Part) {
                        if (!isMultipart) {
                            throw parameterError(i, "@Part parameters can only be used with multipart encoding.");
                        }
                        Part part = (Part) methodParameterAnnotation;
                        com.squareup.okhttp.Headers headers = com.squareup.okhttp.Headers.of("Content-Disposition",
                                "form-data; name=\"" + part.value() + "\"", "Content-Transfer-Encoding",
                                part.encoding());

                        Class<?> rawParameterType = RestUtils.getRawType(methodParameterType);
                        if (Iterable.class.isAssignableFrom(rawParameterType)) {
                            if (!(methodParameterType instanceof ParameterizedType)) {
                                throw parameterError(i,
                                        rawParameterType.getSimpleName() + " must include generic type (e.g., "
                                                + rawParameterType.getSimpleName() + "<String>)");
                            }
                            ParameterizedType parameterizedType = (ParameterizedType) methodParameterType;
                            Type iterableType = RestUtils.getParameterUpperBound(0, parameterizedType);
                            Converter<?, RequestBody> valueConverter = requestBodyConverter(iterableType,
                                    methodParameterAnnotations);
                            action = new RequestAction.Part<>(headers, valueConverter).iterable();
                        } else if (rawParameterType.isArray()) {
                            Class<?> arrayComponentType = boxIfPrimitive(rawParameterType.getComponentType());
                            Converter<?, RequestBody> valueConverter = requestBodyConverter(arrayComponentType,
                                    methodParameterAnnotations);
                            action = new RequestAction.Part<>(headers, valueConverter).array();
                        } else {
                            Converter<?, RequestBody> valueConverter = requestBodyConverter(methodParameterType,
                                    methodParameterAnnotations);
                            action = new RequestAction.Part<>(headers, valueConverter);
                        }

                        gotPart = true;

                    } else if (methodParameterAnnotation instanceof PartMap) {
                        if (!isMultipart) {
                            throw parameterError(i,
                                    "@PartMap parameters can only be used with multipart encoding.");
                        }
                        if (!Map.class.isAssignableFrom(RestUtils.getRawType(methodParameterType))) {
                            throw parameterError(i, "@PartMap parameter type must be Map.");
                        }
                        if (!(methodParameterType instanceof ParameterizedType)) {
                            throw parameterError(i, "Map must include generic types (e.g., Map<String, String>)");
                        }
                        ParameterizedType parameterizedType = (ParameterizedType) methodParameterType;
                        Type keyType = RestUtils.getParameterUpperBound(0, parameterizedType);
                        if (String.class != keyType) {
                            throw parameterError(i, "@PartMap keys must be of type String: " + keyType);
                        }
                        Type valueType = RestUtils.getParameterUpperBound(1, parameterizedType);
                        Converter<?, RequestBody> valueConverter = requestBodyConverter(valueType,
                                methodParameterAnnotations);

                        PartMap partMap = (PartMap) methodParameterAnnotation;
                        action = new RequestAction.PartMap<>(valueConverter, partMap.encoding());
                        gotPart = true;

                    } else if (methodParameterAnnotation instanceof Body) {
                        if (isFormEncoded || isMultipart) {
                            throw parameterError(i,
                                    "@Body parameters cannot be used with form or multi-part encoding.");
                        }
                        if (gotBody) {
                            throw parameterError(i, "Multiple @Body method annotations found.");
                        }

                        Converter<?, RequestBody> converter;
                        try {
                            converter = requestBodyConverter(methodParameterType, methodParameterAnnotations);
                        } catch (RuntimeException e) { // Wide exception range because factories are user code.
                            throw parameterError(e, i, "Unable to create @Body converter for %s",
                                    methodParameterType);
                        }
                        action = new RequestAction.Body<>(converter);
                        gotBody = true;
                    }

                    if (action != null) {
                        if (requestActions[i] != null) {
                            throw parameterError(i, "Multiple Retrofit annotations found, only one allowed.");
                        }
                        requestActions[i] = action;
                    }
                }
            }

            if (requestActions[i] == null) {
                throw parameterError(i, "No Retrofit annotation found.");
            }
        }

        if (relativeUrl == null && !gotUrl) {
            throw RestUtils.methodError(method, "Missing either @%s URL or @Url parameter.", httpMethod);
        }
        if (!isFormEncoded && !isMultipart && !hasBody && gotBody) {
            throw RestUtils.methodError(method, "Non-body HTTP method cannot contain @Body.");
        }
        if (isFormEncoded && !gotField) {
            throw RestUtils.methodError(method, "Form-encoded method must contain at least one @Field.");
        }
        if (isMultipart && !gotPart) {
            throw RestUtils.methodError(method, "Multipart method must contain at least one @Part.");
        }

        this.requestActions = requestActions;
    }

    private void validatePathName(int index, String name) {
        if (!PARAM_NAME_REGEX.matcher(name).matches()) {
            throw parameterError(index, "@Path parameter name must match %s. Found: %s", PARAM_URL_REGEX.pattern(),
                    name);
        }
        // Verify URL replacement name is actually present in the URL path.
        if (!relativeUrlParamNames.contains(name)) {
            throw parameterError(index, "URL \"%s\" does not contain \"{%s}\".", relativeUrl, name);
        }
    }

    public <T> Converter<T, String> stringConverter(Type type, Annotation[] annotations) {
        RestUtils.checkNotNull(type, "type == null");
        RestUtils.checkNotNull(annotations, "annotations == null");

        Converter<?, String> converter = buildConverts.stringConverter(type, annotations);
        if (converter != null) {
            //noinspection unchecked
            return (Converter<T, String>) converter;
        }

        // Nothing matched. Resort to default converter which just calls toString().
        //noinspection unchecked
        return (Converter<T, String>) BuiltInConverters.ToStringConverter.INSTANCE;
    }

    public <T> Converter<T, RequestBody> requestBodyConverter(Type type, Annotation[] annotations) {
        RestUtils.checkNotNull(type, "type == null");
        RestUtils.checkNotNull(annotations, "annotations == null");

        Converter<?, RequestBody> converter = buildConverts.requestBodyConverter(type, annotations);
        if (converter != null) {
            //noinspection unchecked
            return (Converter<T, RequestBody>) converter;
        }

        StringBuilder builder = new StringBuilder("Could not locate RequestBody converter for ").append(type)
                .append(".");
        throw new IllegalArgumentException(builder.toString());
    }

    /**
     * Gets the set of unique path parameters used in the given URI. If a parameter is used twice
     * in the URI, it will only show up once in the set.
     */
    static Set<String> parsePathParameters(String path) {
        Matcher m = PARAM_URL_REGEX.matcher(path);
        Set<String> patterns = new LinkedHashSet<String>();
        while (m.find()) {
            patterns.add(m.group(1));
        }
        return patterns;
    }

    static Class<?> boxIfPrimitive(Class<?> type) {
        if (boolean.class == type)
            return Boolean.class;
        if (byte.class == type)
            return Byte.class;
        if (char.class == type)
            return Character.class;
        if (double.class == type)
            return Double.class;
        if (float.class == type)
            return Float.class;
        if (int.class == type)
            return Integer.class;
        if (long.class == type)
            return Long.class;
        if (short.class == type)
            return Short.class;
        return type;
    }
}