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    @Override
082    public String getName()
083    {
084        return name;
085    }
086
087    @Override
088    public Method getReadMethod()
089    {
090        return readMethod;
091    }
092
093    @Override
094    public Class getType()
095    {
096        return type;
097    }
098
099    @Override
100    public Method getWriteMethod()
101    {
102        return writeMethod;
103    }
104
105    @Override
106    public boolean isRead()
107    {
108        return field != null || readMethod != null;
109    }
110
111    @Override
112    public boolean isUpdate()
113    {
114        return writeMethod != null || (field != null && !isFinal(field));
115    }
116
117    private boolean isFinal(Member member)
118    {
119        return Modifier.isFinal(member.getModifiers());
120    }
121
122    @Override
123    public Object get(Object instance)
124    {
125        if (field == null && readMethod == null)
126        {
127            throw new UnsupportedOperationException(String.format("Class %s does not provide an accessor ('getter') method for property '%s'.", toClassName(instance), name));
128        }
129
130        Throwable fail;
131
132        try
133        {
134            if (field == null)
135                return readMethod.invoke(instance);
136            else
137                return field.get(instance);
138        } catch (InvocationTargetException ex)
139        {
140            fail = ex.getTargetException();
141        } catch (Exception ex)
142        {
143            fail = ex;
144        }
145
146        throw new RuntimeException(ServiceMessages.readFailure(name, instance, fail), fail);
147    }
148
149    @Override
150    public void set(Object instance, Object value)
151    {
152        if (field == null && writeMethod == null)
153        {
154            throw new UnsupportedOperationException(String.format("Class %s does not provide a mutator ('setter') method for property '%s'.",
155                    toClassName(instance),
156                    name
157            ));
158        }
159
160        Throwable fail;
161
162        try
163        {
164            if (field == null)
165                writeMethod.invoke(instance, value);
166            else
167                field.set(instance, value);
168
169            return;
170        } catch (InvocationTargetException ex)
171        {
172            fail = ex.getTargetException();
173        } catch (Exception ex)
174        {
175            fail = ex;
176        }
177
178        throw new RuntimeException(String.format("Error updating property '%s' of %s: %s",
179                name, toClassName(instance),
180                ExceptionUtils.toMessage(fail)), fail);
181    }
182
183    private String toClassName(Object instance)
184    {
185        return instance == null ? "<null>" : instance.getClass().getName();
186    }
187
188    @Override
189    public <T extends Annotation> T getAnnotation(Class<T> annotationClass)
190    {
191        return getAnnnotationProvider().getAnnotation(annotationClass);
192    }
193
194    /**
195     * Creates (as needed) the annotation provider for this property.
196     */
197    private synchronized AnnotationProvider getAnnnotationProvider()
198    {
199        if (annotationProvider == null)
200        {
201            List<AnnotationProvider> providers = CollectionFactory.newList();
202
203            if (readMethod != null)
204                providers.add(new AccessableObjectAnnotationProvider(readMethod));
205
206            if (writeMethod != null)
207                providers.add(new AccessableObjectAnnotationProvider(writeMethod));
208
209            // There's an assumption here, that the fields match the property name (we ignore case
210            // which leads to a manageable ambiguity) and that the field and the getter/setter
211            // are in the same class (i.e., that we don't have a getter exposing a protected field inherted
212            // from a base class, or some other oddity).
213
214            Class cursor = getBeanType();
215
216            out:
217            while (cursor != null)
218            {
219                for (Field f : cursor.getDeclaredFields())
220                {
221                    if (f.getName().equalsIgnoreCase(name))
222                    {
223                        providers.add(new AccessableObjectAnnotationProvider(f));
224
225                        break out;
226                    }
227                }
228
229                cursor = cursor.getSuperclass();
230            }
231
232            annotationProvider = AnnotationProviderChain.create(providers);
233        }
234
235        return annotationProvider;
236    }
237
238    @Override
239    public boolean isCastRequired()
240    {
241        return castRequired;
242    }
243
244    @Override
245    public ClassPropertyAdapter getClassAdapter()
246    {
247        return classAdapter;
248    }
249
250    @Override
251    public Class getBeanType()
252    {
253        return classAdapter.getBeanType();
254    }
255
256    @Override
257    public boolean isField()
258    {
259        return field != null;
260    }
261
262    @Override
263    public Field getField()
264    {
265        return field;
266    }
267
268    @Override
269    public Class getDeclaringClass()
270    {
271        return declaringClass;
272    }
273}