
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 extends QueryModelDAO> partialDAOClass,
Map daoProperties) {
return buildDAO(modelClass, daoInterface, partialDAOClass, daoProperties, null);
}
public static > I buildDAO(Class modelClass,
Class daoInterface,
Class extends QueryModelDAO> 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 extends I> fullDAOClass = completeDAO(modelClass, daoInterface, partialDAOClass, accessControlContextProvider != null,
daoProperties != null && Boolean.parseBoolean((String) daoProperties.get("enableMetrics")));
try {
if (accessControlContextProvider != null) {
Constructor extends I> constructor = fullDAOClass.getDeclaredConstructor(Class.class, Map.class,
AccessControlContextProvider.class);
return constructor.newInstance(modelClass, daoProperties, accessControlContextProvider);
} else {
Constructor extends I> constructor = fullDAOClass.getDeclaredConstructor(Class.class, Map.class);
return constructor.newInstance(modelClass, daoProperties);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
//-------------------------------------------------------------
// Methods - Private
//-------------------------------------------------------------
private static > Class extends I> completeDAO(Class modelClass,
Class daoInterface,
Class extends QueryModelDAO> 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