1/******************************************************************************* 2 * Copyright 2011 See AUTHORS file. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 ******************************************************************************/ 16 17package com.badlogic.gdx.utils; 18 19import java.text.MessageFormat; 20import java.util.Locale; 21 22/** {@code TextFormatter} is used by {@link I18NBundle} to perform argument replacement. 23 * 24 * @author davebaol */ 25class TextFormatter { 26 27 private MessageFormat messageFormat; 28 private StringBuilder buffer; 29 30 public TextFormatter (Locale locale, boolean useMessageFormat) { 31 buffer = new StringBuilder(); 32 if (useMessageFormat) messageFormat = new MessageFormat("", locale); 33 } 34 35 /** Formats the given {@code pattern} replacing its placeholders with the actual arguments specified by {@code args}. 36 * <p> 37 * If this {@code TextFormatter} has been instantiated with {@link #TextFormatter(Locale, boolean) TextFormatter(locale, true)} 38 * {@link MessageFormat} is used to process the pattern, meaning that the actual arguments are properly localized with the 39 * locale of this {@code TextFormatter}. 40 * <p> 41 * On the contrary, if this {@code TextFormatter} has been instantiated with {@link #TextFormatter(Locale, boolean) 42 * TextFormatter(locale, false)} pattern's placeholders are expected to be in the simplified form {0}, {1}, {2} and so on and 43 * they will be replaced with the corresponding object from {@code args} converted to a string with {@code toString()}, so 44 * without taking into account the locale. 45 * <p> 46 * In both cases, there's only one simple escaping rule, i.e. a left curly bracket must be doubled if you want it to be part of 47 * your string. 48 * <p> 49 * It's worth noting that the rules for using single quotes within {@link MessageFormat} patterns have shown to be somewhat 50 * confusing. In particular, it isn't always obvious to localizers whether single quotes need to be doubled or not. For this 51 * very reason we decided to offer the simpler escaping rule above without limiting the expressive power of message format 52 * patterns. So, if you're used to MessageFormat's syntax, remember that with {@code TextFormatter} single quotes never need to 53 * be escaped! 54 * 55 * @param pattern the pattern 56 * @param args the arguments 57 * @return the formatted pattern 58 * @exception IllegalArgumentException if the pattern is invalid */ 59 public String format (String pattern, Object... args) { 60 if (messageFormat != null) { 61 messageFormat.applyPattern(replaceEscapeChars(pattern)); 62 return messageFormat.format(args); 63 } 64 return simpleFormat(pattern, args); 65 } 66 67 // This code is needed because a simple replacement like 68 // pattern.replace("'", "''").replace("{{", "'{'"); 69 // can't properly manage some special cases. 70 // For example, the expected output for {{{{ is {{ but you get {'{ instead. 71 // Also this code is optimized since a new string is returned only if something has been replaced. 72 private String replaceEscapeChars (String pattern) { 73 buffer.setLength(0); 74 boolean changed = false; 75 int len = pattern.length(); 76 for (int i = 0; i < len; i++) { 77 char ch = pattern.charAt(i); 78 if (ch == '\'') { 79 changed = true; 80 buffer.append("''"); 81 } else if (ch == '{') { 82 int j = i + 1; 83 while (j < len && pattern.charAt(j) == '{') 84 j++; 85 int escaped = (j - i) / 2; 86 if (escaped > 0) { 87 changed = true; 88 buffer.append('\''); 89 do { 90 buffer.append('{'); 91 } while ((--escaped) > 0); 92 buffer.append('\''); 93 } 94 if ((j - i) % 2 != 0) buffer.append('{'); 95 i = j - 1; 96 } else { 97 buffer.append(ch); 98 } 99 } 100 return changed ? buffer.toString() : pattern; 101 } 102 103 /** Formats the given {@code pattern} replacing any placeholder of the form {0}, {1}, {2} and so on with the corresponding 104 * object from {@code args} converted to a string with {@code toString()}, so without taking into account the locale. 105 * <p> 106 * This method only implements a small subset of the grammar supported by {@link java.text.MessageFormat}. Especially, 107 * placeholder are only made up of an index; neither the type nor the style are supported. 108 * <p> 109 * If nothing has been replaced this implementation returns the pattern itself. 110 * 111 * @param pattern the pattern 112 * @param args the arguments 113 * @return the formatted pattern 114 * @exception IllegalArgumentException if the pattern is invalid */ 115 private String simpleFormat (String pattern, Object... args) { 116 buffer.setLength(0); 117 boolean changed = false; 118 int placeholder = -1; 119 int patternLength = pattern.length(); 120 for (int i = 0; i < patternLength; ++i) { 121 char ch = pattern.charAt(i); 122 if (placeholder < 0) { // processing constant part 123 if (ch == '{') { 124 changed = true; 125 if (i + 1 < patternLength && pattern.charAt(i + 1) == '{') { 126 buffer.append(ch); // handle escaped '{' 127 ++i; 128 } else { 129 placeholder = 0; // switch to placeholder part 130 } 131 } else { 132 buffer.append(ch); 133 } 134 } else { // processing placeholder part 135 if (ch == '}') { 136 if (placeholder >= args.length) 137 throw new IllegalArgumentException("Argument index out of bounds: " + placeholder); 138 if (pattern.charAt(i - 1) == '{') 139 throw new IllegalArgumentException("Missing argument index after a left curly brace"); 140 if (args[placeholder] == null) 141 buffer.append("null"); // append null argument 142 else 143 buffer.append(args[placeholder].toString()); // append actual argument 144 placeholder = -1; // switch to constant part 145 } else { 146 if (ch < '0' || ch > '9') 147 throw new IllegalArgumentException("Unexpected '" + ch + "' while parsing argument index"); 148 placeholder = placeholder * 10 + (ch - '0'); 149 } 150 } 151 } 152 if (placeholder >= 0) throw new IllegalArgumentException("Unmatched braces in the pattern."); 153 154 return changed ? buffer.toString() : pattern; 155 } 156} 157