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 }