/*******************************************************************************
* Copyright (c) 2012 Pivotal Software, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Pivotal Software, Inc. - initial API and implementation
*******************************************************************************/
package org.grails.ide.eclipse.editor.groovy.types;
import groovyjarjarasm.asm.Opcodes;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.codehaus.groovy.ast.AnnotatedNode;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.FieldNode;
import org.codehaus.groovy.ast.MethodNode;
import org.codehaus.groovy.ast.Parameter;
import org.codehaus.groovy.ast.PropertyNode;
import org.codehaus.groovy.ast.stmt.BlockStatement;
import org.codehaus.groovy.eclipse.codeassist.ProposalUtils;
import org.eclipse.core.runtime.Assert;
import org.eclipse.jdt.groovy.search.VariableScope;
import org.grails.ide.eclipse.editor.groovy.elements.DomainClass;
/**
* @author Andrew Eisenberg
* @since 2.3.0
*/
public class DynamicFinderValidator {
private static final ClassNode[] NO_EXCEPTIONS = new ClassNode[0];
private static final BlockStatement EMPTY_BLOCK = new BlockStatement();
enum CompleteKind { COMPARATOR, COMPARATOR_OPERATOR, NONE, OPERATOR, PROP }
private static final String[] FINDER_COMPARATORS = new String[] {
"Between", "GreaterThan", "GreaterThanEquals", "Ilike", "InList", "IsNull", "IsNotNull", "LessThan", "LessThanEquals", "Like", "Not", "NotEqual" };
private static final String COMPARATORS = concat(FINDER_COMPARATORS);
private static final String[] FINDER_OPERATORS = new String[] { "And", "Or" };
private static final String[] FINDER_OPERATORS_LCASE = new String[] { "and", "or" };
private static final String OPERATORS = concat(FINDER_OPERATORS);
private static final Map<String, ClassNode[]> COMPARATOR_ARGUMENT_MAP = new HashMap<String, ClassNode[]>();
static {
COMPARATOR_ARGUMENT_MAP.put("Between", new ClassNode[] { VariableScope.OBJECT_CLASS_NODE, VariableScope.OBJECT_CLASS_NODE });
COMPARATOR_ARGUMENT_MAP.put("GreaterThan", new ClassNode[] { VariableScope.OBJECT_CLASS_NODE }); // should be Comparable
COMPARATOR_ARGUMENT_MAP.put("GreaterThanEquals", new ClassNode[] { VariableScope.OBJECT_CLASS_NODE }); // should be Comparable
COMPARATOR_ARGUMENT_MAP.put("Ilike", new ClassNode[] { VariableScope.STRING_CLASS_NODE });
COMPARATOR_ARGUMENT_MAP.put("InList", new ClassNode[] { VariableScope.LIST_CLASS_NODE });
COMPARATOR_ARGUMENT_MAP.put("IsNull", new ClassNode[] { });
COMPARATOR_ARGUMENT_MAP.put("IsNotNull", new ClassNode[] { });
COMPARATOR_ARGUMENT_MAP.put("LessThan", new ClassNode[] { VariableScope.OBJECT_CLASS_NODE }); // should be Comparable
COMPARATOR_ARGUMENT_MAP.put("LessThanEquals", new ClassNode[] { VariableScope.OBJECT_CLASS_NODE }); // should be Comparable
COMPARATOR_ARGUMENT_MAP.put("Like", new ClassNode[] { VariableScope.OBJECT_CLASS_NODE });
COMPARATOR_ARGUMENT_MAP.put("Not", new ClassNode[] { VariableScope.OBJECT_CLASS_NODE });
COMPARATOR_ARGUMENT_MAP.put("NotEqual", new ClassNode[] { VariableScope.OBJECT_CLASS_NODE });
COMPARATOR_ARGUMENT_MAP.put(null, new ClassNode[] { VariableScope.OBJECT_CLASS_NODE });
}
private static final Map<String,String> COMPARATOR_LCASE_MAP = new HashMap<String, String>();
static {
for (String comparator : FINDER_COMPARATORS) {
COMPARATOR_LCASE_MAP.put(comparator.toLowerCase(), comparator);
}
}
/**
* Finder prefixes. Grails 1.3.X
*/
private static final String[] FINDER_PREFIXES_1_3_X = new String[] { "countBy", "findBy", "findAllBy", "listOrderBy" };
private static final Map<String,String> FINDER_PREFIXES_MAP_1_3_X = new HashMap<String, String>();
static {
FINDER_PREFIXES_MAP_1_3_X.put("countby", "countBy");
FINDER_PREFIXES_MAP_1_3_X.put("findby", "findBy");
FINDER_PREFIXES_MAP_1_3_X.put("findallby", "findAllBy");
FINDER_PREFIXES_MAP_1_3_X.put("listorderby", "listOrderBy");
}
/**
* Finder prefixes text for pattern. Grails 1.3.X
*/
private static final String PREFIXES_1_3_X = "^" + concat(FINDER_PREFIXES_1_3_X);
/**
* Pattern for checking if a finder starts with a prefix. Grails 1.3.X
*/
private static final Pattern STARTS_WITH_PATTERN_1_3_X = Pattern.compile(PREFIXES_1_3_X);
/**
* Finder prefixes. Grails 2.0.X
*/
private static final String[] FINDER_PREFIXES_2_0_X = new String[] { "countBy", "findBy", "findAllBy", "listOrderBy", "findOrCreateBy", "findOrSaveBy"};
private static final Map<String,String> FINDER_PREFIXES_MAP_2_0_X = new HashMap<String, String>();
static {
FINDER_PREFIXES_MAP_2_0_X.put("countby", "countBy");
FINDER_PREFIXES_MAP_2_0_X.put("findby", "findBy");
FINDER_PREFIXES_MAP_2_0_X.put("findallby", "findAllBy");
FINDER_PREFIXES_MAP_2_0_X.put("listorderby", "listOrderBy");
FINDER_PREFIXES_MAP_2_0_X.put("findorcreateby", "findOrCreateBy");
FINDER_PREFIXES_MAP_2_0_X.put("findorsaveby", "findOrSaveBy");
}
/**
* Finder prefixes text for pattern. Grails 2.0.X
*/
private static final String PREFIXES_2_0_X = "^" + concat(FINDER_PREFIXES_2_0_X);
/**
* Pattern for checking if a finder starts with a prefix. Grails 2.0.X
*/
private static final Pattern STARTS_WITH_PATTERN_2_0_X = Pattern.compile(PREFIXES_2_0_X);
private static String concat(String[] ss) {
StringBuilder sb = new StringBuilder();
sb.append("(");
for (int i = 0; i < ss.length; i++) {
if (i > 0) sb.append("|");
sb.append(ss[i]);
}
sb.append(")");
return sb.toString();
}
private final DomainClass domain;
private Set<String> domainProperties;
private Map<String,String> lcaseDomainPropertiesMap;
/**
* caches pre-calculated findernames
*/
private Map<String, Boolean> finderNameCache;
/**
* This pattern determines what kind of content assist is available
*/
private Pattern splitFinderPattern;
/**
* Regex describing a valid finder
*/
protected Pattern validFinderPattern;
private String prefixes;
private Pattern startsWithPattern;
private Map<String, String> startsWithMap;
protected DynamicFinderValidator(boolean use200, DomainClass domain) {
this.domain = domain;
this.finderNameCache = new HashMap<String, Boolean>();
if (use200) {
prefixes = PREFIXES_2_0_X;
startsWithPattern = STARTS_WITH_PATTERN_2_0_X;
startsWithMap = FINDER_PREFIXES_MAP_2_0_X;
} else {
prefixes = PREFIXES_1_3_X;
startsWithPattern = STARTS_WITH_PATTERN_1_3_X;
startsWithMap = FINDER_PREFIXES_MAP_1_3_X;
}
domainProperties = generateDomainProperties(domain);
lcaseDomainPropertiesMap = lcase(domainProperties);
String concatProperties = concatPropertyNames(domainProperties);
validFinderPattern = Pattern.compile(createValidFinderPatternString(concatProperties));
splitFinderPattern = Pattern.compile(createSplitFinder(concatProperties), Pattern.CASE_INSENSITIVE);
}
private Map<String,String> lcase(Set<String> props) {
Map<String,String> lcaseMap = new HashMap<String,String>(props.size()*2);
for (String prop : props) {
lcaseMap.put(prop.toLowerCase(), prop);
}
return lcaseMap;
}
/**
* For testing only
*/
protected DynamicFinderValidator(boolean use200, Set<String> domainProperties) {
this.domain = null;
this.domainProperties = ensureCapitalized(domainProperties);
this.lcaseDomainPropertiesMap = lcase(this.domainProperties);
this.finderNameCache = new HashMap<String, Boolean>();
if (use200) {
prefixes = PREFIXES_2_0_X;
startsWithPattern = STARTS_WITH_PATTERN_2_0_X;
startsWithMap = FINDER_PREFIXES_MAP_2_0_X;
} else {
prefixes = PREFIXES_1_3_X;
startsWithPattern = STARTS_WITH_PATTERN_1_3_X;
startsWithMap = FINDER_PREFIXES_MAP_1_3_X;
}
String concatProperties = concatPropertyNames(domainProperties);
validFinderPattern = Pattern.compile(createValidFinderPatternString(concatProperties));
splitFinderPattern = Pattern.compile(createSplitFinder(concatProperties), Pattern.CASE_INSENSITIVE & Pattern.UNICODE_CASE);
}
/**
* For testing only
*/
protected DynamicFinderValidator(boolean use200, String ... domainPropertiesArr) {
this.domain = null;
Set<String> domainProperties = new HashSet<String>(Arrays.asList(domainPropertiesArr));
this.domainProperties = ensureCapitalized(domainProperties);
this.lcaseDomainPropertiesMap = lcase(this.domainProperties);
this.finderNameCache = new HashMap<String, Boolean>();
if (use200) {
prefixes = PREFIXES_2_0_X;
startsWithPattern = STARTS_WITH_PATTERN_2_0_X;
startsWithMap = FINDER_PREFIXES_MAP_2_0_X;
} else {
prefixes = PREFIXES_1_3_X;
startsWithPattern = STARTS_WITH_PATTERN_1_3_X;
startsWithMap = FINDER_PREFIXES_MAP_1_3_X;
}
String concatProperties = concatPropertyNames(domainProperties);
validFinderPattern = Pattern.compile(createValidFinderPatternString(concatProperties));
splitFinderPattern = Pattern.compile(createSplitFinder(concatProperties), Pattern.CASE_INSENSITIVE & Pattern.UNICODE_CASE);
}
/**
* Creates a valid declaration based on the name passed in
* @param finderName the name of the dynamic finder
* @return A field declaration corresponding to the dynamic finder
* return a field and not a method so that components can be built up more easily
*/
public FieldNode createFieldDeclaration(String finderName) {
ClassNode declaring = domain != null ? domain.getGroovyClass() : null;
if (declaring == null) {
declaring = VariableScope.OBJECT_CLASS_NODE;
}
FieldNode field = new FieldNode(finderName, Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC,
createReturnType(finderName), declaring, null);
field.setDeclaringClass(declaring);
return field;
}
/**
* Provide a list of content assist proposals for the given finder name
* @param finderName
* @return List of method proposals to complete the potential finder name,
* or empty list if none are available
*/
public List<AnnotatedNode> findProposals(String finderName) {
if (finderName == null || finderName.equals("")) {
return Collections.emptyList();
}
Matcher m = splitFinderPattern.matcher(finderName);
if (!m.matches()) {
return Collections.emptyList();
}
// number of groups = 1 MAX_COUNT * 3 - 1 + 1
// first is prefix, then triplets of component, comparator, operator
// the last triplet cannot have an operator. Then there is the remaining
String prefix = m.group(1);
String remaining = m.group((MAX_COMPONENTS * 3) + 2 - 1); // everything else that is not part of a known group.
String[] props = new String[MAX_COMPONENTS];
String[] comparators = new String[MAX_COMPONENTS];
String[] operators = new String[MAX_COMPONENTS];
for (int i = 0, groupNum = 2; i < MAX_COMPONENTS; i++, groupNum += 3) {
props[i] = m.group(groupNum);
comparators[i] = m.group(groupNum+1);
if (i < MAX_COMPONENTS -1) {
operators[i] = m.group(groupNum+2);
}
}
if (remaining == null) remaining = "";
String lcaseRemaining = remaining.toLowerCase();
// should do better here and check all props
if (props[0] != null && props[0].equals(props[1])) {
// can't have the same component twice
return Collections.emptyList();
}
CompleteKind kind = CompleteKind.NONE;
for (int i = 0; i < MAX_COMPONENTS; i++) {
if (props[i] == null) {
kind = CompleteKind.PROP;
break;
}
if (i < MAX_COMPONENTS - 1) {
if (comparators[i] == null && operators[i] == null) {
kind = CompleteKind.COMPARATOR_OPERATOR;
break;
}
if (operators[i] == null) {
kind = CompleteKind.OPERATOR;
break;
}
} else {
// the last component can have no operator
if (comparators[i] == null) {
kind = CompleteKind.COMPARATOR;
break;
}
}
}
// at this point, kind should not be NONE
// check that all operators are the same
if (operators[0] != null) {
for (int i = 1; i < operators.length; i++) {
if (operators[i] != null && !operators[0].equals(operators[i])) {
return Collections.emptyList();
}
}
}
String caseCorrectedExisting = correctCase(prefix, props, comparators, operators);
List<AnnotatedNode> proposedFinderMethods = null;
String existingOperator;
switch(kind) {
case PROP:
Set<String> lcaseProps = new HashSet<String>(MAX_COMPONENTS, 1);
for (int i = 0; i < props.length; i++) {
if (props[i] != null) {
lcaseProps.add(props[i].toLowerCase());
}
}
// propose all property names, except for the one that is already used
proposedFinderMethods = new ArrayList<AnnotatedNode>(lcaseDomainPropertiesMap.size()*2);
for (Entry<String, String> entry : lcaseDomainPropertiesMap.entrySet()) {
String lcasePropName = entry.getKey();
String propName = entry.getValue();
if (ProposalUtils.looselyMatches(lcaseRemaining, lcasePropName) && !lcaseProps.contains(lcasePropName)) {
proposedFinderMethods.add(createFieldDeclaration(caseCorrectedExisting + propName));
// also add all of methods with the approproate arguments
proposedFinderMethods.add(createMethodDeclaration(caseCorrectedExisting + propName, props, comparators, propName, null));
}
}
break;
case COMPARATOR_OPERATOR:
proposedFinderMethods = new ArrayList<AnnotatedNode>(FINDER_COMPARATORS.length*2 + FINDER_OPERATORS.length);
for (Entry<String, String> entry : COMPARATOR_LCASE_MAP.entrySet()) {
String lcaseComparator = entry.getKey();
if (ProposalUtils.looselyMatches(lcaseRemaining, lcaseComparator)) {
String comparator = entry.getValue();
proposedFinderMethods.add(createFieldDeclaration(caseCorrectedExisting + comparator));
// also add all as methods with appropriate arguments
proposedFinderMethods.add(createMethodDeclaration(caseCorrectedExisting + comparator, props, comparators, null, comparator));
}
}
// fall-through
case OPERATOR:
// propose operator(s)
// only propose if there are any remaining domain properties to propose
int propsLen = 0;
for (int i = 0; i < props.length; i++) {
if (props[i] == null) {
break;
}
propsLen++;
}
if (proposedFinderMethods == null) {
proposedFinderMethods = new ArrayList<AnnotatedNode>(FINDER_OPERATORS.length);
}
if (propsLen < domainProperties.size()) {
// there is at least one more proeprty to propose
existingOperator = operators[0];
for (int i = 0; i < FINDER_OPERATORS.length; i++) {
if (existingOperator == null || FINDER_OPERATORS[i].equalsIgnoreCase(existingOperator)) {
// only look at the operator if it is the same as the first operator
// ie- all operators must be the same.
if (FINDER_OPERATORS_LCASE[i].startsWith(lcaseRemaining)) {
proposedFinderMethods.add(createFieldDeclaration(caseCorrectedExisting + FINDER_OPERATORS[i]));
}
}
}
}
break;
case COMPARATOR:
// propose all comparators
proposedFinderMethods = new ArrayList<AnnotatedNode>(FINDER_COMPARATORS.length*2);
for (Entry<String, String> entry : COMPARATOR_LCASE_MAP.entrySet()) {
String lcaseComparator = entry.getKey();
if (ProposalUtils.looselyMatches(lcaseRemaining, lcaseComparator)) {
String comparator = entry.getValue();
proposedFinderMethods.add(createFieldDeclaration(caseCorrectedExisting + comparator));
// also add all as methods with appropriate arguments
proposedFinderMethods.add(createMethodDeclaration(caseCorrectedExisting + comparator, props, comparators, null, comparator));
}
}
break;
case NONE:
default:
proposedFinderMethods = Collections.emptyList();
}
return proposedFinderMethods;
}
private String correctCase(String prefix,
String[] props, String[] comparators, String[] operators) {
String realPrefix = startsWithMap.get(prefix.toLowerCase());
realPrefix = realPrefix == null ? prefix : realPrefix;
String[] realProps = new String[props.length];
String[] realComparators = new String[comparators.length];
String[] realOperators = new String[comparators.length];
for (int i = 0; i < props.length; i++) {
if (props[i] != null) {
realProps[i] = lcaseDomainPropertiesMap.get(props[i].toLowerCase());
realProps[i] = realProps[i] == null ? props[i] : realProps[i];
} else {
realProps[i] = "";
}
if (comparators[i] != null) {
realComparators[i] = COMPARATOR_LCASE_MAP.get(comparators[i].toLowerCase());
realComparators[i] = realComparators[i] == null ? comparators[i] : realComparators[i];
} else {
realComparators[i] = "";
}
if (i < operators.length) {
if (operators[i] != null) {
realOperators[i] = operators[i].equalsIgnoreCase("Or") ? "Or" : "And";
} else {
realOperators[i] = "";
}
}
}
StringBuilder sb = new StringBuilder();
sb.append(realPrefix);
for (int i = 0; i < props.length; i++) {
sb.append(realProps[i]);
sb.append(realComparators[i]);
if (i < operators.length) {
sb.append(realOperators[i]);
}
}
return sb.toString();
}
private AnnotatedNode createMethodDeclaration(String finderName,
String[] props, String[] comparators, String newPropName, String newComparator) {
// need to determine number of parameters
// first figure out how many properties we need to look at
int numArgs = -1;
if (newPropName != null) {
for (int i = 0; i < props.length; i++) {
if (props[i] == null) {
props[i] = newPropName;
numArgs = i+1;
break;
}
}
}
if (newComparator != null) {
for (int i = 0; i < comparators.length; i++) {
if (comparators[i] == null) {
comparators[i] = newComparator;
numArgs = i+1;
break;
}
}
}
List<Parameter> params = new ArrayList<Parameter>(2);
// now go through each component and comparator.
// determine the kind of parameters they require
for (int i = 0; i < numArgs; i++) {
ClassNode[] classNodes = COMPARATOR_ARGUMENT_MAP.get(comparators[i]);
if (classNodes.length > 0) {
String uncapitalized = uncapitalize(props[i]);
params.add(new Parameter(classNodes[0], uncapitalized));
if (classNodes.length == 2) {
params.add(new Parameter(classNodes[1], uncapitalized + "1"));
}
}
}
MethodNode method = new MethodNode(finderName, Opcodes.ACC_STATIC | Opcodes.ACC_PUBLIC, createReturnType(finderName),
params.toArray(new Parameter[params.size()]), NO_EXCEPTIONS, EMPTY_BLOCK);
if (domain != null) {
method.setDeclaringClass(domain.getGroovyClass());
} else {
method.setDeclaringClass(VariableScope.OBJECT_CLASS_NODE);
}
return method;
}
/**
* @param finderName
* @return true iff this is a complete finder name
*/
public boolean isValidFinderName(String finderName) {
if (finderName == null) {
return false;
}
if (finderNameCache.containsKey(finderName)) {
return finderNameCache.get(finderName);
}
Matcher matcher = validFinderPattern.matcher(finderName);
boolean matches = matcher.matches();
if (matches) {
String[] props = new String[MAX_COMPONENTS];
// still more work to do. must check that the field references are all unique
for (int i = 0, groupNum = 2; i < MAX_COMPONENTS; i++, groupNum += 3) {
props[i] = matcher.group(groupNum);
}
Set<String> existing = new HashSet<String>(5, 1);
for (String prop : props) {
if (existing.contains(prop)) {
matches = false;
break;
}
if (prop == null) {
break;
}
existing.add(prop);
}
if (matches) {
// still more work to do...need to check that all operators are equal
// one less oeprator than components
String op = matcher.group(4);
// start with 1 because we don't need to check the first one against itself
// also, use MAX_COMPONENTS-1 since there are always one fewer operators than components
for (int i = 1, groupNum = 7; i < MAX_COMPONENTS-1; i++, groupNum += 3) {
String nextOp = matcher.group(groupNum);
if (nextOp == null) {
break;
}
if (!op.equals(nextOp)) {
matches = false;
break;
}
}
}
}
finderNameCache.put(finderName, matches);
return matches;
}
/**
* Provide a fail fast way to determine if a name might be a dynamic finder
*
* @param finderName
* @return true iff the start of the finderName is a valid prefix
* for a dynamic finder.
*/
public boolean startsWithDynamicFinder(String finderName) {
Matcher m = startsWithPattern.matcher(finderName);
return m.lookingAt();
}
private String capitalize(String str) {
if (str == null || str.length() == 0) {
return str;
}
StringBuffer buf = new StringBuffer(str.length());
buf.append(Character.toUpperCase(str.charAt(0)));
buf.append(str.substring(1));
return buf.toString();
}
private String uncapitalize(String str) {
if (str == null || str.length() == 0) {
return str;
}
StringBuffer buf = new StringBuffer(str.length());
buf.append(Character.toLowerCase(str.charAt(0)));
buf.append(str.substring(1));
return buf.toString();
}
private String concatPropertyNames(Set<String> props) {
if (props == null || props.size() == 0) {
return "( NO_PROPERTIES )"; // if there are no properties, then there can mever be a match
}
StringBuilder sb = new StringBuilder();
sb.append("(");
int i = props.size();
for (Iterator<String> propIter = props.iterator(); propIter.hasNext(); ) {
sb.append(capitalize(propIter.next()));
if (--i > 0) sb.append("|");
}
sb.append(")");
return sb.toString();
}
/**
* @param finderName
* @return
*/
private ClassNode createReturnType(String finderName) {
if (finderName.startsWith("countBy")) {
return VariableScope.INTEGER_CLASS_NODE;
} else {
ClassNode groovyClass = domain != null ? domain.getGroovyClass() : null;
if (groovyClass == null) {
groovyClass = VariableScope.OBJECT_CLASS_NODE;
}
if (finderName.startsWith("findAllBy") || finderName.startsWith("listOrderBy")) {
ClassNode list = VariableScope.clonedList();
ClassNode thisClass = groovyClass;
list.getGenericsTypes()[0].setType(thisClass);
list.getGenericsTypes()[0].setName(thisClass.getName());
list.getGenericsTypes()[0].setUpperBounds(null);
return list;
} else {
return groovyClass;
}
}
}
private static final int MAX_COMPONENTS = 5;
private String createValidFinderPatternString(String propNameRegx) {
StringBuilder sb = new StringBuilder();
sb.append(prefixes);
sb.append(propNameRegx);
// comparator is optional
sb.append(COMPARATORS + "?");
// then there is an optional operator with one more property name and an optional comparator
// repeat the same for the remaingin components
for (int i = 1; i < MAX_COMPONENTS; i++) {
sb.append("(?:" + OPERATORS + propNameRegx + COMPARATORS + "?)?");
}
sb.append("$");
return sb.toString();
}
/**
* Almost the same as the valid finder patter, except allow for anything at the end.
* Also allow for case-insensitive matching
* @param propNameRegx
* @return
*/
private String createSplitFinder(String propNameRegx) {
StringBuilder sb = new StringBuilder();
sb.append(prefixes);
// allow for an option prop name in case we are completing on the first property name
sb.append(propNameRegx + "?");
// comparator is optional
sb.append(COMPARATORS + "?");
// then there is an optional operator with one more property name and an optional comparator
// repeat the same for the remaingin components
for (int i = 1; i < MAX_COMPONENTS; i++) {
// the property name is optional in case we are completing on the property
sb.append("(?:" + OPERATORS + propNameRegx + "?" + COMPARATORS + "?)?");
}
sb.append("(.*)$");
return sb.toString();
}
private Set<String> ensureCapitalized(Set<String> domainProperties) {
Set<String> newSet = new HashSet<String>();
for (String prop : domainProperties) {
newSet.add(capitalize(prop));
}
return newSet;
}
private Set<String> generateDomainProperties(DomainClass domain) {
Assert.isNotNull(domain, "Domain class should not be null");
List<PropertyNode> domainPropertiesNodes = domain.getDomainProperties();
Set<String> properties = new HashSet<String>(domainPropertiesNodes.size()*2);
for (PropertyNode property : domainPropertiesNodes) {
properties.add(capitalize(property.getName()));
}
return properties;
}
protected List<String> getFinderComponents(String finderName) {
Matcher matcher = validFinderPattern.matcher(finderName);
if (! matcher.matches()) {
return null;
} else {
List<String> matches = new ArrayList<String>(matcher.groupCount());
for (int i = 1; i <= matcher.groupCount(); i++) {
matches.add(matcher.group(i));
}
return matches;
}
}
// for testing
public static void main(String[] args) {
testFinder("findByFooOrBaz", true);
// should have proposals
testFinder("findBy", false);
testFinder("findByB", false);
testFinder("findByFooO", false);
testFinder("findByFooL", false);
testFinder("findByFooLikeAndBazL", false);
testFinder("findByFooLikeA", false);
testFinder("findByFoo", true);
testFinder("findByFooLike", true);
testFinder("findByFooLikeAnd", false);
testFinder("findByFooLikeAndBaz", true);
testFinder("findByFooLikeAndBazLike", true);
testFinder("findByFooLikeAndBazLikeAndBarLikeAndBop", true);
testFinder("findByFooLikeAndBazLikeAndBarLikeAndBopBetween", true);
testFinder("findByFooLikeAndBazLikeAndBarLikeAndBarBetween", false);
testFinder("findByFooLikeAndBazLikeAndBarLikeOrBazBetween", false);
testFinder("findByFooLikeAndBazLikeAndBarLikeAndBBetween", false);
}
private static void testFinder(String finderName, boolean expectMatch) {
DynamicFinderValidator validator = new DynamicFinderValidator(false, "foo", "bar", "baz", "bop");
boolean isValid = validator.isValidFinderName(finderName);
List<AnnotatedNode> proposals = validator.findProposals(finderName);
StringBuilder sb = new StringBuilder();
sb.append((isValid ? " VALID " : " INVALID ") + finderName + " proposals: ");
for (AnnotatedNode anode : proposals) {
if (anode instanceof FieldNode) {
sb.append(((FieldNode) anode).getName() + " ");
} else if (anode instanceof MethodNode) {
sb.append(((MethodNode) anode).getName() + "() ");
}
}
if (isValid != expectMatch) {
System.err.println("Unexpected. Should have " + (expectMatch ? "matched " : "no match ") + finderName);
}
System.out.println(sb);
}
}