Package org.apache.tapestry5.internal.services

Source Code of org.apache.tapestry5.internal.services.PropertyConduitSourceImpl$PropertyConduitBuilder

// Copyright 2007, 2008, 2009, 2010 The Apache Software Foundation
//
// 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.apache.tapestry5.internal.services;

import org.antlr.runtime.ANTLRInputStream;
import org.antlr.runtime.CommonTokenStream;
import org.antlr.runtime.tree.Tree;
import org.apache.tapestry5.PropertyConduit;
import org.apache.tapestry5.internal.antlr.PropertyExpressionLexer;
import org.apache.tapestry5.internal.antlr.PropertyExpressionParser;
import org.apache.tapestry5.internal.util.IntegerRange;
import org.apache.tapestry5.internal.util.MultiKey;
import org.apache.tapestry5.ioc.AnnotationProvider;
import org.apache.tapestry5.ioc.internal.NullAnnotationProvider;
import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
import org.apache.tapestry5.ioc.internal.util.GenericsUtils;
import org.apache.tapestry5.ioc.internal.util.InternalUtils;
import org.apache.tapestry5.ioc.services.*;
import org.apache.tapestry5.ioc.util.AvailableValues;
import org.apache.tapestry5.ioc.util.BodyBuilder;
import org.apache.tapestry5.ioc.util.UnknownValueException;
import org.apache.tapestry5.services.ComponentLayer;
import org.apache.tapestry5.services.InvalidationListener;
import org.apache.tapestry5.services.PropertyConduitSource;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import static org.apache.tapestry5.internal.antlr.PropertyExpressionParser.*;

public class PropertyConduitSourceImpl implements PropertyConduitSource, InvalidationListener
{
    private static final MethodSignature GET_SIGNATURE = new MethodSignature(Object.class, "get", new Class[]
    { Object.class }, null);

    private static final MethodSignature SET_SIGNATURE = new MethodSignature(void.class, "set", new Class[]
    { Object.class, Object.class }, null);

    private static final Method RANGE;

    private static final Method INVERT;

    static
    {
        try
        {
            RANGE = BasePropertyConduit.class.getMethod("range", int.class, int.class);
            INVERT = BasePropertyConduit.class.getMethod("invert", Object.class);
        }
        catch (NoSuchMethodException ex)
        {
            throw new RuntimeException(ex);
        }
    }

    private final AnnotationProvider nullAnnotationProvider = new NullAnnotationProvider();

    private static class ConstructorParameter
    {
        private final String fieldName;

        private final Class type;

        private final Object value;

        ConstructorParameter(String fieldName, Class type, Object value)
        {
            this.fieldName = fieldName;
            this.type = type;
            this.value = value;
        }

        public String getFieldName()
        {
            return fieldName;
        }

        public Class getType()
        {
            return type;
        }

        public Object getValue()
        {
            return value;
        }
    }

    /**
     * Describes all the gory details of one term (one property or method
     * invocation) from within the expression.
     */
    private interface ExpressionTermInfo extends AnnotationProvider
    {

        /**
         * The method to invoke to read the property value, or null.
         */
        Method getReadMethod();

        /**
         * The method to invoke to write the property value, or null. Always
         * null for method terms (which are inherently
         * read-only).
         */
        Method getWriteMethod();

        /**
         * The return type of the method, or the type of the property.
         */
        Class getType();

        /**
         * True if an explicit cast to the return type is needed (typically
         * because of generics).
         */
        boolean isCastRequired();

        /**
         * Returns a user-presentable name identifying the property or method
         * name.
         */
        String getDescription();

        /**
         * Returns the name of the property, if exists. This is also the name of the public field.
         */
        String getPropertyName();

        /**
         * Returns true if the term is actually a public field.
         */
        boolean isField();

        /**
         * Returns the Field if the term is a public field.
         */
        Field getField();

    }

    /**
     * How are null values in intermdiate terms to be handled?
     */
    private enum NullHandling
    {
        /**
         * Add code to check for null and throw exception if null.
         */
        FORBID,

        /**
         * Add code to check for null and short-circuit (i.e., the "?."
         * safe-dereference operator)
         */
        ALLOW,

        /**
         * Add no null check at all.
         */
        IGNORE
    }

    private class GeneratedTerm
    {
        final Type type;

        final String termReference;

        /**
         * @param type
         *            type of variable
         * @param termReference
         *            name of variable, or a constant value
         */
        private GeneratedTerm(Type type, String termReference)
        {
            this.type = type;
            this.termReference = termReference;
        }
    }

