blob: dafc3741b3af1d5286ecaeaef2cb29ad75f8f9fc [file] [log] [blame]
/*
* 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: &lt;String, Object&gt;)
* @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: &lt;String, Object&gt;)
* @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("\\\\,", ",");
}
}