/* * 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.util.MessageFormat} but uses named parameters and supports conditional sub-groups. *

* Example: *

*

Missing field "{fieldName}"[ at location: {location}]!

* */ public class AdvancedMessageFormat { /** Regex that matches "," but not "\," (escaped comma) */ static final Pattern COMMA_SEPARATOR_REGEX = Pattern.compile("(? 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); } //no break here! Must be right before "default" section default: sb.append(ch); i++; } } 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 = (PartFactory)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 (Function)FUNCTIONS.get(functionName); } /** * Formats a message with the given parameters. * @param params a Map of named parameters (Contents: ) * @return the formatted message */ public String format(Map 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: ) * @param target the target StringBuffer to write the formatted message to */ public void format(Map 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 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 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 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 params) { sb.append(text); } public boolean isGenerated(Map 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 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 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 toString() 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 iter = OBJECT_FORMATTERS.iterator(); while (iter.hasNext()) { ObjectFormatter formatter = (ObjectFormatter)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 params) { Object obj = this.function.evaluate(params); formatObject(obj, sb); } public boolean isGenerated(Map 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 parts = new java.util.ArrayList(); private boolean conditional; private boolean hasSections = false; public CompositePart(boolean conditional) { this.conditional = conditional; } private CompositePart(List 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 p = this.parts; //Dropping into a different mode... this.parts = new java.util.ArrayList(); this.parts.add(new CompositePart(p)); hasSections = true; } this.parts.add(new CompositePart(true)); } public void write(StringBuffer sb, Map params) { if (hasSections) { Iterator iter = this.parts.iterator(); while (iter.hasNext()) { CompositePart part = (CompositePart)iter.next(); if (part.isGenerated(params)) { part.write(sb, params); break; } } } else { if (isGenerated(params)) { Iterator iter = this.parts.iterator(); while (iter.hasNext()) { Part part = (Part)iter.next(); part.write(sb, params); } } } } public boolean isGenerated(Map params) { if (hasSections) { Iterator iter = this.parts.iterator(); while (iter.hasNext()) { Part part = (Part)iter.next(); if (part.isGenerated(params)) { return true; } } return false; } else { if (conditional) { Iterator iter = this.parts.iterator(); while (iter.hasNext()) { Part 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("\\\\,", ","); } }