Package org.openengsb.core.weaver.service.internal.model

Source Code of org.openengsb.core.weaver.service.internal.model.ManipulationUtils

/**
* 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);
    }
}
TOP

Related Classes of org.openengsb.core.weaver.service.internal.model.ManipulationUtils

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.