    private final PropertyAccess access;

    private final ClassFactory classFactory;

    private final TypeCoercer typeCoercer;

    private final StringInterner interner;

    /**
     * Because of stuff like Hibernate, we sometimes start with a subclass in
     * some inaccessible class loader and need to
     * work up to a base class from a common class loader.
     */
    private final Map<Class, Class> classToEffectiveClass = CollectionFactory.newConcurrentMap();

    /**
     * Keyed on combination of root class and expression.
     */
    private final Map<MultiKey, PropertyConduit> cache = CollectionFactory.newConcurrentMap();

    private final Invariant invariantAnnotation = new Invariant()
    {
        public Class<? extends Annotation> annotationType()
        {
            return Invariant.class;
        }
    };

    private final AnnotationProvider invariantAnnotationProvider = new AnnotationProvider()
    {
        public <T extends Annotation> T getAnnotation(Class<T> annotationClass)
        {
            if (annotationClass == Invariant.class)
                return annotationClass.cast(invariantAnnotation);

            return null;
        }
    };

    private final PropertyConduit literalTrue;

    private final PropertyConduit literalFalse;

    private final PropertyConduit literalNull;

    /**
     * Encapsulates the process of building a PropertyConduit instance from an
     * expression.
     */
    class PropertyConduitBuilder
    {
        private final Class rootType;

        private final ClassFab classFab;

        private final String expression;

        private final Tree tree;

        private Class conduitPropertyType;

        private String conduitPropertyName;

        private AnnotationProvider annotationProvider = nullAnnotationProvider;

        // Used to create unique variable names.

        private int variableIndex = 0;

        private final List<ConstructorParameter> parameters = CollectionFactory.newList();

        private final BodyBuilder navBuilder = new BodyBuilder();

        PropertyConduitBuilder(Class rootType, String expression, Tree tree)
        {
            this.rootType = rootType;
            this.expression = expression;
            this.tree = tree;

            String name = ClassFabUtils.generateClassName("PropertyConduit");

            this.classFab = classFactory.newClass(name, BasePropertyConduit.class);
        }

        PropertyConduit createInstance()
        {
            createAccessors();

            Object[] parameters = createConstructor();

            Class conduitClass = classFab.createClass();

            try
            {
                return (PropertyConduit) conduitClass.getConstructors()[0].newInstance(parameters);
            }
            catch (Exception ex)
            {
                throw new RuntimeException(ex);
            }
        }

        private Object[] createConstructor()
        {
            List<Class> types = CollectionFactory.newList();

            // $1, $2, $3, $4, $5 ...

            types.add(Class.class);
            types.add(String.class);
            types.add(AnnotationProvider.class);
            types.add(String.class);
            types.add(TypeCoercer.class);

            List<Object> values = CollectionFactory.newList();

            values.add(conduitPropertyType);
            values.add(conduitPropertyName);
            values.add(annotationProvider);
            values.add(interner.format("PropertyConduit[%s %s]", rootType.getName(), expression));
            values.add(typeCoercer);

            BodyBuilder builder = new BodyBuilder().begin();

            builder.addln("super($1,$2,$3,$4,$5);");

            int index = 6;

            for (ConstructorParameter p : parameters)
            {
                types.add(p.getType());
                values.add(p.getValue());

                builder.addln("%s = $%d;", p.getFieldName(), index++);
            }

            builder.end();

            Class[] arrayOfTypes = types.toArray(new Class[0]);

            classFab.addConstructor(arrayOfTypes, null, builder.toString());

            return values.toArray();
        }

        private String addInjection(Class fieldType, Object fieldValue)
        {
            String fieldName = String.format("injected_%s_%d", toSimpleName(fieldType), parameters.size());

            classFab.addField(fieldName, Modifier.PRIVATE | Modifier.FINAL, fieldType);

            parameters.add(new ConstructorParameter(fieldName, fieldType, fieldValue));

            return fieldName;
        }

        private void createNoOp(ClassFab classFab, MethodSignature signature, String format, Object... values)
        {
            String message = String.format(format, values);

            String body = String.format("throw new RuntimeException(\"%s\");", message);

            classFab.addMethod(Modifier.PUBLIC, signature, body);
        }

        private boolean isLeaf(Tree node)
        {
            int type = node.getType();

            return type != DEREF && type != SAFEDEREF;
        }

