com.google.auto.value.processor.escapevelocity.ReferenceNode Maven / Gradle / Ivy
Show all versions of auto-value Show documentation
/*
* Copyright (C) 2015 Google, Inc.
*
* 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.
*/
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 com.google.auto.value.processor.escapevelocity;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.primitives.Primitives;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* A node in the parse tree that is a reference. A reference is anything beginning with {@code $},
* such as {@code $x} or {@code $x[$i].foo($j)}.
*
* @author [email protected] (Éamonn McManus)
*/
abstract class ReferenceNode extends ExpressionNode {
ReferenceNode(int lineNumber) {
super(lineNumber);
}
/**
* A node in the parse tree that is a plain reference such as {@code $x}. This node may appear
* inside a more complex reference like {@code $x.foo}.
*/
static class PlainReferenceNode extends ReferenceNode {
final String id;
PlainReferenceNode(int lineNumber, String id) {
super(lineNumber);
this.id = id;
}
@Override Object evaluate(EvaluationContext context) {
if (context.varIsDefined(id)) {
return context.getVar(id);
} else {
throw new EvaluationException("Undefined reference $" + id);
}
}
@Override
boolean isDefinedAndTrue(EvaluationContext context) {
if (context.varIsDefined(id)) {
return isTrue(context);
} else {
return false;
}
}
}
/**
* A node in the parse tree that is a reference to a property of another reference, like
* {@code $x.foo} or {@code $x[$i].foo}.
*/
static class MemberReferenceNode extends ReferenceNode {
final ReferenceNode lhs;
final String id;
MemberReferenceNode(ReferenceNode lhs, String id) {
super(lhs.lineNumber);
this.lhs = lhs;
this.id = id;
}
private static final String[] PREFIXES = {"get", "is"};
private static final boolean[] CHANGE_CASE = {false, true};
@Override Object evaluate(EvaluationContext context) {
Object lhsValue = lhs.evaluate(context);
if (lhsValue == null) {
throw new EvaluationException("Cannot get member " + id + " of null value");
}
// Velocity specifies that, given a reference .foo, it will first look for getfoo() and then
// for getFoo(), and likewise given .Foo it will look for getFoo() and then getfoo().
for (String prefix : PREFIXES) {
for (boolean changeCase : CHANGE_CASE) {
String baseId = changeCase ? changeInitialCase(id) : id;
String methodName = prefix + baseId;
Method method;
try {
method = lhsValue.getClass().getMethod(methodName);
if (!prefix.equals("is") || method.getReturnType().equals(boolean.class)) {
// Don't consider methods that happen to be called isFoo() but don't return boolean.
return invokeMethod(method, lhsValue, ImmutableList.of());
}
} catch (NoSuchMethodException e) {
// Continue with next possibility
}
}
}
throw new EvaluationException(
"Member " + id + " does not correspond to a public getter of " + lhsValue
+ ", a " + lhsValue.getClass().getName());
}
private static String changeInitialCase(String id) {
int initial = id.codePointAt(0);
String rest = id.substring(Character.charCount(initial));
if (Character.isUpperCase(initial)) {
initial = Character.toLowerCase(initial);
} else if (Character.isLowerCase(initial)) {
initial = Character.toUpperCase(initial);
}
return new StringBuilder().appendCodePoint(initial).append(rest).toString();
}
}
/**
* A node in the parse tree that is an indexing of a reference, like {@code $x[0]} or
* {@code $x.foo[$i]}. Indexing is array indexing or calling the {@code get} method of a list
* or a map.
*/
static class IndexReferenceNode extends ReferenceNode {
final ReferenceNode lhs;
final ExpressionNode index;
IndexReferenceNode(ReferenceNode lhs, ExpressionNode index) {
super(lhs.lineNumber);
this.lhs = lhs;
this.index = index;
}
@Override Object evaluate(EvaluationContext context) {
Object lhsValue = lhs.evaluate(context);
if (lhsValue == null) {
throw new EvaluationException("Cannot index null value");
}
if (lhsValue instanceof List>) {
Object indexValue = index.evaluate(context);
if (!(indexValue instanceof Integer)) {
throw new EvaluationException("List index is not an integer: " + indexValue);
}
List> lhsList = (List>) lhsValue;
int i = (Integer) indexValue;
if (i < 0 || i >= lhsList.size()) {
throw new EvaluationException(
"List index " + i + " is not valid for list of size " + lhsList.size());
}
return lhsList.get(i);
} else if (lhsValue instanceof Map, ?>) {
Object indexValue = index.evaluate(context);
Map, ?> lhsMap = (Map, ?>) lhsValue;
return lhsMap.get(indexValue);
} else {
// In general, $x[$y] is equivalent to $x.get($y). We've covered the most common cases
// above, but for other cases like Multimap we resort to evaluating the equivalent form.
MethodReferenceNode node = new MethodReferenceNode(lhs, "get", ImmutableList.of(index));
return node.evaluate(context);
}
}
}
/**
* A node in the parse tree representing a method reference, like {@code $list.size()}.
*/
static class MethodReferenceNode extends ReferenceNode {
final ReferenceNode lhs;
final String id;
final List args;
MethodReferenceNode(ReferenceNode lhs, String id, List args) {
super(lhs.lineNumber);
this.lhs = lhs;
this.id = id;
this.args = args;
}
/**
* {@inheritDoc}
*
* Evaluating a method expression such as {@code $x.foo($y)} involves looking at the actual
* types of {@code $x} and {@code $y}. The type of {@code $x} must have a public method
* {@code foo} with a parameter type that is compatible with {@code $y}.
*
*
Currently we don't allow there to be more than one matching method. That is a difference
* from Velocity, which blithely allows you to invoke {@link List#remove(int)} even though it
* can't really know that you didn't mean to invoke {@link List#remove(Object)} with an Object
* that just happens to be an Integer.
*
*
The method to be invoked must be visible in a public class or interface that is either the
* class of {@code $x} itself or one of its supertypes. Allowing supertypes is important because
* you may want to invoke a public method like {@link List#size()} on a list whose class is not
* public, such as the list returned by {@link java.util.Collections#singletonList}.
*/
@Override Object evaluate(EvaluationContext context) {
Object lhsValue = lhs.evaluate(context);
if (lhsValue == null) {
throw evaluationException("Cannot invoke method " + id + " on null value");
}
List