001 /*
002 // $Id: //open/util/resgen/src/org/eigenbase/resgen/ResourceDefinition.java#3 $
003 // package org.eigenbase.resgen is an i18n resource generator
004 // Copyright (C) 2005-2005 The Eigenbase Project
005 // Copyright (C) 2005-2005 Disruptive Tech
006 // Copyright (C) 2005-2005 Red Square, Inc.
007 // Portions Copyright (C) 2002-2005 Kana Software, Inc. and others.
008 // All Rights Reserved.
009 //
010 // This library is free software; you can redistribute it and/or modify it
011 // under the terms of the GNU Lesser General Public License as published by
012 // the Free Software Foundation; either version 2.1 of the License, or
013 // (at your option) any later version.
014 //
015 // This library is distributed in the hope that it will be useful, but
016 // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
017 // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
018 // License for more details.
019 //
020 // You should have received a copy of the GNU Lesser General Public License
021 // along with this library; if not, write to the Free Software Foundation, Inc.,
022 // 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
023 //
024 // jhyde, 19 September, 2002
025 */
026 package org.eigenbase.resgen;
027
028 import java.text.MessageFormat;
029 import java.text.Format;
030 import java.text.NumberFormat;
031 import java.text.DateFormat;
032 import java.util.ResourceBundle;
033 import java.util.Properties;
034 import java.lang.reflect.Method;
035 import java.lang.reflect.InvocationTargetException;
036
037 /**
038 * Definition of a resource such as a parameterized message or exception.
039 *
040 * <p>A resource is identified within a {@link ResourceBundle} by a text
041 * <em>key</em>, and has a <em>message</em> in its base locale (which is
042 * usually US-English (en_US)). It may also have a set of properties, which are
043 * represented as name-value pairs.
044 *
045 * <p>A resource definition is immutable.
046 *
047 * @author jhyde
048 * @since 19 September, 2005
049 * @version $Id: //open/util/resgen/src/org/eigenbase/resgen/ResourceDefinition.java#3 $
050 */
051 public class ResourceDefinition
052 {
053 public final String key;
054 public final String baseMessage;
055 private final String[] props;
056
057 private static final String[] EmptyStringArray = new String[0];
058
059 public static final int TYPE_UNKNOWN = -1;
060 public static final int TYPE_STRING = 0;
061 public static final int TYPE_NUMBER = 1;
062 public static final int TYPE_DATE = 2;
063 public static final int TYPE_TIME = 3;
064 private static final String[] TypeNames =
065 {"string", "number", "date", "time"};
066
067 /**
068 * Creates a resource definition with no properties.
069 *
070 * @param key Unique name for this resource definition.
071 * @param baseMessage Message for this resource definition in the base
072 * locale.
073 */
074 public ResourceDefinition(String key, String baseMessage)
075 {
076 this(key, baseMessage, null);
077 }
078
079 /**
080 * Creates a resource definition.
081 *
082 * @param key Unique name for this resource definition.
083 * @param baseMessage Message for this resource definition in the base
084 * locale.
085 * @param props Array of property name/value pairs.
086 * <code>null</code> means the same as an empty array.
087 */
088 public ResourceDefinition(String key, String baseMessage, String[] props)
089 {
090 this.key = key;
091 this.baseMessage = baseMessage;
092 if (props == null) {
093 props = EmptyStringArray;
094 }
095 assert props.length % 2 == 0 :
096 "Must have even number of property names/values";
097 this.props = props;
098 }
099
100 /**
101 * Returns this resource definition's key.
102 */
103 public String getKey()
104 {
105 return key;
106 }
107
108 /**
109 * Returns this resource definition's message in the base locale.
110 * (To find the message in another locale, you will need to load a
111 * resource bundle for that locale.)
112 */
113 public String getBaseMessage()
114 {
115 return baseMessage;
116 }
117
118 /**
119 * Returns the properties of this resource definition.
120 */
121 public Properties getProperties()
122 {
123 final Properties properties = new Properties();
124 for (int i = 0; i < props.length; i++) {
125 String prop = props[i];
126 String value = props[++i];
127 properties.setProperty(prop, value);
128 }
129 return properties;
130 }
131
132 /**
133 * Returns the types of arguments.
134 */
135 public String[] getArgTypes()
136 {
137 return getArgTypes(baseMessage, TypeNames);
138 }
139
140 /**
141 * Creates an instance of this definition with a set of parameters.
142 * This is a factory method, which may be overridden by a derived class.
143 *
144 * @param bundle Resource bundle the resource instance will belong to
145 * (This contains the locale, among other things.)
146 * @param args Arguments to populate the message's parameters.
147 * The arguments must be consistent in number and type with the results
148 * of {@link #getArgTypes}.
149 */
150 public ResourceInstance instantiate(ResourceBundle bundle, Object[] args)
151 {
152 return new Instance(bundle, this, args);
153 }
154
155 /**
156 * Parses a message for the arguments inside it, and
157 * returns an array with the types of those arguments.
158 *
159 * <p>For example, <code>getArgTypes("I bought {0,number} {2}s",
160 * new String[] {"string", "number", "date", "time"})</code>
161 * yields {"number", null, "string"}.
162 * Note the null corresponding to missing message #1.
163 *
164 * @param message Message to be parsed.
165 * @param typeNames Strings to return for types.
166 * @return Array of type names
167 */
168 protected static String[] getArgTypes(String message, String[] typeNames)
169 {
170 assert typeNames.length == 4;
171 Format[] argFormats;
172 try {
173 // We'd like to do
174 // argFormats = format.getFormatsByArgumentIndex()
175 // but it doesn't exist until JDK 1.4, and we'd like this code
176 // to work earlier.
177 Method method = MessageFormat.class.getMethod(
178 "getFormatsByArgumentIndex", (Class[]) null);
179 try {
180 MessageFormat format = new MessageFormat(message);
181 argFormats = (Format[]) method.invoke(format, (Object[]) null);
182 String[] argTypes = new String[argFormats.length];
183 for (int i = 0; i < argFormats.length; i++) {
184 int x = formatToType(argFormats[i]);
185 argTypes[i] = typeNames[x];
186 }
187 return argTypes;
188 } catch (IllegalAccessException e) {
189 throw new RuntimeException(e.toString());
190 } catch (IllegalArgumentException e) {
191 throw new RuntimeException(e.toString());
192 } catch (InvocationTargetException e) {
193 throw new RuntimeException(e.toString());
194 }
195 } catch (NoSuchMethodException e) {
196 // Fallback pre JDK 1.4
197 return getArgTypesByHand(message, typeNames);
198 } catch (SecurityException e) {
199 throw new RuntimeException(e.toString());
200 }
201 }
202
203 protected static String [] getArgTypesByHand(
204 String message,
205 String[] typeNames)
206 {
207 assert typeNames.length == 4;
208 String[] argTypes = new String[10];
209 int length = 0;
210 for (int i = 0; i < 10; i++) {
211 final int type = getArgType(i, message);
212 if (type != TYPE_UNKNOWN) {
213 length = i + 1;
214 argTypes[i] = typeNames[type];
215 }
216 }
217 // Created a truncated copy (but keep intervening nulls).
218 String[] argTypes2 = new String[length];
219 System.arraycopy(argTypes, 0, argTypes2, 0, length);
220 return argTypes2;
221 }
222
223 /**
224 * Returns the type of the <code>i</code>th argument inside a message,
225 * or {@link #TYPE_UNKNOWN} if not found.
226 *
227 * @param i Ordinal of argument
228 * @param message Message to parse
229 * @return Type code ({@link #TYPE_STRING} etc.)
230 */
231 protected static int getArgType(int i, String message) {
232 String arg = "{" + Integer.toString(i); // e.g. "{1"
233 int index = message.lastIndexOf(arg);
234 if (index < 0) {
235 return TYPE_UNKNOWN;
236 }
237 index += arg.length();
238 int end = message.length();
239 while (index < end && message.charAt(index) == ' ') {
240 index++;
241 }
242 if (index < end && message.charAt(index) == ',') {
243 index++;
244 while (index < end && message.charAt(index) == ' ') {
245 index++;
246 }
247 if (index < end) {
248 String sub = message.substring(index);
249 if (sub.startsWith("number")) {
250 return TYPE_NUMBER;
251 } else if (sub.startsWith("date")) {
252 return TYPE_DATE;
253 } else if (sub.startsWith("time")) {
254 return TYPE_TIME;
255 } else if (sub.startsWith("choice")) {
256 return TYPE_UNKNOWN;
257 }
258 }
259 }
260 return TYPE_STRING;
261 }
262
263
264 /**
265 * Converts a {@link Format} to a type code ({@link #TYPE_STRING} etc.)
266 */
267 private static int formatToType(Format format) {
268 if (format == null) {
269 return TYPE_STRING;
270 } else if (format instanceof NumberFormat) {
271 return TYPE_NUMBER;
272 } else if (format instanceof DateFormat) {
273 // might be date or time, but assume it's date
274 return TYPE_DATE;
275 } else {
276 return TYPE_STRING;
277 }
278 }
279
280 /**
281 * Default implementation of {@link ResourceInstance}.
282 */
283 private static class Instance implements ResourceInstance {
284 ResourceDefinition definition;
285 ResourceBundle bundle;
286 Object[] args;
287
288 public Instance(
289 ResourceBundle bundle,
290 ResourceDefinition definition,
291 Object[] args)
292 {
293 this.definition = definition;
294 this.bundle = bundle;
295 this.args = args;
296 }
297
298 public String toString()
299 {
300 String message = bundle.getString(definition.key);
301 MessageFormat format = new MessageFormat(message);
302 format.setLocale(bundle.getLocale());
303 String formattedMessage = format.format(args);
304 return formattedMessage;
305 }
306 }
307 }
308
309 // End ResourceDefinition.java