001// Copyright 2006-2013 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.ioc.AnnotationProvider;
018import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
019import org.apache.tapestry5.ioc.services.ClassPropertyAdapter;
020import org.apache.tapestry5.ioc.services.PropertyAdapter;
021import org.apache.tapestry5.ioc.util.ExceptionUtils;
022
023import java.lang.annotation.Annotation;
024import java.lang.reflect.*;
025import java.util.List;
026
027public class PropertyAdapterImpl implements PropertyAdapter
028{
029    private final ClassPropertyAdapter classAdapter;
030
031    private final String name;
032
033    private final Method readMethod;
034
035    private final Method writeMethod;
036
037    private final Class type;
038
039    private final boolean castRequired;
040
041    // Synchronized by this; lazily initialized
042    private AnnotationProvider annotationProvider;
043
044    private final Field field;
045
046    private final Class declaringClass;
047
048    PropertyAdapterImpl(ClassPropertyAdapter classAdapter, String name, Class type, Method readMethod,
049                        Method writeMethod)
050    {
051        this.classAdapter = classAdapter;
052        this.name = name;
053        this.type = type;
054
055        this.readMethod = readMethod;
056        this.writeMethod = writeMethod;
057
058        declaringClass = readMethod != null ? readMethod.getDeclaringClass() : writeMethod.getDeclaringClass();
059
060        castRequired = readMethod != null && readMethod.getReturnType() != type;
061
062        field = null;
063    }
064
065    PropertyAdapterImpl(ClassPropertyAdapter classAdapter, String name, Class type, Field field)
066    {
067        this.classAdapter = classAdapter;
068        this.name = name;
069        this.type = type;
070
071        this.field = field;
072
073        declaringClass = field.getDeclaringClass();
074
075        castRequired = field.getType() != type;
076
077        readMethod = null;
078        writeMethod = null;
079    }
080
081    public String getName()
082    {
083        return name;
084    }
085
086    public Method getReadMethod()
087    {
088        return readMethod;
089    }
090
091    public Class getType()
092    {
093        return type;
094    }
095
096    public Method getWriteMethod()
097    {
098        return writeMethod;
099    }
100
101    public boolean isRead()
102    {
103        return field != null || readMethod != null;
104    }
105
106    public boolean isUpdate()
107    {
108        return writeMethod != null || (field != null && !isFinal(field));
109    }
110
111    private boolean isFinal(Member member)
112    {
113        return Modifier.isFinal(member.getModifiers());
114    }
115
116    public Object get(Object instance)
117    {
118        if (field == null && readMethod == null)
119        {
120            throw new UnsupportedOperationException(String.format("Class %s does not provide an accessor ('getter') method for property '%s'.", toClassName(instance), name));
121        }
122
123        Throwable fail;
124
125        try
126        {
127            if (field == null)
128                return readMethod.invoke(instance);
129            else
130                return field.get(instance);
131        } catch (InvocationTargetException ex)
132        {
133            fail = ex.getTargetException();
134        } catch (Exception ex)
135        {
136            fail = ex;
137        }
138
139        throw new RuntimeException(ServiceMessages.readFailure(name, instance, fail), fail);
140    }
141
142    public void set(Object instance, Object value)
143    {
144        if (field == null && writeMethod == null)
145        {
146            throw new UnsupportedOperationException(String.format("Class %s does not provide a mutator ('setter') method for property '%s'.",
147                    toClassName(instance),
148                    name
149            ));
150        }
151
152        Throwable fail;
153
154        try
155        {
156            if (field == null)
157                writeMethod.invoke(instance, value);
158            else
159                field.set(instance, value);
160
161            return;
162        } catch (InvocationTargetException ex)
163        {
164            fail = ex.getTargetException();
165        } catch (Exception ex)
166        {
167            fail = ex;
168        }
169
170        throw new RuntimeException(String.format("Error updating property '%s' of %s: %s",
171                name, toClassName(instance),
172                ExceptionUtils.toMessage(fail)), fail);
173    }
174
175    private String toClassName(Object instance)
176    {
177        return instance == null ? "<null>" : instance.getClass().getName();
178    }
179
180    public <T extends Annotation> T getAnnotation(Class<T> annotationClass)
181    {
182        return getAnnnotationProvider().getAnnotation(annotationClass);
183    }
184
185    /**
186     * Creates (as needed) the annotation provider for this property.
187     */
188    private synchronized AnnotationProvider getAnnnotationProvider()
189    {
190        if (annotationProvider == null)
191        {
192            List<AnnotationProvider> providers = CollectionFactory.newList();
193
194            if (readMethod != null)
195                providers.add(new AccessableObjectAnnotationProvider(readMethod));
196
197            if (writeMethod != null)
198                providers.add(new AccessableObjectAnnotationProvider(writeMethod));
199
200            // There's an assumption here, that the fields match the property name (we ignore case
201            // which leads to a manageable ambiguity) and that the field and the getter/setter
202            // are in the same class (i.e., that we don't have a getter exposing a protected field inherted
203            // from a base class, or some other oddity).
204
205            Class cursor = getBeanType();
206
207            out:
208            while (cursor != null)
209            {
210                for (Field f : cursor.getDeclaredFields())
211                {
212                    if (f.getName().equalsIgnoreCase(name))
213                    {
214                        providers.add(new AccessableObjectAnnotationProvider(f));
215
216                        break out;
217                    }
218                }
219
220                cursor = cursor.getSuperclass();
221            }
222
223            annotationProvider = AnnotationProviderChain.create(providers);
224        }
225
226        return annotationProvider;
227    }
228
229    public boolean isCastRequired()
230    {
231        return castRequired;
232    }
233
234    public ClassPropertyAdapter getClassAdapter()
235    {
236        return classAdapter;
237    }
238
239    public Class getBeanType()
240    {
241        return classAdapter.getBeanType();
242    }
243
244    public boolean isField()
245    {
246        return field != null;
247    }
248
249    public Field getField()
250    {
251        return field;
252    }
253
254    public Class getDeclaringClass()
255    {
256        return declaringClass;
257    }
258}