View Javadoc

1   /*
2    * Copyright 2010-2013 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.callbackparams.sandbox;
18  
19  import java.beans.IntrospectionException;
20  import java.beans.Introspector;
21  import java.beans.PropertyDescriptor;
22  import java.beans.PropertyEditor;
23  import java.beans.PropertyEditorManager;
24  import java.lang.annotation.ElementType;
25  import java.lang.annotation.Retention;
26  import java.lang.annotation.RetentionPolicy;
27  import java.lang.annotation.Target;
28  import java.lang.reflect.Method;
29  import org.callbackparams.CallbackFactory;
30  import org.callbackparams.ext.WrappingCallbackImpls;
31  import org.callbackparams.wrap.WrappingSupport;
32  
33  /**
34   * This class has nested resources that can be used to produce callback-values
35   * for {@link Property.Callback} through annotations.
36   * <br/><br/>
37   * Example usage ...
38   * <pre>
39   * &#64;RunWith({@link org.callbackparams.junit4.CallbackParamsRunner})
40   * public class MyTest {
41   *
42   *   &#64;Test
43   *   public void runTest(Property.Callback&lt;MyBean&gt; beanCallback) {
44   *     // ...
45   *   }
46   *
47   *   <b>&#64;Property.BeanClass(MyBean.class)
48   *   &#64;Property.Name("myIntProperty")</b>
49   *   enum MyEnum {
50   *       <b>&#64;Property.Value("21")</b> FOO,
51   *       <b>&#64;Property.Value("67")</b> BAR;
52   *   }
53   * }
54   * </pre>
55   * The annotations around and within <code>MyEnum</code> are meant as a shortcut
56   * that is equivalent with this ...
57   * <pre>
58   *   enum MyEnum <b>implements Property.Callback&lt;MyBean&gt;</b> {
59   *     FOO<b>(21)</b>, BAR<b>(67)</b>;
60   *<b>
61   *     int value;
62   *
63   *     MyEnum(int value) {
64   *       this.value = value;
65   *     }
66   *
67   *     void setValues(MyBean bean) {
68   *         bean.setMyIntProperty(value);
69   *     }
70   *
71   *     void assertValues(MyBean bean) {
72   *       Assert.assertEquals("Value for property \"myIntProperty\" on " + bean,
73   *           value, bean.getMyIntProperty());
74   *     }</b>
75   *   }
76   * </pre>
77   * Thus, a considerable amount of (<b>bold</b>) boilerplate code
78   * can be excluded by using the
79   * annotations, as shown in the first case.
80   * <br/><br/>
81   * This class is meant as a proof-of-concept for the Wrapping API that can
82   * be used to specify an implementation of a callback-interface from a
83   * callback-value that neither implements the callback-interface nor provides
84   * any implementation
85   * through {@link org.callbackparams.CallbackFactory#getCallback()}.
86   * <br/>
87   * This class being a proof-of-concept means that it applies to a
88   * concept that is generally understood among developers (i.e. beans and
89   * their properties) but unlike the other sandbox utility {@link Collector}
90   * the reusability of this class is probably more limited.
91   *
92   * @author Henrik Kaipe
93   */
94  public class Property {
95  
96      @Retention(RetentionPolicy.RUNTIME)
97      @Target({ElementType.FIELD, ElementType.TYPE})
98      public @interface BeanClass {
99          java.lang.Class value();
100     }
101 
102     @Retention(RetentionPolicy.RUNTIME)
103     @Target({ElementType.FIELD, ElementType.TYPE})
104     public @interface Name {
105         String value();
106     }
107 
108     @Retention(RetentionPolicy.RUNTIME)
109     @Target({ElementType.FIELD, ElementType.TYPE})
110     @WrappingCallbackImpls(BasicImpl.class)
111     public @interface Value {
112         String value();
113     }
114 
115     public interface Callback<T> {
116         void setValues(T bean);
117         void assertValues(T bean);
118     }
119 
120     protected static class BasicImpl<T> extends WrappingSupport<T>
121     implements CallbackFactory, Callback<T> {
122 
123         /*
124          * Annotation-values that will be injected ...
125          */
126         protected BeanClass beanClass;
127         protected Name propertyName;
128         protected Value propertyValue;
129 
130         /*
131          * Derived values ...
132          */
133         final PropertyDescriptor descriptor = lookupPropertyDescriptor();
134         final Object[] setterArgument = {createSetterArgument()};
135 
136         /**
137          * To declare checked exceptions during initialization ...
138          */
139         BasicImpl() throws IntrospectionException {}
140 
141         private PropertyDescriptor lookupPropertyDescriptor()
142         throws IntrospectionException {
143             if (null != beanClass && null != propertyName) {
144                 for (PropertyDescriptor pd : Introspector
145                         .getBeanInfo(beanClass.value())
146                         .getPropertyDescriptors()) {
147                     if (pd.getName().equals(propertyName.value())) {
148                         return pd;
149                     }
150                 }
151             }
152             return null;
153         }
154 
155         private Object createSetterArgument() {
156             if (null == descriptor) {
157                 return null;
158             }
159             PropertyEditor editor = PropertyEditorManager
160                     .findEditor(descriptor.getPropertyType());
161             editor.setAsText(propertyValue.value());
162             return editor.getValue();
163         }
164 
165         private static Object invoke(Method m, Object bean, Object[] args) {
166             try {
167                 try {
168                     m.setAccessible(true);
169                 } catch (SecurityException ignore) {
170                     /* Never mind */
171                 }
172                 return m.invoke(bean, args);
173 
174             } catch (Exception x) {
175                 /*ExceptionUtil is not an API-class! so don't do this at home:*/
176                 throw org.callbackparams.support.ExceptionUtil.unchecked(x);
177             }
178         }
179 
180         /**
181          * If all of the necessary annotations
182          * {@link #beanClass}, {@link #propertyName} and {@link #propertyValue}
183          * are found on the {@link #wrappedValue() wrapped value} (or its type)
184          * then the method will return itself; otherwise null is returned.
185          *
186          * @return this object - if all of the necessary annotations
187          * {@link #beanClass}, {@link #propertyName} and {@link #propertyValue}
188          * are found on the {@link #wrappedValue() wrapped value} (or its type);
189          * otherwise null
190          */
191         public Callback<T> getCallback() {
192             if (null == propertyValue) {
193                 throw new IllegalStateException("Wrapped value - "
194                         + wrappedValue()
195                         + " - does not have annotation '@Value'");
196 
197             } else if (null == propertyName) {
198                 System.err.println(wrappedValue()
199                         + " does not have annotation '@Name'");
200                 return null;
201 
202             } else if (null == beanClass) {
203                 System.err.println(wrappedValue()
204                         + " does not have annotation '@BeanClass'");
205                 return null;
206             }
207             return this;
208         }
209 
210         public void setValues(Object bean) {
211             if (beanClass.value().isInstance(bean)) {
212                 invoke(descriptor.getWriteMethod(), bean, setterArgument);
213             }
214         }
215 
216         public void assertValues(Object bean) {
217             if (beanClass.value().isInstance(bean)) {
218                 Object actual = invoke(descriptor.getReadMethod(), bean, null);
219                 if (false == setterArgument[0].equals(actual)) {
220                     throw new AssertionError("Value for property \""
221                             + descriptor.getName() + "\" on " + bean
222                             + ": expected <" + setterArgument[0]
223                             + "> but was <" + actual + ">");
224                 }
225             }
226         }
227     }
228 
229     private Property() {}
230 }