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