aboutsummaryrefslogtreecommitdiffstats
path: root/fop-util/src/main/java
diff options
context:
space:
mode:
authorGlenn Adams <gadams@apache.org>2016-03-12 05:39:53 +0000
committerGlenn Adams <gadams@apache.org>2016-03-12 05:39:53 +0000
commit6a719897d6f98ba89aa08e2f97b2b801be066cbf (patch)
treebcce1bb7e9c6bde85e1167cff6218948f5be9f0a /fop-util/src/main/java
parent2184238c210288138b6703bd63ccb2c8c40300f0 (diff)
downloadxmlgraphics-fop-6a719897d6f98ba89aa08e2f97b2b801be066cbf.tar.gz
xmlgraphics-fop-6a719897d6f98ba89aa08e2f97b2b801be066cbf.zip
Configure fop-{sandbox,servlet,transcoder}, checkstyle, findbugs; fixups for ant targets.
git-svn-id: https://svn.apache.org/repos/asf/xmlgraphics/fop/branches/maven@1734671 13f79535-47bb-0310-9956-ffa450edef68
Diffstat (limited to 'fop-util/src/main/java')
-rw-r--r--fop-util/src/main/java/org/apache/fop/util/DefaultErrorListener.java65
-rw-r--r--fop-util/src/main/java/org/apache/fop/util/XMLResourceBundle.java400
-rw-r--r--fop-util/src/main/java/org/apache/fop/util/text/AdvancedMessageFormat.java493
3 files changed, 958 insertions, 0 deletions
diff --git a/fop-util/src/main/java/org/apache/fop/util/DefaultErrorListener.java b/fop-util/src/main/java/org/apache/fop/util/DefaultErrorListener.java
new file mode 100644
index 000000000..3c554afc7
--- /dev/null
+++ b/fop-util/src/main/java/org/apache/fop/util/DefaultErrorListener.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/* $Id$ */
+
+package org.apache.fop.util;
+
+import javax.xml.transform.ErrorListener;
+import javax.xml.transform.TransformerException;
+
+import org.apache.commons.logging.Log;
+
+/**
+ * Standard ErrorListener implementation for in-FOP use. Some Xalan-J versions don't properly
+ * re-throw exceptions.
+ */
+public class DefaultErrorListener implements ErrorListener {
+
+ private Log log;
+
+ /**
+ * Main constructor
+ * @param log the log instance to send log events to
+ */
+ public DefaultErrorListener(Log log) {
+ this.log = log;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void warning(TransformerException exc) {
+ log.warn(exc.toString());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void error(TransformerException exc) throws TransformerException {
+ throw exc;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void fatalError(TransformerException exc)
+ throws TransformerException {
+ throw exc;
+ }
+
+}
diff --git a/fop-util/src/main/java/org/apache/fop/util/XMLResourceBundle.java b/fop-util/src/main/java/org/apache/fop/util/XMLResourceBundle.java
new file mode 100644
index 000000000..80aec2d2f
--- /dev/null
+++ b/fop-util/src/main/java/org/apache/fop/util/XMLResourceBundle.java
@@ -0,0 +1,400 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/* $Id$ */
+
+package org.apache.fop.util;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+import java.util.Enumeration;
+import java.util.Hashtable;
+import java.util.Locale;
+import java.util.Map;
+import java.util.MissingResourceException;
+import java.util.Properties;
+import java.util.ResourceBundle;
+import java.util.Stack;
+
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.sax.SAXResult;
+import javax.xml.transform.sax.SAXTransformerFactory;
+import javax.xml.transform.stream.StreamSource;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+import org.apache.xmlgraphics.util.QName;
+
+/**
+ * This class is a ResourceBundle that loads its contents from XML files instead of properties
+ * files (like PropertiesResourceBundle).
+ * <p>
+ * The XML format for this resource bundle implementation is the following
+ * (the same as Apache Cocoon's XMLResourceBundle):
+ * <pre>
+ * &lt;catalogue xml:lang="en"&gt;
+ * &lt;message key="key1"&gt;Message &lt;br/&gt; Value 1&lt;/message&gt;
+ * &lt;message key="key2"&gt;Message &lt;br/&gt; Value 1&lt;/message&gt;
+ * ...
+ * &lt;/catalogue&gt;
+ * </pre>
+ */
+public class XMLResourceBundle extends ResourceBundle {
+
+ //Note: Some code here has been copied and adapted from Apache Harmony!
+
+ private Properties resources = new Properties();
+
+ private Locale locale;
+
+ private static SAXTransformerFactory tFactory
+ = (SAXTransformerFactory)SAXTransformerFactory.newInstance();
+
+ /**
+ * Creates a resource bundle from an InputStream.
+ * @param in the stream to read from
+ * @throws IOException if an I/O error occurs
+ */
+ public XMLResourceBundle(InputStream in) throws IOException {
+ try {
+ Transformer transformer = tFactory.newTransformer();
+ StreamSource src = new StreamSource(in);
+ SAXResult res = new SAXResult(new CatalogueHandler());
+ transformer.transform(src, res);
+ } catch (TransformerException e) {
+ throw new IOException("Error while parsing XML resource bundle: " + e.getMessage());
+ }
+ }
+
+ /**
+ * Gets a resource bundle using the specified base name, default locale, and class loader.
+ * @param baseName the base name of the resource bundle, a fully qualified class name
+ * @param loader the class loader from which to load the resource bundle
+ * @return a resource bundle for the given base name and the default locale
+ * @throws MissingResourceException if no resource bundle for the specified base name can be
+ * found
+ * @see java.util.ResourceBundle#getBundle(String)
+ */
+ public static ResourceBundle getXMLBundle(String baseName, ClassLoader loader)
+ throws MissingResourceException {
+ return getXMLBundle(baseName, Locale.getDefault(), loader);
+ }
+
+ /**
+ * Gets a resource bundle using the specified base name, locale, and class loader.
+ * @param baseName the base name of the resource bundle, a fully qualified class name
+ * @param locale the locale for which a resource bundle is desired
+ * @param loader the class loader from which to load the resource bundle
+ * @return a resource bundle for the given base name and locale
+ * @throws MissingResourceException if no resource bundle for the specified base name can be
+ * found
+ * @see java.util.ResourceBundle#getBundle(String, Locale, ClassLoader)
+ */
+ public static ResourceBundle getXMLBundle(String baseName, Locale locale, ClassLoader loader)
+ throws MissingResourceException {
+ if (loader == null) {
+ throw new NullPointerException("loader must not be null");
+ }
+ if (baseName == null) {
+ throw new NullPointerException("baseName must not be null");
+ }
+ assert locale != null;
+ ResourceBundle bundle;
+ if (!locale.equals(Locale.getDefault())) {
+ bundle = handleGetXMLBundle(baseName, "_" + locale, false, loader);
+ if (bundle != null) {
+ return bundle;
+ }
+ }
+ bundle = handleGetXMLBundle(baseName, "_" + Locale.getDefault(), true, loader);
+ if (bundle != null) {
+ return bundle;
+ }
+ throw new MissingResourceException(
+ baseName + " (" + locale + ")", baseName + '_' + locale, null);
+ }
+
+ static class MissingBundle extends ResourceBundle {
+ public Enumeration getKeys() {
+ return null;
+ }
+
+ public Object handleGetObject(String name) {
+ return null;
+ }
+ }
+
+ private static final ResourceBundle MISSING = new MissingBundle();
+ private static final ResourceBundle MISSINGBASE = new MissingBundle();
+
+ private static Map cache = new java.util.WeakHashMap();
+ //<Object, Hashtable<String, ResourceBundle>>
+
+ private static ResourceBundle handleGetXMLBundle(String base, String locale,
+ boolean loadBase, final ClassLoader loader) {
+ XMLResourceBundle bundle = null;
+ String bundleName = base + locale;
+ Object cacheKey = loader != null ? (Object) loader : (Object) "null";
+ Hashtable loaderCache; //<String, ResourceBundle>
+ synchronized (cache) {
+ loaderCache = (Hashtable)cache.get(cacheKey);
+ if (loaderCache == null) {
+ loaderCache = new Hashtable();
+ cache.put(cacheKey, loaderCache);
+ }
+ }
+ ResourceBundle result = (ResourceBundle)loaderCache.get(bundleName);
+ if (result != null) {
+ if (result == MISSINGBASE) {
+ return null;
+ }
+ if (result == MISSING) {
+ if (!loadBase) {
+ return null;
+ }
+ String extension = strip(locale);
+ if (extension == null) {
+ return null;
+ }
+ return handleGetXMLBundle(base, extension, loadBase, loader);
+ }
+ return result;
+ }
+
+ final String fileName = bundleName.replace('.', '/') + ".xml";
+ InputStream stream = (InputStream)AccessController
+ .doPrivileged(new PrivilegedAction() {
+ public Object run() {
+ return loader == null
+ ? ClassLoader.getSystemResourceAsStream(fileName)
+ : loader.getResourceAsStream(fileName);
+ }
+ });
+ if (stream != null) {
+ try {
+ try {
+ bundle = new XMLResourceBundle(stream);
+ } finally {
+ stream.close();
+ }
+ bundle.setLocale(locale);
+ } catch (IOException e) {
+ throw new MissingResourceException(e.getMessage(), base, null);
+ }
+ }
+
+ String extension = strip(locale);
+ if (bundle != null) {
+ if (extension != null) {
+ ResourceBundle parent = handleGetXMLBundle(base, extension, true,
+ loader);
+ if (parent != null) {
+ bundle.setParent(parent);
+ }
+ }
+ loaderCache.put(bundleName, bundle);
+ return bundle;
+ }
+
+ if (extension != null) {
+ ResourceBundle fallback = handleGetXMLBundle(base, extension, loadBase, loader);
+ if (fallback != null) {
+ loaderCache.put(bundleName, fallback);
+ return fallback;
+ }
+ }
+ loaderCache.put(bundleName, loadBase ? MISSINGBASE : MISSING);
+ return null;
+ }
+
+ private void setLocale(String name) {
+ String language = "";
+ String country = "";
+ String variant = "";
+ if (name.length() > 1) {
+ int nextIndex = name.indexOf('_', 1);
+ if (nextIndex == -1) {
+ nextIndex = name.length();
+ }
+ language = name.substring(1, nextIndex);
+ if (nextIndex + 1 < name.length()) {
+ int index = nextIndex;
+ nextIndex = name.indexOf('_', nextIndex + 1);
+ if (nextIndex == -1) {
+ nextIndex = name.length();
+ }
+ country = name.substring(index + 1, nextIndex);
+ if (nextIndex + 1 < name.length()) {
+ variant = name.substring(nextIndex + 1, name.length());
+ }
+ }
+ }
+ this.locale = new Locale(language, country, variant);
+ }
+
+ private static String strip(String name) {
+ int index = name.lastIndexOf('_');
+ if (index != -1) {
+ return name.substring(0, index);
+ }
+ return null;
+ }
+
+ private Enumeration getLocalKeys() {
+ return (Enumeration)resources.propertyNames();
+ }
+
+ /** {@inheritDoc} */
+ public Locale getLocale() {
+ return this.locale;
+ }
+
+ /** {@inheritDoc} */
+ public Enumeration getKeys() {
+ if (parent == null) {
+ return getLocalKeys();
+ }
+ return new Enumeration() {
+ private Enumeration local = getLocalKeys();
+ private Enumeration pEnum = parent.getKeys();
+
+ private Object nextElement;
+
+ private boolean findNext() {
+ if (nextElement != null) {
+ return true;
+ }
+ while (pEnum.hasMoreElements()) {
+ Object next = pEnum.nextElement();
+ if (!resources.containsKey(next)) {
+ nextElement = next;
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public boolean hasMoreElements() {
+ if (local.hasMoreElements()) {
+ return true;
+ }
+ return findNext();
+ }
+
+ public Object nextElement() {
+ if (local.hasMoreElements()) {
+ return local.nextElement();
+ }
+ if (findNext()) {
+ Object result = nextElement;
+ nextElement = null;
+ return result;
+ }
+ // Cause an exception
+ return pEnum.nextElement();
+ }
+ };
+ }
+
+ /** {@inheritDoc} */
+ protected Object handleGetObject(String key) {
+ if (key == null) {
+ throw new NullPointerException("key must not be null");
+ }
+ return resources.get(key);
+ }
+
+ /** {@inheritDoc} */
+ public String toString() {
+ return "XMLResourceBundle: " + getLocale();
+ }
+
+ private class CatalogueHandler extends DefaultHandler {
+
+ private static final String CATALOGUE = "catalogue";
+ private static final String MESSAGE = "message";
+
+ private StringBuffer valueBuffer = new StringBuffer();
+ private Stack elementStack = new Stack();
+ private String currentKey;
+
+ private boolean isOwnNamespace(String uri) {
+ return ("".equals(uri));
+ }
+
+ private QName getParentElementName() {
+ return (QName)elementStack.peek();
+ }
+
+ /** {@inheritDoc} */
+ public void startElement(String uri, String localName, String qName,
+ Attributes atts) throws SAXException {
+ super.startElement(uri, localName, qName, atts);
+ QName elementName = new QName(uri, qName);
+ if (isOwnNamespace(uri)) {
+ if (CATALOGUE.equals(localName)) {
+ //nop
+ } else if (MESSAGE.equals(localName)) {
+ if (!CATALOGUE.equals(getParentElementName().getLocalName())) {
+ throw new SAXException(MESSAGE + " must be a child of " + CATALOGUE);
+ }
+ this.currentKey = atts.getValue("key");
+ } else {
+ throw new SAXException("Invalid element name: " + elementName);
+ }
+ } else {
+ //ignore
+ }
+ this.valueBuffer.setLength(0);
+ elementStack.push(elementName);
+ }
+
+ /** {@inheritDoc} */
+ public void endElement(String uri, String localName, String qName) throws SAXException {
+ super.endElement(uri, localName, qName);
+ elementStack.pop();
+ if (isOwnNamespace(uri)) {
+ if (CATALOGUE.equals(localName)) {
+ //nop
+ } else if (MESSAGE.equals(localName)) {
+ if (this.currentKey == null) {
+ throw new SAXException(
+ "current key is null (attribute 'key' might be mistyped)");
+ }
+ resources.put(this.currentKey, this.valueBuffer.toString());
+ this.currentKey = null;
+ }
+ } else {
+ //ignore
+ }
+ this.valueBuffer.setLength(0);
+ }
+
+ /** {@inheritDoc} */
+ public void characters(char[] ch, int start, int length) throws SAXException {
+ super.characters(ch, start, length);
+ valueBuffer.append(ch, start, length);
+ }
+
+ }
+
+}
diff --git a/fop-util/src/main/java/org/apache/fop/util/text/AdvancedMessageFormat.java b/fop-util/src/main/java/org/apache/fop/util/text/AdvancedMessageFormat.java
new file mode 100644
index 000000000..f8508e082
--- /dev/null
+++ b/fop-util/src/main/java/org/apache/fop/util/text/AdvancedMessageFormat.java
@@ -0,0 +1,493 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/* $Id$ */
+
+package org.apache.fop.util.text;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+import org.apache.xmlgraphics.util.Service;
+
+
+/**
+ * Formats messages based on a template and with a set of named parameters. This is similar to
+ * {@link java.text.MessageFormat} but uses named parameters and supports conditional sub-groups.
+ * <p>
+ * Example:
+ * </p>
+ * <p><code>Missing field "{fieldName}"[ at location: {location}]!</code></p>
+ * <ul>
+ * <li>Curly brackets ("{}") are used for fields.</li>
+ * <li>Square brackets ("[]") are used to delimit conditional sub-groups. A sub-group is
+ * conditional when all fields inside the sub-group have a null value. In the case, everything
+ * between the brackets is skipped.</li>
+ * </ul>
+ */
+public class AdvancedMessageFormat {
+
+ /** Regex that matches "," but not "\," (escaped comma) */
+ static final Pattern COMMA_SEPARATOR_REGEX = Pattern.compile("(?<!\\\\),");
+
+ private static final Map<String, PartFactory> PART_FACTORIES
+ = new java.util.HashMap<String, PartFactory>();
+ private static final List<ObjectFormatter> OBJECT_FORMATTERS
+ = new java.util.ArrayList<ObjectFormatter>();
+ private static final Map<Object, Function> FUNCTIONS
+ = new java.util.HashMap<Object, Function>();
+
+ private CompositePart rootPart;
+
+ static {
+ Iterator<Object> iter;
+ iter = Service.providers(PartFactory.class);
+ while (iter.hasNext()) {
+ PartFactory factory = (PartFactory)iter.next();
+ PART_FACTORIES.put(factory.getFormat(), factory);
+ }
+ iter = Service.providers(ObjectFormatter.class);
+ while (iter.hasNext()) {
+ OBJECT_FORMATTERS.add((ObjectFormatter)iter.next());
+ }
+ iter = Service.providers(Function.class);
+ while (iter.hasNext()) {
+ Function function = (Function)iter.next();
+ FUNCTIONS.put(function.getName(), function);
+ }
+ }
+
+ /**
+ * Construct a new message format.
+ * @param pattern the message format pattern.
+ */
+ public AdvancedMessageFormat(CharSequence pattern) {
+ parsePattern(pattern);
+ }
+
+ private void parsePattern(CharSequence pattern) {
+ rootPart = new CompositePart(false);
+ StringBuffer sb = new StringBuffer();
+ parseInnerPattern(pattern, rootPart, sb, 0);
+ }
+
+ private int parseInnerPattern(CharSequence pattern, CompositePart parent,
+ StringBuffer sb, int start) {
+ assert sb.length() == 0;
+ int i = start;
+ int len = pattern.length();
+ loop:
+ while (i < len) {
+ char ch = pattern.charAt(i);
+ switch (ch) {
+ case '{':
+ if (sb.length() > 0) {
+ parent.addChild(new TextPart(sb.toString()));
+ sb.setLength(0);
+ }
+ i++;
+ int nesting = 1;
+ while (i < len) {
+ ch = pattern.charAt(i);
+ if (ch == '{') {
+ nesting++;
+ } else if (ch == '}') {
+ nesting--;
+ if (nesting == 0) {
+ i++;
+ break;
+ }
+ }
+ sb.append(ch);
+ i++;
+ }
+ parent.addChild(parseField(sb.toString()));
+ sb.setLength(0);
+ break;
+ case ']':
+ i++;
+ break loop; //Current composite is finished
+ case '[':
+ if (sb.length() > 0) {
+ parent.addChild(new TextPart(sb.toString()));
+ sb.setLength(0);
+ }
+ i++;
+ CompositePart composite = new CompositePart(true);
+ parent.addChild(composite);
+ i += parseInnerPattern(pattern, composite, sb, i);
+ break;
+ case '|':
+ if (sb.length() > 0) {
+ parent.addChild(new TextPart(sb.toString()));
+ sb.setLength(0);
+ }
+ parent.newSection();
+ i++;
+ break;
+ case '\\':
+ if (i < len - 1) {
+ i++;
+ ch = pattern.charAt(i);
+ }
+ sb.append(ch);
+ i++;
+ break;
+ default:
+ sb.append(ch);
+ i++;
+ break;
+ }
+ }
+ if (sb.length() > 0) {
+ parent.addChild(new TextPart(sb.toString()));
+ sb.setLength(0);
+ }
+ return i - start;
+ }
+
+ private Part parseField(String field) {
+ String[] parts = COMMA_SEPARATOR_REGEX.split(field, 3);
+ String fieldName = parts[0];
+ if (parts.length == 1) {
+ if (fieldName.startsWith("#")) {
+ return new FunctionPart(fieldName.substring(1));
+ } else {
+ return new SimpleFieldPart(fieldName);
+ }
+ } else {
+ String format = parts[1];
+ PartFactory factory = PART_FACTORIES.get(format);
+ if (factory == null) {
+ throw new IllegalArgumentException(
+ "No PartFactory available under the name: " + format);
+ }
+ if (parts.length == 2) {
+ return factory.newPart(fieldName, null);
+ } else {
+ return factory.newPart(fieldName, parts[2]);
+ }
+ }
+ }
+
+ private static Function getFunction(String functionName) {
+ return FUNCTIONS.get(functionName);
+ }
+
+ /**
+ * Formats a message with the given parameters.
+ * @param params a Map of named parameters (Contents: <String, Object>)
+ * @return the formatted message
+ */
+ public String format(Map<String, Object> params) {
+ StringBuffer sb = new StringBuffer();
+ format(params, sb);
+ return sb.toString();
+ }
+
+ /**
+ * Formats a message with the given parameters.
+ * @param params a Map of named parameters (Contents: <String, Object>)
+ * @param target the target StringBuffer to write the formatted message to
+ */
+ public void format(Map<String, Object> params, StringBuffer target) {
+ rootPart.write(target, params);
+ }
+
+ /**
+ * Represents a message template part. This interface is implemented by various variants of
+ * the single curly braces pattern ({field}, {field,if,yes,no} etc.).
+ */
+ public interface Part {
+
+ /**
+ * Writes the formatted part to a string buffer.
+ * @param sb the target string buffer
+ * @param params the parameters to work with
+ */
+ void write(StringBuffer sb, Map<String, Object> params);
+
+ /**
+ * Indicates whether there is any content that is generated by this message part.
+ * @param params the parameters to work with
+ * @return true if the part has content
+ */
+ boolean isGenerated(Map<String, Object> params);
+ }
+
+ /**
+ * Implementations of this interface parse a field part and return message parts.
+ */
+ public interface PartFactory {
+
+ /**
+ * Creates a new part by parsing the values parameter to configure the part.
+ * @param fieldName the field name
+ * @param values the unparsed parameter values
+ * @return the new message part
+ */
+ Part newPart(String fieldName, String values);
+
+ /**
+ * Returns the name of the message part format.
+ * @return the name of the message part format
+ */
+ String getFormat();
+ }
+
+ /**
+ * Implementations of this interface format certain objects to strings.
+ */
+ public interface ObjectFormatter {
+
+ /**
+ * Formats an object to a string and writes the result to a string buffer.
+ * @param sb the target string buffer
+ * @param obj the object to be formatted
+ */
+ void format(StringBuffer sb, Object obj);
+
+ /**
+ * Indicates whether a given object is supported.
+ * @param obj the object
+ * @return true if the object is supported by the formatter
+ */
+ boolean supportsObject(Object obj);
+ }
+
+ /**
+ * Implementations of this interface do some computation based on the message parameters
+ * given to it. Note: at the moment, this has to be done in a local-independent way since
+ * there is no locale information.
+ */
+ public interface Function {
+
+ /**
+ * Executes the function.
+ * @param params the message parameters
+ * @return the function result
+ */
+ Object evaluate(Map<String, Object> params);
+
+ /**
+ * Returns the name of the function.
+ * @return the name of the function
+ */
+ Object getName();
+ }
+
+ private static class TextPart implements Part {
+
+ private String text;
+
+ public TextPart(String text) {
+ this.text = text;
+ }
+
+ public void write(StringBuffer sb, Map<String, Object> params) {
+ sb.append(text);
+ }
+
+ public boolean isGenerated(Map<String, Object> params) {
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ public String toString() {
+ return this.text;
+ }
+ }
+
+ private static class SimpleFieldPart implements Part {
+
+ private String fieldName;
+
+ public SimpleFieldPart(String fieldName) {
+ this.fieldName = fieldName;
+ }
+
+ public void write(StringBuffer sb, Map<String, Object> params) {
+ if (!params.containsKey(fieldName)) {
+ throw new IllegalArgumentException(
+ "Message pattern contains unsupported field name: " + fieldName);
+ }
+ Object obj = params.get(fieldName);
+ formatObject(obj, sb);
+ }
+
+ public boolean isGenerated(Map<String, Object> params) {
+ Object obj = params.get(fieldName);
+ return obj != null;
+ }
+
+ /** {@inheritDoc} */
+ public String toString() {
+ return "{" + this.fieldName + "}";
+ }
+ }
+
+ /**
+ * Formats an object to a string and writes the result to a string buffer. This method
+ * usually uses the object's <code>toString()</code> method unless there is an
+ * {@link ObjectFormatter} that supports the object. {@link ObjectFormatter}s are registered
+ * through the service provider mechanism defined by the JAR specification.
+ * @param obj the object to be formatted
+ * @param target the target string buffer
+ */
+ public static void formatObject(Object obj, StringBuffer target) {
+ if (obj instanceof String) {
+ target.append(obj);
+ } else {
+ boolean handled = false;
+ Iterator<ObjectFormatter> iter = OBJECT_FORMATTERS.iterator();
+ while (iter.hasNext()) {
+ ObjectFormatter formatter = iter.next();
+ if (formatter.supportsObject(obj)) {
+ formatter.format(target, obj);
+ handled = true;
+ break;
+ }
+ }
+ if (!handled) {
+ target.append(String.valueOf(obj));
+ }
+ }
+ }
+
+ private static class FunctionPart implements Part {
+
+ private Function function;
+
+ public FunctionPart(String functionName) {
+ this.function = getFunction(functionName);
+ if (this.function == null) {
+ throw new IllegalArgumentException("Unknown function: " + functionName);
+ }
+ }
+
+ public void write(StringBuffer sb, Map<String, Object> params) {
+ Object obj = this.function.evaluate(params);
+ formatObject(obj, sb);
+ }
+
+ public boolean isGenerated(Map<String, Object> params) {
+ Object obj = this.function.evaluate(params);
+ return obj != null;
+ }
+
+ /** {@inheritDoc} */
+ public String toString() {
+ return "{#" + this.function.getName() + "}";
+ }
+ }
+
+ private static class CompositePart implements Part {
+
+ protected List<Part> parts = new java.util.ArrayList<Part>();
+ private boolean conditional;
+ private boolean hasSections;
+
+ public CompositePart(boolean conditional) {
+ this.conditional = conditional;
+ }
+
+ private CompositePart(List<Part> parts) {
+ this.parts.addAll(parts);
+ this.conditional = true;
+ }
+
+ public void addChild(Part part) {
+ if (part == null) {
+ throw new NullPointerException("part must not be null");
+ }
+ if (hasSections) {
+ CompositePart composite = (CompositePart) this.parts.get(this.parts.size() - 1);
+ composite.addChild(part);
+ } else {
+ this.parts.add(part);
+ }
+ }
+
+ public void newSection() {
+ if (!hasSections) {
+ List<Part> p = this.parts;
+ //Dropping into a different mode...
+ this.parts = new java.util.ArrayList<Part>();
+ this.parts.add(new CompositePart(p));
+ hasSections = true;
+ }
+ this.parts.add(new CompositePart(true));
+ }
+
+ public void write(StringBuffer sb, Map<String, Object> params) {
+ if (hasSections) {
+ Iterator<Part> iter = this.parts.iterator();
+ while (iter.hasNext()) {
+ Part part = iter.next();
+ if (part.isGenerated(params)) {
+ part.write(sb, params);
+ break;
+ }
+ }
+ } else {
+ if (isGenerated(params)) {
+ Iterator<Part> iter = this.parts.iterator();
+ while (iter.hasNext()) {
+ Part part = iter.next();
+ part.write(sb, params);
+ }
+ }
+ }
+ }
+
+ public boolean isGenerated(Map<String, Object> params) {
+ if (hasSections) {
+ Iterator<Part> iter = this.parts.iterator();
+ while (iter.hasNext()) {
+ Part part = iter.next();
+ if (part.isGenerated(params)) {
+ return true;
+ }
+ }
+ return false;
+ } else {
+ if (conditional) {
+ Iterator<Part> iter = this.parts.iterator();
+ while (iter.hasNext()) {
+ Part part = iter.next();
+ if (!part.isGenerated(params)) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+ }
+
+ /** {@inheritDoc} */
+ public String toString() {
+ return this.parts.toString();
+ }
+ }
+
+
+ static String unescapeComma(String string) {
+ return string.replaceAll("\\\\,", ",");
+ }
+}