All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.fop.util.text.AdvancedMessageFormat Maven / Gradle / Ivy

There is a newer version: 2.10
Show newest version
/*
 * 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: AdvancedMessageFormat.java 1805177 2017-08-16 11:02:44Z ssteiner $ */

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.
 * 

* Example: *

*

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

*
    *
  • Curly brackets ("{}") are used for fields.
  • *
  • 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.
  • *
*/ public class AdvancedMessageFormat { /** Regex that matches "," but not "\," (escaped comma) */ static final Pattern COMMA_SEPARATOR_REGEX = Pattern.compile("(? PART_FACTORIES = new java.util.HashMap(); private static final List OBJECT_FORMATTERS = new java.util.ArrayList(); private static final Map FUNCTIONS = new java.util.HashMap(); private CompositePart rootPart; static { Iterator 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 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 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; 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 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; 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) { 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 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("\\\\,", ","); } }