        private void createGetRoot()
        {
            BodyBuilder builder = new BodyBuilder().begin();

            builder.addln("%s root = (%<s) $1;", ClassFabUtils.toJavaClassName(rootType));

            builder.addln(
                    "if (root == null) throw new NullPointerException(\"Root object of property expression '%s' is null.\");",
                    expression);

            builder.addln("return root;");

            builder.end();

            MethodSignature sig = new MethodSignature(rootType, "getRoot", new Class[]
            { Object.class }, null);

            classFab.addMethod(Modifier.PRIVATE, sig, builder.toString());
        }

        private void addRootVariable(BodyBuilder builder)
        {
            builder.addln("%s root = getRoot($1);", ClassFabUtils.toJavaClassName(rootType));
        }

        private void createAccessors()
        {
            createGetRoot();

            navBuilder.begin();

            String previousReference = "$1";
            Type activeType = rootType;

            Tree node = tree;

            while (!isLeaf(node))
            {
                GeneratedTerm term = processDerefNode(navBuilder, activeType, node, previousReference, "$1");

                activeType = term.type;

                previousReference = term.termReference;

                // Second term is the continuation, possibly another chained
                // DEREF, etc.
                node = node.getChild(1);
            }

            navBuilder.addln("return %s;", previousReference);

            navBuilder.end();
            Class activeClass = GenericsUtils.asClass(activeType);

            MethodSignature sig = new MethodSignature(activeClass, "navigate", new Class[]
            { rootType }, null);

            classFab.addMethod(Modifier.PRIVATE, sig, navBuilder.toString());

            createGetterAndSetter(activeClass, sig, node);
        }

        private void createGetterAndSetter(Class activeType, MethodSignature navigateMethod, Tree node)
        {
            switch (node.getType())
            {
                case IDENTIFIER:
                case INVOKE:

                    // So, a this point, we have the navigation method written
                    // and it covers all but the terminal
                    // de-reference. node is an IDENTIFIER or INVOKE. We're
                    // ready to use the navigation
                    // method to implement get() and set().

                    ExpressionTermInfo info = infoForMember(activeType, node);

                    createSetter(navigateMethod, info);
                    createGetter(navigateMethod, node, info);

                    conduitPropertyType = info.getType();
                    conduitPropertyName = info.getPropertyName();
                    annotationProvider = info;

                    return;

                case RANGEOP:

                    // As currently implemented, RANGEOP can only appear as the
                    // top level, which
                    // means we didn't need the navigate method after all.

                    createRangeOpGetter(node, "root");
                    createNoOpSetter();

                    conduitPropertyType = IntegerRange.class;

                    return;

                case LIST:

                    createListGetter(node, "root");
                    createNoOpSetter();

                    conduitPropertyType = List.class;

                    return;

                case NOT:
                    createNotOpGetter(node, "root");
                    createNoOpSetter();

                    conduitPropertyType = boolean.class;

                    return;

                default:
                    throw unexpectedNodeType(node, IDENTIFIER, INVOKE, RANGEOP, LIST, NOT);
            }
        }

        private void createRangeOpGetter(Tree node, String rootName)
        {
            BodyBuilder builder = new BodyBuilder().begin();

            addRootVariable(builder);

            builder.addln("return %s;", createMethodInvocation(builder, node, rootName, 0, RANGE));

            builder.end();

            classFab.addMethod(Modifier.PUBLIC, GET_SIGNATURE, builder.toString());
        }

        private void createNotOpGetter(Tree node, String rootName)
        {
            BodyBuilder builder = new BodyBuilder().begin();

            addRootVariable(builder);

            builder.addln("return ($w) %s;", createMethodInvocation(builder, node, rootName, 0, INVERT));

            builder.end();

            classFab.addMethod(Modifier.PUBLIC, GET_SIGNATURE, builder.toString());
        }

        public void createListGetter(Tree node, String rootName)
        {
            BodyBuilder builder = new BodyBuilder().begin();

            addRootVariable(builder);

            builder.addln("return %s;", createListConstructor(builder, node, rootName));

            builder.end();

            classFab.addMethod(Modifier.PUBLIC, GET_SIGNATURE, builder.toString());
        }

        private String createListConstructor(BodyBuilder builder, Tree node, String rootName)
        {
            String listName = nextVariableName(List.class);

            int count = node.getChildCount();

            builder.addln("java.util.List %s = new java.util.ArrayList(%d);", listName, count);

            for (int i = 0; i < count; i++)
            {
                GeneratedTerm generatedTerm = subexpression(builder, node.getChild(i), rootName);

                builder.addln("%s.add(($w) %s);", listName, generatedTerm.termReference);
            }

            return listName;
        }

