/*
* #%L
* BroadleafCommerce Common Libraries
* %%
* Copyright (C) 2009 - 2013 Broadleaf Commerce
* %%
* 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%
*/
package org.broadleafcommerce.common.extensibility.jpa.copy;
import org.apache.commons.lang3.StringUtils;
import org.broadleafcommerce.common.extensibility.jpa.convert.BroadleafClassTransformer;
import org.broadleafcommerce.common.logging.LifeCycleEvent;
import org.broadleafcommerce.common.logging.SupportLogManager;
import org.broadleafcommerce.common.logging.SupportLogger;
import org.broadleafcommerce.common.weave.ConditionalDirectCopyTransformMemberDto;
import org.broadleafcommerce.common.weave.ConditionalDirectCopyTransformersManager;
import java.io.ByteArrayInputStream;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import javax.annotation.Resource;
import javax.persistence.EntityListeners;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import javassist.CtField;
import javassist.CtMethod;
import javassist.LoaderClassPath;
import javassist.NotFoundException;
import javassist.bytecode.AnnotationsAttribute;
import javassist.bytecode.ClassFile;
import javassist.bytecode.ConstPool;
import javassist.bytecode.annotation.Annotation;
import javassist.bytecode.annotation.AnnotationMemberValue;
import javassist.bytecode.annotation.ArrayMemberValue;
import javassist.bytecode.annotation.BooleanMemberValue;
import javassist.bytecode.annotation.MemberValue;
import javassist.bytecode.annotation.StringMemberValue;
/**
* This class transformer will copy fields, methods, and interface definitions from a source class to a target class,
* based on the xformTemplates map. It will fail if it encounters any duplicate definitions.
*
* @author Andre Azzolini (apazzolini)
* @author Jeff Fischer
*/
public class DirectCopyClassTransformer extends AbstractClassTransformer implements BroadleafClassTransformer {
protected static List<String> transformedMethods = new ArrayList<String>();
protected static List<String> annotationTransformedClasses = new ArrayList<String>();
protected SupportLogger logger;
protected String moduleName;
protected Map<String, String> xformTemplates = new HashMap<String, String>();
protected Boolean renameMethodOverlaps = false;
protected String renameMethodPrefix = "__";
protected Boolean skipOverlaps = false;
protected Map<String, String> templateTokens = new HashMap<String, String>();
@Resource(name="blDirectCopyIgnorePatterns")
protected List<DirectCopyIgnorePattern> ignorePatterns = new ArrayList<DirectCopyIgnorePattern>();
@Resource(name="blConditionalDirectCopyTransformersManager")
protected ConditionalDirectCopyTransformersManager conditionalDirectCopyTransformersManager;
public DirectCopyClassTransformer(String moduleName) {
this.moduleName = moduleName;
logger = SupportLogManager.getLogger(moduleName, this.getClass());
}
@Override
public void compileJPAProperties(Properties props, Object key) throws Exception {
// When simply copying properties over for Java class files, JPA properties do not need modification
}
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
// Lambdas and anonymous methods in Java 8 do not have a class name defined and so no transformation should be done
if (className == null) {
return null;
}
//Be careful with Apache library usage in this class (e.g. ArrayUtils). Usage will likely cause a ClassCircularityError
//under JRebel. Favor not including outside libraries and unnecessary classes.
CtClass clazz = null;
try {
boolean mySkipOverlaps = skipOverlaps;
boolean myRenameMethodOverlaps = renameMethodOverlaps;
String convertedClassName = className.replace('/', '.');
ClassPool classPool = null;
String xformKey = convertedClassName;
String[] xformVals = null;
Boolean[] xformSkipOverlaps = null;
Boolean[] xformRenameMethodOverlaps = null;
if (!xformTemplates.isEmpty()) {
if (xformTemplates.containsKey(xformKey)) {
xformVals = xformTemplates.get(xformKey).split(",");
classPool = ClassPool.getDefault();
clazz = classPool.makeClass(new ByteArrayInputStream(classfileBuffer), false);
}
} else {
if (annotationTransformedClasses.contains(convertedClassName)) {
logger.warn(convertedClassName + " has already been transformed by a previous instance of DirectCopyTransfomer. " +
"Skipping this annotation based transformation. Generally, annotation-based transformation is handled " +
"by bean id blAnnotationDirectCopyClassTransformer with template tokens being added to " +
"blDirectCopyTransformTokenMap via EarlyStageMergeBeanPostProcessor.");
}
boolean isValidPattern = true;
List<DirectCopyIgnorePattern> matchedPatterns = new ArrayList<DirectCopyIgnorePattern>();
for (DirectCopyIgnorePattern pattern : ignorePatterns) {
boolean isPatternMatch = false;
for (String patternString : pattern.getPatterns()) {
isPatternMatch = convertedClassName.matches(patternString);
if (isPatternMatch) {
break;
}
}
if (isPatternMatch) {
matchedPatterns.add(pattern);
}
isValidPattern = !(isPatternMatch && pattern.getTemplateTokenPatterns() == null);
if (!isValidPattern) {
return null;
}
}
if (isValidPattern) {
classPool = ClassPool.getDefault();
clazz = classPool.makeClass(new ByteArrayInputStream(classfileBuffer), false);
XFormParams params = reviewDirectCopyTransformAnnotations(clazz, mySkipOverlaps, myRenameMethodOverlaps, matchedPatterns);
XFormParams conditionalParams = reviewConditionalDirectCopyTransforms(convertedClassName, matchedPatterns);
if (conditionalParams != null && !conditionalParams.isEmpty()) {
params = combineXFormParams(params, conditionalParams);
}
xformVals = params.getXformVals();
xformSkipOverlaps = params.getXformSkipOverlaps();
xformRenameMethodOverlaps = params.getXformRenameMethodOverlaps();
}
}
if (xformVals != null && xformVals.length > 0) {
logger.debug(String.format("[%s] - Transform - Copying into [%s] from [%s]", LifeCycleEvent.END, xformKey,
StringUtils.join(xformVals, ",")));
// Load the destination class and defrost it so it is eligible for modifications
clazz.defrost();
int index = 0;
for (String xformVal : xformVals) {
// Load the source class
String trimmed = xformVal.trim();
classPool.appendClassPath(new LoaderClassPath(Class.forName(trimmed).getClassLoader()));
CtClass template = classPool.get(trimmed);
// Add in extra interfaces
CtClass[] interfacesToCopy = template.getInterfaces();
for (CtClass i : interfacesToCopy) {
checkInterfaces: {
CtClass[] myInterfaces = clazz.getInterfaces();
for (CtClass myInterface : myInterfaces) {
if (myInterface.getName().equals(i.getName())) {
if (xformSkipOverlaps != null && xformSkipOverlaps[index]) {
break checkInterfaces;
} else {
throw new RuntimeException("Duplicate interface detected " + myInterface.getName());
}
}
}
logger.debug(String.format("Adding interface [%s]", i.getName()));
clazz.addInterface(i);
}
}
//copy over any EntityListeners
ClassFile classFile = clazz.getClassFile();
ClassFile templateFile = template.getClassFile();
ConstPool constantPool = classFile.getConstPool();
buildClassLevelAnnotations(classFile, templateFile, constantPool);
// Copy over all declared fields from the template class
// Note that we do not copy over fields with the @NonCopiedField annotation
CtField[] fieldsToCopy = template.getDeclaredFields();
for (CtField field : fieldsToCopy) {
if (field.hasAnnotation(NonCopied.class)) {
logger.debug(String.format("Not adding field [%s]", field.getName()));
} else {
try {
CtField ctField = clazz.getDeclaredField(field.getName());
String originalSignature = ctField.getSignature();
String mySignature = field.getSignature();
if (!originalSignature.equals(mySignature)) {
throw new IllegalArgumentException("Field with name ("+field.getName()+") and signature " +
"("+field.getSignature()+") is targeted for weaving into ("+clazz.getName()+"). " +
"An incompatible field of the same name and signature of ("+ctField.getSignature()+") " +
"already exists. The field in the target class should be updated to a different name, " +
"or made to have a matching type.");
}
if (xformSkipOverlaps != null && xformSkipOverlaps[index]) {
logger.debug(String.format("Skipping overlapped field [%s]", field.getName()));
continue;
}
} catch (NotFoundException e) {
//do nothing -- field does not exist
}
logger.debug(String.format("Adding field [%s]", field.getName()));
CtField copiedField = new CtField(field, clazz);
boolean defaultConstructorFound = false;
String implClass = getImplementationType(field.getType().getName());
// Look through all of the constructors in the implClass to see
// if there is one that takes zero parameters
try {
CtConstructor[] implConstructors = classPool.get(implClass).getConstructors();
if (implConstructors != null) {
for (CtConstructor cons : implConstructors) {
if (cons.getParameterTypes().length == 0) {
defaultConstructorFound = true;
break;
}
}
}
} catch (NotFoundException e) {
// Do nothing -- if we don't find this implementation, it's probably because it's
// an array. In this case, we will not initialize the field.
}
if (defaultConstructorFound) {
clazz.addField(copiedField, "new " + implClass + "()");
} else {
clazz.addField(copiedField);
}
}
}
// Copy over all declared methods from the template class
CtMethod[] methodsToCopy = template.getDeclaredMethods();
for (CtMethod method : methodsToCopy) {
if (method.hasAnnotation(NonCopied.class)) {
logger.debug(String.format("Not adding method [%s]", method.getName()));
} else {
try {
CtClass[] paramTypes = method.getParameterTypes();
CtMethod originalMethod = clazz.getDeclaredMethod(method.getName(), paramTypes);
if (xformSkipOverlaps != null && xformSkipOverlaps[index]) {
logger.debug(String.format("Skipping overlapped method [%s]", methodDescription(originalMethod)));
continue;
}
if (transformedMethods.contains(methodDescription(originalMethod))) {
throw new RuntimeException("Method already replaced " + methodDescription(originalMethod));
} else {
logger.debug(String.format("Marking as replaced [%s]", methodDescription(originalMethod)));
transformedMethods.add(methodDescription(originalMethod));
}
logger.debug(String.format("Removing method [%s]", method.getName()));
if (xformRenameMethodOverlaps != null && xformRenameMethodOverlaps[index]) {
originalMethod.setName(renameMethodPrefix + method.getName());
} else {
clazz.removeMethod(originalMethod);
}
} catch (NotFoundException e) {
// Do nothing -- we don't need to remove a method because it doesn't exist
}
logger.debug(String.format("Adding method [%s]", method.getName()));
CtMethod copiedMethod = new CtMethod(method, clazz, null);
clazz.addMethod(copiedMethod);
}
}
index++;
}
if (xformTemplates.isEmpty()) {
annotationTransformedClasses.add(convertedClassName);
}
logger.debug(String.format("[%s] - Transform - Copying into [%s] from [%s]", LifeCycleEvent.END, xformKey,
StringUtils.join(xformVals, ",")));
return clazz.toBytecode();
}
} catch (ClassCircularityError error) {
error.printStackTrace();
throw error;
} catch (Exception e) {
throw new RuntimeException("Unable to transform class", e);
} finally {
if (clazz != null) {
try {
clazz.detach();
} catch (Exception e) {
//do nothing
}
}
}
return null;
}
/**
* Combines two {@link org.broadleafcommerce.common.extensibility.jpa.copy.DirectCopyClassTransformer.XFormParams} together with
* first passed in xformParama supercedes the second passed in parameter.
*
* @param defaultParams
* @param conditionalParams
* @return
*/
protected XFormParams combineXFormParams(XFormParams defaultParams, XFormParams conditionalParams) {
XFormParams response = new XFormParams();
Map<String, Boolean> templateSkipMap = new LinkedHashMap<>();
List<String> templates = new ArrayList<String>();
List<Boolean> skips = new ArrayList<Boolean>();
List<Boolean> renames = new ArrayList<Boolean>();
// Add the default Params
if (!defaultParams.isEmpty()) {
for (int iter = 0; iter < defaultParams.getXformVals().length; iter++) {
String defaultParam = defaultParams.getXformVals()[iter];
if (!templateSkipMap.containsKey(defaultParam)) {
templateSkipMap.put(defaultParam, true);
templates.add(defaultParam);
skips.add(defaultParams.getXformSkipOverlaps()[iter]);
renames.add(defaultParams.getXformRenameMethodOverlaps()[iter]);
}
}
}
// Only add Conditional Params if they are not already included
for (int iter = 0; iter < conditionalParams.getXformVals().length; iter++) {
String conditionalValue = conditionalParams.getXformVals()[iter];
if (!templateSkipMap.containsKey(conditionalValue)) {
templates.add(conditionalValue);
skips.add(conditionalParams.getXformSkipOverlaps()[iter]);
renames.add(conditionalParams.getXformRenameMethodOverlaps()[iter]);
}
}
// convert list to arrays
response.setXformVals(templates.toArray(new String[templates.size()]));
response.setXformSkipOverlaps(skips.toArray(new Boolean[skips.size()]));
response.setXformRenameMethodOverlaps(renames.toArray(new Boolean[renames.size()]));
return response;
}
/**
* Retrieves {@link DirectCopyTransformTypes} that are placed as annotations on classes.
* @param clazz
* @param mySkipOverlaps
* @param myRenameMethodOverlaps
* @param matchedPatterns
* @return
*/
protected XFormParams reviewDirectCopyTransformAnnotations(CtClass clazz, boolean mySkipOverlaps, boolean myRenameMethodOverlaps, List<DirectCopyIgnorePattern> matchedPatterns) {
List<?> attributes = clazz.getClassFile().getAttributes();
Iterator<?> itr = attributes.iterator();
List<String> templates = new ArrayList<String>();
List<Boolean> skips = new ArrayList<Boolean>();
List<Boolean> renames = new ArrayList<Boolean>();
XFormParams response = new XFormParams();
check: {
while(itr.hasNext()) {
Object object = itr.next();
if (AnnotationsAttribute.class.isAssignableFrom(object.getClass())) {
AnnotationsAttribute attr = (AnnotationsAttribute) object;
Annotation[] items = attr.getAnnotations();
for (Annotation annotation : items) {
String typeName = annotation.getTypeName();
if (typeName.equals(DirectCopyTransform.class.getName())) {
ArrayMemberValue arrayMember = (ArrayMemberValue) annotation.getMemberValue("value");
for (MemberValue arrayMemberValue : arrayMember.getValue()) {
AnnotationMemberValue member = (AnnotationMemberValue) arrayMemberValue;
Annotation memberAnnot = member.getValue();
ArrayMemberValue annot = (ArrayMemberValue) memberAnnot.getMemberValue("templateTokens");
for (MemberValue memberValue : annot.getValue()) {
String val = ((StringMemberValue) memberValue).getValue();
reviewTemplateTokens(matchedPatterns, templates, val);
}
BooleanMemberValue skipAnnot = (BooleanMemberValue) memberAnnot.getMemberValue("skipOverlaps");
if (skipAnnot != null) {
skips.add(skipAnnot.getValue());
} else {
skips.add(mySkipOverlaps);
}
BooleanMemberValue renameAnnot = (BooleanMemberValue) memberAnnot.getMemberValue("renameMethodOverlaps");
if (renameAnnot != null) {
renames.add(renameAnnot.getValue());
} else {
renames.add(myRenameMethodOverlaps);
}
}
response.setXformVals(templates.toArray(new String[templates.size()]));
response.setXformSkipOverlaps(skips.toArray(new Boolean[skips.size()]));
response.setXformRenameMethodOverlaps(renames.toArray(new Boolean[renames.size()]));
break check;
}
}
}
}
}
return response;
}
/**
* Retrieves {@link DirectCopyTransformTypes} that are conditionally/optionally included via properties file.
* @see org.broadleafcommerce.common.weave.ConditionalDirectCopyTransformersManager
*
* @param convertedClassName
* @param matchedPatterns
* @return
*/
protected XFormParams reviewConditionalDirectCopyTransforms(String convertedClassName, List<DirectCopyIgnorePattern> matchedPatterns) {
XFormParams response = new XFormParams();
List<String> templates = new ArrayList<String>();
List<Boolean> skips = new ArrayList<Boolean>();
List<Boolean> renames = new ArrayList<Boolean>();
if (conditionalDirectCopyTransformersManager.isEntityEnabled(convertedClassName)) {
ConditionalDirectCopyTransformMemberDto dto = conditionalDirectCopyTransformersManager.getTransformMember(convertedClassName);
for (String templateToken : dto.getTemplateTokens()) {
reviewTemplateTokens(matchedPatterns, templates, templateToken);
}
// For each of the templates being applied, ensure that they all have configured the right overlap configs
// Looping through templates and not templateTokens because 1 template token can drive multiple templates
// (e.g.
for (int i = 0; i < templates.size(); i++) {
skips.add(dto.isSkipOverlaps());
renames.add(dto.isRenameMethodOverlaps());
}
response.setXformVals(templates.toArray(new String[templates.size()]));
response.setXformSkipOverlaps(skips.toArray(new Boolean[skips.size()]));
response.setXformRenameMethodOverlaps(renames.toArray(new Boolean[renames.size()]));
}
return response;
}
protected void reviewTemplateTokens(List<DirectCopyIgnorePattern> matchedPatterns, List<String> templates, String val) {
if (val != null && templateTokens.containsKey(val)) {
templateCheck: {
for (DirectCopyIgnorePattern matchedPattern : matchedPatterns) {
for (String ignoreToken : matchedPattern.getTemplateTokenPatterns()) {
if (val.matches(ignoreToken)) {
break templateCheck;
}
}
}
String[] templateVals = templateTokens.get(val).split(",");
templates.addAll(Arrays.asList(templateVals));
}
}
}
protected void buildClassLevelAnnotations(ClassFile classFile, ClassFile templateClassFile, ConstPool constantPool) throws NotFoundException {
List<?> templateAttributes = templateClassFile.getAttributes();
Iterator<?> templateItr = templateAttributes.iterator();
Annotation templateEntityListeners = null;
while(templateItr.hasNext()) {
Object object = templateItr.next();
if (AnnotationsAttribute.class.isAssignableFrom(object.getClass())) {
AnnotationsAttribute attr = (AnnotationsAttribute) object;
Annotation[] items = attr.getAnnotations();
for (Annotation annotation : items) {
String typeName = annotation.getTypeName();
if (typeName.equals(EntityListeners.class.getName())) {
templateEntityListeners = annotation;
}
}
}
}
if (templateEntityListeners != null) {
AnnotationsAttribute annotationsAttribute = new AnnotationsAttribute(constantPool, AnnotationsAttribute.visibleTag);
List<?> attributes = classFile.getAttributes();
Iterator<?> itr = attributes.iterator();
Annotation existingEntityListeners = null;
while(itr.hasNext()) {
Object object = itr.next();
if (AnnotationsAttribute.class.isAssignableFrom(object.getClass())) {
AnnotationsAttribute attr = (AnnotationsAttribute) object;
Annotation[] items = attr.getAnnotations();
for (Annotation annotation : items) {
String typeName = annotation.getTypeName();
if (typeName.equals(EntityListeners.class.getName())) {
logger.debug("Stripping out previous EntityListeners annotation at the class level - will merge into new EntityListeners");
existingEntityListeners = annotation;
continue;
}
annotationsAttribute.addAnnotation(annotation);
}
itr.remove();
}
}
Annotation entityListeners = getEntityListeners(constantPool, existingEntityListeners, templateEntityListeners);
annotationsAttribute.addAnnotation(entityListeners);
classFile.addAttribute(annotationsAttribute);
}
}
protected Annotation getEntityListeners(ConstPool constantPool, Annotation existingEntityListeners, Annotation templateEntityListeners) {
Annotation listeners = new Annotation(EntityListeners.class.getName(), constantPool);
ArrayMemberValue listenerArray = new ArrayMemberValue(constantPool);
Set<MemberValue> listenerMemberValues = new HashSet<MemberValue>();
{
ArrayMemberValue templateListenerValues = (ArrayMemberValue) templateEntityListeners.getMemberValue("value");
listenerMemberValues.addAll(Arrays.asList(templateListenerValues.getValue()));
logger.debug("Adding template values to new EntityListeners");
}
if (existingEntityListeners != null) {
ArrayMemberValue oldListenerValues = (ArrayMemberValue) existingEntityListeners.getMemberValue("value");
listenerMemberValues.addAll(Arrays.asList(oldListenerValues.getValue()));
logger.debug("Adding previous values to new EntityListeners");
}
listenerArray.setValue(listenerMemberValues.toArray(new MemberValue[listenerMemberValues.size()]));
listeners.addMemberValue("value", listenerArray);
return listeners;
}
/**
* This method will do its best to return an implementation type for a given classname. This will allow weaving
* template classes to have initialized values.
*
* We provide default implementations for List, Map, and Set, and will attempt to utilize a default constructor for
* other classes.
*
* If the className contains an '[', we will return null.
*/
protected String getImplementationType(String className) {
if (className.equals("java.util.List")) {
return "java.util.ArrayList";
} else if (className.equals("java.util.Map")) {
return "java.util.HashMap";
} else if (className.equals("java.util.Set")) {
return "java.util.HashSet";
} else if (className.contains("[")) {
return null;
}
return className;
}
protected String methodDescription(CtMethod method) {
return method.getDeclaringClass().getName() + "|" + method.getName() + "|" + method.getSignature();
}
public Map<String, String> getXformTemplates() {
return xformTemplates;
}
public void setXformTemplates(Map<String, String> xformTemplates) {
this.xformTemplates = xformTemplates;
}
public Boolean getRenameMethodOverlaps() {
return renameMethodOverlaps;
}
public void setRenameMethodOverlaps(Boolean renameMethodOverlaps) {
this.renameMethodOverlaps = renameMethodOverlaps;
}
public String getRenameMethodPrefix() {
return renameMethodPrefix;
}
public void setRenameMethodPrefix(String renameMethodPrefix) {
this.renameMethodPrefix = renameMethodPrefix;
}
public Boolean getSkipOverlaps() {
return skipOverlaps;
}
public void setSkipOverlaps(Boolean skipOverlaps) {
this.skipOverlaps = skipOverlaps;
}
public Map<String, String> getTemplateTokens() {
return templateTokens;
}
public void setTemplateTokens(Map<String, String> templateTokens) {
this.templateTokens = templateTokens;
}
public List<DirectCopyIgnorePattern> getIgnorePatterns() {
return ignorePatterns;
}
public void setIgnorePatterns(List<DirectCopyIgnorePattern> ignorePatterns) {
this.ignorePatterns = ignorePatterns;
}
private class XFormParams {
String[] xformVals = null;
Boolean[] xformSkipOverlaps = null;
Boolean[] xformRenameMethodOverlaps = null;
public String[] getXformVals() {
return xformVals;
}
public void setXformVals(String[] xformVals) {
this.xformVals = xformVals;
}
public Boolean[] getXformSkipOverlaps() {
return xformSkipOverlaps;
}
public void setXformSkipOverlaps(Boolean[] xformSkipOverlaps) {
this.xformSkipOverlaps = xformSkipOverlaps;
}
public Boolean[] getXformRenameMethodOverlaps() {
return xformRenameMethodOverlaps;
}
public void setXformRenameMethodOverlaps(Boolean[] xformRenameMethodOverlaps) {
this.xformRenameMethodOverlaps = xformRenameMethodOverlaps;
}
public boolean isEmpty() {
return xformVals == null || xformVals.length == 0;
}
}
}