/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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 org.jooby.spec; import static java.util.Objects.requireNonNull; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.lang.reflect.Type; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Optional; import java.util.Set; import org.jooby.Env; import org.jooby.Jooby; import org.jooby.Request; import org.jooby.Response; import org.jooby.Route; import org.jooby.Session; import org.jooby.internal.RouteMetadata; import org.jooby.internal.mvc.RequestParamNameProviderImpl; import org.jooby.internal.spec.AppCollector; import org.jooby.internal.spec.Context; import org.jooby.internal.spec.ContextImpl; import org.jooby.internal.spec.DocCollector; import org.jooby.internal.spec.ResponseTypeCollector; import org.jooby.internal.spec.RouteCollector; import org.jooby.internal.spec.RouteParamCollector; import org.jooby.internal.spec.RouteParamImpl; import org.jooby.internal.spec.RouteResponseImpl; import org.jooby.internal.spec.RouteSpecImpl; import org.jooby.internal.spec.SourceResolver; import org.jooby.internal.spec.SourceResolverImpl; import org.jooby.internal.spec.TypeFromDoc; import org.jooby.internal.spec.TypeResolverImpl; import org.jooby.mvc.Body; import org.jooby.mvc.Flash; import org.jooby.mvc.Header; import org.jooby.mvc.Local; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.github.javaparser.JavaParser; import com.github.javaparser.ast.CompilationUnit; import com.github.javaparser.ast.Node; import com.google.common.base.Throwables; import com.google.common.collect.Sets; import com.typesafe.config.ConfigFactory; /** * <p> * Process and collect {@link RouteSpec} from {@link Jooby} app. * </p> * * @author edgar * @since 0.15.0 */ public class RouteProcessor { /** SKIP MVC parameters of type: */ @SuppressWarnings("rawtypes") private static final Set<Class> SKIP = Sets.newHashSet(Request.class, Response.class, Session.class, Route.class, Route.Chain.class); /** The logging system. */ private final Logger log = LoggerFactory.getLogger(getClass()); /** * Process a {@link Jooby} application and collect {@link RouteSpec}. * * @param app A jooby app to process. * @return List of route specs. */ public List<RouteSpec> process(final Jooby app) { requireNonNull(app, "App is required."); return process(app, new File(System.getProperty("user.dir")).toPath()); } /** * Process a {@link Jooby} application and collect {@link RouteSpec}. * * @param app A jooby app to process. * @param srcdir Basedir where source code is located. Useful for extracting doc. * @return List of route specs. */ public List<RouteSpec> process(final Jooby app, final Path srcdir) { return processInternal(app, srcdir, null); } /** * Process a {@link Jooby} application and collect {@link RouteSpec}, but also save a compiled * version in the given outdir. * * @param app A jooby app to process. * @param srcdir Basedir where source code is located. Useful for extracting doc. * @param outdir Where to save the compiled route specs. * @return List of route specs. */ public List<RouteSpec> compile(final Jooby app, final Path srcdir, final Path outdir) { requireNonNull(app, "App is required."); requireNonNull(srcdir, "Source dir is required."); requireNonNull(outdir, "Out dir is required."); return processInternal(app, srcdir, outdir); } /** * Process a {@link Jooby} application and collect {@link RouteSpec}. * * @param appClass A jooby class to process. * @param routes Routes to process. * @return List of route specs. */ public List<RouteSpec> process(final Class<? extends Jooby> appClass, final List<Route.Definition> routes) { requireNonNull(appClass, "App class is required."); requireNonNull(routes, "Routes are required."); return processInternal(appClass, routes, new File(System.getProperty("user.dir")).toPath(), null); } /** * Process a {@link Jooby} application and collect {@link RouteSpec}. * * @param appClass A jooby class to process. * @param routes Routes to process. * @param srcdir Basedir where source code is located. Useful for extracting doc. * @return List of route specs. */ public List<RouteSpec> process(final Class<? extends Jooby> appClass, final List<Route.Definition> routes, final Path srcdir) { requireNonNull(appClass, "App class is required."); requireNonNull(routes, "Routes are required."); requireNonNull(srcdir, "Source dir is required."); return processInternal(appClass, routes, srcdir, null); } /** * Process a {@link Jooby} application and collect {@link RouteSpec}. * * @param appClass A jooby app to process. * @param routes Routes to process. * @param srcdir Basedir where source code is located. Useful for extracting doc. * @param outdir Strategy where to save the compiled route specs. * @return List of route specs. */ public List<RouteSpec> compile(final Class<? extends Jooby> appClass, final List<Route.Definition> routes, final Path srcdir, final Path outdir) { requireNonNull(appClass, "App class is required."); requireNonNull(routes, "Routes are required."); requireNonNull(srcdir, "Source dir is required."); requireNonNull(outdir, "Out dir is required."); return processInternal(appClass, routes, srcdir, outdir); } /** * Process a {@link Jooby} application and collect {@link RouteSpec}. * * @param app A jooby app to process. * @param srcdir Basedir where source code is located. Useful for extracting doc. * @param outdir Where to save the compiled route specs. * @return List of route specs. */ private List<RouteSpec> processInternal(final Jooby app, final Path srcdir, final Path outdir) { List<Route.Definition> routes = Jooby.exportRoutes(app); return processInternal(app.getClass(), routes, srcdir, outdir); } /** * Process a {@link Jooby} application and collect {@link RouteSpec}. * * @param app A jooby app to process. * @param srcdir Basedir where source code is located. Useful for extracting doc. * @param outdir Strategy where to save the compiled route specs. * @return List of route specs. */ @SuppressWarnings("unchecked") private List<RouteSpec> processInternal(final Class<? extends Jooby> appClass, final List<Route.Definition> routes, final Path srcdir, final Path outdir) { log.debug("processing {}.spec", appClass.getName()); List<RouteSpec> specs = new ArrayList<>(); try { /** * Source resolver. */ SourceResolver src = new SourceResolverImpl(srcdir); /** * Context with type resolver. */ Context ctx = new ContextImpl(new TypeResolverImpl(appClass.getClassLoader()), src); Optional<List<RouteSpec>> ifspecs = ctx.parseSpec(appClass); if (ifspecs.isPresent()) { // compiled version found. return ifspecs.get(); } /** Collect all routes and process them. */ Set<String> owners = new HashSet<>(); owners.add(appClass.getName()); List<Entry<Object, Node>> routeNodes = new ArrayList<>(); try { /** * Main AST */ CompilationUnit unit = JavaParser.parse(src.resolveSource(appClass).get(), true); /** * Find out app node. */ Node appNode = new AppCollector().accept(unit, ctx); if (appNode == null) { throw new IllegalStateException("Default constructor not found"); } routeNodes = new RouteCollector(owners::add).accept(appNode, ctx); } catch (Exception x) { // ignore source code error log.debug("source code not found", x); } int j = 0; for (int i = 0; i < routes.size(); i++) { Route.Definition route = routes.get(i); Object cursor = null; Method method = null; try { Route.Filter handler = route.filter(); // find out where the route was defined final String owner; if (handler instanceof Route.MethodHandler) { method = ((Route.MethodHandler) handler).method(); owner = method.getDeclaringClass().getName(); owners.add(owner); } else { // lambda script owner = handler.getClass().getName(); } if (owners.stream().filter(o -> owner.startsWith(o)).findFirst().isPresent()) { log.debug(" found {} {}", route.method(), route.pattern()); Entry<Object, Node> entry = routeNodes.get(j++); Object candidate = entry.getKey(); if (candidate instanceof Node) { cursor = candidate; log.debug("\n{}\n", candidate); /** doc and response codes . */ Map<String, Object> doc = new DocCollector(log).accept((Node) candidate, route.method(), ctx); Map<Integer, String> codes = (Map<Integer, String>) doc.remove("@statusCodes"); String desc = (String) doc.remove("@text"); String summary = (String) doc.remove("@summary"); String retDoc = (String) doc.remove("@return"); Type retType = (Type) doc.remove("@type"); /** params and return type */ List<RouteParam> params; RouteResponse rsp; String name = route.name(); if (method == null) { // script params params = new RouteParamCollector(doc, route.method(), route.pattern()) .accept(entry.getValue(), ctx); // script response rsp = new ResponseTypeCollector().accept(entry.getValue(), ctx, retType, retDoc, codes); } else { name = method.getName(); params = mvcParams(route, method, doc); rsp = new RouteResponseImpl( retType == null ? method.getGenericReturnType() : retType, retDoc, codes); } // test if body param is present, when present all the other params are set to query params.stream() .filter(p -> p.paramType() == RouteParamType.BODY) .findFirst() .ifPresent(p -> { params.stream().filter(it -> it != p) .forEach(it -> it.paramType(RouteParamType.QUERY)); }); // ovewrite param type if need it params.stream() .forEach(p -> { p.doc().ifPresent(pdoc -> { TypeFromDoc.parse(entry.getValue(), ctx, pdoc).ifPresent(p::type); }); }); /** Create spec . */ specs.add(new RouteSpecImpl(route, name, summary, desc, params, rsp)); } else { specs.add((RouteSpec) candidate); } } else { log.debug(" ignoring {} {} from {}", route.method(), route.pattern(), owner); } } catch (Exception ex) { if (method != null) { // MVC: String name = method.getName(); List<RouteParam> params = mvcParams(route, method, Collections.emptyMap()); RouteResponseImpl rsp = new RouteResponseImpl(method.getGenericReturnType(), null, new HashMap<>()); /** Create spec . */ specs.add(new RouteSpecImpl(route, name, null, null, params, rsp)); } else { if (cursor == null) { log.debug("ignoring {} {} no source code available", route.method(), route.pattern(), ex); log.info("ignoring {} {} no source code available", route.method(), route.pattern()); } else { log.error("ignoring {} {} reason {}", route.method(), route.pattern(), ex); } } } } } catch (Exception ex) { throw new IllegalStateException("Error while processing " + appClass, ex); } if (outdir != null) { save(outdir.resolve(appClass.getSimpleName() + ".spec"), specs); } log.debug("done"); return specs; } private void save(final Path path, final List<RouteSpec> specs) { File fout = path.toFile(); fout.getParentFile().mkdirs(); try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(fout))) { log.info(" saving {}", fout); out.writeObject(specs); } catch (IOException ex) { throw Throwables.propagate(ex); } } private List<RouteParam> mvcParams(final Route.Definition route, final Method m, final Map<String, Object> doc) { RequestParamNameProviderImpl md = new RequestParamNameProviderImpl( new RouteMetadata(Env.DEFAULT.build(ConfigFactory.empty()))); Parameter[] parameters = m.getParameters(); List<RouteParam> params = new ArrayList<>(parameters.length); for (Parameter parameter : parameters) { if (parameter.isAnnotationPresent(Flash.class) || parameter.isAnnotationPresent(Local.class)) { continue; } if (SKIP.contains(parameter.getType())) { continue; } String name = md.name(parameter); final RouteParamType paramType; if (parameter.getAnnotation(Body.class) != null) { paramType = RouteParamType.BODY; } else if (parameter.getAnnotation(Header.class) != null) { paramType = RouteParamType.HEADER; } else if (route.vars().contains(name)) { paramType = RouteParamType.PATH; } else if (route.method().equals("GET")) { paramType = RouteParamType.QUERY; } else { paramType = RouteParamType.FORM; } String pdoc = (String) doc.get(name); RouteParamImpl param = new RouteParamImpl(name, parameter.getParameterizedType(), paramType, null, pdoc); params.add(param); } return params; } }