        private String createNotOp(BodyBuilder builder, Tree node, String rootName)
        {
            String flagName = nextVariableName(Boolean.class);
            GeneratedTerm term = subexpression(builder, node.getChild(0), rootName);

            builder.addln("boolean %s = invert(($w) %s);", flagName, term.termReference);

            return flagName;
        }

        /**
         * Evalutates the node as a sub expression, storing the result into a
         * new variable, whose name is returned.
         *
         * @param builder
         *            to receive generated code
         * @param node
         *            root of tree of nodes to be evaluated
         * @param rootName
         *            name of variable holding reference to root object of
         *            expression
         * @return GeneratedTerm identifying the name of the variable and its
         *         type
         */
        private GeneratedTerm subexpression(BodyBuilder builder, Tree node, String rootName)
        {
            String previousReference = rootName;
            Class activeType = rootType;

            while (node != null)
            {
                switch (node.getType())
                {
                    case TRUE:
                    case FALSE:

                        previousReference = node.getType() == TRUE ? "true" : "false";
                        activeType = boolean.class;

                        node = null;
                        break;

                    case INTEGER:

                        long integerValue = Long.parseLong(node.getText());

                        previousReference = String.format("%dL", integerValue);
                        activeType = long.class;

                        node = null;

                        break;

                    case DECIMAL:

                        double decimalValue = Double.parseDouble(node.getText());

                        previousReference = String.format(Locale.ENGLISH, "%fd", decimalValue);
                        activeType = double.class;

                        node = null;

                        break;

                    case STRING:

                        String stringValue = node.getText();
                        // Injecting is easier; don't have to fuss with escaping
                        // quotes or such.
                        previousReference = addInjection(String.class, stringValue);
                        activeType = String.class;

                        node = null;

                        break;

                    case DEREF:
                    case SAFEDEREF:

                        GeneratedTerm generated = processDerefNode(builder, activeType, node, previousReference,
                                rootName);

                        previousReference = generated.termReference;
                        activeType = GenericsUtils.asClass(generated.type);

                        node = node.getChild(1);

                        break;

                    case IDENTIFIER:
                    case INVOKE:

                        generated = addAccessForMember(builder, activeType, node, previousReference, rootName,
                                NullHandling.IGNORE);

                        previousReference = generated.termReference;
                        activeType = GenericsUtils.asClass(generated.type);

                        node = null;

                        break;

                    case NOT:

                        previousReference = createNotOp(builder, node, rootName);
                        activeType = boolean.class;

                        node = null;

                        break;

                    case LIST:

                        previousReference = createListConstructor(builder, node, rootName);
                        activeType = List.class;

                        node = null;

                        break;

                    default:
                        throw unexpectedNodeType(node, TRUE, FALSE, INTEGER, DECIMAL, STRING, DEREF, SAFEDEREF,
                                IDENTIFIER, INVOKE, LIST);
                }
            }

            return new GeneratedTerm(activeType, previousReference);
        }

        private void createSetter(MethodSignature navigateMethod, ExpressionTermInfo info)
        {
            // A write method will only be identified if the info is a writable
            // property.
            // Other alternatives: a method as the final term, or a read-only
            // property.

            Method method = info.getWriteMethod();

            if (method == null && !info.isField())
            {
                createNoOpSetter();
                return;
            }

            BodyBuilder builder = new BodyBuilder().begin();

            addRootVariable(builder);

            builder.addln("%s target = navigate(root);", ClassFabUtils.toJavaClassName(navigateMethod.getReturnType()));

            // I.e. due to ?. operator. The navigate method will already have
            // checked for nulls
            // if they are not allowed.

            builder.addln("if (target == null) return;");

            String propertyTypeName = ClassFabUtils.toJavaClassName(info.getType());

            String reference = ClassFabUtils.castReference("$2", propertyTypeName);

            if (info.isField())
            {
                builder.add("target.%s = %s;", info.getPropertyName(), reference);
            }
            else
            {
                builder.addln("target.%s(%s);", method.getName(), reference);
            }

            builder.end();

            classFab.addMethod(Modifier.PUBLIC, SET_SIGNATURE, builder.toString());
        }

        private void createNoOpSetter()
        {
            createNoOp(classFab, SET_SIGNATURE, "Expression '%s' for class %s is read-only.", expression,
                    rootType.getName());
        }

