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