blob: 9f340b8aa469e377e19ad1b22a04f5057958b371 [file] [log] [blame]
/*
* Copyright 2011 Google Inc.
*
* Licensed 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.
*/
package com.google.gwt.i18n.server;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Helper class for parsing MessageFormat-style format strings.
*/
public class MessageFormatUtils {
/**
* Represents an argument in a template string.
*/
public static class ArgumentChunk extends TemplateChunk {
private final int argNumber;
private final String format;
private final Map<String, String> formatArgs;
private final String subFormat;
private final Map<String, String> listArgs;
public ArgumentChunk(int argNumber, Map<String, String> listArgs,
String format, Map<String, String> formatArgs, String subformat) {
this.argNumber = argNumber;
this.format = format;
this.subFormat = subformat;
this.listArgs = listArgs;
this.formatArgs = formatArgs;
}
@Override
public void accept(TemplateChunkVisitor visitor)
throws VisitorAbortException {
visitor.visit(this);
}
/**
* Get the argument number this chunk refers to.
*
* @return the argument number or -1 to refer to the right-most plural
* argument
*/
public int getArgumentNumber() {
return argNumber;
}
public String getFormat() {
return format;
}
public Map<String, String> getFormatArgs() {
return formatArgs;
}
public Map<String, String> getListArgs() {
return listArgs;
}
public String getSubFormat() {
return subFormat;
}
public boolean isList() {
return listArgs != null;
}
@Override
public String toString() {
return "Argument: #=" + argNumber + ", format=" + format + ", subformat="
+ subFormat;
}
@Override
protected String getStringValue(boolean quote) {
StringBuilder buf = new StringBuilder();
buf.append('{');
if (argNumber < 0) {
buf.append('#');
} else {
buf.append(argNumber);
}
Map<String, String> map = listArgs;
if (map != null) {
buf.append(",list");
appendArgs(buf, map, quote);
}
if (format != null || subFormat != null) {
buf.append(',');
}
if (format != null) {
buf.append(quoteMessageFormatChars(format, quote));
appendArgs(buf, formatArgs, quote);
}
if (subFormat != null) {
buf.append(',');
buf.append(subFormat);
}
buf.append('}');
return buf.toString();
}
/**
* @param buf
* @param map
* @param quote
*/
private void appendArgs(
StringBuilder buf, Map<String, String> map, boolean quote) {
char prefix = ':';
for (Map.Entry<String, String> entry : map.entrySet()) {
String key = entry.getKey();
if (quote) {
key = quoteMessageFormatChars(key);
}
buf.append(prefix).append(key);
String value = entry.getValue();
if (value != null) {
if (quote) {
value = quoteMessageFormatChars(value);
}
buf.append('=').append(value);
}
prefix = ',';
}
}
}
/**
* Default implementation of TemplateChunkVisitor -- other implementations
* should extend this if possible to avoid breakage when new TemplateChunk
* subtypes are added.
*/
public static class DefaultTemplateChunkVisitor
implements TemplateChunkVisitor {
/**
* @throws VisitorAbortException
*/
public void visit(ArgumentChunk argChunk) throws VisitorAbortException {
}
/**
* @throws VisitorAbortException
*/
public void visit(StaticArgChunk staticArgChunk)
throws VisitorAbortException {
}
/**
* @throws VisitorAbortException
*/
public void visit(StringChunk stringChunk)
throws VisitorAbortException {
}
}
/**
* Represents the style of quoting used in the source and translated messages.
*/
public enum MessageStyle {
PLAIN {
@Override
public String assemble(List<TemplateChunk> message)
throws VisitorAbortException {
final StringBuilder buf = new StringBuilder();
TemplateChunkVisitor visitor = new DefaultTemplateChunkVisitor() {
@Override
public void visit(ArgumentChunk argChunk)
throws VisitorAbortException {
throw new VisitorAbortException("Argument in Plain message");
}
@Override
public void visit(StaticArgChunk argChunk)
throws VisitorAbortException {
buf.append(argChunk.getString());
}
@Override
public void visit(StringChunk stringChunk)
throws VisitorAbortException {
buf.append(stringChunk.getString());
}
};
for (TemplateChunk chunk : message) {
chunk.accept(visitor);
}
return buf.toString();
}
@Override
public List<TemplateChunk> parse(String template) throws ParseException {
return Arrays.<TemplateChunk>asList(new StringChunk(template));
}
},
MESSAGE_FORMAT {
@Override
public String assemble(List<TemplateChunk> message) {
final StringBuilder buf = new StringBuilder();
for (TemplateChunk part : message) {
buf.append(part.getAsMessageFormatString());
}
return buf.toString();
}
@Override
public List<TemplateChunk> parse(String template) throws ParseException {
int curPos = 0;
boolean inQuote = false;
int templateLen = template.length();
ArrayList<TemplateChunk> chunks = new ArrayList<TemplateChunk>();
TemplateChunk curChunk = null;
while (curPos < templateLen) {
char ch = template.charAt(curPos++);
switch (ch) {
case '\'':
if (curPos < templateLen && template.charAt(curPos) == '\'') {
curChunk = appendString(chunks, curChunk, "'");
++curPos;
break;
}
inQuote = !inQuote;
break;
case '{':
if (inQuote) {
curChunk = appendString(chunks, curChunk, "{");
break;
}
StringBuilder argBuf = new StringBuilder();
boolean argQuote = false;
while (curPos < templateLen) {
ch = template.charAt(curPos++);
if (ch == '\'') {
if (curPos < templateLen && template.charAt(curPos) == '\'') {
argBuf.append(ch);
++curPos;
} else {
argQuote = !argQuote;
}
} else {
if (!argQuote && ch == '}') {
break;
}
argBuf.append(ch);
}
}
if (ch != '}') {
throw new ParseException(
"Invalid message format - { not start of valid argument"
+ template, curPos);
}
if (curChunk != null) {
chunks.add(curChunk);
}
String arg = argBuf.toString();
int firstComma = arg.indexOf(',');
String format = null;
if (firstComma > 0) {
format = arg.substring(firstComma + 1);
arg = arg.substring(0, firstComma);
}
if (!"#".equals(arg) && !Character.isDigit(arg.charAt(0))) {
// static argument
chunks.add(new StaticArgChunk(arg, format));
} else {
int argNumber = -1;
if (!"#".equals(arg)) {
argNumber = Integer.valueOf(arg);
}
Map<String, String> formatArgs = new HashMap<String, String>();
Map<String, String> listArgs = null;
String subFormat = null;
if (format != null) {
int comma = format.indexOf(',');
if (comma >= 0) {
subFormat = format.substring(comma + 1);
format = format.substring(0, comma);
}
format = parseFormatArgs(format, formatArgs);
if ("list".equals(format)) {
listArgs = formatArgs;
formatArgs = new HashMap<String, String>();
format = subFormat;
subFormat = null;
if (format != null) {
comma = format.indexOf(',');
if (comma >= 0) {
subFormat = format.substring(comma + 1);
format = format.substring(0, comma);
}
format = parseFormatArgs(format, formatArgs);
}
}
}
chunks.add(new ArgumentChunk(
argNumber, listArgs, format, formatArgs, subFormat));
}
curChunk = null;
break;
case '\n':
curChunk = appendString(chunks, curChunk, "\\n");
break;
case '\r':
curChunk = appendString(chunks, curChunk, "\\r");
break;
case '\\':
curChunk = appendString(chunks, curChunk, "\\\\");
break;
case '"':
curChunk = appendString(chunks, curChunk, "\\\"");
break;
default:
curChunk = appendString(chunks, curChunk, String.valueOf(ch));
break;
}
}
if (inQuote) {
throw new ParseException(
"Unterminated single quote: " + template, template.length());
}
if (curChunk != null) {
chunks.add(curChunk);
}
return chunks;
}
};
/**
* Assemble a message from its parsed chunks.
*
* @param message
* @return assembled message
* @throws VisitorAbortException
*/
public abstract String assemble(List<TemplateChunk> message)
throws VisitorAbortException;
/**
* Parse a message into chunks.
*
* @param template
* @return a list of chunks
* @throws ParseException
*/
public abstract List<TemplateChunk> parse(String template)
throws ParseException;
/**
* Parse a message template and call the supplied visitor on each parsed
* chunk.
*
* @param template
* @param visitor
* @throws ParseException
* @throws VisitorAbortException
*/
public void parseAccept(String template, TemplateChunkVisitor visitor)
throws ParseException, VisitorAbortException {
for (TemplateChunk chunk : parse(template)) {
chunk.accept(visitor);
}
}
}
/**
* Represents a static argument, which is used to remove markup from
* translator view without having to supply it at each callsite.
*/
public static class StaticArgChunk extends TemplateChunk {
private final String argName;
private final String replacement;
public StaticArgChunk(String argName, String replacement) {
this.argName = argName;
this.replacement = replacement;
}
@Override
public void accept(TemplateChunkVisitor visitor)
throws VisitorAbortException {
visitor.visit(this);
}
public String getArgName() {
return argName;
}
public String getReplacement() {
return replacement;
}
@Override
protected String getStringValue(boolean quoted) {
StringBuilder buf = new StringBuilder();
buf.append('{').append(argName);
if (replacement != null) {
buf.append(',').append(quoteMessageFormatChars(replacement, quoted));
}
buf.append('}');
return buf.toString();
}
}
/**
* Represents a literal string portion of a template string.
*/
public static class StringChunk extends TemplateChunk {
private StringBuilder buf = new StringBuilder();
public StringChunk() {
}
public StringChunk(String str) {
buf.append(str);
}
@Override
public void accept(TemplateChunkVisitor visitor)
throws VisitorAbortException {
visitor.visit(this);
}
public void append(String str) {
buf.append(str);
}
@Override
public boolean isLiteral() {
return true;
}
@Override
public String toString() {
return "StringLiteral: \"" + buf.toString() + "\"";
}
@Override
protected String getStringValue(boolean quote) {
String str = buf.toString();
return quoteMessageFormatChars(str, quote);
}
}
/**
* Represents a parsed chunk of a template.
*/
public abstract static class TemplateChunk {
/**
* Quote a string in the MessageFormat-style.
*
* @param str string to quote, must not be null
* @return quoted string
*/
protected static String quoteMessageFormatChars(String str) {
str = str.replace("'", "''");
str = str.replace("{", "'{'");
str = str.replace("}", "'}'");
return str;
}
/**
* Possibly quote a string in the MessageFormat-style.
*
* @param str
* @return quoted string
*/
protected static String quoteMessageFormatChars(String str, boolean quote) {
return quote ? quoteMessageFormatChars(str) : str;
}
public abstract void accept(TemplateChunkVisitor visitor)
throws VisitorAbortException;
/**
* Returns the string as this chunk would be represented in a MessageFormat
* template, with any required quoting such that reparsing this value would
* produce an equivalent (note, not identical) parse.
*
* Note that the default implementation may not be sufficient for all
* subclasses.
*/
public String getAsMessageFormatString() {
return getStringValue(true);
}
/**
* Returns the string as this chunk would be represented in a MessageFormat
* template, with any quoting removed. Note that this is distinct from
* toString in that the latter is intend for human consumption.
*/
public String getString() {
return getStringValue(false);
}
public boolean isLiteral() {
return false;
}
/**
* Returns the optionally quoted string value of this chunk as represented
* in a MessgeFormat string.
*
* @param quote true if the result should be quoted
* @return optionally quoted MessageFormat string
*/
protected abstract String getStringValue(boolean quote);
}
/**
* Visitor for template chunks.
*/
public interface TemplateChunkVisitor {
void visit(ArgumentChunk argChunk) throws VisitorAbortException;
void visit(StaticArgChunk staticArgChunk) throws VisitorAbortException;
void visit(StringChunk stringChunk) throws VisitorAbortException;
}
/**
* An exception thrown to indicate a visitor wishes to abort processing a
* message.
*/
public static class VisitorAbortException extends Exception {
/**
* Initialize with a required reason.
*
* @param reason human-readable explanation of why the abort occurred
*/
public VisitorAbortException(String reason) {
super(reason);
}
/**
* Initialize with a required reason.
*
* @param reason human-readable explanation of why the abort occurred
* @param cause related exception
*/
public VisitorAbortException(String reason, Throwable cause) {
super(reason, cause);
}
/**
* Initialize with a required reason.
*
* @param cause related exception
*/
public VisitorAbortException(Throwable cause) {
super(cause.getMessage(), cause);
}
}
private static TemplateChunk appendString(ArrayList<TemplateChunk> chunks,
TemplateChunk curChunk, String string) {
if (curChunk != null && !curChunk.isLiteral()) {
chunks.add(curChunk);
curChunk = null;
}
if (curChunk == null) {
curChunk = new StringChunk(string);
} else {
((StringChunk) curChunk).append(string);
}
return curChunk;
}
/**
* Parse any arguments appended to a format. The syntax is:
* format[:tag[=value][:tag[=value]]... for example: "date:tz=EST:showoffset"
*
* @param format format value to parse
* @param formatArgs map to add tag/value pairs to
* @return format portion of supplied string
*/
private static String parseFormatArgs(String format,
Map<String, String> formatArgs) {
int colon = format.indexOf(':');
if (colon >= 0) {
for (String tagValue : format.substring(colon + 1).split(":")) {
int equals = tagValue.indexOf('=');
String value = "";
if (equals >= 0) {
value = tagValue.substring(equals + 1).trim();
tagValue = tagValue.substring(0, equals);
}
formatArgs.put(tagValue.trim(), value);
}
format = format.substring(0, colon);
}
return format;
}
}