/** * Copyright © 2015 Mercateo AG (http://www.mercateo.com) * * 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.mercateo.common.rest.schemagen.link; import static com.mercateo.common.rest.schemagen.link.helper.ParameterAnnotationVisitor.visitAnnotations; import static java.util.Objects.requireNonNull; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.lang.reflect.Type; import java.net.URI; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.stream.Stream; import javax.ws.rs.BeanParam; import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Link; import javax.ws.rs.core.Link.Builder; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.UriBuilder; import com.google.common.collect.Iterables; import com.googlecode.gentyref.GenericTypeReflector; import com.mercateo.common.rest.schemagen.JsonSchemaGenerator; import com.mercateo.common.rest.schemagen.link.relation.Relation; public class LinkCreator { public static final String TARGET_SCHEMA_PARAM_KEY = "targetSchema"; public static final String SCHEMA_PARAM_KEY = "schema"; public static final String METHOD_PARAM_KEY = "method"; private final JsonSchemaGenerator jsonSchemaGenerator; private final LinkFactoryContext linkFactoryContext; /** * @param jsonSchemaGenerator * @param linkFactoryContext * */ LinkCreator(JsonSchemaGenerator jsonSchemaGenerator, LinkFactoryContext linkFactoryContext) { this.jsonSchemaGenerator = requireNonNull(jsonSchemaGenerator); this.linkFactoryContext = linkFactoryContext; } public static Builder setRelation(Relation relation, URI uri) { requireNonNull(relation); requireNonNull(uri); Builder builder = Link.fromUri(uri).rel(relation.getName()); if (requireNonNull(relation).getType().isShouldBeSerialized()) { builder.param("relType", relation.getType().getName()); builder.param("target", relation.getType().getSerializedName()); } return builder; } /** * create a link for a resource method * * @param scopes * list of Scope objects for every scope level * @param relation * relation of method * @return link with schema if applicable */ public Link createFor(List<Scope> scopes, Relation relation) { return createFor(scopes, relation, requireNonNull(linkFactoryContext)); } /** * create a link for a resource method * * @param scopes * list of Scope objects for every scope level * @param relation * relation of method * @param linkFactoryContext * the base URI for resolution of relative URIs and method and * property checkers * @return link with schema if applicable */ public Link createFor(List<Scope> scopes, Relation relation, LinkFactoryContext linkFactoryContext) { final Class<?> resourceClass = scopes.get(0).getInvokedClass(); UriBuilder uriBuilder = UriBuilder.fromResource(resourceClass); Map<String, Object> pathParameters = new HashMap<>(); for (Scope scope : scopes) { final Method method = scope.getInvokedMethod(); final Object[] parameters = scope.getParams(); if (method.isAnnotationPresent(Path.class)) { uriBuilder.path(method.getDeclaringClass(), method.getName()); } pathParameters.putAll(collectPathParameters(scope, parameters)); setQueryParameters(uriBuilder, scope, parameters); } URI uri = mergeUri(linkFactoryContext.getBaseUri(), uriBuilder, pathParameters); Builder builder = setRelation(relation, uri); addLinkProperties(scopes, builder); detectMediaType(scopes, builder); final Scope lastScopedMethod = Iterables.getLast(scopes); addHttpMethod(builder, lastScopedMethod); addSchemaIfNeeded(builder, lastScopedMethod, linkFactoryContext); return builder.build(); } private URI mergeUri(URI baseUri, UriBuilder uriBuilder, Map<String, Object> pathParameters) { URI uri = uriBuilder.buildFromMap(pathParameters); if (baseUri != null) { UriBuilder mergedUriBuilder = UriBuilder.fromUri(baseUri); mergedUriBuilder.path(uri.getPath()); mergedUriBuilder.replaceQuery(uri.getQuery()); return mergedUriBuilder.buildFromMap(pathParameters); } return uri; } private void addLinkProperties(List<Scope> scopes, Builder builder) { final LinkProperties properties = Iterables.getLast(scopes).getInvokedMethod() .getAnnotation(LinkProperties.class); if (properties != null) { Stream.of(properties.value()).forEach(x -> builder.param(x.key(), x.value())); } } private void detectMediaType(Collection<Scope> scopes, Builder builder) { detectMediaType(Iterables.getLast(scopes).getInvokedMethod()).ifPresent(mediatype -> builder .param("mediaType", mediatype)); } private Optional<String> detectMediaType(Method method) { Produces annotation = method.getAnnotation(Produces.class); if (annotation == null) { annotation = method.getDeclaringClass().getAnnotation(Produces.class); } return Optional.ofNullable(annotation).map(produces -> { final String[] values = produces.value(); if (values.length > 0) { return values[0]; } return null; }); } private Map<String, Object> collectPathParameters(Scope scope, Object[] parameters) { final Map<String, Object> pathParameters = new HashMap<>(); visitAnnotations((parameter, parameterIndex, annotation) -> { if (annotation instanceof PathParam) { PathParam pathParamAnnotation = (PathParam) annotation; pathParameters.put(pathParamAnnotation.value(), parameter); } else if (annotation instanceof BeanParam) { BeanParamExtractor beanParamExtractor = new BeanParamExtractor(); pathParameters.putAll(beanParamExtractor.getPathParameters(parameter)); } }, scope.getInvokedMethod(), parameters); return pathParameters; } private void setQueryParameters(final UriBuilder uriBuilder, Scope scope, Object[] parameters) { Type[] realParamTypes = GenericTypeReflector.getExactParameterTypes(scope .getInvokedMethod(), scope.getInvokedClass()); visitAnnotations((parameter, parameterIndex, annotation) -> { if (annotation instanceof QueryParam && parameter != null) { final String parameterName = ((QueryParam) annotation).value(); if (parameter instanceof Iterable) { uriBuilder.queryParam(parameterName, Iterables.toArray((Iterable) parameter, Object.class)); } else { uriBuilder.queryParam(parameterName, parameter.toString()); } } else if (annotation instanceof BeanParam && parameter != null) { if (realParamTypes[parameterIndex] instanceof Class<?>) { BeanParamExtractor beanParamExtractor = new BeanParamExtractor(); Map<String, Object[]> queryParameter = beanParamExtractor.getQueryParameters( parameter); queryParameter.forEach((uriBuilder::queryParam)); } } }, scope.getInvokedMethod(), parameters); } private void addHttpMethod(Builder builder, Scope scope) { final List<Class<? extends Annotation>> httpMethodAnnotations = Arrays.asList(GET.class, POST.class, PUT.class, DELETE.class); final Method invokedMethod = scope.getInvokedMethod(); final Optional<Class<? extends Annotation>> httpMethod = httpMethodAnnotations.stream() .filter(invokedMethod::isAnnotationPresent).findFirst(); if (httpMethod.isPresent()) { builder.param(METHOD_PARAM_KEY, httpMethod.get().getSimpleName()); } else { throw new IllegalArgumentException( "LinkCreator: The method has to be annotated with one of: " + String.join(", ", (Iterable<String>) httpMethodAnnotations.stream().map( Class::getSimpleName).map(m -> '@' + m)::iterator)); } } private void addSchemaIfNeeded(Builder builder, Scope method, LinkFactoryContext linkFactoryContext) { Optional<String> optionalInputSchema = jsonSchemaGenerator.createInputSchema(method, linkFactoryContext.getFieldCheckerForSchema()); optionalInputSchema.ifPresent(s -> builder.param(SCHEMA_PARAM_KEY, s)); Optional<String> mt = detectMediaType(method.getInvokedMethod()); if (mt.isPresent() && MediaType.APPLICATION_JSON.equals(mt.get())) { Optional<String> optionalOutputSchema = jsonSchemaGenerator.createOutputSchema(method, linkFactoryContext.getFieldCheckerForSchema()); optionalOutputSchema.ifPresent(s -> builder.param(TARGET_SCHEMA_PARAM_KEY, s)); } } }