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.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   * </p>
50   *
51   * @deprecated Use {@link IcuMessageComposer}. Will be removed with version 1.0.
52   *             To switch to this version please reformat your compound messages,
53   *             using <a href=
54   *             "http://icu-project.org/apiref/icu4j/com/ibm/icu/text/MessageFormat.html"
55   *             >ICU MessageFormat</a>.
56   */
57  @Deprecated
58  public class DefaultMessageComposer implements MessageComposer
59  { // NOPMD
60    // ********************************* Fields *********************************
61  
62    // --- constants ------------------------------------------------------------
63  
64    // --- members --------------------------------------------------------------
65  
66    /**
67     * Reference to the logger for this class.
68     */
69    private final Log log = LogFactory.getLog(DefaultMessageComposer.class);
70  
71    // ****************************** Initializer *******************************
72  
73    // ****************************** Constructors ******************************
74  
75    /**
76     * Default constructor.
77     */
78    public DefaultMessageComposer()
79    {
80    }
81  
82    // ****************************** Inner Classes *****************************
83  
84    // ********************************* Methods ********************************
85  
86    // --- init -----------------------------------------------------------------
87  
88    // --- get&set --------------------------------------------------------------
89  
90    // --- business -------------------------------------------------------------
91  
92    @Override
93    public String composeMessage(final Object 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 
123         final Method[] declaredMethods = currentClass.getDeclaredMethods();
124         supplyPropertyValues(exception, key, bundle, formatter, messageType,
125             messageArguments, declaredMethods);
126 
127         currentClass = currentClass.getSuperclass();
128       }
129       while (currentClass != null && currentClass != Exception.class
130              && !isAllInformationProvided(messageArguments));
131 
132       replaceMissingInformationDefaults(messageArguments);
133 
134       final String output = formatter.format(messageArguments);
135       return output;
136     }
137     else
138     {
139       return messageTemplate;
140     }
141   }
142 
143   /**
144    * Adds the information provided at class level for the properties of the
145    * parent exception.
146    *
147    * @param exception the exception to supply parent information.
148    * @param key the key to the message template for compound messages.
149    * @param bundle the bundle to access for compound messages.
150    * @param formatter the formatter to configure for compound messages.
151    * @param messageType the message type to supply information for.
152    * @param messageArguments the message arguments to add information to.
153    */
154   private void supplyParentPropertyValues(final Object exception,
155       final String key, final ResourceBundle bundle,
156       final MessageFormat formatter, final MessageType messageType,
157       final Object[] messageArguments)
158   {
159     final Class<?> clazz = exception.getClass();
160     final ParentMessageParam annotation =
161         clazz.getAnnotation(ParentMessageParam.class);
162     if (annotation != null)
163     {
164       final Map<String, List<MessageParamInfo>> map =
165           messageType.getParentMessageParamInfos(annotation);
166       for (final Map.Entry<String, List<MessageParamInfo>> entry : map
167           .entrySet())
168       {
169         final String attribute = entry.getKey(); // NOPMD
170         final List<MessageParamInfo> infos = entry.getValue();
171 
172         for (final MessageParamInfo info : infos)
173         {
174           final int index = Integer.parseInt(info.getPlaceholderId());
175           if (index < messageArguments.length)
176           {
177             Object value = getProperty(exception, attribute);
178             if (value != null)
179             {
180               value = applyOgnl(exception, attribute, info, value);
181               applyCompoundMessageInfo(exception, key, bundle, formatter,
182                   attribute, index);
183               messageArguments[index] = value;
184             }
185           }
186         }
187       }
188     }
189   }
190 
191   /**
192    * This helper checks the OGNL and compound message annotation attribute and
193    * applies the information (if found) to the value and the formatter
194    * configuration.
195    *
196    * @param exception the exception instance whose attributes are in question.
197    * @param attribute the name of the attribute to fetch.
198    * @param info the message info to fetch further information (OGNL path
199    *          value).
200    * @param value the current value used as a base for the OGNL path.
201    * @return the new value with OGNL path applied or the old value if no OGNL
202    *         information is provided.
203    * @throws PropertyAccessConfigurationException on any property configuration
204    *           problem.
205    */
206   private Object applyOgnl(final Object exception, final String attribute,
207       final MessageParamInfo info, final Object value)
208     throws PropertyAccessConfigurationException
209   {
210     final Object newValue;
211     final String ognlPath = info.getOgnlPath();
212     if (ognlPath != null)
213     {
214       newValue = evaluateOgnl(exception, attribute, value, ognlPath);
215     }
216     else
217     {
218       newValue = value;
219     }
220 
221     return newValue;
222   }
223 
224   /**
225    * This helper checks compound message context and applies the information (if
226    * found) to the formatter configuration.
227    *
228    * @param exception the exception instance whose attributes are in question.
229    * @param key the key of the message resource for compound messages.
230    * @param bundle the bundle for compound messages.
231    * @param formatter the formatter to set the format for compound messages.
232    * @param attribute the name of the attribute to fetch.
233    * @param index the index of the place holder currently processed.
234    * @throws PropertyAccessConfigurationException on any property configuration
235    *           problem.
236    */
237   private void applyCompoundMessageInfo(final Object exception,
238       final String key, final ResourceBundle bundle,
239       final MessageFormat formatter, final String attribute, final int index)
240     throws PropertyAccessConfigurationException
241   {
242     final String[] limitStrings =
243         createLimitStrings(exception, attribute, key, index, bundle);
244     if (limitStrings != null)
245     {
246       final ChoiceFormat choiceFormat =
247           new ChoiceFormat(new double[] { 0d, 1d, 2d }, limitStrings);
248       formatter.setFormat(index, choiceFormat);
249     }
250   }
251 
252   /**
253    * Checks if all slots in the array are not <code>null</code>.
254    *
255    * @param messageArguments the array to check.
256    * @return <code>true</code> if all elements of the message arguments array
257    *         are not <code>null</code>, <code>false</code> otherwise.
258    */
259   private static boolean isAllInformationProvided(
260       final Object[] messageArguments)
261   {
262     for (final Object element : messageArguments)
263     {
264       if (element == null)
265       {
266         return false;
267       }
268     }
269     return true;
270   }
271 
272   private static void replaceMissingInformationDefaults(
273       final Object[] messageArguments)
274   {
275     for (int i = messageArguments.length - 1; i >= 0; i--)
276     {
277       if (messageArguments[i] == null)
278       {
279         messageArguments[i] = "-/-";
280       }
281     }
282   }
283 
284   /**
285    * Adds values from the exception's properties to the message arguments.
286    *
287    * @param bean the bean whose properties are read. Usually an exception
288    *          instance.
289    * @param key the key prefix for the for the compound message to fetch.
290    * @param bundle the bundle to access if there is a compound message.
291    * @param formatter the formatter to configure with a choice formatter.
292    * @param messageType the type of message to fill with values.
293    * @param messageArguments the message arguments to collect.
294    * @param fields the fields from the exception to access for values.
295    * @throws PropertyAccessConfigurationException if the OGNL expression is
296    *           present, but cannot be parsed or a compound message lacks
297    *           properties in the resource bundle.
298    */
299   private void supplyPropertyValues(final Object bean, final String key,
300       final ResourceBundle bundle, final MessageFormat formatter,
301       final MessageType messageType, final Object[] messageArguments,
302       final Field[] fields) throws PropertyAccessConfigurationException
303   {
304     String fieldName;
305     for (final Field field : fields)
306     {
307       fieldName = field.getName(); // NOPMD
308       if (log.isTraceEnabled())
309       {
310         log.trace("Processing annotations for field '" + fieldName + "'...");
311       }
312 
313       final MessageParam messageParam = field.getAnnotation(MessageParam.class);
314       if (messageParam != null)
315       {
316         final List<MessageParamInfo> infos =
317             messageType.getMessageParamInfos(fieldName, messageParam);
318         for (final MessageParamInfo info : infos)
319         {
320           final int index = Integer.parseInt(info.getPlaceholderId());
321           if (index < messageArguments.length)
322           {
323             Object value = getValue(bean, field);
324             if (value != null)
325             {
326               value = applyOgnl(bean, fieldName, info, value);
327               applyCompoundMessageInfo(bean, key, bundle, formatter, fieldName,
328                   index);
329               messageArguments[index] = value;
330             }
331           }
332         }
333       }
334     }
335   }
336 
337   /**
338    * Adds values from the exception's properties to the message arguments.
339    *
340    * @param bean the bean whose properties are read. Usually an exception
341    *          instance.
342    * @param key the key prefix for the for the compound message to fetch.
343    * @param bundle the bundle to access if there is a compound message.
344    * @param formatter the formatter to configure with a choice formatter.
345    * @param messageType the type of message to fill with values.
346    * @param messageArguments the message arguments to collect.
347    * @param methods the methods from the exception to access for values.
348    * @throws PropertyAccessConfigurationException if the OGNL expression is
349    *           present, but cannot be parsed or a compound message lacks
350    *           properties in the resource bundle.
351    */
352   private void supplyPropertyValues(final Object bean, final String key,
353       final ResourceBundle bundle, final MessageFormat formatter,
354       final MessageType messageType, final Object[] messageArguments,
355       final Method[] methods) throws PropertyAccessConfigurationException
356   {
357     String methodName;
358     for (final Method method : methods)
359     {
360       methodName = method.getName(); // NOPMD
361       if (log.isTraceEnabled())
362       {
363         log.trace("Processing annotations for method '" + methodName + "'...");
364       }
365 
366       final MessageParam messageParam =
367           method.getAnnotation(MessageParam.class);
368       if (messageParam != null)
369       {
370         final String propertyName = calcPropertyName(methodName);
371         final List<MessageParamInfo> infos =
372             messageType.getMessageParamInfos(propertyName, messageParam);
373         for (final MessageParamInfo info : infos)
374         {
375           final int index = Integer.parseInt(info.getPlaceholderId());
376           if (index < messageArguments.length)
377           {
378             Object value = getValue(bean, method);
379             if (value != null)
380             {
381               value = applyOgnl(bean, propertyName, info, value);
382               applyCompoundMessageInfo(bean, key, bundle, formatter,
383                   methodName, index);
384               messageArguments[index] = value;
385             }
386           }
387         }
388       }
389     }
390   }
391 
392   private static String calcPropertyName(final String methodName)
393   {
394     return methodName.startsWith("get") && methodName.length() > 3 ? methodName
395         .substring(3) : methodName;
396   }
397 
398   /**
399    * Internal helper to create the array of compound messages that allows to
400    * formulate different messages for plurals.
401    *
402    * @param bean the bean whose properties are read. Usually an exception
403    *          instance.
404    * @param fieldName the name of the field currently processed.
405    * @param key the key to the message template.
406    * @param index the index of the current place holder (as provided by the
407    *          annotation to the currently processed field).
408    * @param bundle the bundle to access the templates for the plurals.
409    * @return <code>null</code> if there is no plural or the array of compound
410    *         messages for the plurals.
411    * @throws PropertyAccessConfigurationException if a compound message lacks
412    *           properties in the resource bundle.
413    */
414   private static String[] createLimitStrings(final Object bean,
415       final String fieldName, final String key, final int index,
416       final ResourceBundle bundle) throws PropertyAccessConfigurationException
417   {
418     try
419     {
420       final String compoundKeyPrefix = key + '.' + index;
421 
422       final String key0 = compoundKeyPrefix + ".0";
423       final String key1 = compoundKeyPrefix + ".1";
424       final String keyM = compoundKeyPrefix + ".m";
425 
426       // final boolean isCompoundMessage = bundle.containsKey(key0)
427       // || bundle.containsKey(key1)
428       // || bundle.containsKey(keyM);
429       final boolean isCompoundMessage =
430           containsKey(bundle, key0) || containsKey(bundle, key1)
431               || containsKey(bundle, keyM);
432       final String[] limitStrings;
433       if (isCompoundMessage)
434       {
435         limitStrings =
436             new String[] { bundle.getString(key0), bundle.getString(key1),
437                           bundle.getString(keyM) };
438       }
439       else
440       {
441         limitStrings = null;
442       }
443       return limitStrings;
444     }
445     catch (final MissingResourceException e)
446     {
447       throw new PropertyAccessConfigurationException(e,
448           ConfigurationExceptionCode.COMPOUND_MESSAGE_MISSING, fieldName,
449           bean.getClass());
450     }
451   }
452 
453   /**
454    * Mimics the JSE 1.6 feature to check if a resource bundle contains a key.
455    *
456    * @param bundle the bundle to check for the key.
457    * @param key the key to check.
458    * @return <code>true</code> if the bundle contains a value for the given key,
459    *         <code>false</code> otherwise.
460    */
461   private static boolean containsKey(final ResourceBundle bundle,
462       final String key)
463   {
464     try
465     {
466       bundle.getString(key);
467       return true;
468     }
469     catch (final Exception e)
470     {
471       return false;
472     }
473   }
474 
475   /**
476    * Evaluates the OGNL expression.
477    *
478    * @param bean the instance required for debugging.
479    * @param fieldName the name of the field of the instance required for
480    *          debugging.
481    * @param value the value to derive the requested value for the OGNL.
482    * @param ognlPath the OGNL path.
483    * @return the OGNL evaluated value.
484    * @throws PropertyAccessConfigurationException if the OGNL has an syntax
485    *           error.
486    */
487   private Object evaluateOgnl(final Object bean, final String fieldName,
488       final Object value, final String ognlPath)
489     throws PropertyAccessConfigurationException
490   {
491     try
492     {
493       final OgnlExpression expression = new OgnlExpression(ognlPath);
494       final OgnlContext context = new OgnlContext();
495       return expression.getValue(context, value);
496     }
497     catch (final OgnlException e)
498     {
499       throw new PropertyAccessConfigurationException(e,
500           ConfigurationExceptionCode.CONFIGURATION_OGNL_SYNTAX_ERROR,
501           fieldName, bean.getClass());
502     }
503   }
504 
505   /**
506    * Returns the value of the given field by either calling the getter method
507    * specified in the annotation {@link PropertyInfo} to the given field, by
508    * accessing the field directly (if accessible) or by deriving the getter
509    * method's name from the name of the property.
510    *
511    * @param bean the instance whose value is requested.
512    * @param field the field the value is requested.
513    * @return the value of the field.
514    * @throws ConfigurationException if there is a problem using the method to
515    *           access the requested method.
516    */
517   private static Object getValue(final Object bean, final Field field)
518     throws ConfigurationException
519   {
520     final PropertyInfo propertyInfo = field.getAnnotation(PropertyInfo.class);
521     final Object value;
522     if (propertyInfo != null && !"".equals(propertyInfo.getter()))
523     {
524       final String getter = propertyInfo.getter();
525       value = getValue(bean, field.getName(), getter);
526     }
527     else
528     {
529       final boolean accessible = field.isAccessible();
530       try
531       {
532         field.setAccessible(true);
533         value = getFieldValue(bean, field);
534       }
535       finally
536       {
537         field.setAccessible(accessible);
538       }
539 
540       // if (field.isAccessible())
541       // {
542       // }
543       // else
544       // {
545       // final String fieldName = field.getName();
546       // value = getProperty(bean, fieldName);
547       // }
548     }
549 
550     return value;
551   }
552 
553   private static Object getValue(final Object bean, final Method method)
554     throws ConfigurationException
555   {
556     try
557     {
558       final Object value = method.invoke(bean, new Object[0]);
559       return value;
560     }
561     catch (final SecurityException e)
562     {
563       throw new MethodAccessConfigurationException(e,
564           ConfigurationExceptionCode.CONFIGURATION_SECURITY,
565           calcPropertyName(method.getName()), bean.getClass(), method.getName());
566     }
567     catch (final IllegalArgumentException e)
568     {
569       throw new MethodAccessConfigurationException(e,
570           ConfigurationExceptionCode.CONFIGURATION_NOARG,
571           calcPropertyName(method.getName()), bean.getClass(), method.getName());
572     }
573     catch (final IllegalAccessException e)
574     {
575       throw new MethodAccessConfigurationException(e,
576           ConfigurationExceptionCode.CONFIGURATION_MISSING_GETTER,
577           calcPropertyName(method.getName()), bean.getClass(), method.getName());
578     }
579     catch (final InvocationTargetException e)
580     {
581       throw new MethodAccessConfigurationException(e,
582           ConfigurationExceptionCode.CONFIGURATION_RUNTIME_ACCESS,
583           calcPropertyName(method.getName()), bean.getClass(), method.getName());
584     }
585   }
586 
587   /**
588    * Returns the value of the given field.
589    * <p>
590    * The method assumes the the field is accessible. If not an configuration
591    * exception is thrown.
592    *
593    * @param bean the instance whose field value is requested.
594    * @param field the field whose value is requested.
595    * @return the requested value fetched from the field.
596    */
597   private static Object getFieldValue(final Object bean, final Field field)
598   {
599     try
600     {
601       final Object value = field.get(bean);
602       return value;
603     }
604     catch (final IllegalArgumentException e)
605     {
606       assert false : "The field '" + field.getName()
607                      + "' is not a member of the class '" + bean.getClass()
608                      + "': " + e.getMessage();
609     }
610     catch (final IllegalAccessException e)
611     {
612       assert false : "The field '" + field.getName() + "' in class '"
613                      + bean.getClass() + "' is not accessible: "
614                      + e.getMessage();
615     }
616     // Unfortunately the compiler does not see that we will raise an assertion
617     // exception if one of the caught exceptions is thrown.
618     assert false : "The system should never reach this point.";
619     return null;
620   }
621 
622   /**
623    * Returns the value returned by calling the getter method with the given
624    * name.
625    *
626    * @param bean the instance whose method is invoked.
627    * @param propertyName the name of the property to get the value for.
628    * @param methodName the name of the method to invoke. It is expected that the
629    *          method requires no arguments and returns a value (is a getter).
630    * @return the value returned by calling the method.
631    * @throws MethodAccessConfigurationException if there is a problem using the
632    *           method to access the requested method.
633    */
634   private static Object getValue(final Object bean, final String propertyName,
635       final String methodName) throws MethodAccessConfigurationException
636   {
637     try
638     {
639       final Method method = bean.getClass().getMethod(methodName, new Class[0]);
640       final Object value = method.invoke(bean, new Object[0]);
641       return value;
642     }
643     catch (final SecurityException e)
644     {
645       throw new MethodAccessConfigurationException(e,
646           ConfigurationExceptionCode.CONFIGURATION_SECURITY, propertyName,
647           bean.getClass(), methodName);
648     }
649     catch (final NoSuchMethodException e)
650     {
651       throw new MethodAccessConfigurationException(e,
652           ConfigurationExceptionCode.CONFIGURATION_MISSING_GETTER,
653           propertyName, bean.getClass(), methodName);
654     }
655     catch (final IllegalArgumentException e)
656     {
657       throw new MethodAccessConfigurationException(e,
658           ConfigurationExceptionCode.CONFIGURATION_NOARG, propertyName,
659           bean.getClass(), methodName);
660     }
661     catch (final IllegalAccessException e)
662     {
663       throw new MethodAccessConfigurationException(e,
664           ConfigurationExceptionCode.CONFIGURATION_MISSING_GETTER,
665           propertyName, bean.getClass(), methodName);
666     }
667     catch (final InvocationTargetException e)
668     {
669       throw new MethodAccessConfigurationException(e,
670           ConfigurationExceptionCode.CONFIGURATION_RUNTIME_ACCESS,
671           propertyName, bean.getClass(), methodName);
672     }
673   }
674 
675   /**
676    * Returns the property value. The getter method derived from the property
677    * name is called to access the value.
678    *
679    * @param bean the instance whose property value is requested.
680    * @param propertyName the name of the property.
681    * @return the value of the property.
682    * @throws PropertyAccessConfigurationException if there is a problem using
683    *           the method to access the requested property.
684    */
685   private static Object getProperty(final Object bean, final String propertyName)
686     throws PropertyAccessConfigurationException
687   {
688     try
689     {
690       // final Object value = PropertyUtils.getProperty(instance, propertyName);
691       final String methodName = "get" + Helper.capitalize(propertyName);
692       final Method method = bean.getClass().getMethod(methodName, new Class[0]);
693       final Object value = method.invoke(bean, new Object[0]);
694       return value;
695     }
696     catch (final IllegalAccessException e)
697     {
698       throw new PropertyAccessConfigurationException(e,
699           ConfigurationExceptionCode.CONFIGURATION_INACCESSIBLE_PROPERTY,
700           propertyName, bean.getClass());
701     }
702     catch (final InvocationTargetException e)
703     {
704       throw new PropertyAccessConfigurationException(e,
705           ConfigurationExceptionCode.CONFIGURATION_PROPERTY_RUNTIME_ACCESS,
706           propertyName, bean.getClass());
707     }
708     catch (final NoSuchMethodException e)
709     {
710       throw new PropertyAccessConfigurationException(e,
711           ConfigurationExceptionCode.CONFIGURATION_NO_GETTER_FOR_PROPERTY,
712           propertyName, bean.getClass());
713     }
714   }
715 
716   // --- object basics --------------------------------------------------------
717 }