/*
* Copyright 2009-2017 the original author or authors.
*
* 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.codehaus.groovy.eclipse.dsl.contributions;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import groovy.lang.Closure;
import org.codehaus.groovy.ast.AnnotatedNode;
import org.codehaus.groovy.ast.AnnotationNode;
import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.ConstructorNode;
import org.codehaus.groovy.ast.FieldNode;
import org.codehaus.groovy.ast.GenericsType;
import org.codehaus.groovy.ast.MethodNode;
import org.codehaus.groovy.ast.Parameter;
import org.codehaus.groovy.ast.Variable;
import org.codehaus.groovy.ast.expr.ClassExpression;
import org.codehaus.groovy.ast.expr.ConstantExpression;
import org.codehaus.groovy.eclipse.GroovyLogManager;
import org.codehaus.groovy.eclipse.TraceCategory;
import org.codehaus.groovy.eclipse.dsl.GroovyDSLCoreActivator;
import org.codehaus.groovy.eclipse.dsl.lookup.ResolverCache;
import org.codehaus.groovy.eclipse.dsl.pointcuts.BindingSet;
import org.codehaus.groovy.eclipse.dsl.pointcuts.GroovyDSLDContext;
import org.eclipse.jdt.groovy.search.GenericsMapper;
import org.eclipse.jdt.groovy.search.VariableScope;
/**
* A contribution group will determine the set of contribution elements (eg-
* extra methods, properties, templates, etc) that are added to a particular type
* when the attached pointcut matches.
*
* @author andrew
* @created Nov 17, 2010
*/
public class DSLContributionGroup extends ContributionGroup {
private static final ParameterContribution[] NO_PARAMS = new ParameterContribution[0];
private static final String NO_TYPE = "java.lang.Object";
private static final String NO_NAME = "";
/**
* The closure that comes from the DSLD script.
* It's delegate is set to <code>this</code>.
*/
@SuppressWarnings("rawtypes")
private final Closure contributionClosure;
private VariableScope scope;
// provider that is set for the entire contribution group
// individual contributions can override
private String provider = null;
private ResolverCache resolver;
private Map<String, Collection<Object>> bindings;
private ClassNode currentType;
private Map<String, Object> wormhole;
private boolean staticScope;
private boolean isPrimaryExpression;
public DSLContributionGroup(@SuppressWarnings("rawtypes") Closure contributionClosure) {
this.contributionClosure = contributionClosure;
if (contributionClosure != null) {
contributionClosure.setDelegate(this);
contributionClosure.setResolveStrategy(Closure.DELEGATE_FIRST);
}
}
/**
* This is the main entry point into the contribution
*/
public List<IContributionElement> getContributions(GroovyDSLDContext pattern, BindingSet matches) {
// uh oh...needs to be synchronized, or can we make this class stateless?
synchronized (this) {
List<IContributionElement> result;
try {
this.contributions = new ArrayList<IContributionElement>();
this.scope = pattern.getCurrentScope();
this.resolver = pattern.getResolverCache();
this.bindings = matches.getBindings();
this.currentType = pattern.getCurrentType();
this.wormhole = scope.getWormhole();
this.staticScope = pattern.isStatic();
this.isPrimaryExpression = pattern.isPrimaryNode();
contributionClosure.call();
} catch (Exception e) {
GroovyLogManager.manager.logException(TraceCategory.DSL, e);
} finally {
result = contributions;
// must set targetType here in case someone changed the delegate on us
pattern.setTargetType(currentType);
this.contributions = null;
this.scope = null;
this.resolver = null;
this.bindings = null;
this.currentType = null;
this.wormhole = null;
}
return result;
}
}
@Override
public Object getProperty(String property) {
if ("wormhole".equals(property)) {
return wormhole;
} else if ("currentNode".equals(property)) {
return scope.getCurrentNode();
} else if ("enclosingNode".equals(property)) {
return scope.getEnclosingNode();
} else if ("currentType".equals(property)) {
return currentType;
} else if ("resolver".equals(property)) {
return resolver;
}
return bindings.get(property);
}
void setDelegateType(Object arg) {
ClassNode delegate = asClassNode(arg);
if (delegate != null) {
// also need to set targetType, but only if primary expression
scope.addVariable("delegate", delegate, VariableScope.CLOSURE_CLASS_NODE);
scope.addVariable("getDelegate", delegate, VariableScope.CLOSURE_CLASS_NODE);
contributions.add(new EmptyContributionElement(currentType));
if (isPrimaryExpression) {
// must save for later
currentType = delegate;
}
}
}
private ClassNode asClassNode(Object value) {
if (value == null) {
return null;
} else if (value instanceof String) {
return resolver.resolve((String) value);
} else if (value instanceof ClassNode) {
return (ClassNode) value;
} else if (value instanceof Class) {
return resolver.resolve(((Class<?>) value).getName());
} else {
return resolver.resolve(value.toString());
}
}
/**
* Called by closure to add a method
* @param args
*/
void method(Map<String, Object> args) {
String name = asString(args.get("name"));
Object value = args.get("type");
String returnType = value == null ? "java.lang.Object" : asString(value);
value = args.get("declaringType");
String declaringType = value == null ? getTypeName(currentType) : asString(value);
value = args.get("provider");
String provider = value == null ? this.provider : asString(value); // might be null
value = args.get("doc");
String doc = value == null ? null : asString(value); // might be null
boolean useNamedArgs = asBoolean(args.get("useNamedArgs"));
boolean noParens = asBoolean(args.get("noParens"));
ParameterContribution[] params = extractParams(args, "params");
ParameterContribution[] namedParams = extractParams(args, "namedParams");
ParameterContribution[] optionalParams = extractParams(args, "optionalParams");
boolean isStatic = isStatic(args);
boolean isDeprecated = isDeprecated(args);
if (!staticScope || (staticScope && isStatic)) {
contributions.add(new MethodContributionElement(name == null ? NO_NAME : name, params, namedParams, optionalParams, returnType == null ? NO_TYPE
: returnType, declaringType, isStatic, provider == null ? this.provider : provider, doc, useNamedArgs, noParens, isDeprecated, DEFAULT_RELEVANCE_MULTIPLIER));
}
}
private ParameterContribution[] extractParams(Map<String, Object> args, String paramKind) {
Object value;
@SuppressWarnings("unchecked")
Map<Object, Object> paramsMap = (Map<Object, Object>) args.get(paramKind);
ParameterContribution[] params;
if (paramsMap != null) {
params = new ParameterContribution[paramsMap.size()];
int i = 0;
for (Entry<Object, Object> entry : paramsMap.entrySet()) {
value = entry.getValue();
String type = value == null ? "java.lang.Object" : asString(value);
params[i++] = new ParameterContribution(asString(entry.getKey()), type);
}
} else {
params = NO_PARAMS;
}
return params;
}
private boolean asBoolean(Object object) {
if (object == null) {
return false;
}
if (object instanceof Boolean) {
return (Boolean) object;
}
String str = object.toString();
return str.equalsIgnoreCase("true") ||
str.equalsIgnoreCase("yes");
}
/**
* Called by closure to add a property
*/
void property(Map<String, Object> args) {
String name = asString(args.get("name"));
Object value = args.get("type");
String type = value == null ? NO_TYPE : asString(value);
value = args.get("declaringType");
String declaringType = value == null ? getTypeName(currentType) : asString(value);
value = args.get("provider");
String provider = value == null ? this.provider : asString(value); // might be null
String doc = asString(args.get("doc")); // might be null
boolean isStatic = isStatic(args);
boolean isDeprecated = isDeprecated(args);
if (!staticScope || (staticScope && isStatic)) {
contributions.add(new PropertyContributionElement(name == null ? NO_NAME : name, type,
declaringType, isStatic, provider, doc, isDeprecated, DEFAULT_RELEVANCE_MULTIPLIER));
}
}
/**
* stub...will be used later to add templates
*/
void template(Map<String, String> args) {
}
void delegatesTo(Map<String, Object> args) {
String name = asString(args.get("type"));
boolean isStatic = isStatic(args);
boolean isDeprecated = isDeprecated(args);
boolean asCategory = getBoolean("asCategory", args);
boolean useNamed = getBoolean("useNamed", args);
boolean noParens = getBoolean("noParens", args);
@SuppressWarnings("unchecked")
List<String> except = (List<String>) args.get("except");
ClassNode type = this.resolver.resolve(name);
internalDelegatesTo(type, useNamed, isStatic, asCategory, isDeprecated, except, noParens);
}
void delegatesTo(String className) {
delegatesTo(this.resolver.resolve(className));
}
void delegatesTo(Class<?> clazz) {
ClassNode resolved = this.resolver.resolve(clazz.getCanonicalName());
if (resolved == VariableScope.OBJECT_CLASS_NODE && !clazz.getName().equals(Object.class.getName())) {
// likely that we are trying to resolve a class that is defined inside of a DSLD itself
try {
resolved = ClassHelper.make(clazz);
} catch (Exception e) {
GroovyDSLCoreActivator.logException(e);
}
}
delegatesTo(resolved);
}
/**
* invoked by the closure
* takes an expression and adds all members of its type to the augmented
* class reference.
*/
void delegatesTo(AnnotatedNode expr) {
internalDelegatesTo(expr, false, false, false, false, null, false);
}
void delegatesToUseNamedArgs(String className) {
delegatesToUseNamedArgs(this.resolver.resolve(className));
}
void delegatesToUseNamedArgs(Class<?> clazz) {
delegatesToUseNamedArgs(this.resolver.resolve(clazz.getCanonicalName()));
}
/**
* invoked by the closure
* takes an expression and adds all members of its type to the augmented
* class reference.
*/
void delegatesToUseNamedArgs(AnnotatedNode expr) {
internalDelegatesTo(expr, true, false, false, false, null, false);
}
void delegatesToCategory(String className) {
delegatesToCategory(this.resolver.resolve(className));
}
void delegatesToCategory(Class<?> clazz) {
delegatesToCategory(this.resolver.resolve(clazz.getCanonicalName()));
}
/**
* invoked by the closure
* takes an expression and adds all members of its type to the augmented
* class reference.
*/
void delegatesToCategory(AnnotatedNode expr) {
internalDelegatesTo(expr, false, false, true, false, null, false);
}
/**
* Convert a {@link ClassNode} into a string that includes type parameters
*/
static String getTypeName(ClassNode clazz) {
StringBuilder sb = new StringBuilder();
sb.append(clazz.getName());
if (clazz.getGenericsTypes() != null && clazz.getGenericsTypes().length > 0) {
sb.append('<');
for (GenericsType gt : clazz.getGenericsTypes()) {
sb.append(getTypeName(gt.getType()));
sb.append(',');
}
sb.replace(sb.length()-1, sb.length(), ">");
}
return sb.toString();
}
private void internalDelegatesTo(AnnotatedNode expr, boolean useNamedArgs, boolean isStatic, boolean asCategory, boolean isDeprecated, List<String> exceptions, boolean noParens) {
if (staticScope && !isStatic && !VariableScope.CLASS_CLASS_NODE.equals(currentType)) {
return;
}
ClassNode type;
if (expr instanceof ClassNode) {
type = (ClassNode) expr;
} else if (expr instanceof FieldNode) {
type = ((FieldNode) expr).getType();
} else if (expr instanceof MethodNode) {
type = ((MethodNode) expr).getReturnType();
} else if (expr instanceof ClassExpression) {
type = ((ClassExpression) expr).getType();
} else {
// invalid
if (GroovyLogManager.manager.hasLoggers()) {
GroovyLogManager.manager.log(TraceCategory.DSL, "Cannot invoke delegatesTo() on an invalid object: " + expr);
}
return;
}
if (!type.getName().equals(Object.class.getName())) {
// use this to resolve parameterized types
GenericsMapper mapper = GenericsMapper.gatherGenerics(type, type.redirect());
// naked variants of getter and setter methods must be added at the end
// FIXADE why???
List<IContributionElement> accessorContribs = new ArrayList<IContributionElement>(1);
for (MethodNode method : type.getMethods()) {
if ((exceptions == null || !exceptions.contains(method.getName())) && !(method instanceof ConstructorNode) && ! method.getName().contains("$")) {
ClassNode resolvedReturnType = VariableScope.resolveTypeParameterization(mapper, VariableScope.clone(method.getReturnType()));
if (asCategory) {
delegateToCategoryMethod(useNamedArgs, isStatic, type, method, resolvedReturnType, isDeprecated, accessorContribs, noParens);
} else {
delegateToNonCategoryMethod(useNamedArgs, isStatic, type, method, resolvedReturnType, isDeprecated, accessorContribs, noParens);
}
}
}
contributions.addAll(accessorContribs);
}
}
// FIXADE TODO combine with #delegateToCategoryMethod
private void delegateToNonCategoryMethod(boolean useNamedArgs, boolean isStatic, ClassNode type, MethodNode method, ClassNode resolvedReturnType, boolean isDeprecated,
List<IContributionElement> accessorContribs, boolean noParens) {
String name = method.getName();
contributions.add(new MethodContributionElement(name, toParameterContribution(method.getParameters()), NO_PARAMS,
NO_PARAMS, getTypeName(resolvedReturnType), getTypeName(type), (method.isStatic() || isStatic), provider, null,
useNamedArgs, noParens, isDeprecated, DEFAULT_RELEVANCE_MULTIPLIER));
// also add the associated property if applicable
String prefix;
if ((prefix = isAccessor(method, name, false)) != null) {
accessorContribs.add(new PropertyContributionElement(Character.toLowerCase(name.charAt(prefix.length())) + name.substring(prefix.length()+1), getTypeName(resolvedReturnType),
getTypeName(method.getDeclaringClass()), (method.isStatic() || isStatic), provider, null, isDeprecated, DEFAULT_RELEVANCE_MULTIPLIER));
}
}
private void delegateToCategoryMethod(boolean useNamedArgs, boolean isStatic, ClassNode type, MethodNode method, ClassNode resolvedReturnType, boolean isDeprecated,
List<IContributionElement> accessorContribs, boolean noParens) {
String name = method.getName();
if (method.getParameters() != null && method.getParameters().length > 0) {
ClassNode firstType = method.getParameters()[0].getType();
if ((firstType.isInterface() && currentType.implementsInterface(firstType)) ||
currentType.isDerivedFrom(firstType)) {
contributions.add(new MethodContributionElement(name, toParameterContributionRemoveFirst(method.getParameters()),
NO_PARAMS, NO_PARAMS, getTypeName(resolvedReturnType), getTypeName(type), isStatic, provider, null,
useNamedArgs, noParens, isDeprecated, DEFAULT_RELEVANCE_MULTIPLIER));
// also add the associated property if applicable
String prefix;
if ((prefix = isAccessor(method, name, true)) != null) {
accessorContribs.add(new PropertyContributionElement(Character.toLowerCase(name.charAt(prefix.length())) + name.substring(prefix.length()+1), getTypeName(resolvedReturnType),
getTypeName(method.getDeclaringClass()), (method.isStatic() || isStatic), provider, null, isDeprecated, DEFAULT_RELEVANCE_MULTIPLIER));
}
}
}
}
private String isAccessor(MethodNode method, String name, boolean isCategory) {
int paramCount = isCategory ? 1 : 0;
if (method.getParameters() == null || method.getParameters().length == paramCount) {
if (name.startsWith("get") && name.length() > 3) {
return "get";
} else if (name.startsWith("is") && name.length() > 2) {
return "is";
}
}
return null;
}
private ParameterContribution[] toParameterContribution(Parameter[] params) {
if (params != null) {
ParameterContribution[] contribs = new ParameterContribution[params.length];
for (int i = 0; i < contribs.length; i++) {
contribs[i] = new ParameterContribution(params[i]);
}
return contribs;
} else {
return new ParameterContribution[0];
}
}
private ParameterContribution[] toParameterContributionRemoveFirst(Parameter[] params) {
if (params != null) {
ParameterContribution[] contribs = new ParameterContribution[params.length-1];
for (int i = 1; i < params.length; i++) {
contribs[i-1] = new ParameterContribution(params[i]);
}
return contribs;
} else {
return new ParameterContribution[0];
}
}
void provider(Object args) {
provider = args == null ? null : asString(args);
}
/**
* @param args map passed in from the call to method or property
* @return true iff the static argument is passed in.
*/
private boolean isStatic(Map<?, ?> args) {
return getBoolean("isStatic", args);
}
private boolean isDeprecated(Map<?, ?> args) {
return getBoolean("isDeprecated", args);
}
private boolean getBoolean(String name, Map<?,?> args) {
Object maybeStatic = args.get(name);
if (maybeStatic == null) {
return false;
} else if (maybeStatic instanceof Boolean) {
return (Boolean) maybeStatic;
} else {
return Boolean.getBoolean(maybeStatic.toString());
}
}
/**
* Converts an object into a string
*/
private String asString(Object value) {
if (value == null) {
return null;
} else if (value instanceof String) {
return (String) value;
} else if (value instanceof ClassNode) {
return getTypeName(((ClassNode) value));
} else if (value instanceof FieldNode) {
return getTypeName(((FieldNode) value).getDeclaringClass()) + "." + ((FieldNode) value).getName();
} else if (value instanceof MethodNode) {
return getTypeName(((MethodNode) value).getDeclaringClass()) + "." + ((MethodNode) value).getName();
} else if (value instanceof ConstantExpression) {
return ((ConstantExpression) value).getText();
} else if (value instanceof Variable) {
return ((Variable) value).getName();
} else if (value instanceof AnnotationNode) {
return ((AnnotationNode) value).getClassNode().getName();
} else if (value instanceof Class) {
return ((Class<?>) value).getName();
} else {
return value.toString();
}
}
/**
* Logs a message to the groovy console log if open
* @param msg
*/
Object log(Object msg) {
if (GroovyLogManager.manager.hasLoggers()) {
GroovyLogManager.manager.log(TraceCategory.DSL, "========== " + msg);
}
return msg;
}
}