| /* |
| * 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("\\\\,", ","); |
| } |
| } |