View Javadoc

1   /*
2    * Copyright 2007-2013 smartics, Kronseder & Reiner GmbH
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  package de.smartics.exceptions.i18n.message;
17  
18  import java.lang.reflect.Field;
19  import java.lang.reflect.InvocationTargetException;
20  import java.lang.reflect.Method;
21  import java.util.HashMap;
22  import java.util.LinkedList;
23  import java.util.List;
24  import java.util.Locale;
25  import java.util.Map;
26  import java.util.ResourceBundle;
27  
28  import ognl.OgnlContext;
29  import ognl.OgnlException;
30  
31  import org.apache.commons.logging.Log;
32  import org.apache.commons.logging.LogFactory;
33  
34  import com.ibm.icu.text.MessageFormat;
35  
36  import de.smartics.exceptions.i18n.ConfigurationException;
37  import de.smartics.exceptions.i18n.MessageComposer;
38  import de.smartics.exceptions.i18n.MethodAccessConfigurationException;
39  import de.smartics.exceptions.i18n.PropertyAccessConfigurationException;
40  import de.smartics.exceptions.i18n.app.ConfigurationExceptionCode;
41  import de.smartics.exceptions.i18n.message.MessageParamParser.MessageParamInfo;
42  import de.smartics.exceptions.ognl.OgnlExpression;
43  import de.smartics.util.lang.Arg;
44  
45  /**
46   * Message composer implementation that is bases on
47   * <code><a href="http://icu-project.org/apiref/icu4j/com/ibm/icu/text/MessageFormat.html">MessageFormat</a></code>
48   * of the <a href="http://icu-project.org">icu4j project</a>.
49   */
50  public final class IcuMessageComposer implements MessageComposer
51  {
52    // ********************************* Fields *********************************
53  
54    // --- constants ------------------------------------------------------------
55  
56    // --- members --------------------------------------------------------------
57  
58    /**
59     * Reference to the logger for this class.
60     */
61    private final Log log = LogFactory.getLog(IcuMessageComposer.class);
62  
63    // ****************************** Initializer *******************************
64  
65    // ****************************** Constructors ******************************
66  
67    /**
68     * Default constructor.
69     */
70    public IcuMessageComposer()
71    {
72    }
73  
74    // ****************************** Inner Classes *****************************
75  
76    // ********************************* Methods ********************************
77  
78    // --- init -----------------------------------------------------------------
79  
80    // --- get&set --------------------------------------------------------------
81  
82    // --- business -------------------------------------------------------------
83  
84    @Override
85    public String composeMessage(final Object bean, final Locale locale,
86        final ResourceBundle bundle, final String keyPrefix,
87        final MessageType messageType) throws NullPointerException,
88      IllegalArgumentException
89    {
90      Arg.checkNotNull("messageType", messageType);
91  
92      final String key = messageType.createKey(keyPrefix);
93      final String messageTemplate = bundle.getString(key);
94  
95      final Map<String, Object> messageArgs =
96          createMessageArgs(bean, messageType);
97  
98      final MessageFormat messageFormat =
99          new MessageFormat(messageTemplate, locale);
100 
101     final String message = messageFormat.format(messageArgs);
102 
103     return message;
104   }
105 
106   private Map<String, Object> createMessageArgs(final Object bean,
107       final MessageType messageType)
108   {
109     final List<Class<?>> types = collectParents(bean);
110     final Map<String, Object> args = new HashMap<String, Object>();
111 
112     for (final Class<?> type : types)
113     {
114       addParentMessageParamArgs(args, bean, messageType, type);
115       addMessageParamFieldArgs(args, type, bean, messageType);
116       addMessageParamMethodArgs(args, type, bean, messageType);
117     }
118     return args;
119   }
120 
121   private static List<Class<?>> collectParents(final Object bean)
122   {
123     Class<?> clazz = bean.getClass();
124     final LinkedList<Class<?>> classes = new LinkedList<Class<?>>();
125 
126     do
127     {
128       classes.addFirst(clazz);
129       clazz = clazz.getSuperclass();
130     }
131     while (clazz != Object.class);
132 
133     return classes;
134   }
135 
136   private static void addParentMessageParamArgs(final Map<String, Object> args,
137       final Object bean, final MessageType messageType, final Class<?> clazz)
138   {
139     final ParentMessageParam annotation =
140         clazz.getAnnotation(ParentMessageParam.class);
141     if (annotation != null)
142     {
143       final Map<String, List<MessageParamInfo>> map =
144           messageType.getParentMessageParamInfos(annotation);
145       for (final Map.Entry<String, List<MessageParamInfo>> entry : map
146           .entrySet())
147       {
148         final String attribute = entry.getKey();
149         final List<MessageParamInfo> infos = entry.getValue();
150 
151         for (final MessageParamInfo info : infos)
152         {
153           final String placeholderId = info.getPlaceholderId();
154           Object value = getProperty(bean, attribute);
155           value = normalizeValue(bean, attribute, info, value);
156           args.put(placeholderId, value);
157         }
158       }
159     }
160   }
161 
162   private static Object normalizeValue(final Object bean,
163       final String attribute, final MessageParamInfo info, final Object value)
164   {
165     final Object norm;
166     if (value != null)
167     {
168       norm = applyOgnl(bean, attribute, info, value);
169     }
170     else
171     {
172       norm = "-/-";
173     }
174     return norm;
175   }
176 
177   private void addMessageParamFieldArgs(final Map<String, Object> args,
178       final Class<?> type, final Object bean, final MessageType messageType)
179   {
180     String fieldName;
181     final Field[] fields = type.getDeclaredFields();
182 
183     for (final Field field : fields)
184     {
185       fieldName = field.getName();
186       if (log.isTraceEnabled())
187       {
188         log.trace(String.format("Processing annotations for field '%s'...",
189             fieldName));
190       }
191 
192       final MessageParam messageParam = field.getAnnotation(MessageParam.class);
193       if (messageParam != null)
194       {
195         final List<MessageParamInfo> infos =
196             messageType.getMessageParamInfos(fieldName, messageParam);
197         for (final MessageParamInfo info : infos)
198         {
199           final String placeholderId = info.getPlaceholderId();
200           Object value = getValue(bean, field);
201           value = normalizeValue(bean, fieldName, info, value);
202           args.put(placeholderId, value);
203         }
204       }
205     }
206   }
207 
208   private void addMessageParamMethodArgs(final Map<String, Object> args,
209       final Class<?> type, final Object bean, final MessageType messageType)
210   {
211     String methodName;
212     final Method[] methods = type.getDeclaredMethods();
213     for (final Method method : methods)
214     {
215       methodName = method.getName(); // NOPMD
216       if (log.isTraceEnabled())
217       {
218         log.trace("Processing annotations for method '" + methodName + "'...");
219       }
220 
221       final MessageParam messageParam =
222           method.getAnnotation(MessageParam.class);
223       if (messageParam != null)
224       {
225         final String propertyName = calcPropertyName(methodName);
226         final List<MessageParamInfo> infos =
227             messageType.getMessageParamInfos(propertyName, messageParam);
228         for (final MessageParamInfo info : infos)
229         {
230           final String placeholderId = info.getPlaceholderId();
231           Object value = getValue(bean, method);
232           value = normalizeValue(bean, propertyName, info, value);
233           args.put(placeholderId, value);
234         }
235       }
236     }
237   }
238 
239   private static Object getProperty(final Object bean, final String propertyName)
240     throws PropertyAccessConfigurationException
241   {
242     try
243     {
244       // final Object value = PropertyUtils.getProperty(instance, propertyName);
245       final String methodName = "get" + Helper.capitalize(propertyName);
246       final Method method = bean.getClass().getMethod(methodName, new Class[0]);
247       final Object value = method.invoke(bean, new Object[0]);
248       return value;
249     }
250     catch (final IllegalAccessException e)
251     {
252       throw new PropertyAccessConfigurationException(e,
253           ConfigurationExceptionCode.CONFIGURATION_INACCESSIBLE_PROPERTY,
254           propertyName, bean.getClass());
255     }
256     catch (final InvocationTargetException e)
257     {
258       throw new PropertyAccessConfigurationException(e,
259           ConfigurationExceptionCode.CONFIGURATION_PROPERTY_RUNTIME_ACCESS,
260           propertyName, bean.getClass());
261     }
262     catch (final NoSuchMethodException e)
263     {
264       throw new PropertyAccessConfigurationException(e,
265           ConfigurationExceptionCode.CONFIGURATION_NO_GETTER_FOR_PROPERTY,
266           propertyName, bean.getClass());
267     }
268   }
269 
270   private static Object applyOgnl(final Object exception,
271       final String attribute, final MessageParamInfo info, final Object value)
272     throws PropertyAccessConfigurationException
273   {
274     final Object newValue;
275     final String ognlPath = info.getOgnlPath();
276     if (ognlPath != null)
277     {
278       newValue = evaluateOgnl(exception, attribute, value, ognlPath);
279     }
280     else
281     {
282       newValue = value;
283     }
284 
285     return newValue;
286   }
287 
288   private static Object evaluateOgnl(final Object bean, final String fieldName,
289       final Object value, final String ognlPath)
290     throws PropertyAccessConfigurationException
291   {
292     try
293     {
294       final OgnlExpression expression = new OgnlExpression(ognlPath);
295       final OgnlContext context = new OgnlContext();
296       return expression.getValue(context, value);
297     }
298     catch (final OgnlException e)
299     {
300       throw new PropertyAccessConfigurationException(e,
301           ConfigurationExceptionCode.CONFIGURATION_OGNL_SYNTAX_ERROR,
302           fieldName, bean.getClass());
303     }
304   }
305 
306   private static Object getValue(final Object bean, final Field field)
307     throws ConfigurationException
308   {
309     final PropertyInfo propertyInfo = field.getAnnotation(PropertyInfo.class);
310     final Object value;
311     if (propertyInfo != null && !"".equals(propertyInfo.getter()))
312     {
313       final String getter = propertyInfo.getter();
314       value = getValue(bean, field.getName(), getter);
315     }
316     else
317     {
318       final boolean accessible = field.isAccessible();
319       try
320       {
321         field.setAccessible(true);
322         value = getFieldValue(bean, field);
323       }
324       finally
325       {
326         field.setAccessible(accessible);
327       }
328     }
329 
330     return value;
331   }
332 
333   private static Object getValue(final Object bean, final String propertyName,
334       final String methodName) throws MethodAccessConfigurationException
335   {
336     try
337     {
338       final Method method = bean.getClass().getMethod(methodName, new Class[0]);
339       final Object value = method.invoke(bean, new Object[0]);
340       return value;
341     }
342     catch (final SecurityException e)
343     {
344       throw new MethodAccessConfigurationException(e,
345           ConfigurationExceptionCode.CONFIGURATION_SECURITY, propertyName,
346           bean.getClass(), methodName);
347     }
348     catch (final NoSuchMethodException e)
349     {
350       throw new MethodAccessConfigurationException(e,
351           ConfigurationExceptionCode.CONFIGURATION_MISSING_GETTER,
352           propertyName, bean.getClass(), methodName);
353     }
354     catch (final IllegalArgumentException e)
355     {
356       throw new MethodAccessConfigurationException(e,
357           ConfigurationExceptionCode.CONFIGURATION_NOARG, propertyName,
358           bean.getClass(), methodName);
359     }
360     catch (final IllegalAccessException e)
361     {
362       throw new MethodAccessConfigurationException(e,
363           ConfigurationExceptionCode.CONFIGURATION_MISSING_GETTER,
364           propertyName, bean.getClass(), methodName);
365     }
366     catch (final InvocationTargetException e)
367     {
368       throw new MethodAccessConfigurationException(e,
369           ConfigurationExceptionCode.CONFIGURATION_RUNTIME_ACCESS,
370           propertyName, bean.getClass(), methodName);
371     }
372   }
373 
374   private static Object getValue(final Object bean, final Method method)
375     throws ConfigurationException
376   {
377     try
378     {
379       final Object value = method.invoke(bean, new Object[0]);
380       return value;
381     }
382     catch (final SecurityException e)
383     {
384       throw new MethodAccessConfigurationException(e,
385           ConfigurationExceptionCode.CONFIGURATION_SECURITY,
386           calcPropertyName(method.getName()), bean.getClass(), method.getName());
387     }
388     catch (final IllegalArgumentException e)
389     {
390       throw new MethodAccessConfigurationException(e,
391           ConfigurationExceptionCode.CONFIGURATION_NOARG,
392           calcPropertyName(method.getName()), bean.getClass(), method.getName());
393     }
394     catch (final IllegalAccessException e)
395     {
396       throw new MethodAccessConfigurationException(e,
397           ConfigurationExceptionCode.CONFIGURATION_MISSING_GETTER,
398           calcPropertyName(method.getName()), bean.getClass(), method.getName());
399     }
400     catch (final InvocationTargetException e)
401     {
402       throw new MethodAccessConfigurationException(e,
403           ConfigurationExceptionCode.CONFIGURATION_RUNTIME_ACCESS,
404           calcPropertyName(method.getName()), bean.getClass(), method.getName());
405     }
406   }
407 
408   private static String calcPropertyName(final String methodName)
409   {
410     return methodName.startsWith("get") && methodName.length() > 3 ? methodName
411         .substring(3) : methodName;
412   }
413 
414   private static Object getFieldValue(final Object bean, final Field field)
415   {
416     try
417     {
418       final Object value = field.get(bean);
419       return value;
420     }
421     catch (final IllegalArgumentException e)
422     {
423       assert false : "The field '" + field.getName()
424                      + "' is not a member of the class '" + bean.getClass()
425                      + "': " + e.getMessage();
426     }
427     catch (final IllegalAccessException e)
428     {
429       assert false : "The field '" + field.getName() + "' in class '"
430                      + bean.getClass() + "' is not accessible: "
431                      + e.getMessage();
432     }
433     // Unfortunately the compiler does not see that we will raise an assertion
434     // exception if one of the caught exceptions is thrown.
435     assert false : "The system should never reach this point.";
436     return null;
437   }
438 
439   // --- object basics --------------------------------------------------------
440 
441 }