/** * 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. */ /** * This copy of Woodstox XML processor is licensed under the * Apache (Software) License, version 2.0 ("the License"). * See the License for details about distribution rights, and the * specific rights regarding derivate works. * * You may obtain a copy of the License at: * * http://www.apache.org/licenses/ * * A copy is also included in the downloadable source code package * containing Woodstox, in file "ASL2.0", under the same directory * as this file. */ package org.jooby.internal.spec; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Optional; import java.util.Set; import java.util.function.Consumer; import java.util.function.Function; import org.jooby.Route; import com.github.javaparser.ast.Node; import com.github.javaparser.ast.body.MethodDeclaration; import com.github.javaparser.ast.body.VariableDeclarator; import com.github.javaparser.ast.expr.ClassExpr; import com.github.javaparser.ast.expr.Expression; import com.github.javaparser.ast.expr.LambdaExpr; import com.github.javaparser.ast.expr.MethodCallExpr; import com.github.javaparser.ast.expr.ObjectCreationExpr; import com.github.javaparser.ast.type.ClassOrInterfaceType; import com.github.javaparser.ast.visitor.GenericVisitorAdapter; import com.github.javaparser.ast.visitor.VoidVisitorAdapter; import com.google.common.collect.Maps; public class RouteCollector extends VoidVisitorAdapter<Context> { private List<Map.Entry<Object, Node>> nodes = new ArrayList<>(); private boolean script; private Consumer<String> owners; private Map<String, Node> vars = new HashMap<>(); private RouteCollector(final boolean script, final Consumer<String> owners) { this.script = script; this.owners = owners; } public RouteCollector(final Consumer<String> owners) { this(true, owners); } public RouteCollector() { this(true, owner -> { }); } public List<Entry<Object, Node>> accept(final Node node, final Context ctx) { node.accept(this, ctx); return nodes; } @Override public void visit(final VariableDeclarator n, final Context ctx) { vars.put(n.getId().getName(), n.getInit()); } @Override public void visit(final MethodDeclaration m, final Context ctx) { if (!script) { boolean mvc = m.getAnnotations().stream() .map(it -> it.getName().getName()) .filter(Route.METHODS::contains) .findFirst() .isPresent(); if (mvc) { nodes.add(Maps.immutableEntry(m, m.getBody())); } } } @Override public void visit(final MethodCallExpr n, final Context ctx) { if (script) { Type mvcClass = useMvc(n, ctx); if (mvcClass != null) { mvcRoutes(n, mvcClass, ctx); } else { Type appType = importApp(n, ctx); if (appType != null) { importRoutes(appType, ctx); } else { List<MethodCallExpr> routes = routes(n, ctx); for (MethodCallExpr route : routes) { Optional<LambdaExpr> lambda = route.getArgs().stream() .map(it -> handler(it, ctx)) .filter(it -> it != null) .findFirst(); this.nodes.add(Maps.immutableEntry(route, lambda.get())); } } } } } private void importRoutes(final Type type, final Context ctx) { // compiled or parse? List<Map.Entry<Object, Node>> result = ctx.parseSpec(type).map(specs -> { List<Map.Entry<Object, Node>> nodes = new ArrayList<>(); specs.forEach(spec -> nodes.add(Maps.immutableEntry(spec, null))); return nodes; }).orElseGet(() -> ctx.parse(type) .map(unit -> new RouteCollector(true, owners).accept(unit, ctx)) .orElse(Collections.emptyList())); owners.accept(type.getTypeName()); this.nodes.addAll(result); } @SuppressWarnings("rawtypes") private void mvcRoutes(final Node n, final Type type, final Context ctx) { if (type instanceof Class) { Class sclass = ((Class) type).getSuperclass(); if (sclass != Object.class) { mvcRoutes(n, sclass, ctx); } } List<Map.Entry<Object, Node>> result = ctx.parse(type) .map(unit -> new RouteCollector(false, owners).accept(unit, ctx)) .orElse(Collections.emptyList()); owners.accept(type.getTypeName()); nodes.addAll(result); } private Type useMvc(final MethodCallExpr n, final Context ctx) { if ("use".equals(n.getName())) { List<Expression> args = n.getArgs(); if (args.size() == 1) { Expression arg = args.get(0); if (arg instanceof ClassExpr) { return arg.accept(new TypeCollector(), ctx); } } } return null; } private Type importApp(final MethodCallExpr n, final Context ctx) { Function<Expression, Type> type = expr -> { if (expr instanceof ObjectCreationExpr) { ClassOrInterfaceType t = ((ObjectCreationExpr) expr).getType(); Optional<java.lang.reflect.Type> resolved = ctx.resolveType(n, t.toStringWithoutComments()); if (resolved.isPresent()) { Type c = resolved.get(); if (isJooby(c)) { return c; } } } return null; }; if ("use".equals(n.getName())) { List<Expression> args = n.getArgs(); if (args.size() == 2) { return type.apply(args.get(1)); } if (args.size() == 1) { return type.apply(args.get(0)); } } return null; } private boolean isJooby(final Type type) { Type t = type; while (t instanceof Class) { @SuppressWarnings("rawtypes") Class c = (Class) t; if (c.getTypeName().equals("org.jooby.Jooby")) { return true; } t = c.getSuperclass(); } return false; } private List<MethodCallExpr> routes(final MethodCallExpr expr, final Context ctx) { LinkedList<MethodCallExpr> expressions = new LinkedList<>(); Expression it = expr; while (it instanceof MethodCallExpr) { MethodCallExpr local = (MethodCallExpr) it; String name = local.getName(); int n = 0; if (Route.METHODS.contains(name.toUpperCase())) { n = route(local, ctx); } else if (name.equals("use")) { n = route(local, ctx); } else if (name.equals("all") && AST.scopeOf(local).getName().equals("use")) { n = route(local, ctx); } while (n > 0) { expressions.addFirst(local); n -= 1; } it = local.getScope(); } return expressions; } private int route(final MethodCallExpr expr, final Context ctx) { List<Expression> args = expr.getArgs(); if (args.size() == 1) { if (handler(args.get(0), ctx) != null) { return 1; } } // method(path, [path1, path2], handler()); if (args.size() < 5) { // method(path, lambda) Set<Type> types = new LinkedHashSet<>(); for (int i = 0; i < args.size() - 1; i++) { types.add(args.get(i).accept(new TypeCollector(), ctx)); } boolean str = types.size() == 1 && types.contains(String.class); if (str && handler(args.get(args.size() - 1), ctx) != null) { return args.size() - 1; } } return 0; } private LambdaExpr handler(final Expression expr, final Context ctx) { Node node = vars.getOrDefault(expr.toStringWithoutComments(), expr); return node.accept(new GenericVisitorAdapter<LambdaExpr, Context>() { @Override public LambdaExpr visit(final LambdaExpr n, final Context ctx) { return n; } }, ctx); } }