/*
* Copyright 2003-2010 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.classgen;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.codehaus.groovy.ast.ClassCodeVisitorSupport;
import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.CodeVisitorSupport;
import org.codehaus.groovy.ast.ConstructorNode;
import org.codehaus.groovy.ast.EnumConstantClassNode;
import org.codehaus.groovy.ast.FieldNode;
import org.codehaus.groovy.ast.InnerClassNode;
import org.codehaus.groovy.ast.MethodNode;
import org.codehaus.groovy.ast.Parameter;
import org.codehaus.groovy.ast.expr.*;
import org.codehaus.groovy.ast.stmt.*;
import org.codehaus.groovy.control.CompilationUnit;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.control.messages.SyntaxErrorMessage;
import org.codehaus.groovy.syntax.SyntaxException;
import org.codehaus.groovy.syntax.Token;
import org.codehaus.groovy.syntax.Types;
import org.objectweb.asm.Opcodes;
public class EnumVisitor extends ClassCodeVisitorSupport{
// some constants for modifiers
private static final int FS = Opcodes.ACC_FINAL | Opcodes.ACC_STATIC;
private static final int PS = Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC;
private static final int PUBLIC_FS = Opcodes.ACC_PUBLIC | FS;
private static final int PRIVATE_FS = Opcodes.ACC_PRIVATE | FS;
private final SourceUnit sourceUnit;
public EnumVisitor(CompilationUnit cu, SourceUnit su) {
sourceUnit = su;
}
public void visitClass(ClassNode node) {
if (!node.isEnum()) return;
completeEnum(node);
}
protected SourceUnit getSourceUnit() {
return sourceUnit;
}
private void completeEnum(ClassNode enumClass) {
boolean isAic = isAnonymousInnerClass(enumClass);
// create MIN_VALUE and MAX_VALUE fields
FieldNode minValue = null, maxValue = null, values = null;
if(!isAic) {
// create values field
values = new FieldNode("$VALUES",PRIVATE_FS|Opcodes.ACC_SYNTHETIC,enumClass.makeArray(),enumClass,null);
values.setSynthetic(true);
addMethods(enumClass, values);
// create MIN_VALUE and MAX_VALUE fields
minValue = new FieldNode("MIN_VALUE", PUBLIC_FS, enumClass, enumClass, null);
maxValue = new FieldNode("MAX_VALUE", PUBLIC_FS, enumClass, enumClass, null);
}
addInit(enumClass, minValue, maxValue, values, isAic);
}
private void addMethods(ClassNode enumClass, FieldNode values) {
List methods = enumClass.getMethods();
boolean hasNext = false;
boolean hasPrevious = false;
for (int i = 0; i < methods.size(); i++) {
MethodNode m = (MethodNode) methods.get(i);
if (m.getName().equals("next") && m.getParameters().length == 0) hasNext = true;
if (m.getName().equals("previous") && m.getParameters().length == 0) hasPrevious = true;
if (hasNext && hasPrevious) break;
}
{
// create values() method
MethodNode valuesMethod = new MethodNode("values",PUBLIC_FS,enumClass.makeArray(),new Parameter[0],ClassNode.EMPTY_ARRAY,null);
valuesMethod.setSynthetic(true);
BlockStatement code = new BlockStatement();
MethodCallExpression cloneCall = new MethodCallExpression(new FieldExpression(values), "clone", MethodCallExpression.NO_ARGUMENTS);
cloneCall.setMethodTarget(values.getType().getMethod("clone", Parameter.EMPTY_ARRAY));
code.addStatement(new ReturnStatement(cloneCall));
valuesMethod.setCode(code);
enumClass.addMethod(valuesMethod);
}
if (!hasNext) {
// create next() method, code:
// Day next() {
// int ordinal = ordinal().next()
// if (ordinal >= values().size()) ordinal = 0
// return values()[ordinal]
// }
Token assign = Token.newSymbol(Types.ASSIGN, -1, -1);
Token ge = Token.newSymbol(Types.COMPARE_GREATER_THAN_EQUAL, -1, -1);
MethodNode nextMethod = new MethodNode("next", Opcodes.ACC_PUBLIC | Opcodes.ACC_SYNTHETIC, enumClass, new Parameter[0], ClassNode.EMPTY_ARRAY, null);
nextMethod.setSynthetic(true);
BlockStatement code = new BlockStatement();
BlockStatement ifStatement = new BlockStatement();
ifStatement.addStatement(
new ExpressionStatement(
new BinaryExpression(new VariableExpression("ordinal"), assign, new ConstantExpression(Integer.valueOf(0)))
)
);
code.addStatement(
new ExpressionStatement(
new DeclarationExpression(
new VariableExpression("ordinal"),
assign,
new MethodCallExpression(
new MethodCallExpression(
VariableExpression.THIS_EXPRESSION,
"ordinal",
MethodCallExpression.NO_ARGUMENTS),
"next",
MethodCallExpression.NO_ARGUMENTS
)
)
)
);
code.addStatement(
new IfStatement(
new BooleanExpression(new BinaryExpression(
new VariableExpression("ordinal"),
ge,
new MethodCallExpression(
new FieldExpression(values),
"size",
MethodCallExpression.NO_ARGUMENTS
)
)),
ifStatement,
EmptyStatement.INSTANCE
)
);
code.addStatement(
new ReturnStatement(
new MethodCallExpression(new FieldExpression(values), "getAt", new VariableExpression("ordinal"))
)
);
nextMethod.setCode(code);
enumClass.addMethod(nextMethod);
}
if (!hasPrevious) {
// create previous() method, code:
// Day previous() {
// int ordinal = ordinal().previous()
// if (ordinal < 0) ordinal = values().size() - 1
// return values()[ordinal]
// }
Token assign = Token.newSymbol(Types.ASSIGN, -1, -1);
Token lt = Token.newSymbol(Types.COMPARE_LESS_THAN, -1, -1);
MethodNode nextMethod = new MethodNode("previous", Opcodes.ACC_PUBLIC | Opcodes.ACC_SYNTHETIC, enumClass, new Parameter[0], ClassNode.EMPTY_ARRAY, null);
nextMethod.setSynthetic(true);
BlockStatement code = new BlockStatement();
BlockStatement ifStatement = new BlockStatement();
ifStatement.addStatement(
new ExpressionStatement(
new BinaryExpression(new VariableExpression("ordinal"), assign,
new MethodCallExpression(
new MethodCallExpression(
new FieldExpression(values),
"size",
MethodCallExpression.NO_ARGUMENTS
),
"minus",
new ConstantExpression(Integer.valueOf(1))
)
)
)
);
code.addStatement(
new ExpressionStatement(
new DeclarationExpression(
new VariableExpression("ordinal"),
assign,
new MethodCallExpression(
new MethodCallExpression(
VariableExpression.THIS_EXPRESSION,
"ordinal",
MethodCallExpression.NO_ARGUMENTS),
"previous",
MethodCallExpression.NO_ARGUMENTS
)
)
)
);
code.addStatement(
new IfStatement(
new BooleanExpression(new BinaryExpression(
new VariableExpression("ordinal"),
lt,
new ConstantExpression(Integer.valueOf(0))
)),
ifStatement,
EmptyStatement.INSTANCE
)
);
code.addStatement(
new ReturnStatement(
new MethodCallExpression(new FieldExpression(values), "getAt", new VariableExpression("ordinal"))
)
);
nextMethod.setCode(code);
enumClass.addMethod(nextMethod);
}
{
// create valueOf
Parameter stringParameter = new Parameter(ClassHelper.STRING_TYPE,"name");
MethodNode valueOfMethod = new MethodNode("valueOf",PS,enumClass,new Parameter[]{stringParameter},ClassNode.EMPTY_ARRAY,null);
ArgumentListExpression callArguments = new ArgumentListExpression();
callArguments.addExpression(new ClassExpression(enumClass));
callArguments.addExpression(new VariableExpression("name"));
BlockStatement code = new BlockStatement();
code.addStatement(
new ReturnStatement(
new MethodCallExpression(new ClassExpression(ClassHelper.Enum_Type),"valueOf",callArguments)
)
);
valueOfMethod.setCode(code);
valueOfMethod.setSynthetic(true);
enumClass.addMethod(valueOfMethod);
}
}
private void addInit (ClassNode enumClass, FieldNode minValue,
FieldNode maxValue, FieldNode values,
boolean isAic)
{
addConstructor(enumClass);
// constructor helper
// This method is used instead of calling the constructor as
// calling the constructor may require a table with MetaClass
// selecting the constructor for each enum value. So instead we
// use this method to have a central point for constructor selection
// and only one table. The whole construction is needed because
// Reflection forbids access to the enum constructor.
// code:
// def $INIT(Object[] para) {
// return this(*para)
// }
Parameter[] parameter = new Parameter[]{new Parameter(ClassHelper.OBJECT_TYPE.makeArray(), "para")};
MethodNode initMethod = new MethodNode("$INIT",PUBLIC_FS | Opcodes.ACC_SYNTHETIC,enumClass,parameter,ClassNode.EMPTY_ARRAY,null);
initMethod.setSynthetic(true);
ConstructorCallExpression cce = new ConstructorCallExpression(
ClassNode.THIS,
new ArgumentListExpression(
new SpreadExpression(new VariableExpression("para"))
)
);
BlockStatement code = new BlockStatement();
code.addStatement(new ReturnStatement(cce));
initMethod.setCode(code);
enumClass.addMethod(initMethod);
// static init
List fields = enumClass.getFields();
List arrayInit = new ArrayList();
int value = -1;
Token assign = Token.newSymbol(Types.ASSIGN, -1, -1);
List block = new ArrayList();
FieldNode tempMin = null;
FieldNode tempMax = null;
for (Iterator iterator = fields.iterator(); iterator.hasNext();) {
FieldNode field = (FieldNode) iterator.next();
if ((field.getModifiers()&Opcodes.ACC_ENUM) == 0) continue;
value++;
if (tempMin == null) tempMin = field;
tempMax = field;
ClassNode enumBase = enumClass;
ArgumentListExpression args = new ArgumentListExpression();
args.addExpression(new ConstantExpression(field.getName()));
args.addExpression(new ConstantExpression(Integer.valueOf(value)));
if (field.getInitialExpression()!=null) {
ListExpression oldArgs = (ListExpression) field.getInitialExpression();
for (Iterator oldArgsIterator = oldArgs.getExpressions().iterator(); oldArgsIterator.hasNext();) {
Expression exp = (Expression) oldArgsIterator.next();
if (exp instanceof MapEntryExpression) {
String msg = "The usage of a map entry expression to initialize an Enum is currently not supported, please use an explicit map instead.";
sourceUnit.getErrorCollector().addErrorAndContinue(
new SyntaxErrorMessage(
new SyntaxException(msg + '\n', exp.getLineNumber(), exp.getColumnNumber()), sourceUnit)
);
continue;
}
InnerClassNode inner = null;
if (exp instanceof ClassExpression) {
ClassExpression clazzExp = (ClassExpression) exp;
ClassNode ref = clazzExp.getType();
if (ref instanceof EnumConstantClassNode) {
inner = (InnerClassNode) ref;
}
}
if (inner!=null) {
if (inner.getVariableScope()==null) {
enumBase = inner;
/*
* GROOVY-3985: Remove the final modifier from $INIT method in this case
* so that subclasses of enum generated for enum constants (aic) can provide
* their own $INIT method
*/
initMethod.setModifiers(initMethod.getModifiers() & ~Opcodes.ACC_FINAL);
continue;
}
}
args.addExpression(exp);
}
}
field.setInitialValueExpression(null);
block.add(
new ExpressionStatement(
new BinaryExpression(
new FieldExpression(field),
assign,
new StaticMethodCallExpression(enumBase,"$INIT",args)
)
)
);
arrayInit.add(new FieldExpression(field));
}
if (!isAic) {
if (tempMin!=null) {
block.add(
new ExpressionStatement(
new BinaryExpression(
new FieldExpression(minValue),
assign,
new FieldExpression(tempMin)
)
)
);
block.add(
new ExpressionStatement(
new BinaryExpression(
new FieldExpression(maxValue),
assign,
new FieldExpression(tempMax)
)
)
);
enumClass.addField(minValue);
enumClass.addField(maxValue);
}
block.add(
new ExpressionStatement(
new BinaryExpression(new FieldExpression(values),assign,new ArrayExpression(enumClass,arrayInit))
)
);
enumClass.addField(values);
}
enumClass.addStaticInitializerStatements(block, true);
}
private boolean isAnonymousInnerClass(ClassNode enumClass) {
if (!(enumClass instanceof EnumConstantClassNode)) return false;
InnerClassNode ic = (InnerClassNode) enumClass;
return ic.getVariableScope()==null;
}
private void addConstructor(ClassNode enumClass) {
// first look if there are declared constructors
List ctors = new ArrayList(enumClass.getDeclaredConstructors());
if (ctors.size()==0) {
// add default constructor
ConstructorNode init = new ConstructorNode(Opcodes.ACC_PUBLIC,new Parameter[0],ClassNode.EMPTY_ARRAY,new BlockStatement());
enumClass.addConstructor(init);
ctors.add(init);
}
// for each constructor:
// if constructor does not define a call to super, then transform constructor
// to get String,int parameters at beginning and add call super(String,int)
for (Iterator iterator = ctors.iterator(); iterator.hasNext();) {
boolean chainedThisConstructorCall = false;
ConstructorNode ctor = (ConstructorNode) iterator.next();
ConstructorCallExpression cce = null;
if (ctor.firstStatementIsSpecialConstructorCall()) {
Statement code = ctor.getFirstStatement();
cce = (ConstructorCallExpression) ((ExpressionStatement) code).getExpression();
if(cce.isSuperCall()) {
continue;
} else {
chainedThisConstructorCall = true;
}
}
// we need to add parameters
Parameter[] oldP = ctor.getParameters();
Parameter[] newP = new Parameter[oldP.length+2];
String stringParameterName = getUniqueVariableName("__str",ctor.getCode());
newP[0] = new Parameter(ClassHelper.STRING_TYPE,stringParameterName);
String intParameterName = getUniqueVariableName("__int",ctor.getCode());
newP[1] = new Parameter(ClassHelper.int_TYPE,intParameterName);
System.arraycopy(oldP, 0, newP, 2, oldP.length);
ctor.setParameters(newP);
if(chainedThisConstructorCall) {
TupleExpression args = (TupleExpression) cce.getArguments();
List<Expression> argsExprs = args.getExpressions();
argsExprs.add(0, new VariableExpression(stringParameterName));
argsExprs.add(1, new VariableExpression(intParameterName));
} else {
// and a super call
cce = new ConstructorCallExpression(
ClassNode.SUPER,
new ArgumentListExpression(
new VariableExpression(stringParameterName),
new VariableExpression(intParameterName)
)
);
BlockStatement code = new BlockStatement();
code.addStatement(new ExpressionStatement(cce));
Statement oldCode = ctor.getCode();
if (oldCode!=null) code.addStatement(oldCode);
ctor.setCode(code);
}
}
}
private String getUniqueVariableName(final String name, Statement code) {
if (code==null) return name;
final Object[] found=new Object[1];
CodeVisitorSupport cv = new CodeVisitorSupport() {
public void visitVariableExpression(VariableExpression expression) {
if (expression.getName().equals(name)) found[0]=Boolean.TRUE;
}
};
code.visit(cv);
if (found[0]!=null) return getUniqueVariableName("_"+name, code);
return name;
}
}