org.jooby.internal.apitool.BytecodeRouteParser.java Source code

Java tutorial

Introduction

Here is the source code for org.jooby.internal.apitool.BytecodeRouteParser.java

Source

/**
 * Apache License
 * Version 2.0, January 2004
 * http://www.apache.org/licenses/
 *
 * TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
 *
 * 1. Definitions.
 *
 * "License" shall mean the terms and conditions for use, reproduction,
 * and distribution as defined by Sections 1 through 9 of this document.
 *
 * "Licensor" shall mean the copyright owner or entity authorized by
 * the copyright owner that is granting the License.
 *
 * "Legal Entity" shall mean the union of the acting entity and all
 * other entities that control, are controlled by, or are under common
 * control with that entity. For the purposes of this definition,
 * "control" means (i) the power, direct or indirect, to cause the
 * direction or management of such entity, whether by contract or
 * otherwise, or (ii) ownership of fifty percent (50%) or more of the
 * outstanding shares, or (iii) beneficial ownership of such entity.
 *
 * "You" (or "Your") shall mean an individual or Legal Entity
 * exercising permissions granted by this License.
 *
 * "Source" form shall mean the preferred form for making modifications,
 * including but not limited to software source code, documentation
 * source, and configuration files.
 *
 * "Object" form shall mean any form resulting from mechanical
 * transformation or translation of a Source form, including but
 * not limited to compiled object code, generated documentation,
 * and conversions to other media types.
 *
 * "Work" shall mean the work of authorship, whether in Source or
 * Object form, made available under the License, as indicated by a
 * copyright notice that is included in or attached to the work
 * (an example is provided in the Appendix below).
 *
 * "Derivative Works" shall mean any work, whether in Source or Object
 * form, that is based on (or derived from) the Work and for which the
 * editorial revisions, annotations, elaborations, or other modifications
 * represent, as a whole, an original work of authorship. For the purposes
 * of this License, Derivative Works shall not include works that remain
 * separable from, or merely link (or bind by name) to the interfaces of,
 * the Work and Derivative Works thereof.
 *
 * "Contribution" shall mean any work of authorship, including
 * the original version of the Work and any modifications or additions
 * to that Work or Derivative Works thereof, that is intentionally
 * submitted to Licensor for inclusion in the Work by the copyright owner
 * or by an individual or Legal Entity authorized to submit on behalf of
 * the copyright owner. For the purposes of this definition, "submitted"
 * means any form of electronic, verbal, or written communication sent
 * to the Licensor or its representatives, including but not limited to
 * communication on electronic mailing lists, source code control systems,
 * and issue tracking systems that are managed by, or on behalf of, the
 * Licensor for the purpose of discussing and improving the Work, but
 * excluding communication that is conspicuously marked or otherwise
 * designated in writing by the copyright owner as "Not a Contribution."
 *
 * "Contributor" shall mean Licensor and any individual or Legal Entity
 * on behalf of whom a Contribution has been received by Licensor and
 * subsequently incorporated within the Work.
 *
 * 2. Grant of Copyright License. Subject to the terms and conditions of
 * this License, each Contributor hereby grants to You a perpetual,
 * worldwide, non-exclusive, no-charge, royalty-free, irrevocable
 * copyright license to reproduce, prepare Derivative Works of,
 * publicly display, publicly perform, sublicense, and distribute the
 * Work and such Derivative Works in Source or Object form.
 *
 * 3. Grant of Patent License. Subject to the terms and conditions of
 * this License, each Contributor hereby grants to You a perpetual,
 * worldwide, non-exclusive, no-charge, royalty-free, irrevocable
 * (except as stated in this section) patent license to make, have made,
 * use, offer to sell, sell, import, and otherwise transfer the Work,
 * where such license applies only to those patent claims licensable
 * by such Contributor that are necessarily infringed by their
 * Contribution(s) alone or by combination of their Contribution(s)
 * with the Work to which such Contribution(s) was submitted. If You
 * institute patent litigation against any entity (including a
 * cross-claim or counterclaim in a lawsuit) alleging that the Work
 * or a Contribution incorporated within the Work constitutes direct
 * or contributory patent infringement, then any patent licenses
 * granted to You under this License for that Work shall terminate
 * as of the date such litigation is filed.
 *
 * 4. Redistribution. You may reproduce and distribute copies of the
 * Work or Derivative Works thereof in any medium, with or without
 * modifications, and in Source or Object form, provided that You
 * meet the following conditions:
 *
 * (a) You must give any other recipients of the Work or
 * Derivative Works a copy of this License; and
 *
 * (b) You must cause any modified files to carry prominent notices
 * stating that You changed the files; and
 *
 * (c) You must retain, in the Source form of any Derivative Works
 * that You distribute, all copyright, patent, trademark, and
 * attribution notices from the Source form of the Work,
 * excluding those notices that do not pertain to any part of
 * the Derivative Works; and
 *
 * (d) If the Work includes a "NOTICE" text file as part of its
 * distribution, then any Derivative Works that You distribute must
 * include a readable copy of the attribution notices contained
 * within such NOTICE file, excluding those notices that do not
 * pertain to any part of the Derivative Works, in at least one
 * of the following places: within a NOTICE text file distributed
 * as part of the Derivative Works; within the Source form or
 * documentation, if provided along with the Derivative Works; or,
 * within a display generated by the Derivative Works, if and
 * wherever such third-party notices normally appear. The contents
 * of the NOTICE file are for informational purposes only and
 * do not modify the License. You may add Your own attribution
 * notices within Derivative Works that You distribute, alongside
 * or as an addendum to the NOTICE text from the Work, provided
 * that such additional attribution notices cannot be construed
 * as modifying the License.
 *
 * You may add Your own copyright statement to Your modifications and
 * may provide additional or different license terms and conditions
 * for use, reproduction, or distribution of Your modifications, or
 * for any such Derivative Works as a whole, provided Your use,
 * reproduction, and distribution of the Work otherwise complies with
 * the conditions stated in this License.
 *
 * 5. Submission of Contributions. Unless You explicitly state otherwise,
 * any Contribution intentionally submitted for inclusion in the Work
 * by You to the Licensor shall be under the terms and conditions of
 * this License, without any additional terms or conditions.
 * Notwithstanding the above, nothing herein shall supersede or modify
 * the terms of any separate license agreement you may have executed
 * with Licensor regarding such Contributions.
 *
 * 6. Trademarks. This License does not grant permission to use the trade
 * names, trademarks, service marks, or product names of the Licensor,
 * except as required for reasonable and customary use in describing the
 * origin of the Work and reproducing the content of the NOTICE file.
 *
 * 7. Disclaimer of Warranty. Unless required by applicable law or
 * agreed to in writing, Licensor provides the Work (and each
 * Contributor provides its Contributions) on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
 * implied, including, without limitation, any warranties or conditions
 * of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
 * PARTICULAR PURPOSE. You are solely responsible for determining the
 * appropriateness of using or redistributing the Work and assume any
 * risks associated with Your exercise of permissions under this License.
 *
 * 8. Limitation of Liability. In no event and under no legal theory,
 * whether in tort (including negligence), contract, or otherwise,
 * unless required by applicable law (such as deliberate and grossly
 * negligent acts) or agreed to in writing, shall any Contributor be
 * liable to You for damages, including any direct, indirect, special,
 * incidental, or consequential damages of any character arising as a
 * result of this License or out of the use or inability to use the
 * Work (including but not limited to damages for loss of goodwill,
 * work stoppage, computer failure or malfunction, or any and all
 * other commercial damages or losses), even if such Contributor
 * has been advised of the possibility of such damages.
 *
 * 9. Accepting Warranty or Additional Liability. While redistributing
 * the Work or Derivative Works thereof, You may choose to offer,
 * and charge a fee for, acceptance of support, warranty, indemnity,
 * or other liability obligations and/or rights consistent with this
 * License. However, in accepting such obligations, You may act only
 * on Your own behalf and on Your sole responsibility, not on behalf
 * of any other Contributor, and only if You agree to indemnify,
 * defend, and hold each Contributor harmless for any liability
 * incurred by, or claims asserted against, such Contributor by reason
 * of your accepting any such warranty or additional liability.
 *
 * END OF TERMS AND CONDITIONS
 *
 * APPENDIX: How to apply the Apache License to your work.
 *
 * To apply the Apache License to your work, attach the following
 * boilerplate notice, with the fields enclosed by brackets "{}"
 * replaced with your own identifying information. (Don't include
 * the brackets!)  The text should be enclosed in the appropriate
 * comment syntax for the file format. We also recommend that a
 * file or class name and description of purpose be included on the
 * same "printed page" as the copyright notice for easier
 * identification within third-party archives.
 *
 * Copyright 2014 Edgar Espina
 *
 * 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 org.jooby.internal.apitool;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.io.ByteStreams;
import com.google.common.primitives.Primitives;
import com.google.inject.internal.MoreTypes;
import com.google.inject.util.Types;
import com.typesafe.config.ConfigFactory;
import com.typesafe.config.ConfigValueFactory;
import org.jooby.Env;
import org.jooby.Jooby;
import org.jooby.Request;
import org.jooby.Response;
import org.jooby.Result;
import org.jooby.Route;
import org.jooby.Session;
import org.jooby.Status;
import org.jooby.Upload;
import org.jooby.apitool.RouteMethod;
import org.jooby.apitool.RouteParameter;
import org.jooby.apitool.RouteResponse;
import org.jooby.funzy.Try;
import static org.jooby.funzy.When.when;
import org.jooby.internal.RouteMetadata;
import static org.jooby.internal.apitool.Filters.access;
import static org.jooby.internal.apitool.Filters.and;
import static org.jooby.internal.apitool.Filters.call;
import static org.jooby.internal.apitool.Filters.file;
import static org.jooby.internal.apitool.Filters.getOrCreateKotlinClass;
import static org.jooby.internal.apitool.Filters.is;
import static org.jooby.internal.apitool.Filters.joobyRun;
import static org.jooby.internal.apitool.Filters.kotlinRouteHandler;
import static org.jooby.internal.apitool.Filters.method;
import static org.jooby.internal.apitool.Filters.methodName;
import static org.jooby.internal.apitool.Filters.mount;
import static org.jooby.internal.apitool.Filters.mutantToSomething;
import static org.jooby.internal.apitool.Filters.mutantValue;
import static org.jooby.internal.apitool.Filters.opcode;
import static org.jooby.internal.apitool.Filters.param;
import static org.jooby.internal.apitool.Filters.path;
import static org.jooby.internal.apitool.Filters.scriptRoute;
import static org.jooby.internal.apitool.Filters.use;
import org.jooby.internal.mvc.MvcRoutes;
import org.jooby.mvc.Body;
import org.jooby.mvc.Flash;
import org.jooby.mvc.Header;
import org.jooby.mvc.Local;
import org.jooby.mvc.POST;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.Handle;
import org.objectweb.asm.Opcodes;
import static org.objectweb.asm.Opcodes.GETSTATIC;
import static org.objectweb.asm.Opcodes.INVOKESPECIAL;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldInsnNode;
import org.objectweb.asm.tree.InnerClassNode;
import org.objectweb.asm.tree.InsnNode;
import org.objectweb.asm.tree.IntInsnNode;
import org.objectweb.asm.tree.InvokeDynamicInsnNode;
import org.objectweb.asm.tree.LdcInsnNode;
import org.objectweb.asm.tree.LocalVariableNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.TypeInsnNode;
import org.objectweb.asm.tree.VarInsnNode;
import org.objectweb.asm.util.ASMifier;
import org.objectweb.asm.util.TraceClassVisitor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.inject.Named;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.Writer;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;

public class BytecodeRouteParser {

    static final Set<Class<?>> SKIP_TYPES = ImmutableSet.of(Route.Chain.class, Response.class, Request.class,
            Session.class, Route.class, Flash.class, Local.class);
    static final Predicate<Parameter> TYPE_TO_SKIP = p -> !SKIP_TYPES.contains(p.getType());
    static final Predicate<Parameter> ANNOTATION_TO_SKIP = p -> !Arrays.asList(p.getAnnotations()).stream()
            .filter(a -> SKIP_TYPES.contains(a.annotationType())).findFirst().isPresent();
    static final Predicate<Parameter> SKIP = TYPE_TO_SKIP.and(ANNOTATION_TO_SKIP);

    private static final ObjectMapper mapper = new ObjectMapper();
    private static final String OBJECT = Type.getInternalName(Object.class);
    private static final String RETURN_OBJ = "L" + OBJECT + ";";

    static {
        mapper.setVisibility(mapper.getVisibilityChecker().withFieldVisibility(JsonAutoDetect.Visibility.ANY)
                .withGetterVisibility(JsonAutoDetect.Visibility.NONE)
                .withSetterVisibility(JsonAutoDetect.Visibility.NONE));
        mapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
        SimpleModule module = new SimpleModule();
        module.addSerializer(java.lang.reflect.Type.class, new TypeJsonSerializer());
        module.addDeserializer(java.lang.reflect.Type.class, new TypeJsonDeserializer());
        mapper.registerModule(module);
    }

    /**
     * The logging system.
     */
    private final Logger log = LoggerFactory.getLogger(getClass());

    private final Map<String, ClassNode> cache = new HashMap<>();

    private final DocParser javadoc;

    private final Predicate<MethodInsnNode> scriptRoute;

    private final ClassLoader loader;

    public BytecodeRouteParser(ClassLoader loader, Path dir) {
        javadoc = new DocParser(dir);
        this.loader = loader;
        this.scriptRoute = scriptRoute(loader);
    }

    public List<RouteMethod> read(String classname) {
        String filename = "/" + classname.replace(".", "/") + ".json";
        URL resource = getClass().getResource(filename);
        if (resource != null) {
            try {
                return mapper.readValue(resource,
                        mapper.getTypeFactory().constructCollectionType(ArrayList.class, RouteMethod.class));
            } catch (IOException e) {
                log.error("read of {} resulted in exception", filename, e);
            }
        }
        return null;
    }

    private Path write(Path output, List<RouteMethod> routes) throws IOException {
        Files.createDirectories(output.getParent());
        log.debug("writing {} with {}", output, routes);
        mapper.writer().withDefaultPrettyPrinter().writeValue(output.toFile(), routes);
        return output;
    }

    private Path toFile(Path dir, String classname) {
        Path path = Arrays.asList(classname.split("\\.")).stream().reduce(dir, Path::resolve, Path::resolve);
        ClassNode classNode = loadClass(classname);
        return path.getParent().resolve(classNode.sourceFile.replaceAll("\\.java|\\.kt", "") + ".json");
    }

    public Path export(Path dir, String classname) throws Exception {
        Path output = toFile(dir, classname);
        Files.deleteIfExists(output);
        List<RouteMethod> routes = parse(classname);
        return write(output, routes);
    }

    public List<RouteMethod> parse(String classname) throws Exception {
        ClassNode owner = loadClass(classname);
        List<Object> lambdas = bindMethods(owner, lambdas(loader, owner));

        List<RouteMethod> methods = new ArrayList<>();
        for (Object it : lambdas) {
            log.debug("found: {}", it);
            if (it instanceof RouteMethod) {
                methods.add((RouteMethod) it);
            } else {
                Lambda lambda = (Lambda) it;
                if (lambda.method.isPresent()) {
                    MethodNode method = lambda.method.get();

                    java.lang.reflect.Type returnType;
                    if (method.desc.endsWith("V")) {
                        returnType = void.class;
                    } else if (method.desc.endsWith(RETURN_OBJ)) {
                        returnType = returnType(loader, method);
                    } else {
                        returnType = TypeDescriptorParser.parse(loader,
                                Optional.ofNullable(method.signature).orElse(method.desc));
                    }
                    Integer status;
                    if (returnType instanceof TypeWithStatus) {
                        status = ((TypeWithStatus) returnType).status;
                        returnType = ((TypeWithStatus) returnType).type();
                    } else {
                        status = null;
                    }
                    List<RouteParameter> parameters = params(loader, owner, lambda.pattern, method);
                    RouteResponse routeResponse = new RouteResponse(simplifyType(returnType));
                    if (status != null) {
                        routeResponse.status(
                                ImmutableMap.of(status, Try.apply(() -> Status.valueOf(status.intValue()).reason())
                                        .orElse(status.toString())));
                    }
                    RouteMethod route = new RouteMethod(lambda.name, lambda.pattern, routeResponse)
                            .parameters(parameters);
                    if (lambda.tag != null) {
                        route.attribute("route.tag", scriptRouteTag(lambda.tag));
                    }
                    javadoc(route, javadoc.pop(lambda.declaringClass, lambda.name, lambda.pattern));
                    methods.add(route);
                } else {
                    log.debug("can't bind implementation for {} at {}", lambda, lambda.owner);
                }
            }
        }
        return typeAnalizer(methods);
    }

    private String scriptRouteTag(String tag) {
        String value = Stream.of(tag.split("/")).filter(it -> it.length() > 0)
                .map(it -> Character.toUpperCase(it.charAt(0)) + it.substring(1)).collect(Collectors.joining());
        return value;
    }

    private List<RouteMethod> typeAnalizer(List<RouteMethod> methods) {
        methods.forEach(this::typeAnalizer);
        return methods;
    }

    private void typeAnalizer(RouteMethod route) {
        route.parameters().forEach(p -> p.type());
    }

    private RouteMethod javadoc(final RouteMethod method, final Optional<DocItem> doc) {
        doc.ifPresent(it -> {
            method.description(it.text);
            method.summary(it.summary);
            method.response().description(it.returns);
            method.response().status(it.statusCodes);
            method.parameters().forEach(p -> {
                p.description(it.parameters.get(p.name()));
            });
        });
        return method;
    }

    @SuppressWarnings("unchecked")
    private List<Object> bindMethods(final ClassNode owner, final List<Object> lambdas) {
        // Find target bindMethods for each lambda expression
        List<MethodNode> methods = owner.methods;
        return lambdas.stream().flatMap(e -> {
            if (e instanceof RouteMethod) {
                return Stream.of(e);
            }
            Lambda it = (Lambda) e;
            if (it.method.isPresent()) {
                return Stream.of(it);
            }
            return methods.stream()
                    .filter(m -> owner.name.equals(it.owner) && it.implementationName.equals(m.name)
                            && it.desc.equals(m.desc))
                    .findFirst().map(m -> Stream.of((Object) it.method(m))).orElseGet(() -> {
                        ClassNode jump = loadClass(it.owner);
                        List<Object> ext = bindMethods(jump, Arrays.asList(it));
                        return ext.stream();
                    });
        }).collect(Collectors.toList());
    }

    private List<Object> lambdas(final ClassLoader loader, final ClassNode owner, MethodNode method) {
        List<Object> result = new ArrayList<>();
        new Insns(method)
                // get(pattern, ); or post, put, etc...
                .on(InvokeDynamicInsnNode.class, it -> {
                    log.debug("found candidate: {}", it);
                    it.next().filter(is(MethodInsnNode.class)).map(MethodInsnNode.class::cast).filter(scriptRoute)
                            .findFirst().ifPresent(m -> {
                                log.debug("found script route: {}.{}{}", m.owner, m.name, m.desc);
                                Lambda.create(loader, scriptRoute, owner.name.replace("/", "."), it.node, null)
                                        .forEach(result::add);
                            });
                })
                // path(String)
                .on(path(loader, owner.name), it -> {
                    log.debug("found path: " + it);
                    result.addAll(pathOperator(owner, owner.methods, it));
                })
                // use(Mvc class)
                .on(use(loader, owner.name), it -> mvc(it, result::add))
                // use(new App());
                .on(mount(loader, owner.name), it -> {
                    log.debug("found mount: " + it);
                    it.prev().filter(and(is(MethodInsnNode.class), opcode(INVOKESPECIAL))).findFirst()
                            .map(MethodInsnNode.class::cast).ifPresent(node -> {
                                List<Lambda> lambdas = lambdas(loader, loadClass(node.owner)).stream()
                                        .filter(Lambda.class::isInstance).map(Lambda.class::cast)
                                        .collect(Collectors.toList());
                                Insn.ldcFor(node).stream().map(e -> e.cst.toString()).findFirst()
                                        .ifPresent(prefix -> {
                                            IntStream.range(0, lambdas.size())
                                                    .forEach(i -> lambdas.set(i, lambdas.get(i).prefix(prefix)));
                                        });
                                result.addAll(lambdas);
                            });
                }).forEach();
        log.debug("results: {}", result);
        return result;
    }

    private List<Lambda> pathOperator(ClassNode owner, List<MethodNode> methods, Insn<MethodInsnNode> it) {
        List<Lambda> result = new ArrayList<>();
        Insn.ldcFor(it.node).stream().map(e -> e.cst.toString()).findFirst().ifPresent(path -> {
            it.prev().filter(is(InvokeDynamicInsnNode.class)).findFirst().map(InvokeDynamicInsnNode.class::cast)
                    .ifPresent(n -> {
                        Arrays.asList(n.bsmArgs).stream().filter(Handle.class::isInstance).findFirst()
                                .map(Handle.class::cast).ifPresent(handle -> {
                                    methods.stream().filter(m -> m.name.equals(handle.getName())).findFirst()
                                            .ifPresent(pathAction -> {
                                                log.debug("pathAction {}", path);
                                                lambdas(loader, owner, pathAction).stream()
                                                        .filter(Lambda.class::isInstance).map(Lambda.class::cast)
                                                        .forEach(lambda -> {
                                                            result.add(lambda.prefix(path).tag(path));
                                                        });
                                            });

                                });
                    });
        });
        log.debug("pathOperator: {}", result);
        return result;
    }

    @SuppressWarnings("unchecked")
    private List<Object> lambdas(final ClassLoader loader, final ClassNode owner) {
        List compiled = read(owner.name);
        if (compiled != null) {
            return compiled;
        }

        if (owner.sourceFile.endsWith(".kt")) {
            return kotlinSource(loader, owner);
        } else {
            List<MethodNode> methods = owner.methods;
            List<Object> handles = methods.stream().filter(access(Opcodes.ACC_SYNTHETIC).negate())
                    .flatMap(method -> lambdas(loader, owner, method).stream()).collect(Collectors.toList());
            return handles;
        }
    }

    private void mvcRoutes(String path, final Class type, Consumer<RouteMethod> callback) {
        Env env = Env.DEFAULT
                .build(ConfigFactory.empty().withValue("application.env", ConfigValueFactory.fromAnyRef("dev")));
        MvcRoutes.routes(env, new RouteMetadata(env), "", true, type).forEach(r -> {
            RouteMethod method = toRouteMethod(r);
            javadoc(method, javadoc.pop(type.getName(), r.method(), r.pattern()));
            if (path.length() > 0) {
                method.pattern(Route.normalize(path) + method.pattern());
            }
            // Set default tag
            Annotation rootPath = type.getAnnotation(org.jooby.mvc.Path.class);
            if (rootPath != null) {
                method.attribute("route.tag", mvcRouteTag(type.getSimpleName()));
            }
            callback.accept(method);
        });
    }

    private String mvcRouteTag(String name) {
        /** Replace commons class suffix for Mvc classes: */
        return name.replace("Controller", "").replace("Manager", "").replace("Api", "").replace("API", "")
                .replace("Mvc", "").replace("MVC", "");
    }

    @SuppressWarnings("unchecked")
    private List<Object> kotlinSource(final ClassLoader loader, final ClassNode owner) {
        List<Object> result = kotlinLambdas(loader, owner);

        if (result.size() == 0) {
            // Try main
            List<MethodNode> methods = owner.methods;
            methods.stream().filter(method("main", String.class.getName() + "[]")).findFirst().ifPresent(main -> {
                log.debug("found main method: {}.main", owner.name);
                new Insns(main).on(joobyRun(loader), n -> {
                    log.debug("found run(::Type, *args)");
                    n.prev().filter(and(is(FieldInsnNode.class), opcode(GETSTATIC))).findFirst()
                            .map(FieldInsnNode.class::cast).ifPresent(f -> {
                                ClassNode mainOwner = loadClass(f.owner);
                                log.debug("found ::{}", mainOwner.name);
                                mainOwner.methods.stream().filter(kotlinRouteHandler()).findFirst().ifPresent(m -> {
                                    log.debug("{}.invoke({})", mainOwner.name, ((MethodNode) m).desc);
                                    new Insns((MethodNode) m)
                                            .on(is(TypeInsnNode.class).and(opcode(Opcodes.NEW)), it -> {
                                                ClassNode lambda = loadClass(it.node.desc);
                                                log.debug("source {}", lambda.name);
                                                result.addAll(kotlinLambdas(loader, lambda));
                                            }).forEach();
                                });
                            });
                }).forEach();
            });
        }
        return result;
    }

    @SuppressWarnings("unchecked")
    private List<Object> kotlinLambdas(final ClassLoader loader, final ClassNode owner) {
        List<Object> result = new ArrayList<>();
        List<InnerClassNode> innerClasses = owner.innerClasses;
        for (InnerClassNode innerClass : innerClasses) {
            ClassNode innerNode = loadClass(innerClass.name);
            result.addAll(kotlinLambda(loader, innerNode));
        }

        return result;
    }

    private List<Object> kotlinLambda(final ClassLoader loader, final ClassNode owner, MethodNode method) {
        List<Object> result = new ArrayList<>();
        new Insns(method).on(scriptRoute, it -> {
            log.debug("  lambda candidate: {}", it.node);
            it.prev().filter(and(is(FieldInsnNode.class), opcode(GETSTATIC))).findFirst()
                    .map(FieldInsnNode.class::cast).ifPresent(field -> {
                        ClassNode lambda = loadClass(field.owner);
                        log.debug("  lambda: {}", field.owner);
                        lambda.methods.stream().filter(kotlinRouteHandler()).forEach(e -> {
                            MethodNode m = (MethodNode) e;
                            log.debug("    implementation: {}.{}()", lambda.name, m.name, m.desc);
                            Lambda.create(field.owner, Optional.empty(), it.node, m).forEach(result::add);
                        });
                    });
        })
                // use(Mvc class)
                .on(use(loader, "org.jooby.Kooby"), it -> mvc(it, result::add))
                // route ("...") {...}
                .on(call(loader, "org.jooby.Kooby", "route", String.class, "kotlin.jvm.functions.Function1"),
                        it -> {
                            Optional<String> prefix = Insn.ldcFor(it.node).stream().map(e -> e.cst.toString())
                                    .findFirst();
                            it.prev().filter(and(is(FieldInsnNode.class), opcode(GETSTATIC))).findFirst()
                                    .map(FieldInsnNode.class::cast).ifPresent(field -> {
                                        ClassNode lambda = loadClass(field.owner);
                                        lambda.methods.stream().filter(method("invoke", "org.jooby.KRouteGroup"))
                                                .forEach(m -> {
                                                    new Insns((MethodNode) m).on(scriptRoute, n -> {
                                                        n.prev().filter(
                                                                and(is(FieldInsnNode.class), opcode(GETSTATIC)))
                                                                .findFirst().map(FieldInsnNode.class::cast)
                                                                .ifPresent(f -> {
                                                                    ClassNode route = loadClass(f.owner);
                                                                    route.methods.stream()
                                                                            .filter(kotlinRouteHandler())
                                                                            .forEach(e -> Lambda
                                                                                    .create(f.owner, prefix, n.node,
                                                                                            (MethodNode) e)
                                                                                    .forEach(result::add));
                                                                });
                                                    }).forEach();
                                                });
                                    });
                        })
                // use(Jooby())
                .on(mount(loader, Jooby.class.getName()), it -> {
                    it.prev().filter(and(is(MethodInsnNode.class), opcode(INVOKESPECIAL))).findFirst()
                            .map(MethodInsnNode.class::cast)
                            .ifPresent(n -> result.addAll(lambdas(loader, loadClass(n.owner))));
                })
                // path(String) {...}
                .on(path(loader, "org.jooby.Kooby"), it -> {
                    result.addAll(kotlinPathOperator(owner, owner.methods, it));
                }).forEach();
        return result;
    }

    private void mvc(Insn<MethodInsnNode> it, Consumer<Object> consumer) {
        log.debug("found mvc {}", it);
        it.prev().filter(is(LdcInsnNode.class)).findFirst().map(LdcInsnNode.class::cast)
                .filter(ldc -> ldc.cst instanceof Type).ifPresent(ldc -> {
                    String arg0 = Type.getArgumentTypes(it.node.desc)[0].getClassName();
                    String prefix = "";
                    if (arg0.equals(String.class.getName())) {
                        prefix = new Insn<>(it.method, ldc.getPrevious()).prev().filter(is(LdcInsnNode.class))
                                .findFirst().map(LdcInsnNode.class::cast).map(n -> n.cst.toString()).orElse("");
                    }
                    String mvcClass = ((Type) ldc.cst).getClassName();
                    mvcRoutes(prefix, (Class) loadType(loader, mvcClass), consumer::accept);
                });
    }

    private List<Object> kotlinLambda(final ClassLoader loader, final ClassNode owner) {
        List<Object> result = new ArrayList<>();
        log.debug("visiting lambda class: {}", owner.name);
        List<MethodNode> methods = owner.methods;
        methods.stream().filter(method("invoke", "org.jooby.Jooby").or(method("invoke", "org.jooby.Kooby")))
                .findFirst().ifPresent(method -> {
                    log.debug("  invoke: {}", method.desc);
                    result.addAll(kotlinLambda(loader, owner, method));
                });
        return result;
    }

    private List<Lambda> kotlinPathOperator(ClassNode owner, List<MethodNode> methods, Insn<MethodInsnNode> it) {
        List<Lambda> result = new ArrayList<>();
        it.prev().filter(and(is(MethodInsnNode.class), opcode(INVOKESPECIAL))).findFirst()
                .map(MethodInsnNode.class::cast).ifPresent(node -> {
                    Insn.ldcFor(node).stream().map(e -> e.cst.toString()).findFirst().ifPresent(path -> {
                        it.prev().filter(and(is(MethodInsnNode.class), opcode(INVOKESPECIAL))).findFirst()
                                .map(MethodInsnNode.class::cast).ifPresent(n -> {
                                    ClassNode classNode = loadClass(n.owner);
                                    classNode.methods.stream().map(MethodNode.class::cast).filter(methodName("run"))
                                            .findFirst().ifPresent(runMethod -> {
                                                kotlinLambda(loader, owner, (MethodNode) runMethod).stream()
                                                        .filter(Lambda.class::isInstance).map(Lambda.class::cast)
                                                        .map(lambda -> lambda.prefix(path).tag(path))
                                                        .forEach(result::add);
                                            });
                                });
                    });
                });
        return result;
    }

    private List<RouteParameter> params(final ClassLoader loader, final ClassNode owner, final String pattern,
            final MethodNode lambda) {
        List<RouteParameter> result = new ArrayList<>();

        new Insns(lambda).on(param(loader), it -> {
            String name = parameterName(it.node).orElse("arg" + result.size());

            AbstractInsnNode next = it.node.getNext();
            Object value = paramValue(loader, owner, lambda, next);
            if (value != next) {
                // there is a default value, move next
                next = next.getNext();
            } else {
                value = null;
            }

            java.lang.reflect.Type parameterType = parameterType(loader, next);
            // boolean are ICONST_0 or ICONST_1
            if (boolean.class.equals(parameterType) && Integer.class.isInstance(value)) {
                value = (((Integer) value)).intValue() == 1;
            }
            result.add(new RouteParameter(name, kind(pattern, it.node.name, name), simplifyType(parameterType),
                    value));
        }).forEach();

        return result;
    }

    private RouteParameter.Kind kind(final String pattern, final String method, final String name) {
        if (method.equals("header")) {
            return RouteParameter.Kind.HEADER;
        }
        if (method.equals("param") || method.equals("params")) {
            return isPathParam(pattern, name) ? RouteParameter.Kind.PATH : RouteParameter.Kind.QUERY;
        }
        if (method.equals("form")) {
            return RouteParameter.Kind.FORM;
        }
        if (method.equals("file") || method.equals("files")) {
            return RouteParameter.Kind.FILE;
        }
        return RouteParameter.Kind.BODY;
    }

    private static boolean isPathParam(final String pattern, final String name) {
        if (pattern.contains("{" + name + "}") || pattern.contains(":" + name)) {
            return true;
        }
        return false;
    }

    private Object paramValue(final ClassLoader loader, final ClassNode owner, final MethodNode method,
            final AbstractInsnNode n) {
        if (n instanceof LdcInsnNode) {

            Object cst = ((LdcInsnNode) n).cst;
            if (cst instanceof Type) {
                boolean typePresent = new Insn<>(method, n).next().filter(is(MethodInsnNode.class)).findFirst()
                        .map(MethodInsnNode.class::cast).filter(mutantToSomething().or(getOrCreateKotlinClass()))
                        .isPresent();
                if (typePresent) {
                    return null;
                }
                return loadType(loader, ((Type) cst).getClassName());
            }
            return cst;
        } else if (n instanceof InsnNode) {
            InsnNode insn = (InsnNode) n;
            switch (insn.getOpcode()) {
            case Opcodes.ICONST_0:
                return 0;
            case Opcodes.ICONST_1:
                return 1;
            case Opcodes.ICONST_2:
                return 2;
            case Opcodes.ICONST_3:
                return 3;
            case Opcodes.ICONST_4:
                return 4;
            case Opcodes.ICONST_5:
                return 5;
            case Opcodes.LCONST_0:
                return 0L;
            case Opcodes.LCONST_1:
                return 1L;
            case Opcodes.FCONST_0:
                return 0f;
            case Opcodes.FCONST_1:
                return 1f;
            case Opcodes.FCONST_2:
                return 2f;
            case Opcodes.DCONST_0:
                return 0d;
            case Opcodes.DCONST_1:
                return 1d;
            case Opcodes.ICONST_M1:
                return -1;
            case Opcodes.ACONST_NULL:
                return null;
            }
        } else if (n instanceof IntInsnNode) {
            IntInsnNode intinsn = (IntInsnNode) n;
            return intinsn.operand;
        } else if (n instanceof FieldInsnNode) {
            // toEnum
            FieldInsnNode finsn = (FieldInsnNode) n;
            if (finsn.getOpcode() == GETSTATIC) {
                java.lang.reflect.Type possibleEnum = loadType(loader, finsn.owner);
                if (MoreTypes.getRawType(possibleEnum).isEnum()) {
                    return finsn.name;
                }
            }
        }
        return n;
    }

    private java.lang.reflect.Type parameterType(final ClassLoader loader, final AbstractInsnNode n) {
        if (n instanceof MethodInsnNode) {
            MethodInsnNode node = (MethodInsnNode) n;
            if (mutantValue().test(node)) {
                /** value(); intValue(); booleanValue(); */
                return TypeDescriptorParser.parse(loader, node.desc);
            } else if (mutantToSomething().test(node) || getOrCreateKotlinClass().test(node)) {
                /** to(String.class); toOptional; toList(); */
                String owner = Type.getReturnType(node.desc).getClassName();
                AbstractInsnNode prev = node.getPrevious();
                if (prev instanceof FieldInsnNode && ((MethodInsnNode) n).name.equals("toEnum")) {
                    /** toEnum(Letter.A); */
                    return loadType(loader, ((FieldInsnNode) prev).owner);
                }
                java.lang.reflect.Type toType = String.class;
                if (prev instanceof LdcInsnNode) {
                    /** to(Foo.class); */
                    Object cst = ((LdcInsnNode) prev).cst;
                    if (cst instanceof Type) {
                        toType = loadType(loader, ((Type) cst).getClassName());
                    }
                } else if (prev instanceof FieldInsnNode) {
                    toType = loadType(loader, ((FieldInsnNode) prev).owner);
                }
                // JoobyKt.toOptional
                AbstractInsnNode next = node.getNext();
                if (next instanceof MethodInsnNode) {
                    String joobyKt = ((MethodInsnNode) next).owner;
                    String methodName = ((MethodInsnNode) next).name;
                    if ("toOptional".equals(methodName) && "org/jooby/JoobyKt".equals(joobyKt)) {
                        owner = Optional.class.getName();
                    }
                }

                Set<String> skipOwners = ImmutableSet.of(Object.class.getName(), Enum.class.getName(),
                        "kotlin.reflect.KClass");
                if (skipOwners.contains(owner)) {
                    return toType;
                }

                /** toList(Foo.class); */
                return Types.newParameterizedType(loadType(loader, owner), toType);
            }
        } else if (n instanceof VarInsnNode) {
            return new Insn<>(null, n).prev().filter(is(MethodInsnNode.class)).findFirst()
                    .map(MethodInsnNode.class::cast).filter(file()).map(m -> {
                        return m.name.equals("files") ? Types.newParameterizedType(List.class, File.class)
                                : File.class;
                    }).orElse(Object.class);
        } else if (n instanceof TypeInsnNode) {
            TypeInsnNode typeInsn = (TypeInsnNode) n;
            if (typeInsn.getOpcode() == Opcodes.CHECKCAST) {
                return loadType(loader, typeInsn.desc);
            }
        } else if (n != null && Opcodes.DUP == n.getOpcode()) {
            // Kotlin 1.2.x
            // mv.visitInsn(DUP);
            // mv.visitLdcInsn("req.param(\"p1\")");
            // mv.visitMethodInsn(INVOKESTATIC, "kotlin/jvm/internal/Intrinsics", "checkExpressionValueIsNotNull", "(Ljava/lang/Object;Ljava/lang/String;)V", false);
            // mv.visitMethodInsn(INVOKESTATIC, "org/jooby/JoobyKt", "getValue", "(Lorg/jooby/Mutant;)Ljava/lang/String;", false);
            AbstractInsnNode next = new Insn<>(null, n).next().filter(MethodInsnNode.class::isInstance).skip(1)
                    .findFirst().orElse(null);
            java.lang.reflect.Type result = parameterType(loader, next);
            if (result == Object.class) {
                next = new Insn<>(null, n).next().filter(TypeInsnNode.class::isInstance).findFirst().orElse(null);
                result = parameterType(loader, next);
            }
            return result;
        } else if (n instanceof FieldInsnNode) {
            AbstractInsnNode next = n.getNext();
            if (next instanceof MethodInsnNode) {
                if (((MethodInsnNode) next).name.equals("toOptional")) {
                    return Types.newParameterizedType(Optional.class, loadType(loader, ((FieldInsnNode) n).owner));
                } else if (((MethodInsnNode) next).name.equals("getOrCreateKotlinClass")) {
                    return loadType(loader, ((FieldInsnNode) n).owner);
                }
            }
        }
        return Object.class;
    }

    private Optional<String> parameterName(final MethodInsnNode node) {
        if (node.name.equals("body") || node.name.equals("form") || node.name.equals("params")) {
            return Optional.of(node.name);
        }

        /** Collect all ldc between a previous method invocation and current invocation: */
        List<LdcInsnNode> ldc = Insn.ldcFor(node);
        return ldc.size() > 0 ? Optional.of((String) ldc.get(0).cst) : Optional.empty();
    }

    @SuppressWarnings("unchecked")
    private java.lang.reflect.Type returnType(final ClassLoader loader, final MethodNode m) throws Exception {
        return new Insns(m).last().prev().filter(and(is(InsnNode.class), opcode(Opcodes.ARETURN))).findFirst()
                .map(AbstractInsnNode::getPrevious).map(previous -> {
                    /** return 1; return true; return new Foo(); */
                    if (previous instanceof MethodInsnNode) {
                        MethodInsnNode minnsn = ((MethodInsnNode) previous);
                        if (minnsn.name.equals("<init>")) {
                            return loadType(loader, minnsn.owner);
                        }
                        String desc = minnsn.desc;
                        java.lang.reflect.Type type = TypeDescriptorParser.parse(loader, desc);
                        if (type.getTypeName().equals(Result.class.getName())) {
                            return new TypeWithStatus(type, statusCodeFor(minnsn));
                        }
                        return type;
                    }
                    /** return "String" | int | double */
                    if (previous instanceof LdcInsnNode) {
                        Object cst = ((LdcInsnNode) previous).cst;
                        if (cst instanceof Type) {
                            return TypeDescriptorParser.parse(loader, ((Type) cst).getDescriptor());
                        }
                        return cst.getClass();
                    }
                    /** return variable */
                    if (previous instanceof VarInsnNode) {
                        VarInsnNode varInsn = (VarInsnNode) previous;
                        return localVariable(loader, m, varInsn);
                    }

                    return Object.class;
                }).orElse(Object.class);
    }

    private Integer statusCodeFor(MethodInsnNode node) {
        if (node.name.equals("noContent")) {
            return 204;
        }
        if (node.name.equals("with")) {
            AbstractInsnNode previous = node.getPrevious();
            if (previous instanceof IntInsnNode) {
                return ((IntInsnNode) previous).operand;
            }
            if (previous instanceof FieldInsnNode) {
                String owner = ((FieldInsnNode) previous).owner.replace("/", ".");
                if (owner.equals(Status.class.getName())) {
                    String statusName = ((FieldInsnNode) previous).name;
                    return Try.apply(() -> Status.class.getDeclaredField(statusName).get(null))
                            .map(it -> ((Status) it).value()).orElse(null);
                }
            }
        }
        return null;
    }

    @SuppressWarnings("unchecked")
    static java.lang.reflect.Type localVariable(final ClassLoader loader, final MethodNode m,
            final VarInsnNode varInsn) {
        if (varInsn.getOpcode() == Opcodes.ALOAD) {
            List<LocalVariableNode> vars = m.localVariables;
            LocalVariableNode var = vars.stream().filter(v -> v.index == varInsn.var).findFirst().orElse(null);
            if (var != null) {
                String signature = "()" + Optional.ofNullable(var.signature).orElse(var.desc);
                return TypeDescriptorParser.parse(loader, signature);
            }
        }
        return Object.class;
    }

    static Class loadType(final ClassLoader loader, final String name) {
        return when(name).<Class>is("boolean", boolean.class).is("char", char.class).is("byte", byte.class)
                .is("short", short.class).is("int", int.class).is("long", long.class).is("float", float.class)
                .is("double", double.class).orElseGet(() -> loader.loadClass(name.replace("/", ".")));
    }

    static java.lang.reflect.Type simplifyType(final java.lang.reflect.Type type) {
        Class<?> rawType = MoreTypes.getRawType(type);
        if (Primitives.isWrapperType(rawType)) {
            return Primitives.unwrap(rawType);
        }
        return type;
    }

    static Writer writer(final Logger log, final String owner) {
        return new Writer() {
            StringBuilder buff = new StringBuilder();

            @Override
            public void write(final char[] cbuf, final int off, final int len) throws IOException {
                buff.append(cbuf, off, len);
            }

            @Override
            public void flush() throws IOException {
                log.info("{}:\n{}", owner, buff);
            }

            @Override
            public void close() throws IOException {
            }
        };
    }

    private ClassNode loadClass(String name) {
        return Try.apply(() -> {
            String cname = name.replace("/", ".");
            ClassNode node = cache.get(cname);
            if (node == null) {
                String rname = cname.replace(".", "/") + ".class";
                try (InputStream in = loader.getResourceAsStream(rname)) {
                    if (in == null) {
                        throw new FileNotFoundException(rname + " using " + loader);
                    }
                    ClassReader reader = new ClassReader(ByteStreams.toByteArray(in));
                    node = new ClassNode();
                    reader.accept(node, 0);
                    cache.put(cname, node);
                    if (log.isDebugEnabled()) {
                        log.info("Source: {}; Class: {}", node.sourceFile, node.name);
                        reader.accept(
                                new TraceClassVisitor(null, new ASMifier(), new PrintWriter(writer(log, name))), 0);
                    }
                }
            }
            return node;
        }).get();
    }

    private static RouteMethod toRouteMethod(final Route.Definition route) {
        Method handler = ((Route.MethodHandler) route.filter()).method();
        return new RouteMethod(route.method(), route.pattern(), new RouteResponse(handler.getGenericReturnType()))
                .name(route.name())
                .parameters(Arrays.asList(handler.getParameters()).stream().filter(SKIP)
                        .map(it -> BytecodeRouteParser.toRouteParameter(route.pattern(), it))
                        .collect(Collectors.toList()));
    }

    private static RouteParameter toRouteParameter(String pattern, final Parameter p) {
        Annotation[] annotations = p.getAnnotations();
        Supplier<String> name = () -> {
            for (int i = 0; i < annotations.length; i++) {
                Class<? extends Annotation> annotationType = annotations[i].annotationType();
                if (annotationType == Named.class) {
                    return ((Named) annotations[i]).value();
                }
                if (annotationType == Header.class) {
                    return ((Header) annotations[i]).value();
                }
            }
            return p.getName();
        };

        String pname = name.get();
        Supplier<RouteParameter.Kind> kind = () -> {
            if (p.getType() == Upload.class) {
                return RouteParameter.Kind.FILE;
            }
            for (int i = 0; i < annotations.length; i++) {
                Class<? extends Annotation> annotationType = annotations[i].annotationType();
                if (annotationType == Header.class) {
                    return RouteParameter.Kind.HEADER;
                } else if (annotationType == Body.class) {
                    return RouteParameter.Kind.BODY;
                }
            }
            boolean hasBody = Arrays.asList(p.getDeclaringExecutable().getParameters()).stream().filter(it -> Stream
                    .of(it.getAnnotations()).filter(e -> e.annotationType() == Body.class).findFirst().isPresent())
                    .findFirst().isPresent();
            if (isPathParam(pattern, pname)) {
                return RouteParameter.Kind.PATH;
            }
            boolean formLike = !hasBody && p.getDeclaringExecutable().getAnnotation(POST.class) != null;
            if (formLike) {
                return RouteParameter.Kind.FORM;
            }
            return RouteParameter.Kind.QUERY;
        };
        return new RouteParameter(pname, kind.get(), p.getParameterizedType(), null);
    }

    private static class TypeWithStatus implements java.lang.reflect.Type {
        private final java.lang.reflect.Type forwarding;
        final Integer status;

        TypeWithStatus(java.lang.reflect.Type forwarding, Integer status) {
            this.forwarding = forwarding;
            this.status = status;
        }

        public java.lang.reflect.Type type() {
            if (status != null && status == 204 && forwarding.getTypeName().equals(Result.class.getName())) {
                return void.class;
            }
            return forwarding;
        }

        @Override
        public String getTypeName() {
            return forwarding.getTypeName();
        }
    }

}