/**
* Copyright (c) 2013-2016, The SeedStack authors <http://seedstack.org>
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.seedstack.seed.rest.internal;
import com.damnhandy.uri.template.UriTemplate;
import com.damnhandy.uri.template.UriTemplateBuilder;
import org.seedstack.seed.rest.Rel;
import org.seedstack.seed.rest.RestConfig;
import org.seedstack.seed.rest.hal.Link;
import org.seedstack.seed.rest.internal.jsonhome.HintScanner;
import org.seedstack.seed.rest.internal.jsonhome.Hints;
import org.seedstack.seed.rest.internal.jsonhome.Resource;
import javax.servlet.ServletContext;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Scans the JAX-RS resources for building JSON-HOME resources and HAL links.
*/
class ResourceScanner {
private final Map<String, List<Method>> resourceByRel = new HashMap<>();
private final Map<String, Resource> jsonHomeResources = new HashMap<>();
private final Map<String, Link> halLinks = new HashMap<>();
private final RestConfig restConfig;
private final String servletContextPath;
/**
* Constructor.
*
* @param restConfig the REST configuration object.
* @param servletContext the servlet context
*/
ResourceScanner(RestConfig restConfig, ServletContext servletContext) {
this.restConfig = restConfig;
this.servletContextPath = servletContext == null ? "" : servletContext.getContextPath();
}
/**
* Scans a collection of resources.
*
* @param classes the resource to scan
* @return itself
*/
ResourceScanner scan(final Collection<Class<?>> classes) {
for (Class<?> aClass : classes) {
collectHttpMethodsWithRel(aClass);
}
buildJsonHomeResources();
buildHalLink();
return this;
}
private void collectHttpMethodsWithRel(Class<?> aClass) {
for (Method method : aClass.getDeclaredMethods()) {
if (RelSpecification.INSTANCE.isSatisfiedBy(method)) {
Rel relAnnotation = RESTReflect.findRel(method);
if (relAnnotation == null || "".equals(relAnnotation.value())) {
throw new IllegalStateException("Missing rel value on " + method.toGenericString());
}
registerMethod(relAnnotation.value(), method);
}
}
}
private void registerMethod(String rel, Method method) {
List<Method> methods = resourceByRel.get(rel);
if (methods == null) {
methods = new ArrayList<>();
}
methods.add(method);
resourceByRel.put(rel, methods);
}
/**
* Returns the JSON-HOME resources.
*
* @return resource map
*/
Map<String, Resource> jsonHomeResources() {
return jsonHomeResources;
}
/**
* Returns the HAL links.
*
* @return the link map
*/
Map<String, Link> halLinks() {
return halLinks;
}
private void buildJsonHomeResources() {
for (Map.Entry<String, List<Method>> entry : resourceByRel.entrySet()) {
// Extends the rel with baseRel
String rel = entry.getKey();
String absoluteRel = UriBuilder.uri(restConfig.getBaseRel(), rel);
Resource resource = null;
List<Method> methods = entry.getValue();
for (Method method : methods) {
Resource currentResource = buildJsonHomeResource(restConfig.getBaseParam(), absoluteRel, method);
if (resource == null) {
resource = currentResource;
} else {
resource.merge(currentResource);
}
}
if (resource != null) {
jsonHomeResources.put(absoluteRel, resource);
}
}
}
private Resource buildJsonHomeResource(String baseParam, String rel, Method method) {
Resource currentResource = null;
if (JsonHomeSpecification.INSTANCE.isSatisfiedBy(method)) {
String path = RESTReflect.findPath(method);
if (path == null) {
return null;
}
Hints hints = new HintScanner().findHint(method);
String absolutePath = UriBuilder.uri(servletContextPath, restConfig.getPath(), path);
if (isTemplated(absolutePath)) {
Map<String, String> pathParams = RESTReflect.findPathParams(baseParam, method);
Map<String, String> queryParams = RESTReflect.findQueryParams(baseParam, method);
currentResource = new Resource(rel, absolutePath, pathParams, queryParams, hints);
} else {
currentResource = new Resource(rel, absolutePath, hints);
}
}
return currentResource;
}
private boolean isTemplated(String path) {
return UriTemplate.fromTemplate(path).expressionCount() > 0;
}
private void buildHalLink() {
for (Map.Entry<String, List<Method>> entry : resourceByRel.entrySet()) {
String rel = entry.getKey();
List<Method> methodsByRel = entry.getValue();
String path = RESTReflect.findPath(methodsByRel.get(0));
if (path == null) {
throw new IllegalStateException("Path not found for rel: " + rel);
}
UriTemplateBuilder uriTemplateBuilder = UriTemplate.buildFromTemplate(path);
Set<String> queryParams = findAllQueryParamsForRel(methodsByRel);
if (!queryParams.isEmpty()) {
uriTemplateBuilder.query(queryParams.toArray(new String[queryParams.size()]));
}
String absolutePath = UriBuilder.uri(servletContextPath, restConfig.getPath(), uriTemplateBuilder.build().getTemplate());
halLinks.put(rel, new Link(absolutePath));
}
}
private Set<String> findAllQueryParamsForRel(List<Method> methodsByRel) {
// A resource correspond to one URI but one URI can correspond to multiple method
// so we have to look on all the methods to find the query parameters
Set<String> queryParams = new HashSet<>();
for (Method method : methodsByRel) {
queryParams.addAll(RESTReflect.findQueryParams("", method).keySet());
}
return queryParams;
}
}