001// Copyright 2006, 2007, 2010, 2011, 2012 The Apache Software Foundation
002//
003// Licensed under the Apache License, Version 2.0 (the "License");
004// you may not use this file except in compliance with the License.
005// You may obtain a copy of the License at
006//
007// http://www.apache.org/licenses/LICENSE-2.0
008//
009// Unless required by applicable law or agreed to in writing, software
010// distributed under the License is distributed on an "AS IS" BASIS,
011// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012// See the License for the specific language governing permissions and
013// limitations under the License.
014
015package org.apache.tapestry5.ioc.internal.services;
016
017import org.apache.tapestry5.commons.internal.services.ServiceMessages;
018import org.apache.tapestry5.commons.services.*;
019import org.apache.tapestry5.ioc.services.Builtin;
020import org.apache.tapestry5.ioc.services.PropertyShadowBuilder;
021import org.apache.tapestry5.plastic.*;
022
023import java.lang.reflect.Method;
024
025public class PropertyShadowBuilderImpl implements PropertyShadowBuilder
026{
027    private final PropertyAccess propertyAccess;
028
029    private final PlasticProxyFactory proxyFactory;
030
031    public PropertyShadowBuilderImpl(@Builtin
032                                     PlasticProxyFactory proxyFactory,
033
034                                     PropertyAccess propertyAccess)
035    {
036        this.proxyFactory = proxyFactory;
037        this.propertyAccess = propertyAccess;
038    }
039
040    @Override
041    public <T> T build(final Object source, final String propertyName, final Class<T> propertyType)
042    {
043        final Class sourceClass = source.getClass();
044        final PropertyAdapter adapter = propertyAccess.getAdapter(sourceClass).getPropertyAdapter(propertyName);
045
046        // TODO: Perhaps extend ClassPropertyAdapter to do these checks?
047
048        if (adapter == null)
049            throw new RuntimeException(ServiceMessages.noSuchProperty(sourceClass, propertyName));
050
051        if (!adapter.isRead())
052        {
053            throw new RuntimeException(
054                    String.format("Class %s does not provide an accessor ('getter') method for property '%s'.",
055                            source.getClass().getName(), propertyName));
056        }
057
058        if (!propertyType.isAssignableFrom(adapter.getType()))
059            throw new RuntimeException(ServiceMessages.propertyTypeMismatch(propertyName, sourceClass,
060                    adapter.getType(), propertyType));
061
062        ClassInstantiator instantiator = proxyFactory.createProxy(propertyType, new PlasticClassTransformer()
063        {
064            @Override
065            public void transform(PlasticClass plasticClass)
066            {
067                final PlasticField sourceField = plasticClass.introduceField(sourceClass, "source").inject(source);
068
069                PlasticMethod delegateMethod = plasticClass.introducePrivateMethod(propertyType.getName(),
070                        "readProperty", null, null);
071
072                // You don't do this using MethodAdvice, because then we'd have to use reflection to access the read
073                // method.
074
075                delegateMethod.changeImplementation(new InstructionBuilderCallback()
076                {
077                    @Override
078                    public void doBuild(InstructionBuilder builder)
079                    {
080                        builder.loadThis().getField(sourceField);
081                        builder.invoke(sourceClass, propertyType, adapter.getReadMethod().getName());
082
083                        // Now add the null check.
084
085                        builder.dupe().when(Condition.NULL, new InstructionBuilderCallback()
086                        {
087                            @Override
088                            public void doBuild(InstructionBuilder builder)
089                            {
090                                builder.throwException(
091                                        NullPointerException.class,
092                                        String.format(
093                                                "Unable to delegate method invocation to property '%s' of %s, because the property is null.",
094                                                propertyName, source));
095                            }
096                        });
097
098                        builder.returnResult();
099                    }
100                });
101
102                for (Method m : propertyType.getMethods())
103                {
104                    plasticClass.introduceMethod(m).delegateTo(delegateMethod);
105                }
106
107                plasticClass.addToString(String.format("<Shadow: property %s of %s>", propertyName, source));
108            }
109        });
110
111        return propertyType.cast(instantiator.newInstance());
112    }
113}