package act.controller.bytecode; /*- * #%L * ACT Framework * %% * Copyright (C) 2014 - 2017 ActFramework * %% * 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. * #L% */ import act.Act; import act.app.App; import act.app.AppByteCodeScannerBase; import act.app.AppClassLoader; import act.app.event.AppEventId; import act.asm.*; import act.asm.signature.SignatureReader; import act.asm.signature.SignatureVisitor; import act.conf.AppConfig; import act.controller.Controller; import act.controller.annotation.Port; import act.controller.annotation.UrlContext; import act.controller.meta.*; import act.handler.builtin.controller.RequestHandlerProxy; import act.route.RouteSource; import act.route.Router; import act.util.*; import org.osgl.$; import org.osgl.Osgl; import org.osgl.http.H; import org.osgl.logging.L; import org.osgl.logging.Logger; import org.osgl.mvc.annotation.With; import org.osgl.mvc.util.Binder; import org.osgl.util.C; import org.osgl.util.E; import org.osgl.util.ListBuilder; import org.osgl.util.S; import java.lang.annotation.Annotation; import java.util.*; /** * New controller scanner implementation */ public class ControllerByteCodeScanner extends AppByteCodeScannerBase { private final static Logger logger = L.get(ControllerByteCodeScanner.class); private Router router; private ControllerClassMetaInfo classInfo; private volatile ControllerClassMetaInfoManager classInfoBase; public ControllerByteCodeScanner() { } @Override protected boolean shouldScan(String className) { boolean possibleController = config().possibleControllerClass(className); classInfo = new ControllerClassMetaInfo().possibleController(possibleController); return possibleController; } @Override protected void onAppSet() { router = app().router(); } @Override public ByteCodeVisitor byteCodeVisitor() { return new _ByteCodeVisitor(); } @Override public void scanFinished(String className) { if (classInfo.isController()) { classInfoBase().registerControllerMetaInfo(classInfo); } } private ControllerClassMetaInfoManager classInfoBase() { if (null == classInfoBase) { synchronized (this) { if (null == classInfoBase) { classInfoBase = app().classLoader().controllerClassMetaInfoManager(); } } } return classInfoBase; } private class _ByteCodeVisitor extends ByteCodeVisitor { private String[] ports = {}; private Set<String> methodNames = new HashSet<>(); private void checkMethodName(String methodName) { if (methodNames.contains(methodName)) { throw AsmException.of("Duplicate action/interceptor method name found: %s", methodName); } methodNames.add(methodName); } @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { logger.trace("Scanning %s", name); classInfo.className(name); String className = name.replace('/', '.'); if (router.possibleController(className)) { classInfo.isController(true); } Type superType = Type.getObjectType(superName); classInfo.superType(superType); if (isAbstract(access)) { classInfo.setAbstract(); } super.visit(version, access, name, signature, superName, interfaces); } @Override public AnnotationVisitor visitAnnotation(String desc, boolean visible) { AnnotationVisitor av = super.visitAnnotation(desc, visible); if (Type.getType(Controller.class).getDescriptor().equals(desc)) { classInfo.isController(true); return new ControllerAnnotationVisitor(av); } else if (Type.getType(UrlContext.class).getDescriptor().equals(desc)) { classInfo.isController(true); return new UrlContextAnnotationVisitor(av); } else if (Type.getType(Port.class).getDescriptor().equals(desc)) { return new PortAnnotationVisitor(av); } if (Type.getType(With.class).getDescriptor().equals(desc)) { classInfo.isController(true); return new ClassWithAnnotationVisitor(av); } return super.visitAnnotation(desc, visible); } @Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions); if (!classInfo.possibleController() || !isEligibleMethod(access, name)) { return mv; } String className = classInfo.className(); boolean isRoutedMethod = router.isActionMethod(className, name); return new ActionMethodVisitor(isRoutedMethod, mv, access, name, desc, signature, exceptions); } private boolean isEligibleMethod(int access, String name) { return !isAbstract(access) && !isConstructor(name); } private class StringArrayVisitor extends AnnotationVisitor { protected ListBuilder<String> strings = ListBuilder.create(); public StringArrayVisitor(AnnotationVisitor av) { super(ASM5, av); } @Override public void visit(String name, Object value) { strings.add(value.toString()); super.visit(name, value); } } private class ClassWithAnnotationVisitor extends AnnotationVisitor { public ClassWithAnnotationVisitor(AnnotationVisitor av) { super(ASM5, av); } @Override public AnnotationVisitor visitArray(String name) { AnnotationVisitor av = super.visitArray(name); if ("value".equals(name)) { return new StringArrayVisitor(av) { @Override public void visitEnd() { String[] sa = new String[strings.size()]; sa = strings.toArray(sa); classInfo.addWith(sa); super.visitEnd(); } }; } return av; } } private class ControllerAnnotationVisitor extends AnnotationVisitor { ControllerAnnotationVisitor(AnnotationVisitor av) { super(ASM5, av); } @Override public void visit(String name, Object value) { if ("value".equals(name)) { classInfo.contextPath(value.toString()); } } @Override public AnnotationVisitor visitArray(String name) { AnnotationVisitor av = super.visitArray(name); if ("port".equals(name)) { return new StringArrayVisitor(av) { @Override public void visitEnd() { ports = new String[strings.size()]; ports = strings.toArray(ports); super.visitEnd(); } }; } return av; } } private class UrlContextAnnotationVisitor extends AnnotationVisitor { UrlContextAnnotationVisitor(AnnotationVisitor av) { super(ASM5, av); } @Override public void visit(String name, Object value) { if ("value".equals(name)) { classInfo.contextPath(value.toString()); } } } private class PortAnnotationVisitor extends AnnotationVisitor { PortAnnotationVisitor(AnnotationVisitor av) { super(ASM5, av); } @Override public AnnotationVisitor visitArray(String name) { AnnotationVisitor av = super.visitArray(name); if ("value".equals(name)) { return new StringArrayVisitor(av) { @Override public void visitEnd() { ports = new String[strings.size()]; ports = strings.toArray(ports); super.visitEnd(); } }; } return av; } } private class ActionMethodVisitor extends MethodVisitor implements Opcodes { private String methodName; private int access; private String desc; private String signature; private boolean isStatic; private String[] exceptions; private boolean requireScan; private boolean isRoutedMethod; private HandlerMethodMetaInfo methodInfo; private PropertySpec.MetaInfo propSpec; private Map<Integer, List<ParamAnnoInfoTrait>> paramAnnoInfoList = C.newMap(); private Map<Integer, List<GeneralAnnoInfo>> genericParamAnnoInfoList = C.newMap(); private BitSet contextInfo = new BitSet(); private $.Var<Boolean> isVirtual = $.var(false); private HandlerWithAnnotationVisitor withAnnotationVisitor; private $.Var<Boolean> isGlobal = $.var(false); private List<InterceptorAnnotationVisitor> interceptorAnnotationVisitors = new ArrayList<>(); ActionMethodVisitor(boolean isRoutedMethod, MethodVisitor mv, int access, String methodName, String desc, String signature, String[] exceptions) { super(ASM5, mv); this.isRoutedMethod = isRoutedMethod; this.access = access; this.methodName = methodName; this.desc = desc; this.signature = signature; this.exceptions = exceptions; this.isStatic = isStatic(access); if (classInfo.isAbstract()) { this.isVirtual.set(true); } if (isRoutedMethod) { markRequireScan(); } } @Override public AnnotationVisitor visitAnnotation(String desc, boolean visible) { AnnotationVisitor av = super.visitAnnotation(desc, visible); Type type = Type.getType(desc); String className = type.getClassName(); Class<? extends Annotation> c = $.classForName(className); if (Virtual.class.getName().equals(c.getName())) { isVirtual.set(true); return av; } if (Global.class.getName().equals(c.getName())) { isGlobal.set(true); return av; } if (Type.getType(With.class).getDescriptor().equals(desc)) { classInfo.isController(true); withAnnotationVisitor = new HandlerWithAnnotationVisitor(av); return withAnnotationVisitor; } if (ControllerClassMetaInfo.isActionAnnotation(c)) { checkMethodName(methodName); markRequireScan(); ActionMethodMetaInfo tmp = new ActionMethodMetaInfo(classInfo); methodInfo = tmp; classInfo.addAction(tmp); if (null != propSpec) { methodInfo.propertySpec(propSpec); } return new ActionAnnotationVisitor(av, ControllerClassMetaInfo.lookupHttpMethod(c), ControllerClassMetaInfo.isActionUtilAnnotation(c), isStatic); } else if (ControllerClassMetaInfo.isInterceptorAnnotation(c)) { checkMethodName(methodName); markRequireScan(); InterceptorAnnotationVisitor visitor = new InterceptorAnnotationVisitor(av, c); methodInfo = visitor.info; if (null != propSpec) { methodInfo.propertySpec(propSpec); } interceptorAnnotationVisitors.add(visitor); return visitor; } else if ($.eq(AsmTypes.PROPERTY_SPEC.asmType(), type)) { propSpec = new PropertySpec.MetaInfo(); if (null != methodInfo) { methodInfo.propertySpec(propSpec); } return new AnnotationVisitor(ASM5, av) { @Override public AnnotationVisitor visitArray(String name) { AnnotationVisitor av0 = super.visitArray(name); if (S.eq("value", name)) { return new AnnotationVisitor(ASM5, av0) { @Override public void visit(String name, Object value) { super.visit(name, value); propSpec.onValue(S.string(value)); } }; } else if (S.eq("cli", name)) { return new AnnotationVisitor(ASM5, av0) { @Override public void visit(String name, Object value) { super.visit(name, value); propSpec.onCli(S.string(value)); } }; } else if (S.eq("http", name)) { return new AnnotationVisitor(ASM5, av0) { @Override public void visit(String name, Object value) { super.visit(name, value); propSpec.onHttp(S.string(value)); } }; } else { return av0; } } }; } else if ($.eq(AsmTypes.DISABLE_JSON_CIRCULAR_REF_DETECT.asmType(), type)) { methodInfo.disableJsonCircularRefDetect(true); } //markNotTargetClass(); return av; } @Override public AnnotationVisitor visitParameterAnnotation(int parameter, String desc, boolean visible) { AnnotationVisitor av = super.visitParameterAnnotation(parameter, desc, visible); Type type = Type.getType(desc); if ($.eq(type, AsmTypes.PARAM.asmType())) { return new ParamAnnotationVisitor(av, parameter); } else if ($.eq(type, AsmTypes.BIND.asmType())) { return new BindAnnotationVisitor(av, parameter); } else if ($.eq(type, AsmTypes.CONTEXT.asmType())) { contextInfo.set(parameter); return av; } else { //return av; GeneralAnnoInfo info = new GeneralAnnoInfo(type); List<GeneralAnnoInfo> list = genericParamAnnoInfoList.get(parameter); if (null == list) { list = C.newList(); genericParamAnnoInfoList.put(parameter, list); } list.add(info); return new GeneralAnnoInfo.Visitor(av, info); } } @Override public void visitEnd() { if (!requireScan()) { super.visitEnd(); return; } if (isGlobal.get()) { for (InterceptorAnnotationVisitor visitor : interceptorAnnotationVisitors) { visitor.registerGlobalInterceptor(); } } classInfo.isController(true); if (null == methodInfo) { ActionMethodMetaInfo action = new ActionMethodMetaInfo(classInfo); methodInfo = action; classInfo.addAction(action); } if (null != withAnnotationVisitor) { if (methodInfo instanceof ActionMethodMetaInfo) { ActionMethodMetaInfo actionInfo = $.cast(methodInfo); actionInfo.addWith(withAnnotationVisitor.withArray); } } final HandlerMethodMetaInfo info = methodInfo; info.name(methodName); boolean isStatic = AsmTypes.isStatic(access); if (isStatic) { info.invokeStaticMethod(); } else { info.invokeInstanceMethod(); } info.returnType(Type.getReturnType(desc)); Type[] argTypes = Type.getArgumentTypes(desc); boolean ctxByParam = false; for (int i = 0; i < argTypes.length; ++i) { Type type = argTypes[i]; if (AsmTypes.ACTION_CONTEXT.asmType().equals(type)) { ctxByParam = true; info.appContextViaParam(i); } HandlerParamMetaInfo param = new HandlerParamMetaInfo().type(type); if (contextInfo.get(i)) { param.setContext(); } List<ParamAnnoInfoTrait> paraAnnoList = paramAnnoInfoList.get(i); if (null != paraAnnoList) { for (ParamAnnoInfoTrait trait : paraAnnoList) { trait.attachTo(param); } } List<GeneralAnnoInfo> list = genericParamAnnoInfoList.get(i); if (null != list) { param.addGeneralAnnotations(list); } info.addParam(param); } if (!ctxByParam) { if (classInfo.hasCtxField() && !isStatic) { info.appContextViaField(classInfo.ctxField()); } else { info.appContextViaLocalStorage(); } } if (null != signature) { SignatureReader sr = new SignatureReader(signature); final $.Var<Integer> id = new $.Var<Integer>(-1); sr.accept(new SignatureVisitor(ASM5) { boolean startParsing; @Override public SignatureVisitor visitParameterType() { id.set(id.get() + 1); return this; } @Override public SignatureVisitor visitTypeArgument(char wildcard) { if (wildcard == '=') { startParsing = true; } return this; } @Override public void visitClassType(String name) { if (startParsing) { Type type = Type.getObjectType(name); int n = id.get(); if (n < 0) { info.returnComponentType(type); } else { info.param(n).componentType(type); } } startParsing = false; } }); } super.visitEnd(); } private class HandlerWithAnnotationVisitor extends AnnotationVisitor { private String[] withArray; public HandlerWithAnnotationVisitor(AnnotationVisitor av) { super(ASM5, av); } @Override public AnnotationVisitor visitArray(String name) { AnnotationVisitor av = super.visitArray(name); if ("value".equals(name)) { return new StringArrayVisitor(av) { @Override public void visitEnd() { String[] sa = new String[strings.size()]; sa = strings.toArray(sa); withArray = sa; super.visitEnd(); } }; } return av; } } private void markRequireScan() { this.requireScan = true; } private boolean requireScan() { return requireScan; } private class InterceptorAnnotationVisitor extends AnnotationVisitor implements Opcodes { private InterceptorMethodMetaInfo info; private InterceptorType interceptorType; public InterceptorAnnotationVisitor(AnnotationVisitor av, Class<? extends Annotation> annoCls) { super(ASM5, av); interceptorType = InterceptorType.of(annoCls); info = interceptorType.createMetaInfo(classInfo); classInfo.addInterceptor(info, annoCls); } @Override public void visit(String name, Object value) { if ("priority".equals(name)) { info.priority((Integer) value); } super.visit(name, value); } @Override public AnnotationVisitor visitArray(String name) { if ("only".equals(name)) { return new OnlyValueVisitor(av); } else if ("except".equals(name)) { return new ExceptValueVisitor(av); } else if ("value".equals(name)) { if (info instanceof CatchMethodMetaInfo) { return new CatchValueVisitor(av); } } return super.visitArray(name); } void registerGlobalInterceptor() { RequestHandlerProxy.registerGlobalInterceptor(info, interceptorType); } private class OnlyValueVisitor extends StringArrayVisitor { public OnlyValueVisitor(AnnotationVisitor av) { super(av); } @Override public void visitEnd() { String[] sa = new String[strings.size()]; sa = strings.toArray(sa); info.addOnly(sa); super.visitEnd(); } } private class ExceptValueVisitor extends StringArrayVisitor { public ExceptValueVisitor(AnnotationVisitor av) { super(av); } @Override public void visitEnd() { String[] sa = new String[strings.size()]; sa = strings.toArray(sa); info.addExcept(sa); super.visitEnd(); } } private class CatchValueVisitor extends AnnotationVisitor { List<String> exceptions = C.newList(); public CatchValueVisitor(AnnotationVisitor av) { super(ASM5, av); } @Override public void visit(String name, Object value) { exceptions.add(((Type) value).getClassName()); } @Override public void visitEnd() { CatchMethodMetaInfo ci = (CatchMethodMetaInfo) info; ci.exceptionClasses(exceptions); super.visitEnd(); } } } private class ActionAnnotationVisitor extends AnnotationVisitor implements Opcodes { List<H.Method> httpMethods = C.newList(); List<String> paths = C.newList(); boolean isUtil; boolean isStatic; public ActionAnnotationVisitor(AnnotationVisitor av, H.Method method, boolean isUtil, boolean staticMethod) { super(ASM5, av); if (null != method) { httpMethods.add(method); } this.isUtil = isUtil; this.isStatic = staticMethod; } @Override public AnnotationVisitor visitArray(String name) { AnnotationVisitor av = super.visitArray(name); if ("value".equals(name)) { return new AnnotationVisitor(ASM5, av) { @Override public void visit(String name, Object value) { super.visit(name, value); paths.add((String) value); } }; } else if ("methods".equals(name)) { return new AnnotationVisitor(ASM5, av) { @Override public void visitEnum(String name, String desc, String value) { super.visitEnum(name, desc, value); String enumClass = Type.getType(desc).getClassName(); if (H.Method.class.getName().equals(enumClass)) { H.Method method = H.Method.valueOf(value); httpMethods.add(method); } } }; } else { return av; } } @Override public void visitEnd() { super.visitEnd(); if (isUtil) { return; } if (httpMethods.isEmpty()) { // start(*) match httpMethods.addAll(H.Method.actionMethods()); } final List<Router> routers = C.newList(); if (null == ports || ports.length == 0) { routers.add(app().router()); } else { App app = app(); for (String portName : ports) { Router r = app.router(portName); if (null == r) { if (S.eq(AppConfig.PORT_CLI_OVER_HTTP, portName)) { // cli over http is disabled return; } throw E.invalidConfiguration("Cannot find configuration for named port[%s]", portName); } routers.add(r); } } if (paths.isEmpty()) { paths.add(""); } /* * Note we need to schedule route registration after all app code scanned because we need the * parent context information be set on class meta info, which is done after controller scanning */ app().jobManager().on(AppEventId.APP_CODE_SCANNED, new RouteRegister(httpMethods, paths, methodName, routers, classInfo, classInfo.isAbstract() && !isStatic, isVirtual)); } } private abstract class ParamAnnotationVisitorBase<T extends ParamAnnoInfoTrait> extends AnnotationVisitor implements Opcodes { protected int index; protected T info; public ParamAnnotationVisitorBase(AnnotationVisitor av, int index) { super(ASM5, av); this.index = index; this.info = createAnnotationInfo(index); } @Override public void visitEnd() { List<ParamAnnoInfoTrait> traits = paramAnnoInfoList.get(index); if (null == traits) { traits = C.newList(); paramAnnoInfoList.put(index, traits); } else { for (ParamAnnoInfoTrait trait : traits) { if (!info.compatibleWith(trait)) { throw E.unexpected(info.compatibilityErrorMessage(trait)); } } } traits.add(info); super.visitEnd(); } protected abstract T createAnnotationInfo(int index); } private class ParamAnnotationVisitor extends ParamAnnotationVisitorBase<ParamAnnoInfo> { public ParamAnnotationVisitor(AnnotationVisitor av, int index) { super(av, index); } @Override protected ParamAnnoInfo createAnnotationInfo(int index) { return new ParamAnnoInfo(index); } @Override public void visit(String name, Object value) { if (S.eq("value", name)) { info.bindName((String) value); } else if (S.eq("defVal", name)) { info.defVal(String.class, value); } else if (S.eq("defIntVal", name)) { info.defVal(Integer.class, value); } else if (S.eq("defBooleanVal", name)) { info.defVal(Boolean.class, value); } else if (S.eq("defLongVal", name)) { info.defVal(Long.class, value); } else if (S.eq("defDoubleVal", name)) { info.defVal(Double.class, value); } else if (S.eq("defFloatVal", name)) { info.defVal(Float.class, value); } else if (S.eq("defCharVal", name)) { info.defVal(Character.class, value); } else if (S.eq("defByteVal", name)) { info.defVal(Byte.class, name); } super.visit(name, value); } private <T> T c(Object v) { return $.cast(v); } } private class BindAnnotationVisitor extends ParamAnnotationVisitorBase<BindAnnoInfo> { public BindAnnotationVisitor(AnnotationVisitor av, int index) { super(av, index); } @Override protected BindAnnoInfo createAnnotationInfo(int index) { return new BindAnnoInfo(index); } @Override public void visit(String name, Object value) { if ("model".endsWith(name)) { info.model((String) value); } } @Override public AnnotationVisitor visitArray(String name) { AnnotationVisitor av = super.visitArray(name); if ("value".equals(name)) { return new AnnotationVisitor(ASM5, av) { @Override public void visit(String name, Object value) { Type type = (Type) value; Class<? extends Binder> c = $.classForName(type.getClassName(), getClass().getClassLoader()); info.binder(c); } }; } return av; } } } } private static class RouteRegister implements Runnable { List<Router> routers; List<String> paths; String methodName; ControllerClassMetaInfo classInfo; List<H.Method> httpMethods; $.Var<Boolean> isVirtual; boolean noRegister; // do not register virtual method of an abstract class RouteRegister(List<H.Method> methods, List<String> paths, String methodName, List<Router> routers, ControllerClassMetaInfo classInfo, boolean noRegister, $.Var<Boolean> isVirtual) { this.routers = routers; this.paths = paths; this.methodName = methodName; this.classInfo = classInfo; this.httpMethods = methods; this.noRegister = noRegister; this.isVirtual = isVirtual; } @Override public void run() { final Set<String> contexts = new HashSet<>(); if (!noRegister) { String contextPath = classInfo.contextPath(); String className = classInfo.className(); String action = S.newSizedBuffer(className.length() + methodName.length() + 1).append(className).append(".").append(methodName).toString(); registerOnContext(contextPath, action); contexts.add(contextPath); } if (!isVirtual.get()) { // not virtual handler method, so don't need to register sub class routes return; } // now check on sub classes App app = Act.app(); final AppClassLoader classLoader = app.classLoader(); ClassNode node = classLoader.classInfoRepository().node(classInfo.className()); node.visitSubTree(new $.Visitor<ClassNode>() { @Override public void visit(ClassNode classNode) throws Osgl.Break { String className = classNode.name(); ControllerClassMetaInfo subClassInfo = classLoader.controllerClassMetaInfo(className); if (null != subClassInfo) { String subClassContextPath = subClassInfo.contextPath(); if (null != subClassContextPath) { if (!contexts.contains(subClassContextPath)) { registerOnContext(subClassContextPath, S.builder(subClassInfo.className()).append(".").append(methodName).toString()); contexts.add(subClassContextPath); } else { throw E.invalidConfiguration("the context path of Sub controller %s has already been registered: %s", className, subClassContextPath); } } } } }, true, true); } private void registerOnContext(String ctxPath, String action) { S.Buffer sb = S.newBuffer(); for (Router r: routers) { for (String actionPath : paths) { if (!actionPath.startsWith("/")) { if (!(S.blank(ctxPath) || "/".equals(ctxPath))) { if (ctxPath.endsWith("/")) { ctxPath = ctxPath.substring(0, ctxPath.length() - 1); } sb.setLength(0); sb.append(ctxPath); if (!actionPath.startsWith("/")) { sb.append("/"); } sb.append(actionPath); actionPath = sb.toString(); } } for (H.Method m : httpMethods) { r.addMapping(m, actionPath, action, RouteSource.ACTION_ANNOTATION); } } } } } }