/* * Copyright (C) 2011 Peransin Nicolas. * Use is subject to license terms. */ package org.mypsycho.text; import java.lang.reflect.InvocationTargetException; import java.text.ChoiceFormat; import java.text.FieldPosition; import java.text.Format; import java.text.MessageFormat; import java.text.ParsePosition; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.beanutils.NestedNullException; import org.apache.commons.beanutils.PropertyUtils; /** * A message format which handles property navigation. * * @author Peransin Nicolas */ public class BeanMessageFormat extends java.text.Format { private static final long serialVersionUID = 6479157306784022952L; MessageFormat inner; List<ArgumentMap> maps; static final Pattern indexPattern = Pattern.compile("(\\d+)(\\.(.*))?"); /** * The root is used as foctory for inner format. */ BeanMessageFormat root = this; private BeanMessageFormat(String pattern, BeanMessageFormat ancestor) { root = ancestor; maps = root.maps; inner = root.createFormat(mapPattern(pattern)); applyFormats(inner); } /** * Constructs a MessageFormat for the default locale and the * specified pattern. * The constructor first sets the locale, then parses the pattern and * creates a list of subformats for the format elements contained in it. * Patterns and their interpretation are specified in the * <a href="#patterns">class description</a>. * * @param pattern the pattern for this message format * @exception IllegalArgumentException if the pattern is invalid */ public BeanMessageFormat(String pattern) { applyPattern(pattern); } public void applyPattern(String pattern) { maps = new ArrayList<ArgumentMap>(); inner = root.createFormat(mapPattern(pattern)); applyFormats(inner); } protected MessageFormat createFormat(String pattern) { // Should/could use ExtendedMessageFormat from commons.apache.org return new java.text.MessageFormat(pattern); } /** * Constructs a MessageFormat for the specified locale and * pattern. * The constructor first sets the locale, then parses the pattern and * creates a list of subformats for the format elements contained in it. * Patterns and their interpretation are specified in the * <a href="#patterns">class description</a>. * * @param pattern the pattern for this message format * @param locale the locale for this message format * @exception IllegalArgumentException if the pattern is invalid * @since 1.4 */ public BeanMessageFormat(String pattern, Locale locale) { this(pattern); inner.setLocale(locale); } /** * Do something TODO. * <p> * Details of the function. * </p> */ private void applyFormats(java.text.MessageFormat subFormat) { for (Format format : subFormat.getFormats()) { if (!(format instanceof ChoiceFormat)) { continue; } ChoiceFormat choice = (ChoiceFormat) format; String[] choiceFormats = (String[]) choice.getFormats(); for (int i = 0; i < choiceFormats.length; i++) { String innerFormat = choiceFormats[i]; if (innerFormat.contains("{")) { BeanMessageFormat recursive = new BeanMessageFormat(innerFormat, root); choiceFormats[i] = recursive.inner.toPattern(); } } choice.setChoices(choice.getLimits(), choiceFormats); } } /** * Sets the locale to be used when creating or comparing subformats. * This affects subsequent calls * <ul> * <li>to the {@link #applyPattern applyPattern} and {@link #toPattern * toPattern} methods if format elements specify a format type and therefore * have the subformats created in the <code>applyPattern</code> method, as * well as * <li>to the <code>format</code> and {@link #formatToCharacterIterator * formatToCharacterIterator} methods if format elements do not specify a * format type and therefore have the subformats created in the formatting * methods. * </ul> * Subformats that have already been created are not affected. * * @param locale the locale to be used when creating or comparing subformats */ public void setLocale(Locale locale) { inner.setLocale(locale); } /** * Gets the locale that's used when creating or comparing subformats. * * @return the locale used when creating or comparing subformats */ public Locale getLocale() { return inner.getLocale(); } /** * Creates a MessageFormat with the given pattern and uses it * to format the given arguments. This is equivalent to * <blockquote> * <code>new {@link #MessageFormat(String) MessageFormat}(pattern).{@link * #format(java.lang.Object[], java.lang.StringBuffer, java.text.FieldPosition) format * }(arguments, new StringBuffer(), null).toString()</code> * </blockquote> * * @exception IllegalArgumentException if the pattern is invalid, * or if an argument in the <code>arguments</code> array * is not of the type expected by the format element(s) * that use it. */ public static String format(String pattern, Object... arguments) { return new BeanMessageFormat(pattern).format(arguments); } protected String mapPattern(String pattern) { StringBuilder[] parts = { new StringBuilder(pattern.length()), // pattern new StringBuilder(), // index new StringBuilder() // option }; int iPart = 0; boolean inQuote = false; int braceStack = 0; for (int i = 0; i < pattern.length(); ++i) { char ch = pattern.charAt(i); if (iPart == 0) { parts[iPart].append(ch); if (ch == '\'') { if (i + 1 < pattern.length() && pattern.charAt(i+1) == '\'') { parts[0].append('\''); ++i; } else { inQuote = !inQuote; } } else if (ch == '{' && !inQuote) { iPart = 1; } } else if (inQuote) { // just copy quotes in parts parts[iPart].append(ch); if (ch == '\'') { inQuote = false; } } else { switch (ch) { case ',': if (iPart < parts.length - 1) { iPart += 1; } parts[iPart].append(ch); break; case '{': ++braceStack; parts[iPart].append(ch); break; case '}': if (braceStack == 0) { // back to main pattern iPart = 0; int index = maps.size(); maps.add(createMap(parts[1].toString())); parts[0].append(index); parts[0].append(parts[2]); parts[1].setLength(0); parts[2].setLength(0); } else { --braceStack; } parts[iPart].append(ch); break; case '\'': inQuote = true; // fall through, so we keep quotes in other parts default: parts[iPart].append(ch); break; } } } if (braceStack == 0 && iPart != 0) { throw new IllegalArgumentException("Unmatched braces in the pattern."); } return parts[0].toString(); } protected int readIndex(String expr) { Matcher m = indexPattern.matcher(expr); if (!m.find()) { throw new IllegalArgumentException("can't parse argument number " + expr); } expr = m.group(1); // get the argument number int argumentNumber = Integer.parseInt(expr); if (argumentNumber < 0) { throw new IllegalArgumentException("negative argument number " + argumentNumber); } return argumentNumber; } /* * (non-Javadoc) * * @see java.text.Format#format(java.lang.Object, java.lang.StringBuffer, * java.text.FieldPosition) */ @Override public StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos) { Object[] values = (Object[]) obj; Object[] mappeds = new Object[maps.size()]; for (int iMap = 0; iMap < mappeds.length; iMap++) { ArgumentMap map = maps.get(iMap); mappeds[iMap] = (map.index < values.length) ? map.map(values[map.index]) : null; } return inner.format(mappeds, toAppendTo, pos); } /* * (non-Javadoc) * * @see java.text.Format#parseObject(java.lang.String, * java.text.ParsePosition) */ @Override public Object parseObject(String source, ParsePosition pos) { throw new UnsupportedOperationException(); } protected ArgumentMap createMap(String expr) { Matcher m = indexPattern.matcher(expr); if (!m.find()) { throw new IllegalArgumentException("can't parse argument number " + expr); } // get the argument number int argumentNumber = Integer.parseInt(m.group(1)); if (argumentNumber < 0) { throw new IllegalArgumentException("negative argument number " + argumentNumber); } return new ArgumentMap(argumentNumber, m.group(3)); } protected class ArgumentMap { protected int index = -1; protected String path = null; protected ArgumentMap(int i, String expr) { index = i; path = expr; } public Object map(Object object) { if (path == null) { return object; } if (object == null) { return null; } return BeanMessageFormat.this.root.map(object, path); } } /** * Interpret the property path of the object. * <p> * The default implementation use * {@link org.apache.commons.beanutils.PropertyUtils} * </p>. * * @param bean * @param path * @return property value * @throws IllegalArgumentException if the value cannot be interpreted */ protected Object map(Object bean, String path) throws IllegalArgumentException { try { return PropertyUtils.getProperty(bean, path); } catch (NestedNullException e) { return null; } catch (IllegalAccessException e) { throw new IllegalArgumentException(e); } catch (InvocationTargetException e) { throw new IllegalArgumentException(e); } catch (NoSuchMethodException e) { throw new IllegalArgumentException(e); } } }