View Javadoc

1   /*
2    * Copyright 2007-2011 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.text.ChoiceFormat;
22  import java.text.MessageFormat;
23  import java.util.List;
24  import java.util.Locale;
25  import java.util.Map;
26  import java.util.MissingResourceException;
27  import java.util.ResourceBundle;
28  
29  import ognl.OgnlContext;
30  import ognl.OgnlException;
31  
32  import org.apache.commons.logging.Log;
33  import org.apache.commons.logging.LogFactory;
34  
35  import de.smartics.exceptions.i18n.ConfigurationException;
36  import de.smartics.exceptions.i18n.MessageComposer;
37  import de.smartics.exceptions.i18n.MethodAccessConfigurationException;
38  import de.smartics.exceptions.i18n.PropertyAccessConfigurationException;
39  import de.smartics.exceptions.i18n.app.ConfigurationExceptionCode;
40  import de.smartics.exceptions.i18n.message.MessageParamParser.MessageParamInfo;
41  import de.smartics.exceptions.ognl.OgnlExpression;
42  
43  /**
44   * The message composer creates messages from message templates by replacing the
45   * place holders with localized parameter values.
46   * <p>
47   * The message composer is an utility class to be instantiated without internal
48   * state.
49   *
50   * @author <a href="mailto:robert.reiner@smartics.de">Robert Reiner</a>
51   * @version $Revision$
52   */
53  public class DefaultMessageComposer implements MessageComposer
54  {
55    // ********************************* Fields *********************************
56  
57    // --- constants ------------------------------------------------------------
58  
59    // --- members --------------------------------------------------------------
60  
61    /**
62     * Reference to the logger for this class.
63     */
64    private final Log log = LogFactory.getLog(DefaultMessageComposer.class);
65  
66    // ****************************** Initializer *******************************
67  
68    // ****************************** Constructors ******************************
69  
70    /**
71     * Default constructor.
72     */
73    public DefaultMessageComposer()
74    {
75    }
76  
77    // ****************************** Inner Classes *****************************
78  
79    // ********************************* Methods ********************************
80  
81    // --- init -----------------------------------------------------------------
82  
83    // --- get&set --------------------------------------------------------------
84  
85    // --- business -------------------------------------------------------------
86  
87    /**
88     * {@inheritDoc}
89     *
90     * @see de.smartics.exceptions.i18n.MessageComposer#composeMessage(Throwable,
91     *      Locale, ResourceBundle, String, MessageType)
92     */
93    public String composeMessage(final Throwable exception, final Locale locale,
94        final ResourceBundle bundle, final String keyPrefix,
95        final MessageType messageType)
96    {
97      if (messageType == null)
98      {
99        throw new NullPointerException(
100           "Message type to compose a message must not be 'null'.");
101     }
102 
103     final String key = messageType.createKey(keyPrefix);
104     final String messageTemplate = bundle.getString(key);
105 
106     final byte maxIndex = MessageTemplateAnalyser.maxIndex(messageTemplate);
107     if (maxIndex > -1)
108     {
109       final Object[] messageArguments = new Object[maxIndex + 1];
110 
111       final MessageFormat formatter =
112           new MessageFormat(messageTemplate, locale);
113       Class<?> currentClass = exception.getClass();
114       do
115       {
116         supplyParentPropertyValues(exception, key, bundle, formatter,
117             messageType, messageArguments);
118 
119         final Field[] declaredFields = currentClass.getDeclaredFields();
120         supplyPropertyValues(exception, key, bundle, formatter, messageType,
121             messageArguments, declaredFields);
122         currentClass = currentClass.getSuperclass();
123       }
124       while (currentClass != null && currentClass != Exception.class
125              && !isAllInformationProvided(messageArguments));
126 
127       final String output = formatter.format(messageArguments);
128       return output;
129     }
130     else
131     {
132       return messageTemplate;
133     }
134   }
135 
136   /**
137    * Adds the information provided at class level for the properties of the
138    * parent exception.
139    *
140    * @param exception the exception to supply parent information.
141    * @param key the key to the message template for compound messages.
142    * @param bundle the bundle to access for compound messages.
143    * @param formatter the formatter to configure for compound messages.
144    * @param messageType the message type to supply information for.
145    * @param messageArguments the message arguments to add information to.
146    */
147   private void supplyParentPropertyValues(final Throwable exception,
148       final String key, final ResourceBundle bundle,
149       final MessageFormat formatter, final MessageType messageType,
150       final Object[] messageArguments)
151   {
152     final Class<?> clazz = exception.getClass();
153     final ParentMessageParam annotation =
154         clazz.getAnnotation(ParentMessageParam.class);
155     if (annotation != null)
156     {
157       final Map<String, List<MessageParamInfo>> map =
158           messageType.getParentMessageParamInfos(annotation);
159       for (Map.Entry<String, List<MessageParamInfo>> entry : map.entrySet())
160       {
161         final String attribute = entry.getKey(); // NOPMD
162         final List<MessageParamInfo> infos = entry.getValue();
163 
164         for (MessageParamInfo info : infos)
165         {
166           final int index = info.getIndex();
167           if (index < messageArguments.length)
168           {
169             Object value = getProperty(exception, attribute);
170             if (value != null)
171             {
172               value = applyOgnl(exception, attribute, info, value);
173               applyCompoundMessageInfo(exception, key, bundle, formatter,
174                   attribute, index);
175               messageArguments[index] = value;
176             }
177           }
178         }
179       }
180     }
181   }
182 
183   /**
184    * This helper checks the OGNL and compound message annotation attribute and
185    * applies the information (if found) to the value and the formatter
186    * configuration.
187    *
188    * @param exception the exception instance whose attributes are in question.
189    * @param attribute the name of the attribute to fetch.
190    * @param info the message info to fetch further information (OGNL path
191    *          value).
192    * @param value the current value used as a base for the OGNL path.
193    * @return the new value with OGNL path applied or the old value if no OGNL
194    *         information is provided.
195    * @throws PropertyAccessConfigurationException on any property configuration
196    *           problem.
197    */
198   private Object applyOgnl(final Throwable exception, final String attribute,
199       final MessageParamInfo info, final Object value)
200     throws PropertyAccessConfigurationException
201   {
202     final Object newValue;
203     final String ognlPath = info.getOgnlPath();
204     if (ognlPath != null)
205     {
206       newValue = evaluateOgnl(exception, attribute, value, ognlPath);
207     }
208     else
209     {
210       newValue = value;
211     }
212 
213     return newValue;
214   }
215 
216   /**
217    * This helper checks compound message context and applies the information (if
218    * found) to the formatter configuration.
219    *
220    * @param exception the exception instance whose attributes are in question.
221    * @param key the key of the message resource for compound messages.
222    * @param bundle the bundle for compound messages.
223    * @param formatter the formatter to set the format for compound messages.
224    * @param attribute the name of the attribute to fetch.
225    * @param index the index of the place holder currently processed.
226    * @throws PropertyAccessConfigurationException on any property configuration
227    *           problem.
228    */
229   private void applyCompoundMessageInfo(final Throwable exception,
230       final String key, final ResourceBundle bundle,
231       final MessageFormat formatter, final String attribute, final int index)
232     throws PropertyAccessConfigurationException
233   {
234     final String[] limitStrings =
235         createLimitStrings(exception, attribute, key, index, bundle);
236     if (limitStrings != null)
237     {
238       final ChoiceFormat choiceFormat = new ChoiceFormat(new double[]
239       { 0d, 1d, 2d }, limitStrings);
240       formatter.setFormat(index, choiceFormat);
241     }
242   }
243 
244   /**
245    * Checks if all slots in the array are not <code>null</code>.
246    *
247    * @param messageArguments the array to check.
248    * @return <code>true</code> if all elements of the message arguments array
249    *         are not <code>null</code>, <code>false</code> otherwise.
250    */
251   private static boolean isAllInformationProvided(
252       final Object[] messageArguments)
253   {
254     for (Object element : messageArguments)
255     {
256       if (element == null)
257       {
258         return false;
259       }
260     }
261     return true;
262   }
263 
264   /**
265    * Adds values from the exception's properties to the message arguments.
266    *
267    * @param exception the exception whose properties are read.
268    * @param key the key prefix for the for the compound message to fetch.
269    * @param bundle the bundle to access if there is a compound message.
270    * @param formatter the formatter to configure with a choice formatter.
271    * @param messageType the type of message to fill with values.
272    * @param messageArguments the message arguments to collect.
273    * @param fields the fields from the exception to access for values.
274    * @throws PropertyAccessConfigurationException if the OGNL expression is
275    *           present, but cannot be parsed or a compound message lacks
276    *           properties in the resource bundle.
277    */
278   private void supplyPropertyValues(final Throwable exception,
279       final String key, final ResourceBundle bundle,
280       final MessageFormat formatter, final MessageType messageType,
281       final Object[] messageArguments, final Field[] fields)
282     throws PropertyAccessConfigurationException
283   {
284     String fieldName;
285     for (Field field : fields)
286     {
287       fieldName = field.getName(); // NOPMD
288       if (log.isTraceEnabled())
289       {
290         log.trace("Processing annotations for field '" + fieldName + "'...");
291       }
292 
293       final MessageParam messageParam = field.getAnnotation(MessageParam.class);
294       if (messageParam != null)
295       {
296         final List<MessageParamInfo> infos =
297             messageType.getMessageParamInfos(messageParam);
298         for (MessageParamInfo info : infos)
299         {
300           final int index = info.getIndex();
301           if (index < messageArguments.length)
302           {
303             Object value = getValue(exception, field);
304             if (value != null)
305             {
306               value = applyOgnl(exception, fieldName, info, value);
307               applyCompoundMessageInfo(exception, key, bundle, formatter,
308                   fieldName, index);
309               messageArguments[index] = value;
310             }
311           }
312         }
313       }
314     }
315   }
316 
317   /**
318    * Internal helper to create the array of compound messages that allows to
319    * formulate different messages for plurals.
320    *
321    * @param exception the exception whose properties are read.
322    * @param fieldName the name of the field currently processed.
323    * @param key the key to the message template.
324    * @param index the index of the current place holder (as provided by the
325    *          annotation to the currently processed field).
326    * @param bundle the bundle to access the templates for the plurals.
327    * @return <code>null</code> if there is no plural or the array of compound
328    *         messages for the plurals.
329    * @throws PropertyAccessConfigurationException if a compound message lacks
330    *           properties in the resource bundle.
331    */
332   private static String[] createLimitStrings(final Throwable exception,
333       final String fieldName, final String key, final int index,
334       final ResourceBundle bundle) throws PropertyAccessConfigurationException
335   {
336     try
337     {
338       final String compoundKeyPrefix = key + '.' + index;
339 
340       final String key0 = compoundKeyPrefix + ".0";
341       final String key1 = compoundKeyPrefix + ".1";
342       final String keyM = compoundKeyPrefix + ".m";
343 
344       // final boolean isCompoundMessage = bundle.containsKey(key0)
345       // || bundle.containsKey(key1)
346       // || bundle.containsKey(keyM);
347       final boolean isCompoundMessage =
348           containsKey(bundle, key0) || containsKey(bundle, key1)
349               || containsKey(bundle, keyM);
350       final String[] limitStrings;
351       if (isCompoundMessage)
352       {
353         limitStrings =
354             new String[]
355             { bundle.getString(key0), bundle.getString(key1),
356              bundle.getString(keyM) };
357       }
358       else
359       {
360         limitStrings = null;
361       }
362       return limitStrings;
363     }
364     catch (final MissingResourceException e)
365     {
366       throw new PropertyAccessConfigurationException(e,
367           ConfigurationExceptionCode.COMPOUND_MESSAGE_MISSING, fieldName,
368           exception.getClass());
369     }
370   }
371 
372   /**
373    * Mimics the JSE 1.6 feature to check if a resource bundle contains a key.
374    *
375    * @param bundle the bundle to check for the key.
376    * @param key the key to check.
377    * @return <code>true</code> if the bundle contains a value for the given key,
378    *         <code>false</code> otherwise.
379    */
380   private static boolean containsKey(final ResourceBundle bundle,
381       final String key)
382   {
383     try
384     {
385       bundle.getString(key);
386       return true;
387     }
388     catch (final Exception e)
389     {
390       return false;
391     }
392   }
393 
394   /**
395    * Evaluates the OGNL expression.
396    *
397    * @param exception the instance required for debugging.
398    * @param fieldName the name of the field of the instance required for
399    *          debugging.
400    * @param value the value to derive the requested value for the OGNL.
401    * @param ognlPath the OGNL path.
402    * @return the OGNL evaluated value.
403    * @throws PropertyAccessConfigurationException if the OGNL has an syntax
404    *           error.
405    */
406   private Object evaluateOgnl(final Throwable exception,
407       final String fieldName, final Object value, final String ognlPath)
408     throws PropertyAccessConfigurationException
409   {
410     try
411     {
412       final OgnlExpression expression = new OgnlExpression(ognlPath);
413       final OgnlContext context = new OgnlContext();
414       return expression.getValue(context, value);
415     }
416     catch (final OgnlException e)
417     {
418       throw new PropertyAccessConfigurationException(e,
419           ConfigurationExceptionCode.CONFIGURATION_OGNL_SYNTAX_ERROR,
420           fieldName, exception.getClass());
421     }
422   }
423 
424   /**
425    * Returns the value of the given field by either calling the getter method
426    * specified in the annotation {@link PropertyInfo} to the given field, by
427    * accessing the field directly (if accessible) or by deriving the getter
428    * method's name from the name of the property.
429    *
430    * @param instance the instance whose value is requested.
431    * @param field the field the value is requested.
432    * @return the value of the field.
433    * @throws ConfigurationException if there is a problem using the method to
434    *           access the requested method.
435    */
436   private static Object getValue(final Object instance, final Field field)
437     throws ConfigurationException
438   {
439     final PropertyInfo propertyInfo = field.getAnnotation(PropertyInfo.class);
440     final Object value;
441     if (propertyInfo != null && !"".equals(propertyInfo.getter()))
442     {
443       final String getter = propertyInfo.getter();
444       value = getValue(instance, field.getName(), getter);
445     }
446     else
447     {
448       if (field.isAccessible())
449       {
450         value = getFieldValue(instance, field);
451       }
452       else
453       {
454         final String fieldName = field.getName();
455         value = getProperty(instance, fieldName);
456       }
457     }
458 
459     return value;
460   }
461 
462   /**
463    * Returns the value of the given field.
464    * <p>
465    * The method assumes the the field is accessible. If not an configuration
466    * exception is thrown.
467    *
468    * @param instance the instance whose field value is requested.
469    * @param field the field whose value is requested.
470    * @return the requested value fetched from the field.
471    */
472   private static Object getFieldValue(final Object instance, final Field field)
473   {
474     try
475     {
476       final Object value = field.get(instance);
477       return value;
478     }
479     catch (final IllegalArgumentException e)
480     {
481       assert false : "The field '" + field.getName()
482                      + "' is not a member of the class '" + instance.getClass()
483                      + "': " + e.getMessage();
484     }
485     catch (final IllegalAccessException e)
486     {
487       assert false : "The field '" + field.getName() + "' in class '"
488                      + instance.getClass() + "' is not accessible: "
489                      + e.getMessage();
490     }
491     // Unfortunately the compiler does not see that we will raise an assertion
492     // exception if one of the caught exceptions is thrown.
493     assert false : "The system should never reach this point.";
494     return null;
495   }
496 
497   /**
498    * Returns the value returned by calling the getter method with the given
499    * name.
500    *
501    * @param instance the instance whose method is invoked.
502    * @param propertyName the name of the property to get the value for.
503    * @param methodName the name of the method to invoke. It is expected that the
504    *          method requires no arguments and returns a value (is a getter).
505    * @return the value returned by calling the method.
506    * @throws MethodAccessConfigurationException if there is a problem using the
507    *           method to access the requested method.
508    */
509   private static Object getValue(final Object instance,
510       final String propertyName, final String methodName)
511     throws MethodAccessConfigurationException
512   {
513     try
514     {
515       final Method method =
516           instance.getClass().getMethod(methodName, new Class[0]);
517       final Object value = method.invoke(instance, new Object[0]);
518       return value;
519     }
520     catch (final SecurityException e)
521     {
522       throw new MethodAccessConfigurationException(e,
523           ConfigurationExceptionCode.CONFIGURATION_SECURITY, propertyName,
524           instance.getClass(), methodName);
525     }
526     catch (final NoSuchMethodException e)
527     {
528       throw new MethodAccessConfigurationException(e,
529           ConfigurationExceptionCode.CONFIGURATION_MISSING_GETTER,
530           propertyName, instance.getClass(), methodName);
531     }
532     catch (final IllegalArgumentException e)
533     {
534       throw new MethodAccessConfigurationException(e,
535           ConfigurationExceptionCode.CONFIGURATION_NOARG, propertyName,
536           instance.getClass(), methodName);
537     }
538     catch (final IllegalAccessException e)
539     {
540       throw new MethodAccessConfigurationException(e,
541           ConfigurationExceptionCode.CONFIGURATION_MISSING_GETTER,
542           propertyName, instance.getClass(), methodName);
543     }
544     catch (final InvocationTargetException e)
545     {
546       throw new MethodAccessConfigurationException(e,
547           ConfigurationExceptionCode.CONFIGURATION_RUNTIME_ACCESS,
548           propertyName, instance.getClass(), methodName);
549     }
550   }
551 
552   /**
553    * Returns the property value. The getter method derived from the property
554    * name is called to access the value.
555    *
556    * @param instance the instance whose property value is requested.
557    * @param propertyName the name of the property.
558    * @return the value of the property.
559    * @throws PropertyAccessConfigurationException if there is a problem using
560    *           the method to access the requested property.
561    */
562   private static Object getProperty(final Object instance,
563       final String propertyName) throws PropertyAccessConfigurationException
564   {
565     try
566     {
567       // final Object value = PropertyUtils.getProperty(instance, propertyName);
568       final String methodName = "get" + Helper.capitalize(propertyName);
569       final Method method =
570           instance.getClass().getMethod(methodName, new Class[0]);
571       final Object value = method.invoke(instance, new Object[0]);
572       return value;
573     }
574     catch (final IllegalAccessException e)
575     {
576       throw new PropertyAccessConfigurationException(e,
577           ConfigurationExceptionCode.CONFIGURATION_INACCESSIBLE_PROPERTY,
578           propertyName, instance.getClass());
579     }
580     catch (final InvocationTargetException e)
581     {
582       throw new PropertyAccessConfigurationException(e,
583           ConfigurationExceptionCode.CONFIGURATION_PROPERTY_RUNTIME_ACCESS,
584           propertyName, instance.getClass());
585     }
586     catch (final NoSuchMethodException e)
587     {
588       throw new PropertyAccessConfigurationException(e,
589           ConfigurationExceptionCode.CONFIGURATION_NO_GETTER_FOR_PROPERTY,
590           propertyName, instance.getClass());
591     }
592   }
593 
594   // --- object basics --------------------------------------------------------
595 }