View Javadoc

1   /***
2    * Copyright 2009 Martin Vysny.
3    *
4    * This file is part of WebVM.
5    *
6    * WebVM is free software: you can redistribute it and/or modify
7    * it under the terms of the GNU General Public License as published by
8    * the Free Software Foundation, either version 3 of the License, or
9    * (at your option) any later version.
10   *
11   * WebVM is distributed in the hope that it will be useful,
12   * but WITHOUT ANY WARRANTY; without even the implied warranty of
13   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14   * GNU General Public License for more details.
15   *
16   * You should have received a copy of the GNU General Public License
17   * along with WebVM.  If not, see <http://www.gnu.org/licenses/>.
18   */
19  package sk.baka.webvm.config;
20  
21  import java.lang.reflect.Field;
22  import java.lang.reflect.Modifier;
23  import java.util.Collections;
24  import java.util.HashMap;
25  import java.util.Map;
26  import java.util.Properties;
27  import java.util.logging.Level;
28  import java.util.logging.Logger;
29  
30  /***
31   * A simple binder which transfers properties from a bean to a Property instance.
32   * @author Martin Vysny
33   */
34  public final class Binder {
35  
36      /***
37       * Copies all properties annotated by {@link Bind} to the view.
38       *
39       * @param bean
40       *            the bean to copy from/to
41       * @param properties the properties to copy from/to
42       * @param beanToMap
43       *            if <code>true</code> then data from the bean are copied into
44       *            the view. If <code>false</code> then view's data will be
45       *            copied into the bean.
46       * @param validate
47       *            if <code>true</code> then validation constraints are
48       *            checked.
49       * @return an empty map if no validation constraints were violated; a map of
50       *         a property key to an error message if violation occurred.
51       */
52      public static Map<String, String> bindBeanMap(final Object bean,
53              final Properties properties, final boolean beanToMap, final boolean validate) {
54          if (bean == null) {
55              throw new IllegalArgumentException("bean");
56          }
57          if (properties == null) {
58              throw new IllegalArgumentException("properties");
59          }
60          final Map<String, String> result = new HashMap<String, String>();
61          try {
62              for (final Field field : bean.getClass().getFields()) {
63                  final Bind annotation = field.getAnnotation(Bind.class);
64                  if (annotation == null) {
65                      continue;
66                  }
67                  if (beanToMap) {
68                      bindBeanToMap(field, bean, validate, result, properties);
69                  } else {
70                      bindMapToBean(field, properties, result, validate, bean);
71                  }
72              }
73          } catch (IllegalAccessException e) {
74              throw new IllegalStateException("Cannot read/write a field", e);
75          }
76          return result;
77      }
78  
79      /***
80       * Logs all warnings to given logger.
81       * @param log log warnings here
82       * @param warnings the warnings.
83       */
84      public static void log(final Logger log, final Map<String, String> warnings) {
85          for (final Map.Entry<String, String> e : warnings.entrySet()) {
86              log.log(Level.WARNING, e.getKey() + ": " + e.getValue());
87          }
88      }
89  
90      private static void bindBeanToMap(final Field sourceField, final Object sourceBean, final boolean validate, final Map<String, String> validation, final Properties target) throws IllegalArgumentException, IllegalAccessException {
91          final Bind annotation = sourceField.getAnnotation(Bind.class);
92          final Object fieldValue = sourceField.get(sourceBean);
93          if (validate) {
94              validation.putAll(checkValidity(fieldValue, annotation));
95          }
96          if (fieldValue != null) {
97              target.put(annotation.key(), fieldValue.toString());
98          } else {
99              target.remove(annotation.key());
100         }
101     }
102 
103     private static void bindMapToBean(final Field targetField, final Properties source, final Map<String, String> validation, final boolean validate, final Object targetBean) throws IllegalArgumentException, IllegalAccessException {
104         final Bind annotation = targetField.getAnnotation(Bind.class);
105         final Class<?> fieldClass = primitiveToClass(targetField.getType());
106         final String value = source.getProperty(annotation.key());
107         if (value == null) {
108             return;
109         }
110         final Object fieldValue;
111         try {
112             fieldValue = stringToValue(value.trim(), fieldClass);
113         } catch (final Exception ex) {
114             validation.put(annotation.key(), "Failed to parse " + value + ": " + ex.toString());
115             return;
116         }
117         if (validate) {
118             validation.putAll(checkValidity(fieldValue, annotation));
119         }
120         targetField.set(targetBean, fieldValue);
121         return;
122     }
123 
124     private static Object stringToValue(final String value, final Class<?> requiredClass) {
125         if (requiredClass == String.class) {
126             return value;
127         }
128         if (requiredClass == Integer.class) {
129             return Integer.parseInt(value);
130         }
131         if (Enum.class.isAssignableFrom(requiredClass)) {
132             return Enum.valueOf(requiredClass.asSubclass(Enum.class), value);
133         }
134         throw new IllegalArgumentException("Unsupported class: " + requiredClass);
135     }
136 
137     @SuppressWarnings("unchecked")
138     private static Map<String, String> checkValidity(final Object object,
139             Bind annotation) {
140         if (object instanceof Number) {
141             final int intVal = ((Number) object).intValue();
142             if (intVal < annotation.min()) {
143                 final String errMsg = annotation.key() + ": Number too small: " + intVal + " must be at least " + annotation.min();
144                 return Collections.singletonMap(annotation.key(), errMsg);
145             }
146             if (intVal > annotation.max()) {
147                 final String errMsg = annotation.key() + ": number too big: " + intVal + " must be at most " + annotation.min();
148                 return Collections.singletonMap(annotation.key(), errMsg);
149             }
150         }
151         return Collections.emptyMap();
152     }
153     private static final Map<Class<?>, Class<?>> primitiveToClass = new HashMap<Class<?>, Class<?>>();
154 
155 
156     static {
157         primitiveToClass.put(int.class, Integer.class);
158         primitiveToClass.put(double.class, Double.class);
159         primitiveToClass.put(float.class, Float.class);
160         primitiveToClass.put(boolean.class, Boolean.class);
161         primitiveToClass.put(byte.class, Byte.class);
162         primitiveToClass.put(char.class, Character.class);
163         primitiveToClass.put(long.class, Long.class);
164     }
165 
166     private static Class<?> primitiveToClass(final Class<?> clazz) {
167         if (!clazz.isPrimitive()) {
168             return clazz;
169         }
170         return primitiveToClass.get(clazz);
171     }
172 
173     /***
174      * Copies all bindable properties from given bean to given bean.
175      *
176      * @param <T>
177      *            the bean type
178      * @param from
179      *            source bean
180      * @param to
181      *            target bean
182      */
183     public static <T> void copy(T from, T to) {
184         try {
185             for (final Field field : from.getClass().getFields()) {
186                 final Bind annotation = field.getAnnotation(Bind.class);
187                 if (annotation == null) {
188                     continue;
189                 }
190                 if (!Modifier.isPublic(field.getModifiers())) {
191                     continue;
192                 }
193                 final Object fieldValue = field.get(from);
194                 field.set(to, fieldValue);
195             }
196         } catch (IllegalAccessException e) {
197             throw new IllegalArgumentException("Cannot read/write annotated field", e);
198         }
199     }
200 
201     private Binder() {
202         throw new AssertionError();
203     }
204 }