All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.iternine.jeppetto.dao.DAOBuilder Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2011-2017 Jeppetto and Jonathan Thompson
 *
 * 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.
 */

package org.iternine.jeppetto.dao;


import org.iternine.jeppetto.dao.annotation.DataAccessMethod;
import org.iternine.jeppetto.enhance.ClassLoadingUtil;

import com.yammer.metrics.core.TimerContext;
import javassist.CannotCompileException;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtField;
import javassist.CtMethod;
import javassist.CtNewConstructor;
import javassist.CtNewMethod;
import javassist.NotFoundException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;


public class DAOBuilder {

    //-------------------------------------------------------------
    // Variables - Private
    //-------------------------------------------------------------

    private static final AtomicInteger count = new AtomicInteger(0);
    private static final Logger logger = LoggerFactory.getLogger(DAOBuilder.class);


    //-------------------------------------------------------------
    // Methods - Public
    //-------------------------------------------------------------


    public static > I buildDAO(Class modelClass,
                                                                  Class daoInterface,
                                                                  Class> partialDAOClass,
                                                                  Map daoProperties) {
        return buildDAO(modelClass, daoInterface, partialDAOClass, daoProperties, null);
    }


    public static > I buildDAO(Class modelClass,
                                                                  Class daoInterface,
                                                                  Class> partialDAOClass,
                                                                  Map daoProperties,
                                                                  AccessControlContextProvider accessControlContextProvider) {
        if (AccessControlDAO.class.isAssignableFrom(daoInterface)) {
            // Verify the DAO implementation can support AccessControlDAO...

            // ...if not assignable from AccessControlDAO, then fail...
            if (!AccessControlDAO.class.isAssignableFrom(partialDAOClass)) {
                throw new RuntimeException("Concrete DAO doesn't support AccessControlDAO (expected by the DAO interface)");
            }

            // ...if no matching constructor, then fail...
            try {
                partialDAOClass.getDeclaredConstructor(Class.class, Map.class, AccessControlContextProvider.class);
            } catch (Exception e) {
                throw new RuntimeException("Concrete DAO doesn't support AccessControlDAO (expected by the DAO interface)");
            }

            // TODO: validate AccessControlDAO methods exist

            if (accessControlContextProvider == null) {
                throw new RuntimeException("No AccessControlContextProvider specified.");
            }
        }

        Class fullDAOClass = completeDAO(modelClass, daoInterface, partialDAOClass, accessControlContextProvider != null,
                                                      daoProperties != null && Boolean.parseBoolean((String) daoProperties.get("enableMetrics")));

        try {
            if (accessControlContextProvider != null) {
                Constructor constructor = fullDAOClass.getDeclaredConstructor(Class.class, Map.class,
                                                                                           AccessControlContextProvider.class);

                return constructor.newInstance(modelClass, daoProperties, accessControlContextProvider);
            } else {
                Constructor constructor = fullDAOClass.getDeclaredConstructor(Class.class, Map.class);

                return constructor.newInstance(modelClass, daoProperties);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }


    //-------------------------------------------------------------
    // Methods - Private
    //-------------------------------------------------------------

    private static > Class completeDAO(Class modelClass,
                                                                                       Class daoInterface,
                                                                                       Class> partialDAOClass,
                                                                                       boolean accessControlEnabled,
                                                                                       boolean metricsEnabled) {
        try {
            ClassPool pool = ClassPool.getDefault();

            pool.insertClassPath(new ClassClassPath(daoInterface));

            CtClass fullDAOCtClass = pool.makeClass(String.format("%s$%d", daoInterface.getName(), count.incrementAndGet()));
            CtClass partialDAOCtClass = pool.get(partialDAOClass.getName());
            CtClass daoInterfaceCtClass = pool.get(daoInterface.getName());

            fullDAOCtClass.setSuperclass(partialDAOCtClass);
            fullDAOCtClass.addInterface(daoInterfaceCtClass);

            buildConstructor(fullDAOCtClass, accessControlEnabled);
            buildNeededMethods(fullDAOCtClass, partialDAOCtClass, daoInterfaceCtClass, modelClass, accessControlEnabled, metricsEnabled);

            return ClassLoadingUtil.toClass(fullDAOCtClass);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }


    private static void buildConstructor(CtClass fullDAOCtClass, boolean accessControlEnabled)
            throws CannotCompileException {
        String constructorCode;

        if (accessControlEnabled) {
            constructorCode = String.format("public %s(Class entityClass, java.util.Map daoProperties, org.iternine.jeppetto.dao.AccessControlContextProvider accessControlContextProvider) { " +
                                            "    super(entityClass, daoProperties, accessControlContextProvider); " +
                                            "}",
                                            fullDAOCtClass.getSimpleName());
        } else {
            constructorCode = String.format("public %s(Class entityClass, java.util.Map daoProperties) { " +
                                            "    super(entityClass, daoProperties); " +
                                            "}",
                                            fullDAOCtClass.getSimpleName());
        }

        fullDAOCtClass.addConstructor(CtNewConstructor.make(constructorCode, fullDAOCtClass));
    }


    private static  void buildNeededMethods(CtClass fullDAOCtClass, CtClass partialDAOCtClass, CtClass daoInterfaceCtClass,
                                               Class modelClass, boolean accessControlEnabled, boolean metricsEnabled)
            throws CannotCompileException, ClassNotFoundException, NotFoundException {
        // Look through all methods to find which ones need to be implemented.
        for (CtMethod interfaceMethod : daoInterfaceCtClass.getMethods()) {
            try {
                CtMethod daoMethod = partialDAOCtClass.getMethod(interfaceMethod.getName(), interfaceMethod.getSignature());

                // The method is present in the partial class.
                if (!Modifier.isAbstract(daoMethod.getModifiers())) {
                    if (metricsEnabled && shouldAddMetricsToMethod(interfaceMethod, daoInterfaceCtClass)) {
                        logger.debug("Generating metrics delegate for method " + daoMethod.getName() + "()");

                        CtMethod delegator = CtNewMethod.delegator(daoMethod, fullDAOCtClass);

                        insertMetrics(fullDAOCtClass, delegator, daoInterfaceCtClass);

                        fullDAOCtClass.addMethod(delegator);
                    }

                    continue;
                }

                // If we're here, the method does not have a concrete implementation.  Fall through to implement it.
            } catch (NotFoundException ignore) {
                // If we're here, the method is not present in the partial class.  Fall through to implement it.
            }

            CtMethod daoMethod = implementMethod(fullDAOCtClass, interfaceMethod, modelClass, accessControlEnabled);

            if (metricsEnabled) {
                insertMetrics(fullDAOCtClass, daoMethod, daoInterfaceCtClass);
            }
        }
    }


    private static boolean shouldAddMetricsToMethod(CtMethod interfaceMethod, CtClass daoInterfaceCtClass) {
        // Check if the method is directly declared in the interface.  If yes, it was likely implemented for
        // performance or to accomplish something Jeppetto doesn't offer and we should add metrics.
        try {
            daoInterfaceCtClass.getDeclaredMethod(interfaceMethod.getName(), interfaceMethod.getParameterTypes());

            return true;
        } catch (NotFoundException ignore) {
        }

        // Check if the method is directly declared in the GenericDAO interface.  If yes, it is in the set of
        // common DAO methods that we want metrics for.
        try {
            ClassPool.getDefault().get(GenericDAO.class.getName()).getDeclaredMethod(interfaceMethod.getName(),
                                                                                     interfaceMethod.getParameterTypes());

            return true;
        } catch (NotFoundException ignore) {
        }

        return false;
    }


    private static void insertMetrics(CtClass fullDAOCtClass, CtMethod daoMethod, CtClass daoInterfaceCtClass)
            throws CannotCompileException, NotFoundException {
        final String timerField = createTimerField(fullDAOCtClass, daoMethod, daoInterfaceCtClass);

        logger.debug("Adding metrics to method " + daoMethod.getName() + "()");

        daoMethod.addLocalVariable("__tc", ClassPool.getDefault().get(TimerContext.class.getName()));
        daoMethod.insertBefore("__tc = this." + timerField + ".time();");
        daoMethod.insertAfter("__tc.stop();", false);
    }


    private static String createTimerField(CtClass fullCtClass, CtMethod daoMethod, CtClass daoInterfaceCtClass)
            throws CannotCompileException {
        String timerField = "__" + daoMethod.getName() + "Timer";
        String timerDeclaration = "private final com.yammer.metrics.core.Timer " + timerField
                                  + "  = com.yammer.metrics.Metrics.newTimer(" + daoInterfaceCtClass.getName() + ".class, "
                                  +                                          "\"" + daoMethod.getName() + "\");";

        logger.debug("Adding Timer field: " + timerField);

        fullCtClass.addField(CtField.make(timerDeclaration, fullCtClass));

        return timerField;
    }


    private static  CtMethod implementMethod(CtClass fullDAOCtClass, CtMethod interfaceMethod,
                                                Class modelClass, boolean accessControlEnabled)
            throws CannotCompileException, ClassNotFoundException {
        CtMethod daoMethod = CtNewMethod.copy(interfaceMethod, fullDAOCtClass, null);
        StringBuilder sb = new StringBuilder();
        DataAccessMethod dataAccessMethod;
        OperationType operationType;

        sb.append("{\n"
                  + "    java.util.Iterator argsIterator = java.util.Arrays.asList($args).iterator();\n"
                  + "    org.iternine.jeppetto.dao.QueryModel queryModel = new org.iternine.jeppetto.dao.QueryModel();\n\n");

        if ((dataAccessMethod = (DataAccessMethod) interfaceMethod.getAnnotation(DataAccessMethod.class)) != null) {
            operationType = buildQueryModelFromAnnotation(dataAccessMethod, sb);

            if (accessControlEnabled) {
                if (dataAccessMethod.useAccessControlContextArgument()) {
                    sb.append("    queryModel.setAccessControlContext((org.iternine.jeppetto.dao.AccessControlContext) argsIterator.next());\n\n");
                } else {
                    sb.append("    queryModel.setAccessControlContext(getAccessControlContextProvider().getCurrent());\n\n");
                }
            }
        } else {
            // deal w/ '...As()' case
            operationType = buildQueryModelFromMethodName(interfaceMethod.getName(), sb);

            if (accessControlEnabled) {
                if (interfaceMethod.getName().endsWith("As")) {
                    sb.append("    queryModel.setAccessControlContext((org.iternine.jeppetto.dao.AccessControlContext) argsIterator.next());\n\n");
                } else {
                    sb.append("    queryModel.setAccessControlContext(getAccessControlContextProvider().getCurrent());\n\n");
                }
            }
        }

        switch (operationType) {
        case Read:
            buildReturnClause(interfaceMethod, sb, modelClass);

            break;
        case Update:
            buildUpdateClause(sb);

            break;
        case Delete:
            buildDeleteClause(sb);

            break;
        }

        sb.append('\n').append('}');

        if (logger.isDebugEnabled()) {
            logDerivedMethod(interfaceMethod, sb);
        }

        try {
            daoMethod.setBody(sb.toString());
        } catch (CannotCompileException e) {
            throw new RuntimeException("Unable to add method:\n" + sb.toString(), e);
        }

        fullDAOCtClass.addMethod(daoMethod);

        return daoMethod;
    }


    private static OperationType buildQueryModelFromAnnotation(DataAccessMethod dataAccessMethod, StringBuilder sb) {
        if (dataAccessMethod.operation() == OperationType.Update) {
            sb.append("    org.iternine.jeppetto.dao.updateobject.UpdateObject updateObject = (org.iternine.jeppetto.dao.updateobject.UpdateObject) argsIterator.next();\n\n");
        }

        if (dataAccessMethod.conditions() != null && dataAccessMethod.conditions().length > 0) {
            for (org.iternine.jeppetto.dao.annotation.Condition conditionAnnotation : dataAccessMethod.conditions()) {
                sb.append(String.format("    queryModel.addCondition(buildCondition(\"%s\", org.iternine.jeppetto.dao.ConditionType.%s, argsIterator));\n",
                                        conditionAnnotation.field(), conditionAnnotation.type().name()));
            }

            sb.append('\n');
        }

        if (dataAccessMethod.associations() != null && dataAccessMethod.associations().length > 0) {
            for (org.iternine.jeppetto.dao.annotation.Association associationAnnotation : dataAccessMethod.associations()) {
                for (org.iternine.jeppetto.dao.annotation.Condition conditionAnnotation : associationAnnotation.conditions()) {
                    sb.append(String.format("    queryModel.addAssociationCondition(\"%s\", buildCondition(\"%s\", org.iternine.jeppetto.dao.ConditionType.%s, argsIterator));\n",
                                            associationAnnotation.field(), conditionAnnotation.field(), conditionAnnotation.type().name()));
                }
            }

            sb.append('\n');
        }

        if (dataAccessMethod.projections() != null && dataAccessMethod.projections().length > 0) {
            sb.append(String.format("    queryModel.setProjection(buildProjection(\"%s\", org.iternine.jeppetto.dao.ProjectionType.%s, argsIterator));\n\n",
                                    dataAccessMethod.projections()[0].field(), dataAccessMethod.projections()[0].type().name()));
        }

        if (dataAccessMethod.sorts() != null && dataAccessMethod.sorts().length > 0) {
            for (org.iternine.jeppetto.dao.annotation.Sort sort : dataAccessMethod.sorts()) {
                sb.append(String.format("    queryModel.addSort(org.iternine.jeppetto.dao.SortDirection.%s, \"%s\");\n", sort.direction().name(), sort.field()));
            }

            sb.append('\n');
        }

        if (dataAccessMethod.limitResults()) {
            sb.append("    queryModel.setMaxResults(((Integer) argsIterator.next()).intValue());\n\n");
        }

        if (dataAccessMethod.skipResults()) {
            sb.append("    queryModel.setFirstResult(((Integer) argsIterator.next()).intValue());\n\n");
        }

        return dataAccessMethod.operation();
    }


    /**
     * We build 'findBy', 'countBy', 'updateBy', and 'deleteBy' QueryModels in the following way:
     * 

* findBy*[OrderBy*][AndLimit][AndSkip] * countBy*[OrderBy*][AndLimit][AndSkip] * updateBy* * deleteBy* *

* Query parts are of the following forms: *

* : column value must equal positional argument value * Equal : column value must equal positional argument value * NotEqual : column value must not equal positional argument value * GreaterThan : column value must be greater than positional argument value * GreaterThanEqual : column value must be greater than or equal to positional argument value * LessThan : column value must be less than positional argument value * LessThanEqual : column value must be less than or equal to positional argument value * Between : column value must be between the next two positional argument values * Within : column value must be in a java.util.Collection of values * NotWithin : column value must not be in a java.util.Collection of values * IsNull : column value must be null * IsNotNull : column value must not be null *

* Additionally, strings of the following form: *

* HavingWith* *

* Can be specified to find results that have associations to other objects with the interpreted query parts. * Multiple associations can be specified, but note that each hangs off the root object, not each other * (e.g. List findByHavingFontWithColorHavingFormattingWithJustification() would assume an association * between Word and both Font and Formatting, not Word to Font to Formatting). *

* Order parts are of the following forms: *

* Asc : order by the column value ascending * Desc : order by the column value descending * : order by the column value ascending *

* Limiting the result size and pagination are indicated by the AndLimit and AndSkip phrases. These must be * at the end of the DAO method name, and be in that order. It is acceptable to omit one or the other if it * isn't needed. Both clauses expect to find an integer value in the parameter list after all the other * parameters are specified. For example, to paginate through a potentially long list of people with the same last * name, one could declare a method findBySurnameAndLimitAndSkip(String surname, int limitCount, int skipCount) * * @param methodName of the method to construct a QueryModel from * @param sb the StringBuilder to place the resulting logic into * * @return the OperationType that the methodName refers to. */ private static OperationType buildQueryModelFromMethodName(String methodName, StringBuilder sb) { String queryString; OperationType operationType; if (methodName.startsWith("findBy")) { queryString = methodName.substring("findBy".length(), methodName.length() - (methodName.endsWith("As") ? "As".length() : 0)); operationType = OperationType.Read; } else if (methodName.startsWith("countBy")) { sb.append(" queryModel.setProjection(buildProjection(\"\", org.iternine.jeppetto.dao.ProjectionType.RowCount, argsIterator));\n\n"); queryString = methodName.substring("countBy".length(), methodName.length() - (methodName.endsWith("As") ? "As".length() : 0)); operationType = OperationType.Read; } else if (methodName.startsWith("updateBy")) { queryString = methodName.substring("updateBy".length(), methodName.length() - (methodName.endsWith("As") ? "As".length() : 0)); operationType = OperationType.Update; sb.append(" org.iternine.jeppetto.dao.updateobject.UpdateObject updateObject = (org.iternine.jeppetto.dao.updateobject.UpdateObject) argsIterator.next();\n\n"); } else if (methodName.startsWith("deleteBy")) { queryString = methodName.substring("deleteBy".length(), methodName.length() - (methodName.endsWith("As") ? "As".length() : 0)); operationType = OperationType.Delete; } else { throw new UnsupportedOperationException("Don't know how to handle '" + methodName + "'"); } int orderByIndex = queryString.indexOf("OrderBy"); boolean limitResults; boolean skipResults; if (skipResults = queryString.endsWith("AndSkip")) { queryString = queryString.substring(0, queryString.length() - "AndSkip".length()); } if (limitResults = queryString.endsWith("AndLimit")) { queryString = queryString.substring(0, queryString.length() - "AndLimit".length()); } String[] queryParts; String orderParts; if (orderByIndex == -1) { queryParts = queryString.split("Having"); orderParts = null; } else { queryParts = queryString.substring(0, orderByIndex).split("Having"); orderParts = queryString.substring(orderByIndex + "OrderBy".length()); } if (queryParts[0] != null) { if (queryParts[0].length() > 0) { String[] conditionStrings = queryParts[0].split("And"); for (String conditionString : conditionStrings) { String conditionName = getConditionNameFromString(conditionString); sb.append(String.format(" queryModel.addCondition(buildCondition(\"%s\", org.iternine.jeppetto.dao.ConditionType.%s, argsIterator));\n", pruneFieldNameFromString(conditionString, conditionName), conditionName)); } sb.append('\n'); } for (int i = 1; i < queryParts.length; i++) { String associationString = queryParts[i]; int withIndex = associationString.indexOf("With"); // If -1, exception. Okay. String[] conditionStrings = associationString.substring(withIndex + 4, associationString.length()).split("And"); for (String conditionString : conditionStrings) { String conditionName = getConditionNameFromString(conditionString); sb.append(String.format(" queryModel.addAssociationCondition(\"%s\", buildCondition(\"%s\", org.iternine.jeppetto.dao.ConditionType.%s, argsIterator));\n", Character.toLowerCase(associationString.charAt(0)) + associationString.substring(1, withIndex), pruneFieldNameFromString(conditionString, conditionName), conditionName)); } sb.append('\n'); } } if (orderParts != null && operationType == OperationType.Read) { for (String orderPart : orderParts.split("And")) { SortDirection sortDirection; String fieldName; if (orderPart.endsWith("Desc")) { sortDirection = SortDirection.Descending; fieldName = pruneFieldNameFromString(orderPart, "Desc"); } else { sortDirection = SortDirection.Ascending; fieldName = pruneFieldNameFromString(orderPart, "Asc"); } sb.append(String.format(" queryModel.addSort(org.iternine.jeppetto.dao.SortDirection.%s, \"%s\");\n", sortDirection.name(), fieldName)); } sb.append('\n'); } if (limitResults) { sb.append(" queryModel.setMaxResults(((Integer) argsIterator.next()).intValue());\n\n"); } if (skipResults) { sb.append(" queryModel.setFirstResult(((Integer) argsIterator.next()).intValue());\n\n"); } return operationType; } private static String getConditionNameFromString(String conditionString) { for (ConditionType conditionType : ConditionType.values()) { if (conditionString.endsWith(conditionType.name())) { return conditionType.name(); } } // If we don't find a matching ConditionType, assume "Equal" return ConditionType.Equal.name(); } private static String pruneFieldNameFromString(String conditionString, String trailingPart) { StringBuilder fieldName = new StringBuilder(); if (conditionString.endsWith(trailingPart)) { fieldName.append(conditionString.substring(0, conditionString.length() - trailingPart.length())); } else { fieldName.append(conditionString); } fieldName.setCharAt(0, Character.toLowerCase(conditionString.charAt(0))); return fieldName.toString(); } private static void buildReturnClause(CtMethod method, StringBuilder sb, Class modelClass) { try { String returnTypeName = method.getReturnType().getName(); if (modelClass.getName().equals(returnTypeName)) { if (method.getExceptionTypes().length > 0) { sb.append("\n return ($r) findUniqueUsingQueryModel(queryModel);"); } else { sb.append( " try {\n" + " return ($r) findUniqueUsingQueryModel(queryModel);\n" + " } catch (org.iternine.jeppetto.dao.NoSuchItemException e) {\n" + " return null;\n" + " }"); } } else if ("java.util.Set".equals(returnTypeName)) { sb.append( " java.util.Set result = new java.util.HashSet();\n" + " for (java.util.Iterator iterator = findUsingQueryModel(queryModel).iterator(); iterator.hasNext(); ) {\n" + " result.add(iterator.next());\n" + " }\n" + " \n" + " return result;"); } else if ("java.util.List".equals(returnTypeName) || "java.util.Collection".equals(returnTypeName)) { sb.append( " java.util.List result = new java.util.ArrayList();\n" + " for (java.util.Iterator iterator = findUsingQueryModel(queryModel).iterator(); iterator.hasNext(); ) {\n" + " result.add(iterator.next());\n" + " }\n" + " \n" + " return result;"); } else if ("java.lang.Iterable".equals(returnTypeName)) { sb.append( "\n return findUsingQueryModel(queryModel);"); } else if ("int".equals(returnTypeName)) { sb.append( "\n return ((Number) projectUsingQueryModel(queryModel)).intValue();"); } else if ("long".equals(returnTypeName)) { sb.append( "\n return ((Number) projectUsingQueryModel(queryModel)).longValue();"); } else if ("double".equals(returnTypeName)) { sb.append( "\n return ((Number) projectUsingQueryModel(queryModel)).doubleValue();"); } else { if (Iterable.class.isAssignableFrom(Class.forName(returnTypeName))) { sb.append( "\n return ($r) findUsingQueryModel(queryModel);"); } else { sb.append("\n return ($r) projectUsingQueryModel(queryModel);"); } } } catch (NotFoundException | ClassNotFoundException e) { throw new RuntimeException(e); } } private static void buildUpdateClause(StringBuilder sb) { sb.append("\n return updateUsingQueryModel(updateObject, queryModel);"); } private static void buildDeleteClause(StringBuilder sb) { sb.append("\n deleteUsingQueryModel(queryModel);"); } private static void logDerivedMethod(CtMethod interfaceMethod, StringBuilder sb) { try { String parameters = ""; String exceptions = "\n throws "; int parameterCount = 0; for (CtClass parameterType : interfaceMethod.getParameterTypes()) { if (parameters.length() > 0) { parameters = parameters + ", "; } parameters = parameters + parameterType.getSimpleName() + " a" + parameterCount++; } for (CtClass exceptionType : interfaceMethod.getExceptionTypes()) { exceptions = exceptions + exceptionType.getSimpleName(); } logger.debug(String.format("Adding DAO method implementation: \n\n" + "public %s %s(%s) %s %s\n\n", interfaceMethod.getReturnType().getSimpleName(), interfaceMethod.getName(), parameters, exceptions.length() > 17 ? exceptions : "", sb.toString())); } catch (NotFoundException e) { throw new RuntimeException(e); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy