/**
* Licensed to the Austrian Association for Software Tool Integration (AASTI)
* under one or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information regarding copyright
* ownership. The AASTI licenses this file to you 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.openengsb.core.weaver.service.internal.model;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import org.openengsb.core.api.model.FileWrapper;
import org.openengsb.core.api.model.OpenEngSBModel;
import org.openengsb.core.api.model.OpenEngSBModelEntry;
import org.openengsb.core.api.model.annotation.IgnoredModelField;
import org.openengsb.core.api.model.annotation.Model;
import org.openengsb.core.api.model.annotation.OpenEngSBModelId;
import org.openengsb.core.edb.api.EDBConstants;
import org.openengsb.core.util.ModelUtils;
import org.osgi.framework.Version;
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.CtPrimitiveType;
import javassist.LoaderClassPath;
import javassist.Modifier;
import javassist.NotFoundException;
/**
* This util class does the byte code manipulation to enhance domain models. It uses Javassist as code manipulation
* library.
*/
public final class ManipulationUtils {
private static final Logger LOGGER = LoggerFactory.getLogger(ManipulationUtils.class);
private static final String TAIL_FIELD = ModelUtils.MODEL_TAIL_FIELD_NAME;
private static final String LOGGER_FIELD = "_INTERNAL_LOGGER";
private static ClassPool cp = ClassPool.getDefault();
private static boolean initiated = false;
private ManipulationUtils() {
}
/**
* Appends a class loader to the class pool.
*/
public static void appendClassLoader(ClassLoader loader) {
cp.appendClassPath(new LoaderClassPath(loader));
}
private static void initiate() {
cp.importPackage("java.util");
cp.importPackage("java.lang.reflect");
cp.importPackage("org.openengsb.core.api.model");
cp.importPackage("org.slf4j");
initiated = true;
}
/**
* Try to enhance the object defined by the given byte code. Returns the enhanced class or null, if the given class
* is no model, as byte array. The version of the model will be set statical to 1.0.0. There may be class loaders
* appended, if needed.
*/
public static byte[] enhanceModel(byte[] byteCode, ClassLoader... loaders) throws IOException,
CannotCompileException {
return enhanceModel(byteCode, new Version("1.0.0"), loaders);
}
/**
* Try to enhance the object defined by the given byte code. Returns the enhanced class or null, if the given class
* is no model, as byte array. There may be class loaders appended, if needed.
*/
public static byte[] enhanceModel(byte[] byteCode, Version modelVersion, ClassLoader... loaders)
throws IOException, CannotCompileException {
CtClass cc = doModelModifications(byteCode, modelVersion, loaders);
if (cc == null) {
return null;
}
byte[] newClass = cc.toBytecode();
cc.defrost();
cc.detach();
return newClass;
}
/**
* Try to perform the actual model enhancing.
*/
private static CtClass doModelModifications(byte[] byteCode, Version modelVersion, ClassLoader... loaders) {
if (!initiated) {
initiate();
}
CtClass cc = null;
LoaderClassPath[] classloaders = new LoaderClassPath[loaders.length];
try {
InputStream stream = new ByteArrayInputStream(byteCode);
cc = cp.makeClass(stream);
if (!JavassistUtils.hasAnnotation(cc, Model.class.getName())) {
return null;
}
LOGGER.debug("Model to enhance: {}", cc.getName());
for (int i = 0; i < loaders.length; i++) {
classloaders[i] = new LoaderClassPath(loaders[i]);
cp.appendClassPath(classloaders[i]);
}
doEnhancement(cc, modelVersion);
LOGGER.info("Finished model enhancing for class {}", cc.getName());
} catch (IOException e) {
LOGGER.error("IOException while trying to enhance model", e);
} catch (RuntimeException e) {
LOGGER.error("RuntimeException while trying to enhance model", e);
} catch (CannotCompileException e) {
LOGGER.error("CannotCompileException while trying to enhance model", e);
} catch (NotFoundException e) {
LOGGER.error("NotFoundException while trying to enhance model", e);
} catch (ClassNotFoundException e) {
LOGGER.error("ClassNotFoundException while trying to enhance model", e);
} finally {
for (int i = 0; i < loaders.length; i++) {
if (classloaders[i] != null) {
cp.removeClassPath(classloaders[i]);
}
}
}
return cc;
}
/**
* Does the steps for the model enhancement.
*/
private static void doEnhancement(CtClass cc, Version modelVersion) throws CannotCompileException,
NotFoundException, ClassNotFoundException {
CtClass inter = cp.get(OpenEngSBModel.class.getName());
cc.addInterface(inter);
addFields(cc);
addGetOpenEngSBModelTail(cc);
addSetOpenEngSBModelTail(cc);
addRetrieveModelName(cc);
addRetrieveModelVersion(cc, modelVersion);
addOpenEngSBModelEntryMethod(cc);
addRemoveOpenEngSBModelEntryMethod(cc);
addRetrieveInternalModelId(cc);
addRetrieveInternalModelTimestamp(cc);
addRetrieveInternalModelVersion(cc);
addToOpenEngSBModelValues(cc);
addToOpenEngSBModelEntries(cc);
cc.setModifiers(cc.getModifiers() & ~Modifier.ABSTRACT);
}
/**
* Adds the fields for the model tail and the logger to the class.
*/
private static void addFields(CtClass clazz) throws CannotCompileException, NotFoundException {
CtField tail = CtField.make(String.format("private Map %s = new HashMap();", TAIL_FIELD), clazz);
clazz.addField(tail);
String loggerDefinition = "private static final Logger %s = LoggerFactory.getLogger(%s.class.getName());";
CtField logger = CtField.make(String.format(loggerDefinition, LOGGER_FIELD, clazz.getName()), clazz);
clazz.addField(logger);
}
/**
* Adds the getOpenEngSBModelTail method to the class.
*/
private static void addGetOpenEngSBModelTail(CtClass clazz) throws CannotCompileException, NotFoundException {
CtClass[] params = generateClassField();
CtMethod method = new CtMethod(cp.get(List.class.getName()), "getOpenEngSBModelTail", params, clazz);
StringBuilder body = new StringBuilder();
body.append(createTrace("Called getOpenEngSBModelTail"))
.append(String.format("return new ArrayList(%s.values());", TAIL_FIELD));
method.setBody(createMethodBody(body.toString()));
clazz.addMethod(method);
}
/**
* Adds the setOpenEngSBModelTail method to the class.
*/
private static void addSetOpenEngSBModelTail(CtClass clazz) throws CannotCompileException, NotFoundException {
CtClass[] params = generateClassField(List.class);
CtMethod method = new CtMethod(CtClass.voidType, "setOpenEngSBModelTail", params, clazz);
StringBuilder builder = new StringBuilder();
builder.append(createTrace("Called setOpenEngSBModelTail"))
.append("if($1 != null) {for(int i = 0; i < $1.size(); i++) {")
.append("OpenEngSBModelEntry entry = (OpenEngSBModelEntry) $1.get(i);")
.append(String.format("%s.put(entry.getKey(), entry); } }", TAIL_FIELD));
method.setBody(createMethodBody(builder.toString()));
clazz.addMethod(method);
}
/**
* Adds the retreiveModelName method to the class.
*/
private static void addRetrieveModelName(CtClass clazz) throws CannotCompileException, NotFoundException {
CtClass[] params = generateClassField();
CtMethod method = new CtMethod(cp.get(String.class.getName()), "retrieveModelName", params, clazz);
StringBuilder builder = new StringBuilder();
builder.append(createTrace("Called retrieveModelName"))
.append(String.format("return \"%s\";", clazz.getName()));
method.setBody(createMethodBody(builder.toString()));
clazz.addMethod(method);
}
/**
* Adds the retreiveModelName method to the class.
*/
private static void addRetrieveModelVersion(CtClass clazz, Version modelVersion) throws CannotCompileException,
NotFoundException {
CtClass[] params = generateClassField();
CtMethod method = new CtMethod(cp.get(String.class.getName()), "retrieveModelVersion", params, clazz);
StringBuilder builder = new StringBuilder();
builder.append(createTrace("Called retrieveModelVersion"))
.append(String.format("return \"%s\";", modelVersion.toString()));
method.setBody(createMethodBody(builder.toString()));
clazz.addMethod(method);
}
/**
* Adds the addOpenEngSBModelEntry method to the class.
*/
private static void addOpenEngSBModelEntryMethod(CtClass clazz) throws NotFoundException, CannotCompileException {
CtClass[] params = generateClassField(OpenEngSBModelEntry.class);
CtMethod method = new CtMethod(CtClass.voidType, "addOpenEngSBModelEntry", params, clazz);
StringBuilder builder = new StringBuilder();
builder.append(createTrace("Called addOpenEngSBModelEntry"))
.append(String.format("if ($1 != null) { %s.put($1.getKey(), $1);}", TAIL_FIELD));
method.setBody(createMethodBody(builder.toString()));
clazz.addMethod(method);
}
/**
* Adds the removeOpenEngSBModelEntry method to the class.
*/
private static void addRemoveOpenEngSBModelEntryMethod(CtClass clazz) throws NotFoundException,
CannotCompileException {
CtClass[] params = generateClassField(String.class);
CtMethod method = new CtMethod(CtClass.voidType, "removeOpenEngSBModelEntry", params, clazz);
StringBuilder builder = new StringBuilder();
builder.append(createTrace("Called removeOpenEngSBModelEntry"))
.append(String.format("if ($1 != null) { %s.remove($1);}", TAIL_FIELD));
method.setBody(createMethodBody(builder.toString()));
clazz.addMethod(method);
}
/**
* Adds the retrieveInternalModelId method to the class.
*/
private static void addRetrieveInternalModelId(CtClass clazz) throws NotFoundException,
CannotCompileException {
CtField modelIdField = null;
CtClass temp = clazz;
while (temp != null) {
for (CtField field : temp.getDeclaredFields()) {
if (JavassistUtils.hasAnnotation(field, OpenEngSBModelId.class.getName())) {
modelIdField = field;
break;
}
}
temp = temp.getSuperclass();
}
CtClass[] params = generateClassField();
CtMethod valueMethod = new CtMethod(cp.get(Object.class.getName()), "retrieveInternalModelId", params, clazz);
StringBuilder builder = new StringBuilder();
builder.append(createTrace("Called retrieveInternalModelId"));
CtMethod idFieldGetter = getFieldGetter(modelIdField, clazz);
if (modelIdField == null || idFieldGetter == null) {
builder.append("return null;");
} else {
builder.append(String.format("return %s();", idFieldGetter.getName()));
}
valueMethod.setBody(createMethodBody(builder.toString()));
clazz.addMethod(valueMethod);
CtMethod nameMethod =
new CtMethod(cp.get(String.class.getName()), "retrieveInternalModelIdName", generateClassField(), clazz);
if (modelIdField == null) {
nameMethod.setBody(createMethodBody("return null;"));
} else {
nameMethod.setBody(createMethodBody("return \"" + modelIdField.getName() + "\";"));
}
clazz.addMethod(nameMethod);
}
/**
* Adds the retrieveInternalModelTimestamp method to the class.
*/
private static void addRetrieveInternalModelTimestamp(CtClass clazz) throws NotFoundException,
CannotCompileException {
CtClass[] params = generateClassField();
CtMethod method = new CtMethod(cp.get(Long.class.getName()), "retrieveInternalModelTimestamp", params, clazz);
StringBuilder builder = new StringBuilder();
builder.append(createTrace("Called retrieveInternalModelTimestamp"))
.append(String.format("return (Long) ((OpenEngSBModelEntry)%s.get(\"%s\")).getValue();",
TAIL_FIELD, EDBConstants.MODEL_TIMESTAMP));
method.setBody(createMethodBody(builder.toString()));
clazz.addMethod(method);
}
/**
* Adds the retrieveInternalModelVersion method to the class.
*/
private static void addRetrieveInternalModelVersion(CtClass clazz) throws NotFoundException,
CannotCompileException {
CtClass[] params = generateClassField();
CtMethod method = new CtMethod(cp.get(Integer.class.getName()), "retrieveInternalModelVersion", params, clazz);
StringBuilder builder = new StringBuilder();
builder.append(createTrace("Called retrieveInternalModelVersion"))
.append(String.format("return (Integer) ((OpenEngSBModelEntry)%s.get(\"%s\")).getValue();",
TAIL_FIELD, EDBConstants.MODEL_VERSION));
method.setBody(createMethodBody(builder.toString()));
clazz.addMethod(method);
}
/**
* Adds the toOpenEngSBModelValues method to the class.
*/
private static void addToOpenEngSBModelValues(CtClass clazz) throws NotFoundException,
CannotCompileException, ClassNotFoundException {
StringBuilder builder = new StringBuilder();
CtClass[] params = generateClassField();
CtMethod m = new CtMethod(cp.get(List.class.getName()), "toOpenEngSBModelValues", params, clazz);
builder.append(createTrace("Add elements of the model tail"))
.append("List elements = new ArrayList();\n")
.append(createTrace("Add properties of the model"))
.append(createModelEntryList(clazz))
.append("return elements;");
m.setBody(createMethodBody(builder.toString()));
clazz.addMethod(m);
}
/**
* Adds the getOpenEngSBModelEntries method to the class.
*/
private static void addToOpenEngSBModelEntries(CtClass clazz) throws NotFoundException,
CannotCompileException, ClassNotFoundException {
CtClass[] params = generateClassField();
CtMethod m = new CtMethod(cp.get(List.class.getName()), "toOpenEngSBModelEntries", params, clazz);
StringBuilder builder = new StringBuilder();
builder.append(createTrace("Add elements of the model tail"))
.append("List elements = new ArrayList();\n")
.append(String.format("elements.addAll(%s.values());\n", TAIL_FIELD))
.append("elements.addAll(toOpenEngSBModelValues());\n")
.append("return elements;");
m.setBody(createMethodBody(builder.toString()));
clazz.addMethod(m);
}
/**
* Generates the list of OpenEngSBModelEntries which need to be added. Also adds the entries of the super classes.
*/
private static String createModelEntryList(CtClass clazz) throws NotFoundException, CannotCompileException {
StringBuilder builder = new StringBuilder();
CtClass tempClass = clazz;
while (tempClass != null) {
for (CtField field : tempClass.getDeclaredFields()) {
String property = field.getName();
if (property.equals(TAIL_FIELD) || property.equals(LOGGER_FIELD)
|| JavassistUtils.hasAnnotation(field, IgnoredModelField.class.getName())) {
builder.append(createTrace(String.format("Skip property '%s' of the model", property)));
continue;
}
builder.append(handleField(field, clazz));
}
tempClass = tempClass.getSuperclass();
}
return builder.toString();
}
/**
* Analyzes the given field and add logic based on the type of the field.
*/
private static String handleField(CtField field, CtClass clazz) throws NotFoundException, CannotCompileException {
StringBuilder builder = new StringBuilder();
CtClass fieldType = field.getType();
String property = field.getName();
if (fieldType.equals(cp.get(File.class.getName()))) {
return handleFileField(property, clazz);
}
CtMethod getter = getFieldGetter(field, clazz);
if (getter == null) {
LOGGER.warn(String.format("Ignoring property '%s' since there is no getter for it defined", property));
} else if (fieldType.isPrimitive()) {
builder.append(createTrace(String.format("Handle primitive type property '%s'", property)));
CtPrimitiveType primitiveType = (CtPrimitiveType) fieldType;
String wrapperName = primitiveType.getWrapperName();
builder.append(String.format(
"elements.add(new OpenEngSBModelEntry(\"%s\", %s.valueOf(%s()), %s.class));\n",
property, wrapperName, getter.getName(), wrapperName));
} else {
builder.append(createTrace(String.format("Handle property '%s'", property)))
.append(String.format("elements.add(new OpenEngSBModelEntry(\"%s\", %s(), %s.class));\n",
property, getter.getName(), fieldType.getName()));
}
return builder.toString();
}
/**
* Creates the logic which is needed to handle fields which are File types, since they need special treatment.
*/
private static String handleFileField(String property, CtClass clazz) throws NotFoundException,
CannotCompileException {
String wrapperName = property + "wrapper";
StringBuilder builder = new StringBuilder();
builder.append(createTrace(String.format("Handle File type property '%s'", property)))
.append(String.format("if(%s == null) {", property))
.append(String.format("elements.add(new OpenEngSBModelEntry(\"%s\"", wrapperName))
.append(", null, FileWrapper.class));}\n else {")
.append(String.format("FileWrapper %s = new FileWrapper(%s);\n", wrapperName, property))
.append(String.format("%s.serialize();\n", wrapperName))
.append(String.format("elements.add(new OpenEngSBModelEntry(\"%s\",%s,%s.getClass()));}\n",
wrapperName, wrapperName, wrapperName));
addFileFunction(clazz, property);
return builder.toString();
}
/**
* Returns the getter to a given field of the given class object and returns null if there is no getter for the
* given field defined.
*/
private static CtMethod getFieldGetter(CtField field, CtClass clazz) throws NotFoundException {
if (field == null) {
return null;
}
return getFieldGetter(field, clazz, false);
}
/**
* Returns the getter method in case it exists or returns null if this is not the case. The failover parameter is
* needed to deal with boolean types, since it should be allowed to allow getters in the form of "isXXX" or
* "getXXX".
*/
private static CtMethod getFieldGetter(CtField field, CtClass clazz, boolean failover) throws NotFoundException {
CtMethod method = new CtMethod(field.getType(), "descCreateMethod", new CtClass[]{}, clazz);
String desc = method.getSignature();
String getter = getPropertyGetter(field, failover);
try {
return clazz.getMethod(getter, desc);
} catch (NotFoundException e) {
// try once again with getXXX instead of isXXX
if (isBooleanType(field)) {
return getFieldGetter(field, clazz, true);
}
LOGGER.debug(String.format("No getter with the name '%s' and the description '%s' found", getter, desc));
return null;
}
}
/**
* Returns the name of the corresponding getter to a properties name and type. The failover is needed to support
* both getter name types for boolean properties.
*/
private static String getPropertyGetter(CtField field, boolean failover) throws NotFoundException {
String property = field.getName();
if (!failover && isBooleanType(field)) {
return String.format("is%s%s", Character.toUpperCase(property.charAt(0)), property.substring(1));
} else {
return String.format("get%s%s", Character.toUpperCase(property.charAt(0)), property.substring(1));
}
}
/**
* Returns true if the given field is a boolean type (primitive or wrapper) and false if it is not the case
*/
private static boolean isBooleanType(CtField field) throws NotFoundException {
String typeName = field.getType().getName();
return typeName.equals("java.lang.Boolean") || typeName.equals("boolean");
}
/**
* Generates a CtClass field out of a Class field.
*/
private static CtClass[] generateClassField(Class<?>... classes) throws NotFoundException {
CtClass[] result = new CtClass[classes.length];
for (int i = 0; i < classes.length; i++) {
result[i] = cp.get(classes[i].getName());
}
return result;
}
/**
* Adds the functionality that the models can handle File objects themselves.
*/
private static void addFileFunction(CtClass clazz, String property)
throws NotFoundException, CannotCompileException {
String wrapperName = property + "wrapper";
String funcName = "set";
funcName = funcName + Character.toUpperCase(wrapperName.charAt(0));
funcName = funcName + wrapperName.substring(1);
String setterName = "set";
setterName = setterName + Character.toUpperCase(property.charAt(0));
setterName = setterName + property.substring(1);
CtClass[] params = generateClassField(FileWrapper.class);
CtMethod newFunc = new CtMethod(CtClass.voidType, funcName, params, clazz);
newFunc.setBody("{ " + setterName + "($1.returnFile());\n }");
clazz.addMethod(newFunc);
}
/**
* Returns the string which represents a logger tracing call with the given message
*/
private static String createTrace(String message) {
return String.format("%s.trace(\"%s\");\n", LOGGER_FIELD, message);
}
/**
* Wraps a body string with a beginning and ending braces
*/
private static String createMethodBody(String body) {
return String.format("{%s}", body);
}
}