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    */
025    package org.eigenbase.resgen;
026    
027    import java.text.MessageFormat;
028    import java.text.Format;
029    import java.text.NumberFormat;
030    import java.text.DateFormat;
031    import java.util.ResourceBundle;
032    import java.util.Properties;
033    import java.lang.reflect.Method;
034    import 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     */
050    public 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