/*******************************************************************************
* 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.elements;
import groovyjarjarasm.asm.Opcodes;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.codehaus.groovy.ast.AnnotatedNode;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.FieldNode;
import org.codehaus.groovy.ast.GenericsType;
import org.codehaus.groovy.ast.MethodNode;
import org.codehaus.groovy.ast.PropertyNode;
import org.codehaus.jdt.groovy.model.GroovyCompilationUnit;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.Path;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.groovy.search.AbstractSimplifiedTypeLookup.TypeAndDeclaration;
import org.eclipse.jdt.groovy.search.VariableScope;
import org.grails.ide.eclipse.core.internal.plugins.GrailsCore;
import org.grails.ide.eclipse.core.internal.plugins.GrailsElementKind;
import org.grails.ide.eclipse.core.internal.plugins.PerProjectPluginCache;
import org.grails.ide.eclipse.core.model.ContributedMethod;
import org.grails.ide.eclipse.core.model.ContributedProperty;
import org.grails.ide.eclipse.editor.groovy.types.DynamicFinderValidator;
import org.grails.ide.eclipse.editor.groovy.types.FinderValidatorFactory;
import org.grails.ide.eclipse.editor.groovy.types.PerProjectMemberCache;
import org.grails.ide.eclipse.editor.groovy.types.PerProjectNamedQueriesHolder;
import org.grails.ide.eclipse.editor.groovy.types.PerProjectTypeCache;
/**
* @author Andrew Eisenberg
* @author Christian Dupuis
* @author Nieraj Singh
* @created Nov 23, 2009
*/
public class DomainClass extends AbstractGrailsElement implements INavigableGrailsElement {
// these are extra references htat are only available in the mappings method
private static final String[] MAPPINGS_FIELDS = new String[] { "tablePerHierarchy", "tablePerSubclass" /*, "id", "version"*/ }; // the version and id fields are inserted directly into the class, so no need to add it here
private static final String[] CONTSTRAINTS_FIELDS = new String[] { "blank", "creditCard", "email", "inList", "length", "min", "minLength", "minSize", "matches", "max", "maxLength", "maxSize", "notEqual", "nullable", "range", "scale", "size", "unique", "url" };
private static final String NAMED_CRITERIA_PROXY = "org.codehaus.groovy.grails.orm.hibernate.cfg.NamedCriteriaProxy";
// these are default fields that can be created when in the SCRIPT context
public static final String[] staticFields = new String[] {
"belongsTo", // list
"hasMany", // list
"embedded", // list
"transients", // list
"mapping", // closure
"constraints", // closure
"namedQueries", // closure
"hasOne" // class
};
public class NamedQueryClassNode extends ClassNode {
public NamedQueryClassNode() {
super(NAMED_CRITERIA_PROXY, VariableScope.OBJECT_CLASS_NODE.getModifiers(), VariableScope.GROOVY_OBJECT_SUPPORT);
this.isPrimaryNode = false;
this.setRedirect(typeCache.getClassNode(NAMED_CRITERIA_PROXY));
this.setGenericsPlaceHolder(true);
GenericsType gt = new GenericsType();
gt.setType(DomainClass.this.getGroovyClass());
gt.setName(gt.getType().getName());
this.setGenericsTypes(new GenericsType[] { gt });
}
public DomainClass getDomainClass() {
return DomainClass.this;
}
/**
* Search for other named queries and for dynamic finders
* @param declaringType
* @param name
* @param scope
* @return
*/
public TypeAndDeclaration lookupTypeAndDeclaration(ClassNode declaringType,
String name, VariableScope scope) {
// dynamic finders
DynamicFinderValidator internalFinderValidator = getFinderValidator();
if (internalFinderValidator.isValidFinderName(name)) {
// prefer the cached finder
TypeAndDeclaration typeAndDeclaration = findCached(name);
if (typeAndDeclaration != null) {
return typeAndDeclaration;
}
FieldNode field = internalFinderValidator.createFieldDeclaration(name);
cacheGeneratedMember(field);
return new TypeAndDeclaration(field.getType(), field, DomainClass.this.getGroovyClass(), "Dynamic finder");
}
// namedQueries
for (String namedQuery : getNamedQueries()) {
if (name.equals(namedQuery)) {
// prefer the cached query
TypeAndDeclaration typeAndDeclaration = findCached(name);
if (typeAndDeclaration != null) {
return typeAndDeclaration;
}
FieldNode criteria = createNamedCriteria(DomainClass.this.getGroovyClass(), namedQuery);
return new TypeAndDeclaration(criteria.getType(), criteria, DomainClass.this.getGroovyClass(), "Named Query");
}
}
return null;
}
}
private final PerProjectPluginCache pluginCache;
private final PerProjectMemberCache memberCache;
private final PerProjectTypeCache typeCache;
private DynamicFinderValidator finderValidator;
private List<PropertyNode> cachedDomainProperties = null;
private String[] cachedNamedQueries = null;
protected DomainClass(GroovyCompilationUnit unit) {
super(unit);
pluginCache = GrailsCore.get().connect(unit.getJavaProject().getProject(), PerProjectPluginCache.class);
memberCache = GrailsCore.get().connect(unit.getJavaProject().getProject(), PerProjectMemberCache.class);
typeCache = GrailsCore.get().connect(unit.getJavaProject().getProject(), PerProjectTypeCache.class);
}
public GrailsElementKind getKind() {
return GrailsElementKind.DOMAIN_CLASS;
}
public ControllerClass getControllerClass() {
return ControllerClass.getControllerClassForElement(unit, getPrimaryTypeName());
}
public TagLibClass getTagLibClass() {
return TagLibClass.getTagLibClassForElement(unit, getPrimaryTypeName());
}
public ServiceClass getServiceClass() {
return ServiceClass.getServiceClassForElement(unit, getPrimaryTypeName());
}
public TestClass getTestClass() {
return TestClass.getTestClassForElement(this, unit, getPrimaryTypeName());
}
public DomainClass getDomainClass() {
return this;
}
public List<PropertyNode> getDomainProperties() {
if (cachedDomainProperties != null) {
return cachedDomainProperties;
}
ClassNode clazz = getGroovyClass();
if (clazz == null) {
return Collections.emptyList();
}
cachedDomainProperties = new ArrayList<PropertyNode>();
internalGetDomainProperties(clazz, cachedDomainProperties);
return cachedDomainProperties;
}
public void internalGetDomainProperties(ClassNode clazz,
List<PropertyNode> domainProperties) {
List<PropertyNode> thisDomainProperties = clazz.getProperties();
for (PropertyNode property : thisDomainProperties) {
if (!isStatic(property) && ! property.getName().equals("metaClass")) {
domainProperties.add(property);
}
}
ClassNode superClass = clazz.getSuperClass();
if (superClass != null && !superClass.getName().equals("java.lang.Object") && !superClass.getName().equals("groovy.lang.GroovyObjectSupport")) {
internalGetDomainProperties(superClass, domainProperties);
}
}
public boolean isStatic(PropertyNode property) {
FieldNode field = property.getField();
if (field != null) {
return field.isStatic();
}
return property.isStatic();
}
// all the extra fields that can go in the mappings class
public static String[] getMappingsFields() {
return MAPPINGS_FIELDS;
}
public static String[] getContstraintsFields() {
return CONTSTRAINTS_FIELDS;
}
// not quite right...need a way to specify that these fields come from grails in the assist window
// also need to distinguish between lists and closures in the initialization area
public List<String> getUnimplementedStaticFields() {
List<String> unimplementedFields = new ArrayList<String>(staticFields.length);
ClassNode clazz = getGroovyClass();
if (clazz != null) {
for (String field : staticFields) {
if (!clazz.hasProperty(field)) {
unimplementedFields.add(field);
}
}
}
return unimplementedFields;
}
public boolean isMappingField(AnnotatedNode node) {
if (node instanceof PropertyNode) {
node = ((PropertyNode) node).getField();
}
return node instanceof FieldNode &&
((FieldNode) node).isStatic() &&
((FieldNode) node).getName().equals("mapping");
}
public boolean isConstraintsField(AnnotatedNode node) {
if (node instanceof PropertyNode) {
node = ((PropertyNode) node).getField();
}
return node instanceof FieldNode &&
((FieldNode) node).isStatic() &&
((FieldNode) node).getName().equals("constraints");
}
// this method is called if the target CU is a domain class
public void initializeTypeLookup(VariableScope scope) {
ClassNode node = getGroovyClass();
if (node == null) return;
populateInjectedServices(scope);
FieldNode field = scope.getEnclosingFieldDeclaration();
if (field != null) {
if (isMappingField(field)) {
for (String fieldName : MAPPINGS_FIELDS) {
scope.addVariable(fieldName, VariableScope.VOID_CLASS_NODE, node);
}
}
}
}
// this method is called whenever the current type is a domain class
public TypeAndDeclaration lookupTypeAndDeclaration(ClassNode declaringType,
String name, VariableScope scope) {
// first check to see if we've already come across this name
// before in the current Domain class
TypeAndDeclaration typeAndDeclaration = findCached(name);
if (typeAndDeclaration != null) {
return typeAndDeclaration;
}
// static members
Map<String, ClassNode[]> staticDomainMembers = memberCache.getStaticDomainMembers();
// must check for containsKey since some of the values are purposely null
if (staticDomainMembers.containsKey(name)) {
ClassNode[] staticDomainTypes = staticDomainMembers.get(name);
ClassNode retType;
ClassNode inferredDeclingType;
if (staticDomainTypes[0] == null) {
retType = declaringType;
} else if (staticDomainTypes[0].redirect() == VariableScope.LIST_CLASS_NODE) {
retType = VariableScope.clone(staticDomainTypes[0]);
GenericsType genericsType = retType.getGenericsTypes()[0];
genericsType.setType(declaringType);
genericsType.setName(declaringType.getName());
genericsType.setUpperBounds(null);
genericsType.setLowerBound(null);
} else {
retType = staticDomainTypes[0];
}
if (staticDomainTypes[1] == null) {
inferredDeclingType = declaringType;
} else {
inferredDeclingType = staticDomainTypes[1];
}
FieldNode cached = new FieldNode(name, Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, retType, inferredDeclingType, null);
cached.setDeclaringClass(inferredDeclingType);
cacheGeneratedMember(cached);
return new TypeAndDeclaration(retType, cached, inferredDeclingType);
}
// dynamic finder prefixes
// must do a containsKey() first because some of the values are null
Map<String, ClassNode> dynamicDomainMembers = memberCache.getDynamicDomainMembers();
if (dynamicDomainMembers.containsKey(name)) {
ClassNode dynamicDomainType = dynamicDomainMembers.get(name);
if (dynamicDomainType == null) {
dynamicDomainType = declaringType;
} else if (dynamicDomainType.redirect() == VariableScope.LIST_CLASS_NODE) {
dynamicDomainType = VariableScope.clone(dynamicDomainType);
GenericsType genericsType = dynamicDomainType.getGenericsTypes()[0];
genericsType.setType(declaringType);
genericsType.setName(declaringType.getName());
genericsType.setUpperBounds(null);
genericsType.setLowerBound(null);
}
FieldNode cached = new FieldNode(name, Opcodes.ACC_PUBLIC, dynamicDomainType, declaringType, null);
cached.setDeclaringClass(declaringType);
cacheGeneratedMember(cached);
return new TypeAndDeclaration(dynamicDomainType, cached);
}
// non-static members
Map<String, ClassNode> nonstaticDomainMembers = memberCache.getNonstaticDomainMembers();
ClassNode nonStaticType = nonstaticDomainMembers.get(name);
if (nonStaticType != null) {
FieldNode cached = new FieldNode(name, Opcodes.ACC_PUBLIC, nonStaticType, declaringType, null);
cached.setDeclaringClass(declaringType);
cacheGeneratedMember(cached);
return new TypeAndDeclaration(nonStaticType, cached);
}
// look for dynamic finders
DynamicFinderValidator internalFinderValidator = getFinderValidator();
if (internalFinderValidator.isValidFinderName(name)) {
FieldNode field = internalFinderValidator.createFieldDeclaration(name);
cacheGeneratedMember(field);
return new TypeAndDeclaration(field.getType(), field, declaringType, "Dynamic finder");
}
// look for namedQueries
for (String namedQuery : getNamedQueries()) {
if (name.equals(namedQuery)) {
FieldNode criteria = createNamedCriteria(declaringType, namedQuery);
return new TypeAndDeclaration(criteria.getType(), criteria, declaringType, "Named Query");
}
}
// now look at properties and methods contributed by plugins
Map<String, ContributedProperty> contributedProperties = pluginCache.getAllDomainProperties();
if (contributedProperties.containsKey(name)) {
ClassNode type = contributedProperties.get(name).getType();
return new TypeAndDeclaration(type == null ? declaringType : type, declaringType);
}
Map<String, Set<ContributedMethod>> contributedMethods = pluginCache.getAllDomainMethods();
if (contributedMethods.containsKey(name)) {
ClassNode returnType = contributedMethods.get(name).iterator().next().getReturnType();
return new TypeAndDeclaration(returnType == null ? declaringType : returnType, declaringType);
}
// now check to see if we are in the constraints field
if (isConstraintsField(scope.getEnclosingFieldDeclaration())) {
for (PropertyNode domainProp : getDomainProperties()) {
if (domainProp.getName().equals(name)) {
return new TypeAndDeclaration(VariableScope.VOID_CLASS_NODE, domainProp, declaringType, "Constraints property");
}
}
}
return null;
}
public TypeAndDeclaration findCached(String name) {
TypeAndDeclaration typeAndDeclaration = null;
AnnotatedNode cached = getCachedMember(name);
if (cached != null) {
ClassNode type;
ClassNode inferredDeclaringType = cached.getDeclaringClass();
if (cached instanceof FieldNode) {
type = ((FieldNode) cached).getType();
} else if (cached instanceof MethodNode) {
type = ((MethodNode) cached).getReturnType();
} else {
type = getGroovyClass();
}
typeAndDeclaration = new TypeAndDeclaration(type, cached, inferredDeclaringType);
}
return typeAndDeclaration;
}
public FieldNode createNamedCriteria(ClassNode declaringType,
String namedQuery) {
ClassNode criteriaType = getNamedQueryProxyType(namedQuery);
FieldNode criteria = new FieldNode(namedQuery, Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, criteriaType, declaringType, null);
criteria.setDeclaringClass(declaringType);
cacheGeneratedMember(criteria);
return criteria;
}
private ClassNode getNamedQueryProxyType(String namedQuery) {
return new NamedQueryClassNode();
}
public Map<String, Set<ContributedMethod>> getAllContributedMethods() {
return pluginCache.getAllDomainMethods();
}
public Map<String, ContributedProperty> getAllContributedProperties() {
return pluginCache.getAllDomainProperties();
}
public Map<String, ClassNode[]> getStaticMembers() {
return memberCache.getStaticDomainMembers();
}
public Map<String, ClassNode> getDynamicFinderMembers() {
return memberCache.getDynamicDomainMembers();
}
public Map<String, ClassNode> getNonstaticMembers() {
return memberCache.getNonstaticDomainMembers();
}
public static boolean isFieldReference(String name) {
return name.equals("contraints") ||
name.equals("properties") ||
name.equals("errors") ||
name.equals("id");
}
public IFolder getGSPFolder() {
// project name/grails-app/views/domainClassName/elementName.gsp
StringBuilder sb = new StringBuilder();
sb.append(unit.getJavaProject().getElementName()).append("/grails-app/views/");
sb.append(gspFolderName());
IFolder folder = ResourcesPlugin.getWorkspace().getRoot().getFolder(new Path(sb.toString()));
return folder;
}
private String gspFolderName() {
String name = unit.getElementName();
int dotIndex = name.indexOf(".");
if (dotIndex > 0) {
name = Character.toLowerCase(name.charAt(0)) + name.substring(1, dotIndex);
return name;
} else {
return name;
}
}
public String getAssociatedDomainClassName() {
String className = getGroovyClass().getName();
return className;
}
public String[] getNamedQueries() {
if (cachedNamedQueries == null) {
PerProjectNamedQueriesHolder namedQueriesCache = GrailsCore.get().connect(unit.getJavaProject().getProject(), PerProjectNamedQueriesHolder.class);
cachedNamedQueries = namedQueriesCache.findNamedQueries(this);
}
return cachedNamedQueries;
}
public DynamicFinderValidator getFinderValidator() {
if (finderValidator == null) {
finderValidator = new FinderValidatorFactory().createValidator(getDomainClass());
}
return finderValidator;
}
public static DomainClass getDomainClassForElement(ICompilationUnit unit, String typeName) {
String controllerName = typeName + ".groovy"; //$NON-NLS-1$
String packageName = unit.getParent().getElementName();
IJavaProject javaProject = unit.getJavaProject();
GrailsProject gp = GrailsWorkspaceCore.get().create(javaProject);
return gp.getDomainClass(packageName, controllerName);
}
}