/* * 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; for (ObjectFormatter formatter : OBJECT_FORMATTERS) { 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) { for (Part part : this.parts) { if (part.isGenerated(params)) { part.write(sb, params); break; } } } else { if (isGenerated(params)) { for (Part part : this.parts) { part.write(sb, params); } } } } public boolean isGenerated(Map<String, Object> params) { if (hasSections) { for (Part part : this.parts) { if (part.isGenerated(params)) { return true; } } return false; } else { if (conditional) { for (Part part : this.parts) { if (!part.isGenerated(params)) { return false; } } } return true; } } /** {@inheritDoc} */ public String toString() { return this.parts.toString(); } } static String unescapeComma(String string) { return string.replaceAll("\\\\,", ","); } }