Java tutorial
/** * 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(); } } }