        private void createGetter(MethodSignature navigateMethod, Tree node, ExpressionTermInfo info)
        {
            Method method = info.getReadMethod();

            if (method == null && !info.isField())
            {
                createNoOp(classFab, GET_SIGNATURE, "Expression %s for class %s is write-only.", expression,
                        rootType.getName());
                return;
            }

            BodyBuilder builder = new BodyBuilder().begin();

            addRootVariable(builder);

            builder.addln("%s target = navigate(root);", ClassFabUtils.toJavaClassName(navigateMethod.getReturnType()));

            // I.e. due to ?. operator. The navigate method will already have
            // checked for nulls
            // if they are not allowed.

            builder.addln("if (target == null) return null;");

            String reference = info.isField() ? info.getPropertyName() : createMethodInvocation(builder, node, "root",
                    method);

            builder.addln("return ($w) target.%s;", reference);

            builder.end();

            classFab.addMethod(Modifier.PUBLIC, GET_SIGNATURE, builder.toString());
        }

        /**
         * Creates a method invocation call for the given node (an INVOKE node).
         *
         * @param bodyBuilder
         *            may receive new code to define variables for some
         *            sub-expressions
         * @param node
         *            the INVOKE node; child #1 and up are parameter expressions
         *            to the method being invoked
         * @param rootName
         *            name of variable holding reference to root object of
         *            expression
         * @param method
         *            defines the name and parameter types of the method to
         *            invoke
         * @return method invocation string (the name of the method and any
         *         parameters, ready to be added to a method
         *         body)
         */
        private String createMethodInvocation(BodyBuilder bodyBuilder, Tree node, String rootName, Method method)
        {
            return createMethodInvocation(bodyBuilder, node, rootName, 1, method);
        }

        /**
         * Creates a method invocation call for the given node
         *
         * @param bodyBuilder
         *            may receive new code to define variables for some
         *            sub-expressions
         * @param node
         *            the node containing child nodes for the parameters
         * @param rootName
         *            name of variable holding reference to root object of
         *            expression
         * @param childOffset
         *            the offset to the first parameter (for example, this is 1
         *            for an INVOKE node)
         * @param method
         *            defines the name and parameter types of the method to
         *            invoke
         * @return method invocation string (the name of the method and any
         *         parameters, ready to be added to a method
         *         body)
         */
        private String createMethodInvocation(BodyBuilder bodyBuilder, Tree node, String rootName, int childOffset,
                Method method)
        {
            Class[] parameterTypes = method.getParameterTypes();

            StringBuilder builder = new StringBuilder();

            builder.append(method.getName());
            builder.append("(");

            for (int i = 0; i < parameterTypes.length; i++)
            {
                // child(0) is the method name, child(1) is the first parameter,
                // etc.

                GeneratedTerm generatedTerm = subexpression(bodyBuilder, node.getChild(i + childOffset), rootName);
                String currentReference = generatedTerm.termReference;

                Class actualType = GenericsUtils.asClass(generatedTerm.type);

                Class parameterType = parameterTypes[i];

                boolean needsUnwrap = false;

                if (!parameterType.isAssignableFrom(actualType))
                {
                    String coerced = nextVariableName(parameterType);

                    String call = String.format("coerce(($w) %s, %s)", currentReference,
                            addInjection(Class.class, parameterType));

                    String parameterTypeName = ClassFabUtils.toJavaClassName(parameterType);

                    bodyBuilder.addln("%s %s = %s;", parameterTypeName, coerced,
                            ClassFabUtils.castReference(call, parameterTypeName));

                    currentReference = coerced;
                }
                else
                {
                    needsUnwrap = parameterType.isPrimitive() && !actualType.isPrimitive();
                }

                if (i > 0)
                    builder.append(", ");

                builder.append(currentReference);

                if (needsUnwrap)
                {
                    builder.append(".").append(ClassFabUtils.getUnwrapMethodName(parameterType)).append("()");
                }
            }

            return builder.append(")").toString();
        }

        /**
         * Extends the navigate method for a node, which will be a DEREF or
         * SAFEDERF.
         */
        private GeneratedTerm processDerefNode(BodyBuilder builder, Type activeType, Tree node,
                String previousVariableName, String rootName)
        {
            // The first child is the term.

            Tree term = node.getChild(0);

            boolean allowNull = node.getType() == SAFEDEREF;

            // Returns the type of the method/property ... this is the wrapped
            // (i.e. java.lang.Integer) type if
            // the real type is primitive. It also reflects generics information
            // that may have been associated
            // with the underlying method.

            return addAccessForMember(builder, activeType, term, previousVariableName, rootName,
                    allowNull ? NullHandling.ALLOW : NullHandling.FORBID);
        }

