package org.springframework.roo.addon.finder;
import static org.springframework.roo.model.JavaType.LONG_OBJECT;
import static org.springframework.roo.model.JdkJavaType.MAP;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Service;
import org.springframework.roo.classpath.customdata.CustomDataKeys;
import org.springframework.roo.classpath.details.BeanInfoUtils;
import org.springframework.roo.classpath.details.FieldMetadata;
import org.springframework.roo.classpath.details.MemberHoldingTypeDetails;
import org.springframework.roo.classpath.details.MethodMetadata;
import org.springframework.roo.classpath.scanner.MemberDetails;
import org.springframework.roo.model.JavaSymbolName;
import org.springframework.roo.model.JavaType;
/**
* Default implementation of {@link DynamicFinderServices}.
*
* @author Stefan Schmidt
* @author Alan Stewart
* @since 1.0
*/
@Component
@Service
public class DynamicFinderServicesImpl implements DynamicFinderServices {
private Set<JavaSymbolName> createFinders(final FieldMetadata field,
final Set<JavaSymbolName> finders, final String prepend,
final boolean isFirst) {
final Set<JavaSymbolName> tempFinders = new HashSet<JavaSymbolName>();
if (isNumberOrDate(field.getFieldType())) {
for (final ReservedToken keyWord : ReservedTokenHolder.NUMERIC_TOKENS) {
tempFinders.addAll(populateFinders(finders, field, prepend,
isFirst, keyWord.getValue()));
}
}
else if (field.getFieldType().equals(JavaType.STRING)) {
for (final ReservedToken keyWord : ReservedTokenHolder.STRING_TOKENS) {
tempFinders.addAll(populateFinders(finders, field, prepend,
isFirst, keyWord.getValue()));
}
}
else if (field.getFieldType().equals(JavaType.BOOLEAN_OBJECT)
|| field.getFieldType().equals(JavaType.BOOLEAN_PRIMITIVE)) {
for (final ReservedToken keyWord : ReservedTokenHolder.BOOLEAN_TOKENS) {
tempFinders.addAll(populateFinders(finders, field, prepend,
isFirst, keyWord.getValue()));
}
}
else {
tempFinders.addAll(populateFinders(finders, field, prepend,
isFirst, ""));
}
return tempFinders;
}
/**
* Returns the {@link JavaType} from the specified {@link MemberDetails}
* object;
* <p>
* If the found type is abstract the next {@link MemberHoldingTypeDetails}
* is searched.
*
* @param memberDetails the {@link MemberDetails} to search (required)
* @return the first non-abstract JavaType, or null if not found
*/
private JavaType getConcreteJavaType(final MemberDetails memberDetails) {
Validate.notNull(memberDetails, "Member details required");
JavaType javaType = null;
for (final MemberHoldingTypeDetails memberHoldingTypeDetails : memberDetails
.getDetails()) {
if (Modifier.isAbstract(memberHoldingTypeDetails.getModifier())) {
continue;
}
javaType = memberHoldingTypeDetails.getName();
}
return javaType;
}
public List<JavaSymbolName> getFinders(final MemberDetails memberDetails,
final String plural, final int depth,
final Set<JavaSymbolName> exclusions) {
Validate.notNull(memberDetails, "Member details required");
Validate.notBlank(plural, "Plural required");
Validate.notNull(depth,
"The depth of combinations used for finder signatures combinations required");
Validate.notNull(exclusions, "Exclusions required");
final SortedSet<JavaSymbolName> finders = new TreeSet<JavaSymbolName>();
final List<FieldMetadata> fields = memberDetails.getFields();
for (int i = 0; i < depth; i++) {
final SortedSet<JavaSymbolName> tempFinders = new TreeSet<JavaSymbolName>();
for (final FieldMetadata field : fields) {
// Ignoring java.util.Map field types (see ROO-194)
if (field == null || field.getFieldType().equals(MAP)) {
continue;
}
if (exclusions.contains(field.getFieldName())) {
continue;
}
if (i == 0) {
tempFinders.addAll(createFinders(field, finders, "find"
+ plural + "By", true));
}
else {
tempFinders.addAll(createFinders(field, finders, "And",
false));
tempFinders.addAll(createFinders(field, finders, "Or",
false));
}
}
finders.addAll(tempFinders);
}
return Collections.unmodifiableList(new ArrayList<JavaSymbolName>(
finders));
}
private Token getFirstToken(final SortedSet<FieldToken> fieldTokens,
final String finder, final String originalFinder,
final String simpleTypeName) {
for (final FieldToken fieldToken : fieldTokens) {
if (finder.startsWith(fieldToken.getValue())) {
return fieldToken;
}
}
for (final ReservedToken reservedToken : ReservedTokenHolder.ALL_TOKENS) {
if (finder.startsWith(reservedToken.getValue())) {
return reservedToken;
}
}
if (finder.length() > 0) {
// TODO: Make this a FinderFieldTokenMissingException instead, to
// make it easier to detect this
throw new FinderFieldTokenMissingException(
"Dynamic finder is unable to match '" + finder
+ "' token of '" + originalFinder
+ "' finder definition in " + simpleTypeName
+ ".java");
}
return null; // Finder does not start with reserved or field token
}
private String getJpaQuery(final List<Token> tokens,
final String simpleTypeName, final JavaSymbolName finderName,
final String plural, final String entityName) {
final String typeName = StringUtils.defaultIfEmpty(entityName,
simpleTypeName);
final StringBuilder builder = new StringBuilder();
builder.append("SELECT o FROM ").append(typeName);
builder.append(" AS o WHERE ");
FieldToken lastFieldToken = null;
boolean isNewField = true;
boolean isFieldApplied = false;
for (final Token token : tokens) {
if (token instanceof ReservedToken) {
final String reservedToken = token.getValue();
if (lastFieldToken == null) {
continue;
}
final String fieldName = lastFieldToken.getField()
.getFieldName().getSymbolName();
boolean setField = true;
if (!lastFieldToken.getField().getFieldType()
.isCommonCollectionType()) {
if (isNewField) {
if (reservedToken.equalsIgnoreCase("Like")) {
builder.append("LOWER(").append("o.")
.append(fieldName).append(')');
}
else {
builder.append("o.").append(fieldName);
}
isNewField = false;
isFieldApplied = false;
}
if (reservedToken.equalsIgnoreCase("And")) {
if (!isFieldApplied) {
builder.append(" = :").append(fieldName);
isFieldApplied = true;
}
builder.append(" AND ");
setField = false;
}
else if (reservedToken.equalsIgnoreCase("Or")) {
if (!isFieldApplied) {
builder.append(" = :").append(fieldName);
isFieldApplied = true;
}
builder.append(" OR ");
setField = false;
}
else if (reservedToken.equalsIgnoreCase("Between")) {
builder.append(" BETWEEN ")
.append(":min")
.append(lastFieldToken.getField()
.getFieldName()
.getSymbolNameCapitalisedFirstLetter())
.append(" AND ")
.append(":max")
.append(lastFieldToken.getField()
.getFieldName()
.getSymbolNameCapitalisedFirstLetter())
.append(" ");
setField = false;
isFieldApplied = true;
}
else if (reservedToken.equalsIgnoreCase("Like")) {
builder.append(" LIKE ");
setField = true;
}
else if (reservedToken.equalsIgnoreCase("IsNotNull")) {
builder.append(" IS NOT NULL ");
setField = false;
isFieldApplied = true;
}
else if (reservedToken.equalsIgnoreCase("IsNull")) {
builder.append(" IS NULL ");
setField = false;
isFieldApplied = true;
}
else if (reservedToken.equalsIgnoreCase("Not")) {
builder.append(" IS NOT ");
}
else if (reservedToken.equalsIgnoreCase("NotEquals")) {
builder.append(" != ");
}
else if (reservedToken.equalsIgnoreCase("LessThan")) {
builder.append(" < ");
}
else if (reservedToken.equalsIgnoreCase("LessThanEquals")) {
builder.append(" <= ");
}
else if (reservedToken.equalsIgnoreCase("GreaterThan")) {
builder.append(" > ");
}
else if (reservedToken
.equalsIgnoreCase("GreaterThanEquals")) {
builder.append(" >= ");
}
else if (reservedToken.equalsIgnoreCase("Equals")) {
builder.append(" = ");
}
if (setField) {
if (builder.toString().endsWith("LIKE ")) {
builder.append("LOWER(:").append(fieldName)
.append(") ");
}
else {
builder.append(':').append(fieldName).append(' ');
}
isFieldApplied = true;
}
}
}
else {
lastFieldToken = (FieldToken) token;
isNewField = true;
}
}
if (isNewField) {
if (lastFieldToken != null
&& !lastFieldToken.getField().getFieldType()
.isCommonCollectionType()) {
builder.append("o.").append(
lastFieldToken.getField().getFieldName()
.getSymbolName());
}
isFieldApplied = false;
}
if (!isFieldApplied) {
if (lastFieldToken != null
&& !lastFieldToken.getField().getFieldType()
.isCommonCollectionType()) {
builder.append(" = :").append(
lastFieldToken.getField().getFieldName()
.getSymbolName());
}
}
return builder.toString().trim();
}
private List<MethodMetadata> getLocatedMutators(
final MemberDetails memberDetails) {
final List<MethodMetadata> locatedMutators = new ArrayList<MethodMetadata>();
for (final MethodMetadata method : memberDetails.getMethods()) {
if (isMethodOfInterest(method)) {
locatedMutators.add(method);
}
}
return locatedMutators;
}
private List<JavaSymbolName> getParameterNames(final List<Token> tokens,
final JavaSymbolName finderName, final String plural) {
final List<JavaSymbolName> parameterNames = new ArrayList<JavaSymbolName>();
for (int i = 0; i < tokens.size(); i++) {
final Token token = tokens.get(i);
if (token instanceof FieldToken) {
final String fieldName = ((FieldToken) token).getField()
.getFieldName().getSymbolName();
parameterNames.add(new JavaSymbolName(fieldName));
}
else {
if ("Between".equals(token.getValue())) {
final Token field = tokens.get(i - 1);
if (field instanceof FieldToken) {
final JavaSymbolName fieldName = parameterNames
.get(parameterNames.size() - 1);
// Remove the last field token
parameterNames.remove(parameterNames.size() - 1);
// Replace by a min and a max value
parameterNames
.add(new JavaSymbolName(
"min"
+ fieldName
.getSymbolNameCapitalisedFirstLetter()));
parameterNames
.add(new JavaSymbolName(
"max"
+ fieldName
.getSymbolNameCapitalisedFirstLetter()));
}
}
else if ("IsNull".equals(token.getValue())
|| "IsNotNull".equals(token.getValue())) {
final Token field = tokens.get(i - 1);
if (field instanceof FieldToken) {
parameterNames.remove(parameterNames.size() - 1);
}
}
}
}
return parameterNames;
}
private List<JavaType> getParameterTypes(final List<Token> tokens,
final JavaSymbolName finderName, final String plural) {
final List<JavaType> parameterTypes = new ArrayList<JavaType>();
for (int i = 0; i < tokens.size(); i++) {
final Token token = tokens.get(i);
if (token instanceof FieldToken) {
parameterTypes.add(((FieldToken) token).getField()
.getFieldType());
}
else {
if ("Between".equals(token.getValue())) {
final Token field = tokens.get(i - 1);
if (field instanceof FieldToken) {
parameterTypes.add(parameterTypes.get(parameterTypes
.size() - 1));
}
}
else if ("IsNull".equals(token.getValue())
|| "IsNotNull".equals(token.getValue())) {
final Token field = tokens.get(i - 1);
if (field instanceof FieldToken) {
parameterTypes.remove(parameterTypes.size() - 1);
}
}
}
}
return parameterTypes;
}
public QueryHolder getQueryHolder(final MemberDetails memberDetails,
final JavaSymbolName finderName, final String plural,
final String entityName) {
Validate.notNull(memberDetails, "Member details required");
Validate.notNull(finderName, "Finder name required");
Validate.notBlank(plural, "Plural required");
List<Token> tokens;
try {
tokens = tokenize(memberDetails, finderName, plural);
}
catch (final FinderFieldTokenMissingException e) {
return null;
}
catch (final InvalidFinderException e) {
return null;
}
final String simpleTypeName = getConcreteJavaType(memberDetails)
.getSimpleTypeName();
final String jpaQuery = getJpaQuery(tokens, simpleTypeName, finderName,
plural, entityName);
final List<JavaType> parameterTypes = getParameterTypes(tokens,
finderName, plural);
final List<JavaSymbolName> parameterNames = getParameterNames(tokens,
finderName, plural);
return new QueryHolder(jpaQuery, parameterTypes, parameterNames, tokens);
}
private boolean isMethodOfInterest(final MethodMetadata method) {
return method.getMethodName().getSymbolName().startsWith("set")
&& method.getModifier() == Modifier.PUBLIC;
}
private boolean isNumberOrDate(final JavaType fieldType) {
return fieldType.equals(JavaType.DOUBLE_OBJECT)
|| fieldType.equals(JavaType.FLOAT_OBJECT)
|| fieldType.equals(JavaType.INT_OBJECT)
|| fieldType.equals(LONG_OBJECT)
|| fieldType.equals(JavaType.SHORT_OBJECT)
|| fieldType.getFullyQualifiedTypeName().equals(
Date.class.getName())
|| fieldType.getFullyQualifiedTypeName().equals(
Calendar.class.getName());
}
private boolean isTransient(final FieldMetadata field) {
return Modifier.isTransient(field.getModifier())
|| field.getCustomData().keySet()
.contains(CustomDataKeys.TRANSIENT_FIELD);
}
private Set<JavaSymbolName> populateFinders(
final Set<JavaSymbolName> finders, final FieldMetadata field,
final String prepend, final boolean isFirst, final String keyWord) {
final Set<JavaSymbolName> tempFinders = new HashSet<JavaSymbolName>();
if (isTransient(field)) {
// No need to add transient fields
}
else if (isFirst) {
final String finderName = prepend
+ field.getFieldName()
.getSymbolNameCapitalisedFirstLetter() + keyWord;
tempFinders.add(new JavaSymbolName(finderName));
}
else {
for (final JavaSymbolName finder : finders) {
final String finderName = finder.getSymbolName();
if (!finderName.contains(field.getFieldName()
.getSymbolNameCapitalisedFirstLetter())) {
tempFinders.add(new JavaSymbolName(finderName
+ prepend
+ field.getFieldName()
.getSymbolNameCapitalisedFirstLetter()
+ keyWord));
}
}
}
return tempFinders;
}
private List<Token> tokenize(final MemberDetails memberDetails,
final JavaSymbolName finderName, final String plural) {
final String simpleTypeName = getConcreteJavaType(memberDetails)
.getSimpleTypeName();
String finder = finderName.getSymbolName();
// Just in case it starts with findBy we can remove it here
final String findBy = "find" + plural + "By";
if (finder.startsWith(findBy)) {
finder = finder.substring(findBy.length());
}
// If finder still contains the findBy sequence it is most likely a
// wrong finder (ie someone pasted the finder string accidentally twice
if (finder.contains(findBy)) {
throw new InvalidFinderException("Dynamic finder definition for '"
+ finderName.getSymbolName() + "' in " + simpleTypeName
+ ".java is invalid");
}
final SortedSet<FieldToken> fieldTokens = new TreeSet<FieldToken>();
for (final MethodMetadata method : getLocatedMutators(memberDetails)) {
final FieldMetadata field = BeanInfoUtils.getFieldForPropertyName(
memberDetails, method.getParameterNames().get(0));
// If we did find a field matching the first parameter name of the
// mutator method we can add it to the finder ITD
if (field != null) {
fieldTokens.add(new FieldToken(field));
}
}
final List<Token> tokens = new ArrayList<Token>();
while (finder.length() > 0) {
final Token token = getFirstToken(fieldTokens, finder,
finderName.getSymbolName(), simpleTypeName);
if (token != null) {
if (token instanceof FieldToken
|| token instanceof ReservedToken) {
tokens.add(token);
}
finder = finder.substring(token.getValue().length());
}
}
return tokens;
}
}