package restx.apidocs;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.google.common.base.CaseFormat;
import com.google.common.base.Optional;
import com.google.common.collect.*;
import restx.*;
import restx.admin.AdminModule;
import restx.description.*;
import restx.endpoint.Endpoint;
import restx.endpoint.EndpointParameterMapperRegistry;
import restx.factory.Component;
import restx.factory.Factory;
import restx.factory.Name;
import restx.factory.NamedComponent;
import restx.jackson.FrontObjectMapperFactory;
import restx.jackson.StdJsonProducerEntityRoute;
import restx.security.PermissionFactory;
import restx.security.RestxSecurityManager;
import javax.inject.Inject;
import javax.inject.Named;
import java.io.IOException;
import java.util.*;
import static restx.apidocs.ApiDocsIndexRoute.getRouterApiPath;
/**
* Serves the swagger api declaration of one router, which looks like that:
* <pre>
{
apiVersion: "0.2",
swaggerVersion: "1.1",
basePath: "http://petstore.swagger.wordnik.com/api",
resourcePath: "/pet.{format}"
...
apis: [...]
models: {...}
}
</pre>
*
* See <a href="https://github.com/wordnik/swagger-core/wiki/API-Declaration">API Declaration</a>
*/
@Component
public class ApiDeclarationRoute extends StdJsonProducerEntityRoute {
private final Factory factory;
private final RestxSecurityManager securityManager;
private PermissionFactory permissionFactory;
@Inject
public ApiDeclarationRoute(@Named(FrontObjectMapperFactory.WRITER_NAME) ObjectWriter writer,
Factory factory, RestxSecurityManager securityManager, PermissionFactory permissionFactory,
EndpointParameterMapperRegistry registry) {
super("ApiDeclarationRoute", Map.class, writer, Endpoint.of("GET", "/@/api-docs/{router}"), permissionFactory, registry);
this.factory = factory;
this.securityManager = securityManager;
this.permissionFactory = permissionFactory;
}
@Override
protected Optional<?> doRoute(RestxRequest restxRequest, RestxRequestMatch match, Object body) throws IOException {
securityManager.check(restxRequest, match, permissionFactory.hasRole(AdminModule.RESTX_ADMIN_ROLE));
String routerName = match.getPathParam("router");
Optional<NamedComponent<RestxRouter>> router = getRouterByName(factory, routerName);
if (!router.isPresent()) {
return Optional.absent();
}
List<ResourceDescription> apis = buildApis(router.get());
return Optional.of(ImmutableMap.builder()
.put("apiVersion", "0.1") // TODO
.put("swaggerVersion", "1.1")
.put("basePath", restxRequest.getBaseNetworkPath())
.put("name", router.get().getComponent().getClass().getName().replaceAll("Router$", ""))
.put("apis", apis)
.build());
}
static Optional<NamedComponent<RestxRouter>> getRouterByName(Factory f, String routerName) {
routerName = CaseFormat.LOWER_HYPHEN.to(CaseFormat.UPPER_CAMEL, routerName);
Optional<NamedComponent<RestxRouter>> router = Optional.absent();
ImmutableList<String> suffixes = ImmutableList.of("ResourceRouter", "", "Resource", "Router");
for (int i = 0; i < suffixes.size() && !router.isPresent(); i++) {
router = f.queryByName(Name.of(RestxRouter.class, routerName + suffixes.get(i))).optional().findOne();
}
return router;
}
private List<ResourceDescription> buildApis(NamedComponent<RestxRouter> router) {
return fillRelatedOperations(router.getName().getName(), describeAllRoutes(router.getComponent()));
}
private List<ResourceDescription> fillRelatedOperations(final String name, List<ResourceDescription> apis) {
Multimap<String, OperationReference> operationsByType = getOperationReferencesByType();
Multimap<String, OperationReference> operationsByPath = getOperationReferencesByPath();
String routerApiPath = getRouterApiPath(name);
for (final ResourceDescription api : apis) {
for (final OperationDescription operation : api.operations) {
Set<OperationReference> related = new LinkedHashSet<>(operation.relatedOperations);
// add related by type
related.addAll(operationsByType.get(getTargetType(operation.responseClass)));
Optional<OperationParameterDescription> bodyParameter = operation.findBodyParameter();
if (bodyParameter.isPresent()) {
related.addAll(operationsByType.get(getTargetType(bodyParameter.get().dataType)));
}
// add related by path
for (OperationReference operationReference : operationsByPath.get(api.path)) {
related.add(operationReference);
}
// remove reference to self
for (Iterator<OperationReference> iterator = related.iterator(); iterator.hasNext(); ) {
OperationReference operationReference = iterator.next();
if ( routerApiPath.equals(operationReference.apiDocName)
&& api.path.equals(operationReference.path)
&& operation.httpMethod.equals(operationReference.httpMethod)) {
iterator.remove();
break;
}
}
operation.relatedOperations = new ArrayList<>(related);
}
}
return apis;
}
private Multimap<String, OperationReference> getOperationReferencesByType() {
Multimap<String, OperationReference> operationsByType = ArrayListMultimap.create();
Set<NamedComponent<RestxRouter>> routers = factory.queryByClass(RestxRouter.class).find();
for (NamedComponent<RestxRouter> r : routers) {
String routerApiPath = getRouterApiPath(r.getName().getName());
for (ResourceDescription resourceDescription : describeAllRoutes(r.getComponent())) {
for (OperationDescription operation : resourceDescription.operations) {
OperationReference ref = buildReferenceFor(routerApiPath, resourceDescription.path, operation);
addIfRelevant(operationsByType, ref, operation.responseClass);
Optional<OperationParameterDescription> bodyParameter = operation.findBodyParameter();
if (bodyParameter.isPresent()) {
addIfRelevant(operationsByType, ref, bodyParameter.get().dataType);
}
}
}
}
return operationsByType;
}
private Multimap<String, OperationReference> getOperationReferencesByPath() {
Multimap<String, OperationReference> operationsByPath = ArrayListMultimap.create();
Set<NamedComponent<RestxRouter>> routers = factory.queryByClass(RestxRouter.class).find();
for (NamedComponent<RestxRouter> r : routers) {
String routerApiPath = getRouterApiPath(r.getName().getName());
for (ResourceDescription resourceDescription : describeAllRoutes(r.getComponent())) {
for (OperationDescription operation : resourceDescription.operations) {
OperationReference ref = buildReferenceFor(routerApiPath, resourceDescription.path, operation);
operationsByPath.put(resourceDescription.path, ref);
}
}
}
return operationsByPath;
}
private void addIfRelevant(Multimap<String, OperationReference> operationsByType, OperationReference ref, String dataType) {
String targetType = getTargetType(dataType);
if (!targetType.equals("void") && !targetType.equals("Status")) {
operationsByType.put(targetType, ref);
}
}
private String getTargetType(String dataType) {
return dataType == null ? "void" : dataType.replaceAll("LIST\\[(.+)\\]", "$1");
}
private OperationReference buildReferenceFor(String routerApiPath, String path, OperationDescription operation) {
OperationReference ref = new OperationReference();
ref.apiDocName = routerApiPath;
ref.path = path;
ref.httpMethod = operation.httpMethod;
ref.responseClass = operation.responseClass;
Optional<OperationParameterDescription> bodyParameter = operation.findBodyParameter();
ref.requestClass = bodyParameter.isPresent() ? bodyParameter.get().dataType : "void";
return ref;
}
private List<ResourceDescription> describeAllRoutes(RestxRouter component) {
ImmutableList<RestxRoute> routes = component.getRoutes();
List<ResourceDescription> apis = Lists.newArrayList();
for (RestxRoute route : routes) {
if (route instanceof DescribableRoute) {
DescribableRoute describableRoute = (DescribableRoute) route;
apis.addAll(describableRoute.describe());
}
}
return apis;
}
}