        private String nextVariableName(Class type)
        {
            return String.format("var_%s_%d", toSimpleName(type), variableIndex++);
        }

        private String toSimpleName(Class type)
        {
            if (type.isArray())
            {
                Class<?> componentType = type.getComponentType();

                while (componentType.isArray())
                {
                    componentType = componentType.getComponentType();
                }

                return InternalUtils.lastTerm(componentType.getName()) + "_array";
            }
            return InternalUtils.lastTerm(type.getName());
        }

        private GeneratedTerm addAccessForMember(BodyBuilder builder, Type activeType, Tree term,
                String previousVariableName, String rootName, NullHandling nullHandling)
        {
            assertNodeType(term, IDENTIFIER, INVOKE);
            Class activeClass = GenericsUtils.asClass(activeType);
            // Get info about this property or method.

            ExpressionTermInfo info = infoForMember(activeClass, term);

            Method method = info.getReadMethod();

            if (method == null && !info.isField())
                throw new RuntimeException(String.format(
                        "Property '%s' of class %s is not readable (it has no read accessor method).",
                        info.getDescription(), activeClass.getName()));

            Type termType;
            /*
             * It's not possible for the ClassPropertyAdapter to know about the generic info for all the properties of
             * a class. For instance; if the type arguments of a field are provided by a subclass.
             */
            if (info.isField())
            {
                termType = GenericsUtils.extractActualType(activeType, info.getField());
            }
            else
            {
                termType = GenericsUtils.extractActualType(activeType, method);
            }

            Class termClass = GenericsUtils.asClass(termType);

            final Class wrappedType = ClassFabUtils.getWrapperType(termClass);

            String wrapperTypeName = ClassFabUtils.toJavaClassName(wrappedType);

            final String variableName = nextVariableName(wrappedType);

            String reference = info.isField() ? info.getPropertyName() : createMethodInvocation(builder, term,
                    rootName, method);

            builder.add("%s %s = ", wrapperTypeName, variableName);

            // Casts are needed for primitives, and for the case where
            // generics are involved.

            if (termClass.isPrimitive())
            {
                builder.add(" ($w) ");
            }
            else if (info.isCastRequired() || info.getType() != termClass)
            {
                builder.add(" (%s) ", wrapperTypeName);
            }

            builder.addln("%s.%s;", previousVariableName, reference);

            switch (nullHandling)
            {
                case ALLOW:
                    builder.addln("if (%s == null) return null;", variableName);
                    break;

                case FORBID:
                    // Perform a null check on intermediate terms.
                    builder.addln("if (%s == null) %s.nullTerm(\"%s\", \"%s\", $1);", variableName,
                            PropertyConduitSourceImpl.class.getName(), info.getDescription(), expression);
                    break;

                default:
                    break;
            }

            return new GeneratedTerm(wrappedType == termClass ? termType : wrappedType, variableName);
        }

        private void assertNodeType(Tree node, int... expected)
        {
            int type = node.getType();

            for (int e : expected)
            {
                if (type == e)
                    return;
            }

            throw unexpectedNodeType(node, expected);
        }

        private RuntimeException unexpectedNodeType(Tree node, int... expected)
        {
            List<String> tokenNames = CollectionFactory.newList();

            for (int i = 0; i < expected.length; i++)
                tokenNames.add(PropertyExpressionParser.tokenNames[expected[i]]);

            String message = String.format("Node %s was type %s, but was expected to be (one of) %s.",
                    node.toStringTree(), PropertyExpressionParser.tokenNames[node.getType()],
                    InternalUtils.joinSorted(tokenNames));

            return new RuntimeException(message);
        }

        private ExpressionTermInfo infoForMember(Class activeType, Tree node)
        {
            if (node.getType() == INVOKE)
                return infoForInvokeNode(activeType, node);

            return infoForPropertyOrPublicField(activeType, node);
        }

        private ExpressionTermInfo infoForPropertyOrPublicField(Class activeType, Tree node)
        {
            String propertyName = node.getText();

            ClassPropertyAdapter classAdapter = access.getAdapter(activeType);
            final PropertyAdapter adapter = classAdapter.getPropertyAdapter(propertyName);

            if (adapter == null)
            {
                List<String> names = classAdapter.getPropertyNames();

                throw new UnknownValueException(String.format(
                        "Class %s does not contain a property (or public field) named '%s'.", activeType.getName(),
                        propertyName), new AvailableValues("Properties (and public fields)", names));
            }

            return createExpressionTermInfoForProperty(adapter);
        }

