1 /***
2 * Ambient - A music player for the Android platform
3 Copyright (C) 2007 Martin Vysny
4
5 This program is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, either version 3 of the License, or
8 (at your option) any later version.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18 package sk.baka.ambient.commons;
19
20 import java.lang.reflect.Field;
21 import java.lang.reflect.Method;
22 import java.lang.reflect.Modifier;
23 import java.text.ParseException;
24 import java.util.Collections;
25 import java.util.HashMap;
26 import java.util.Map;
27
28 import sk.baka.ambient.AmbientApplication;
29 import sk.baka.ambient.R;
30 import android.content.SharedPreferences.Editor;
31 import android.util.Log;
32 import android.view.View;
33 import android.widget.CheckBox;
34 import android.widget.EditText;
35 import android.widget.Spinner;
36
37 /***
38 * Binds a POJO bean to a view. Currently only public fields are supported - no
39 * getters/setters.
40 *
41 * @author Martin Vysny
42 */
43 public final class Binder {
44 private Binder() {
45
46 }
47
48 /***
49 * Copies all properties annotated by {@link Bind} to a map.
50 *
51 * @param bean
52 * the bean to copy from/to
53 * @param map
54 * the map to use
55 * @param beanToMap
56 * if <code>true</code> then data from the bean are copied into
57 * the map. If <code>false</code> then map's data will be
58 * copied into the bean.
59 * @param validate
60 * if <code>true</code> then validation constraints are
61 * checked.
62 * @return an empty map if no validation constraints were violated; a map of
63 * view id to an error message if violation occured.
64 */
65 public static Map<Integer, String> bindBeanMap(final Object bean,
66 final Map<String, Object> map, final boolean beanToMap,
67 final boolean validate) {
68 final Map<Integer, String> result = new HashMap<Integer, String>();
69 try {
70 for (final Field field : bean.getClass().getFields()) {
71 final Bind annotation = field.getAnnotation(Bind.class);
72 if (annotation == null)
73 continue;
74 if (!Modifier.isPublic(field.getModifiers()))
75 continue;
76 if (beanToMap) {
77 final Object fieldValue = field.get(bean);
78 if (validate) {
79 result.putAll(checkValidity(fieldValue, annotation));
80 }
81 map.put(field.getName(), fieldValue);
82 } else {
83 Object fieldValue = map.get(field.getName());
84 if (fieldValue == null)
85 continue;
86 if (validate)
87 result.putAll(checkValidity(fieldValue, annotation));
88
89 if ((fieldValue instanceof Integer)
90 && field.getType().isEnum()) {
91 final int ordinal = (Integer) fieldValue;
92 fieldValue = ordinal < 0 ? null : field.getType()
93 .getEnumConstants()[ordinal];
94 }
95 field.set(bean, fieldValue);
96 }
97 }
98 } catch (IllegalAccessException e) {
99 throw new RuntimeException(e);
100 }
101 return result;
102 }
103
104 /***
105 * Copies all properties annotated by {@link Bind} to an editor.
106 *
107 * @param bean
108 * the bean to copy from
109 * @param editor
110 * the map to use
111 * @param validate
112 * if <code>true</code> then validation constraints are
113 * checked.
114 * @return an empty map if no validation constraints were violated; a map of
115 * view id to an error message if violation occured.
116 */
117 public static Map<Integer, String> bindBeanToEditor(final Object bean,
118 final Editor editor, final boolean validate) {
119 final Map<Integer, String> result = new HashMap<Integer, String>();
120 try {
121 for (final Field field : bean.getClass().getFields()) {
122 final Bind annotation = field.getAnnotation(Bind.class);
123 if (annotation == null)
124 continue;
125 if (!Modifier.isPublic(field.getModifiers()))
126 continue;
127 final Object fieldValue = field.get(bean);
128 final Class<?> type = field.getType();
129 if (validate)
130 result.putAll(checkValidity(fieldValue, annotation));
131 final String k = field.getName();
132 final Object v = fieldValue;
133 put(k, v, type, editor);
134 }
135 } catch (IllegalAccessException e) {
136 throw new RuntimeException(e);
137 }
138 return result;
139 }
140
141 /***
142 * Puts given object into an editor.
143 *
144 * @param <V>
145 * the value type
146 * @param k
147 * key name
148 * @param v
149 * value
150 * @param type
151 * the value class
152 * @param editor
153 * the editor.
154 */
155 public static <V> void put(final String k, final V v,
156 final Class<? extends V> type, final Editor editor) {
157 if (v instanceof String) {
158 editor.putString(k, (String) v);
159 return;
160 }
161 if (v instanceof Float) {
162 editor.putFloat(k, (Float) v);
163 return;
164 }
165 if (v instanceof Integer) {
166 editor.putInt(k, (Integer) v);
167 return;
168 }
169 if (v instanceof Boolean) {
170 editor.putBoolean(k, (Boolean) v);
171 return;
172 }
173 if (v instanceof Long) {
174 editor.putLong(k, (Long) v);
175 return;
176 }
177 if (type.isEnum()) {
178 editor.putInt(k, v == null ? -1 : ((Enum<?>) v).ordinal());
179 return;
180 }
181 throw new IllegalArgumentException("Object " + type.getName()
182 + " not writable to an editor");
183 }
184
185 /***
186 * Copies all properties annotated by {@link Bind} to the view.
187 *
188 * @param bean
189 * the bean to copy from/to
190 * @param parent
191 * the view to use for performing View lookup.
192 * @param beanToView
193 * if <code>true</code> then data from the bean are copied into
194 * the view. If <code>false</code> then view's data will be
195 * copied into the bean.
196 * @param validate
197 * if <code>true</code> then validation constraints are
198 * checked.
199 * @return an empty map if no validation constraints were violated; a map of
200 * view id to an error message if violation occurred.
201 */
202 public static Map<Integer, String> bindBeanView(final Object bean,
203 final View parent, final boolean beanToView, final boolean validate) {
204 if (bean == null) {
205 throw new NullArgumentException("bean");
206 }
207 if (parent == null) {
208 throw new NullArgumentException("parent");
209 }
210 final Map<Integer, String> result = new HashMap<Integer, String>();
211 try {
212 for (final Field field : bean.getClass().getFields()) {
213 final Bind annotation = field.getAnnotation(Bind.class);
214 if (annotation == null)
215 continue;
216 final View view = parent.findViewById(annotation.viewId());
217 if (view == null)
218 throw new RuntimeException("No view with ID "
219 + annotation.viewId());
220 if (beanToView) {
221 final Object fieldValue = field.get(bean);
222 if (validate)
223 result.putAll(checkValidity(fieldValue, annotation));
224 assignValueToView(fieldValue, view, annotation.escape());
225 } else {
226 final Class<?> fieldClass = field.getType();
227 final Object fieldValue = assignViewToValue(fieldClass,
228 view, annotation.escape());
229 if (validate)
230 result.putAll(checkValidity(fieldValue, annotation));
231 field.set(bean, fieldValue);
232 }
233 }
234 } catch (IllegalAccessException e) {
235 throw new RuntimeException(e);
236 }
237 return result;
238 }
239
240 private static String formatErrMsg(final Bind bind, final int errorMsgId,
241 final Object... args) {
242 final String errMsg = MiscUtils.format(errorMsgId, args);
243 if (bind.captionId() == -1)
244 return errMsg;
245 final String caption = AmbientApplication.getInstance().getString(
246 bind.captionId());
247 return caption + ": " + errMsg;
248 }
249
250 @SuppressWarnings("unchecked")
251 private static Map<Integer, String> checkValidity(final Object object,
252 Bind annotation) {
253 if (object instanceof Number) {
254 final int intVal = ((Number) object).intValue();
255 if (intVal < annotation.min()) {
256 final String errMsg = formatErrMsg(annotation,
257 R.string.errNumberTooSmall, annotation.min());
258 return Collections.singletonMap(annotation.viewId(), errMsg);
259 }
260 if (intVal > annotation.max()) {
261 final String errMsg = formatErrMsg(annotation,
262 R.string.errNumberTooBig, annotation.max());
263 return Collections.singletonMap(annotation.viewId(), errMsg);
264 }
265 }
266 if ((object instanceof String) && annotation.tagFormat()) {
267 try {
268 new TagFormatter((String) object);
269 } catch (ParseException e) {
270 Log.i(Binder.class.getSimpleName(), e.getMessage(), e);
271 final String errMsg = formatErrMsg(annotation,
272 R.string.errStringNotTagFormat, e.getMessage());
273 return Collections.singletonMap(annotation.viewId(), errMsg);
274 }
275 }
276 return Collections.EMPTY_MAP;
277 }
278
279 /***
280 * Assigns given value to the view
281 *
282 * @param fieldValue
283 * the value
284 * @param view
285 * the view
286 * @param escape
287 * if <code>true</code> then the string value is
288 * {@link #escape(String) escaped}.
289 */
290 private static void assignValueToView(final Object fieldValue,
291 final View view, boolean escape) {
292 if (view instanceof CheckBox) {
293 ((CheckBox) view).setChecked((Boolean) fieldValue);
294 } else if (view instanceof EditText) {
295 String value = fieldValue == null ? "" : fieldValue.toString();
296 if (escape) {
297 value = escape(value);
298 }
299 ((EditText) view).setText(value);
300 } else if (view instanceof Spinner) {
301 final int ordinal;
302 if (fieldValue instanceof Enum<?>) {
303 ordinal = ((Enum<?>) fieldValue).ordinal();
304 } else {
305 ordinal = ((Number) fieldValue).intValue();
306 }
307 ((Spinner) view).setSelection(ordinal);
308 } else
309 throw new IllegalArgumentException("Unsupported view class: "
310 + view.getClass().getName());
311 }
312
313 private static String escape(String value) {
314 if (value == null)
315 return null;
316 return value.replace("\n", "//n");
317 }
318
319 private static String unescape(String value) {
320 if (value == null)
321 return null;
322 return value.replace("//n", "\n");
323 }
324
325 /***
326 * Retrieves a value from a control.
327 *
328 * @param fieldClass
329 * class of the value.
330 * @param view
331 * the control.
332 * @param unescape
333 * if <code>true</code> then the string value is
334 * {@link #unescape(String) unescaped}.
335 * @return value with given class.
336 */
337 private static Object assignViewToValue(final Class<?> fieldClass,
338 final View view, final boolean unescape) {
339 final Class<?> fc = primitiveToClass(fieldClass);
340 if (view instanceof CheckBox) {
341 if (!Boolean.class.isAssignableFrom(fc))
342 throw new IllegalArgumentException(fc.getName()
343 + " not assignable from CheckBox");
344 return ((CheckBox) view).isChecked();
345 } else if (view instanceof EditText) {
346 String text = ((EditText) view).getText().toString();
347 if (unescape) {
348 text = unescape(text);
349 }
350 if (String.class.isAssignableFrom(fieldClass)) {
351 return text;
352 }
353 final Class<?> targetClass = isNumber(fieldClass);
354 if (targetClass != null) {
355
356 try {
357 final Method valueOf = targetClass.getMethod("valueOf",
358 new Class[] { String.class });
359 return valueOf.invoke(null, new Object[] { text });
360 } catch (final Exception ex) {
361 throw new RuntimeException(ex);
362 }
363 }
364 throw new IllegalArgumentException(fieldClass.getName()
365 + " not assignable from EditText");
366 } else if (view instanceof Spinner) {
367 final int ordinal = ((Spinner) view).getSelectedItemPosition();
368 if (fieldClass.isEnum()) {
369 return fieldClass.getEnumConstants()[ordinal];
370 }
371 return Integer.valueOf(ordinal);
372 }
373 throw new IllegalArgumentException("Unsupported view class: "
374 + view.getClass().getName());
375 }
376
377 private final static Map<Class<?>, Class<?>> primitiveToClass = new HashMap<Class<?>, Class<?>>();
378 static {
379 primitiveToClass.put(int.class, Integer.class);
380 primitiveToClass.put(double.class, Double.class);
381 primitiveToClass.put(float.class, Float.class);
382 primitiveToClass.put(boolean.class, Boolean.class);
383 primitiveToClass.put(byte.class, Byte.class);
384 primitiveToClass.put(char.class, Character.class);
385 primitiveToClass.put(long.class, Long.class);
386 }
387
388 private static Class<?> primitiveToClass(final Class<?> clazz) {
389 if (!clazz.isPrimitive())
390 return clazz;
391 return primitiveToClass.get(clazz);
392 }
393
394 private static Class<?> isNumber(final Class<?> clazz) {
395 final Class<?> counterpart = primitiveToClass(clazz);
396 if (Number.class.isAssignableFrom(counterpart))
397 return counterpart;
398 return null;
399 }
400
401 /***
402 * Copies all bindable properties from given bean to given bean.
403 *
404 * @param <T>
405 * the bean type
406 * @param from
407 * source bean
408 * @param to
409 * target bean
410 */
411 public static <T> void copy(T from, T to) {
412 try {
413 for (final Field field : from.getClass().getFields()) {
414 final Bind annotation = field.getAnnotation(Bind.class);
415 if (annotation == null)
416 continue;
417 if (!Modifier.isPublic(field.getModifiers()))
418 continue;
419 final Object fieldValue = field.get(from);
420 field.set(to, fieldValue);
421 }
422 } catch (IllegalAccessException e) {
423 throw new RuntimeException(e);
424 }
425 }
426 }