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