        private ExpressionTermInfo createExpressionTermInfoForProperty(final PropertyAdapter adapter)
        {
            return new ExpressionTermInfo()
            {
                public Method getReadMethod()
                {
                    return adapter.getReadMethod();
                }

                public Method getWriteMethod()
                {
                    return adapter.getWriteMethod();
                }

                public Class getType()
                {
                    return adapter.getType();
                }

                public boolean isCastRequired()
                {
                    return adapter.isCastRequired();
                }

                public String getDescription()
                {
                    return adapter.getName();
                }

                public <T extends Annotation> T getAnnotation(Class<T> annotationClass)
                {
                    return adapter.getAnnotation(annotationClass);
                }

                public String getPropertyName()
                {
                    return adapter.getName();
                }

                public boolean isField()
                {
                    return adapter.isField();
                }

                public Field getField()
                {
                    return adapter.getField();
                }
            };
        }

        private ExpressionTermInfo infoForInvokeNode(Class activeType, Tree node)
        {
            String methodName = node.getChild(0).getText();

            int parameterCount = node.getChildCount() - 1;

            try
            {
                final Method method = findMethod(activeType, methodName, parameterCount);

                if (method.getReturnType().equals(void.class))
                    throw new RuntimeException(String.format("Method %s.%s() returns void.", activeType.getName(),
                            methodName));

                final Class genericType = GenericsUtils.extractGenericReturnType(activeType, method);

                return new ExpressionTermInfo()
                {
                    public Method getReadMethod()
                    {
                        return method;
                    }

                    public Method getWriteMethod()
                    {
                        return null;
                    }

                    public Class getType()
                    {
                        return genericType;
                    }

                    public boolean isCastRequired()
                    {
                        return genericType != method.getReturnType();
                    }

                    public String getDescription()
                    {
                        return new MethodSignature(method).getUniqueId();
                    }

                    public <T extends Annotation> T getAnnotation(Class<T> annotationClass)
                    {
                        return method.getAnnotation(annotationClass);
                    }

                    public String getPropertyName()
                    {
                        return null;
                    }

                    public boolean isField()
                    {
                        return false;
                    }

                    public Field getField()
                    {
                        return null;
                    }
                };
            }
            catch (NoSuchMethodException ex)
            {
                throw new RuntimeException(String.format("No public method '%s()' in class %s.", methodName,
                        activeType.getName()));
            }
        }

        private Method findMethod(Class activeType, String methodName, int parameterCount) throws NoSuchMethodException
        {
            for (Method method : activeType.getMethods())
            {

                if (method.getParameterTypes().length == parameterCount
                        && method.getName().equalsIgnoreCase(methodName))
                    return method;
            }

            // TAP5-330
            if (activeType != Object.class)
                return findMethod(Object.class, methodName, parameterCount);

            throw new NoSuchMethodException(ServicesMessages.noSuchMethod(activeType, methodName));
        }
    }

    public PropertyConduitSourceImpl(PropertyAccess access, @ComponentLayer
    ClassFactory classFactory, TypeCoercer typeCoercer, StringInterner interner)
    {
        this.access = access;
        this.classFactory = classFactory;
        this.typeCoercer = typeCoercer;
        this.interner = interner;

        literalTrue = createLiteralConduit(Boolean.class, true);
        literalFalse = createLiteralConduit(Boolean.class, false);
        literalNull = createLiteralConduit(Void.class, null);
    }

    public PropertyConduit create(Class rootClass, String expression)
    {
        assert rootClass != null;
        assert InternalUtils.isNonBlank(expression);
        Class effectiveClass = toEffectiveClass(rootClass);

        MultiKey key = new MultiKey(effectiveClass, expression);

        PropertyConduit result = cache.get(key);

        if (result == null)
        {
            result = build(effectiveClass, expression);
            cache.put(key, result);
        }

        return result;
    }

    private Class toEffectiveClass(Class rootClass)
    {
        Class result = classToEffectiveClass.get(rootClass);

        if (result == null)
        {
            result = classFactory.importClass(rootClass);

            classToEffectiveClass.put(rootClass, result);
        }

        return result;
    }

