/* * Copyright 2015 Red Hat, Inc. and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * * 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.kie.maven.plugin; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import org.drools.core.phreak.ReactiveCollection; import org.drools.core.phreak.ReactiveList; import org.drools.core.phreak.ReactiveObject; import org.drools.core.phreak.ReactiveObjectUtil; import org.drools.core.phreak.ReactiveSet; import org.drools.core.spi.Tuple; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javassist.CannotCompileException; import javassist.ClassPool; import javassist.CtClass; import javassist.CtField; import javassist.CtMethod; import javassist.CtNewMethod; import javassist.Modifier; import javassist.NotFoundException; import javassist.CtField.Initializer; import javassist.bytecode.BadBytecode; import javassist.bytecode.CodeIterator; import javassist.bytecode.ConstPool; import javassist.bytecode.MethodInfo; import javassist.bytecode.Opcode; import javassist.bytecode.SignatureAttribute; import javassist.bytecode.SignatureAttribute.ClassType; import javassist.bytecode.SignatureAttribute.MethodSignature; import javassist.bytecode.SignatureAttribute.TypeArgument; import javassist.bytecode.stackmap.MapMaker; /* * kudos to hibernate-enhance-maven-plugin. */ public class BytecodeInjectReactive { public static final String DROOLS_PREFIX = "$$_drools_"; public static final String FIELD_WRITER_PREFIX = DROOLS_PREFIX + "write_"; public static final String DROOLS_LIST_OF_TUPLES = DROOLS_PREFIX + "lts"; public static final Logger LOG = LoggerFactory.getLogger(BytecodeInjectReactive.class); private Map<String, CtMethod> writeMethods; private ClassPool cp; public BytecodeInjectReactive(ClassPool cp) { this.cp = cp; init(); } public static BytecodeInjectReactive newInstance(ClassPool cp) { return new BytecodeInjectReactive(cp); } /** * Utility method for returning the (inferred) classpath of classloading from the given Class. * @param clazz the enclosing Class. * @return the (inferred) classpath of clazz */ public static String classpathFromClass(Class<?> clazz) { String aname = clazz.getPackage().getName().replaceAll("\\.", "/") + "/" + clazz.getSimpleName()+".class"; String apath = ClassLoader.getSystemClassLoader().getResource( aname).getPath(); String path = null; if (apath.contains("!")) { path = apath.substring(0, apath.indexOf("!")).replace("file:", ""); } else { path = apath.substring(0, apath.indexOf(aname)); } return path; } private void init() { this.writeMethods = new HashMap<String, CtMethod>(); } public byte[] injectReactive(String classname) throws Exception { init(); CtClass droolsPojo = cp.get(classname); if (collectReactiveFields(droolsPojo).size() == 0) { LOG.info("Skipped bytecode injection in class " + droolsPojo.getName()+ " because no fields candidated for reactivity."); return droolsPojo.toBytecode(); } droolsPojo.addInterface( cp.get(ReactiveObject.class.getName()) ); CtField ltsCtField = new CtField( cp.get(Collection.class.getName()), DROOLS_LIST_OF_TUPLES, droolsPojo ); ltsCtField.setModifiers(Modifier.PRIVATE); ClassType listOfTuple = new SignatureAttribute.ClassType(Collection.class.getName(), new TypeArgument[]{new TypeArgument( new SignatureAttribute.ClassType(Tuple.class.getName()) )}); ltsCtField.setGenericSignature( listOfTuple.encode() ); // Do not use the Initializer.byNew... as those method always pass at least 1 parameter which is "this". droolsPojo.addField(ltsCtField, Initializer.byExpr("new java.util.HashSet();")); final CtMethod getLeftTuplesCtMethod = CtNewMethod.make( "public java.util.Collection getLeftTuples() {\n" + " return this.$$_drools_lts != null ? this.$$_drools_lts : java.util.Collections.emptyList();\n"+ "}", droolsPojo ); MethodSignature getLeftTuplesSignature = new MethodSignature(null, null, listOfTuple, null); getLeftTuplesCtMethod.setGenericSignature(getLeftTuplesSignature.encode()); droolsPojo.addMethod(getLeftTuplesCtMethod); final CtMethod addLeftTupleCtMethod = CtNewMethod.make( "public void addLeftTuple("+Tuple.class.getName()+" leftTuple) {\n" + " if ($$_drools_lts == null) {\n" + " $$_drools_lts = new java.util.HashSet();\n" + " }\n" + " $$_drools_lts.add(leftTuple);\n" + "}", droolsPojo ); droolsPojo.addMethod(addLeftTupleCtMethod); final CtMethod removeLeftTupleCtMethod = CtNewMethod.make( "public void removeLeftTuple("+Tuple.class.getName()+" leftTuple) {\n" + " $$_drools_lts.remove(leftTuple);\n" + "}", droolsPojo ); droolsPojo.addMethod(removeLeftTupleCtMethod); Map<String, CtField> fieldsMap = collectReactiveFields(droolsPojo); for (CtField f : fieldsMap.values()) { LOG.debug("Preparing field writer method for field: {}.", f); writeMethods.put(f.getName(), makeWriter(droolsPojo, f)); } enhanceAttributesAccess(fieldsMap, droolsPojo); // first call CtClass.toClass() before the original class is loaded, it will persist the bytecode instrumentation changes in the classloader. return droolsPojo.toBytecode(); } protected void enhanceAttributesAccess(Map<String, CtField> fieldsMap, CtClass managedCtClass) throws Exception { final ConstPool constPool = managedCtClass.getClassFile().getConstPool(); final ClassPool classPool = managedCtClass.getClassPool(); for ( Object oMethod : managedCtClass.getClassFile().getMethods() ) { final MethodInfo methodInfo = (MethodInfo) oMethod; final String methodName = methodInfo.getName(); // skip methods added by enhancement, and abstract methods (methods without any code) if ( methodName.startsWith( DROOLS_PREFIX ) || methodInfo.getCodeAttribute() == null ) { continue; } try { final CodeIterator itr = methodInfo.getCodeAttribute().iterator(); while ( itr.hasNext() ) { final int index = itr.next(); final int op = itr.byteAt( index ); if ( op != Opcode.PUTFIELD && op != Opcode.GETFIELD ) { continue; } final String fieldName = constPool.getFieldrefName( itr.u16bitAt( index + 1 ) ); CtField ctField = fieldsMap.get(fieldName); if (ctField == null ) { continue; } // if we are in constructors, only need to intercept assignment statement for Reactive Collection/List/... (regardless they may be final) if ( methodInfo.isConstructor() && !( isCtFieldACollection(ctField) ) ) { continue; } if (op == Opcode.PUTFIELD) { // addMethod is a safe add, if constant already present it return the existing value without adding. final int methodIndex = addMethod( constPool, writeMethods.get(fieldName) ); itr.writeByte( Opcode.INVOKEVIRTUAL, index ); itr.write16bit( methodIndex, index + 1 ); } } methodInfo.getCodeAttribute().setAttribute( MapMaker.make( classPool, methodInfo ) ); } catch (BadBytecode bb) { final String msg = String.format( "Unable to perform field access transformation in method [%s]", methodName ); throw new Exception( msg, bb ); } } } private static CtMethod write(CtClass target, String format, Object ... args) throws CannotCompileException { final String body = String.format( format, args ); LOG.debug( "writing method into [{}]:\n{}\n", target.getName(), body ); final CtMethod method = CtNewMethod.make( body, target ); target.addMethod( method ); return method; } /** * Add Method to ConstPool. If method was not in the ConstPool will add and return index, otherwise will return index of already existing entry of constpool */ private static int addMethod(ConstPool cPool, CtMethod method) { // addMethodrefInfo is a safe add, if constant already present it return the existing value without adding. return cPool.addMethodrefInfo( cPool.getThisClassInfo(), method.getName(), method.getSignature() ); } private CtMethod makeWriter(CtClass managedCtClass, CtField field) throws Exception { final String fieldName = field.getName(); final String writerName = FIELD_WRITER_PREFIX + fieldName; return write( managedCtClass, "public void %s(%s %s) {%n%s%n}", writerName, field.getType().getName(), fieldName, buildWriteInterceptionBodyFragment( field ) ); } private String buildWriteInterceptionBodyFragment(CtField field) throws NotFoundException { // remember: In the source text given to setBody(), the identifiers starting with $ have special meaning // $0, $1, $2, ... this and actual parameters LOG.debug("buildWriteInterceptionBodyFragment: {} {}", field.getType().getClass(), field.getType()); if ( isCtFieldACollection(field) ) { if ( field.getType().equals(cp.get(Set.class.getName())) ) { // it implements Set, so wrap accordingly with ReactiveSet: return String.format( " this.%1$s = new "+ReactiveSet.class.getName()+"($1); ", field.getName() ); } if ( field.getType().equals(cp.get(List.class.getName())) ) { // it implements List, so wrap accordingly with ReactiveList: return String.format( " this.%1$s = new "+ReactiveList.class.getName()+"($1); ", field.getName() ); } return String.format( " this.%1$s = new "+ReactiveCollection.class.getName()+"($1); ", field.getName() ); } // 2nd line will result in: ReactiveObjectUtil.notifyModification((ReactiveObject) this); // and that is fine because ASM: INVOKESTATIC org/drools/core/phreak/ReactiveObjectUtil.notifyModification (Lorg/drools/core/phreak/ReactiveObject;)V return String.format( " this.%1$s = $1;%n" + " "+ReactiveObjectUtil.class.getName()+".notifyModification($0); ", field.getName() ); } private Map<String, CtField> collectReactiveFields(CtClass managedCtClass) { final Map<String, CtField> persistentFieldMap = new HashMap<String, CtField>(); for ( CtField ctField : managedCtClass.getDeclaredFields() ) { // skip static fields, skip final fields, and skip fields added by enhancement if ( Modifier.isStatic( ctField.getModifiers() ) || ctField.getName().startsWith( DROOLS_PREFIX ) ) { continue; } // skip outer reference in inner classes if ( "this$0".equals( ctField.getName() ) ) { continue; } // optimization: skip final field, unless it is a Reactive Collection/List/... in which case we need to consider anyway: if ( Modifier.isFinal( ctField.getModifiers()) ) { if ( !isCtFieldACollection(ctField) ) { continue; } } persistentFieldMap.put( ctField.getName(), ctField ); } // CtClass.getFields() does not return private fields, while CtClass.getDeclaredFields() does not return inherit for ( CtField ctField : managedCtClass.getFields() ) { if ( ctField.getDeclaringClass().equals( managedCtClass ) ) { // Already processed above continue; } if ( Modifier.isStatic( ctField.getModifiers() ) || ctField.getName().startsWith( DROOLS_PREFIX ) ) { continue; } persistentFieldMap.put( ctField.getName(), ctField ); } return persistentFieldMap; } /** * Verify that CtField is exactly the java.util.Collection, java.util.List or java.util.Set, otherwise cannot instrument the class' field */ private boolean isCtFieldACollection(CtField ctField) { try { return ctField.getType().equals(cp.get(Collection.class.getName())) || ctField.getType().equals(cp.get(List.class.getName())) || ctField.getType().equals(cp.get(Set.class.getName())) ; } catch (NotFoundException e) { e.printStackTrace(); return false; } } }