    /**
     * Clears its caches when the component class loader is invalidated; this is
     * because it will be common to generate
     * conduits rooted in a component class (which will no longer be valid and
     * must be released to the garbage
     * collector).
     */
    public void objectWasInvalidated()
    {
        cache.clear();
        classToEffectiveClass.clear();
    }

    /**
     * Builds a subclass of {@link BasePropertyConduit} that implements the
     * get() and set() methods and overrides the
     * constructor. In a worst-case race condition, we may build two (or more)
     * conduits for the same
     * rootClass/expression, and it will get sorted out when the conduit is
     * stored into the cache.
     *
     * @param rootClass
     *            class of root object for expression evaluation
     * @param expression
     *            expression to be evaluated
     * @return the conduit
     */
    private PropertyConduit build(final Class rootClass, String expression)
    {
        Tree tree = parse(expression);

        try
        {
            switch (tree.getType())
            {
                case TRUE:

                    return literalTrue;

                case FALSE:

                    return literalFalse;

                case NULL:

                    return literalNull;

                case INTEGER:

                    // Leading '+' may screw this up.
                    // TODO: Singleton instance for "0", maybe "1"?

                    return createLiteralConduit(Long.class, new Long(tree.getText()));

                case DECIMAL:

                    // Leading '+' may screw this up.
                    // TODO: Singleton instance for "0.0"?

                    return createLiteralConduit(Double.class, new Double(tree.getText()));

                case STRING:

                    return createLiteralConduit(String.class, tree.getText());

                case RANGEOP:

                    Tree fromNode = tree.getChild(0);
                    Tree toNode = tree.getChild(1);

                    // If the range is defined as integers (not properties, etc.)
                    // then it is possible to calculate the value here, once, and not
                    // build a new class.

                    if (fromNode.getType() != INTEGER || toNode.getType() != INTEGER)
                        break;

                    int from = Integer.parseInt(fromNode.getText());
                    int to = Integer.parseInt(toNode.getText());

                    IntegerRange ir = new IntegerRange(from, to);

                    return createLiteralConduit(IntegerRange.class, ir);

                case THIS:

                    return createLiteralThisPropertyConduit(rootClass);

                default:
                    break;
            }

            return new PropertyConduitBuilder(rootClass, expression, tree).createInstance();
        }
        catch (Exception ex)
        {
            throw new PropertyExpressionException(String.format("Exception generating conduit for expression '%s': %s",
                    expression, InternalUtils.toMessage(ex)), expression, ex);
        }
    }

    private PropertyConduit createLiteralThisPropertyConduit(final Class rootClass)
    {
        return new PropertyConduit()
        {
            public Object get(Object instance)
            {
                return instance;
            }

            public void set(Object instance, Object value)
            {
                throw new RuntimeException(ServicesMessages.literalConduitNotUpdateable());
            }

            public Class getPropertyType()
            {
                return rootClass;
            }

            public <T extends Annotation> T getAnnotation(Class<T> annotationClass)
            {
                return invariantAnnotationProvider.getAnnotation(annotationClass);
            }
        };
    }

    private <T> PropertyConduit createLiteralConduit(Class<T> type, T value)
    {
        return new LiteralPropertyConduit(type, invariantAnnotationProvider, interner.format(
                "LiteralPropertyConduit[%s]", value), typeCoercer, value);
    }

    private Tree parse(String expression)
    {
        InputStream is = new ByteArrayInputStream(expression.getBytes());

        ANTLRInputStream ais;

        try
        {
            ais = new ANTLRInputStream(is);
        }
        catch (IOException ex)
        {
            throw new RuntimeException(ex);
        }

        PropertyExpressionLexer lexer = new PropertyExpressionLexer(ais);

        CommonTokenStream tokens = new CommonTokenStream(lexer);

        PropertyExpressionParser parser = new PropertyExpressionParser(tokens);

        try
        {
            return (Tree) parser.start().getTree();
        }
        catch (Exception ex)
        {
            throw new RuntimeException(String.format("Error parsing property expression '%s': %s.", expression,
                    ex.getMessage()), ex);
        }
    }

    /**
     * May be invoked from fabricated PropertyConduit instances.
     */
    public static void nullTerm(String term, String expression, Object root)
    {
        String message = String.format("Property '%s' (within property expression '%s', of %s) is null.", term,
                expression, root);

        throw new NullPointerException(message);
    }
}
TOP

Related Classes of org.apache.tapestry5.internal.services.PropertyConduitSourceImpl$PropertyConduitBuilder

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.