001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.wicket.model;
018
019import java.text.MessageFormat;
020import java.util.Arrays;
021import java.util.Locale;
022
023import org.apache.wicket.Application;
024import org.apache.wicket.Component;
025import org.apache.wicket.Localizer;
026import org.apache.wicket.Session;
027import org.apache.wicket.core.util.string.interpolator.PropertyVariableInterpolator;
028import org.apache.wicket.resource.loader.ComponentStringResourceLoader;
029import org.apache.wicket.util.lang.Args;
030import org.apache.wicket.util.string.Strings;
031
032
033/**
034 * This model class encapsulates the full power of localization support within the Wicket framework.
035 * It combines the flexible Wicket resource loading mechanism with property expressions, property
036 * models and standard Java <code>MessageFormat</code> substitutions. This combination should be
037 * able to solve any dynamic localization requirement that a project has.
038 * <p>
039 * The model should be created with four parameters, which are described in detail below:
040 * <ul>
041 * <li><b>resourceKey </b>- This is the most important parameter as it contains the key that should
042 * be used to obtain resources from any string resource loaders. This parameter is mandatory: a null
043 * value will throw an exception. Typically it will contain an ordinary string such as
044 * &quot;label.username&quot;. To add extra power to the key functionality the key may also contain
045 * a property expression which will be evaluated if the model parameter (see below) is not null.
046 * This allows keys to be changed dynamically as the application is running. For example, the key
047 * could be &quot;product.${product.id}&quot; which prior to rendering will call
048 * model.getObject().getProduct().getId() and substitute this value into the resource key before is
049 * is passed to the loader.
050 * <li><b>component </b>- This parameter should be a component that the string resource is relative
051 * to. In a simple application this will usually be the Page on which the component resides. For
052 * reusable components/containers that are packaged with their own string resource bundles it should
053 * be the actual component/container rather than the page. For more information on this please see
054 * {@link org.apache.wicket.resource.loader.ComponentStringResourceLoader}. The relative component
055 * may actually be {@code null} if this model is wrapped on assignment (
056 * {@link IComponentAssignedModel}) or when all resource loading is to be done from a global
057 * resource loader. However, we recommend that a relative component is still supplied even in the
058 * latter case in order to 'future proof' your application with regards to changing resource loading
059 * strategies.
060 * <li><b>model </b>- This parameter is mandatory if either the resourceKey, the found string
061 * resource (see below) or any of the substitution parameters (see below) contain property
062 * expressions. Where property expressions are present they will all be evaluated relative to this
063 * model object. If there are no property expressions present then this model parameter may be
064 * <code>null</code>
065 * <li><b>parameters </b>- This parameter allows an array of objects to be passed for
066 * substitution on the found string resource (see below) using a standard
067 * <code>java.text.MessageFormat</code> object. Each parameter may be an ordinary Object, in which
068 * case it will be processed by the standard formatting rules associated with
069 * <code>java.text.MessageFormat</code>. Alternatively, the parameter may be an instance of
070 * <code>IModel</code> in which case the <code>getObject()</code> method will be applied prior to
071 * the parameter being passed to the <code>java.text.MessageFormat</code>. This allows such features
072 * dynamic parameters that are obtained using a <code>PropertyModel</code> object or even nested
073 * string resource models. Unlike the other parameters listed above this one can not be provided 
074 * as constructor parameter but rather using method {@link #setParameters(Object...)}.
075 * </ul>
076 * As well as the supplied parameters, the found string resource can contain formatting information.
077 * It may contain property expressions in which case these are evaluated using the model object
078 * supplied when the string resource model is created. The string resource may also contain
079 * <code>java.text.MessageFormat</code> style markup for replacement of parameters. Where a string
080 * resource contains both types of formatting information then the property expression will be
081 * applied first.
082 * <p>
083 * <b>Example Bean </b>
084 * <p>
085 * In the next examples we will use the following class as bundle model:
086 * <pre>
087 * public class WeatherStation implements Serializable
088 *  {
089 *    private final String name = "Europe's main weather station";
090 *
091 *    private String currentStatus = "sunny";
092 *
093 *    private double currentTemperature = 25.7;
094 *
095 *    private String units = "\u00B0C";
096 *  }
097 * </pre>
098 * <p>
099 * <b>Example 1 </b>
100 * <p>
101 * In its simplest form, the model can be used as follows:
102 * 
103 * <pre>
104 * public class MyPage extends WebPage&lt;Void&gt;
105 * {
106 *    public MyPage(final PageParameters parameters)
107 *    {
108 *        add(new Label(&quot;username&quot;, new StringResourceModel(&quot;label.username&quot;, this)));
109 *    }
110 * }
111 * </pre>
112 * 
113 * Where the resource bundle for the page contains the entry <code>label.username=Username</code>
114 * <p>
115 * <b>Example 2 </b>
116 * <p>
117 * In this example, the resource key is selected based on the evaluation of a property expression:
118 * 
119 * <pre>
120 * public class MyPage extends WebPage&lt;Void&gt;
121 * {
122 *     public MyPage(final PageParameters parameters)
123 *     {
124 *         WeatherStation ws = new WeatherStation();
125 *         add(new Label(&quot;weatherMessage&quot;,
126 *             new StringResourceModel(&quot;weather.${currentStatus}&quot;, this, new Model&lt;WeatherStation&gt;(ws)));
127 *     }
128 * }
129 * </pre>
130 * 
131 * Which will call the WeatherStation.getCurrentStatus() method each time the string resource model
132 * is used and where the resource bundle for the page contains the entries:
133 * 
134 * <pre>
135 * weather.sunny=Don't forget sunscreen!
136 * weather.raining=You might need an umbrella
137 * weather.snowing=Got your skis?
138 * weather.overcast=Best take a coat to be safe
139 * </pre>
140 * 
141 * <p>
142 * <b>Example 3 </b>
143 * <p>
144 * In this example the found resource string contains a property expression that is substituted via
145 * the model:
146 * 
147 * <pre>
148 * public class MyPage extends WebPage&lt;Void&gt;
149 * {
150 *     public MyPage(final PageParameters parameters)
151 *     {
152 *         WeatherStation ws = new WeatherStation();
153 *         add(new Label(&quot;weatherMessage&quot;,
154 *             new StringResourceModel(&quot;weather.message&quot;, this, new Model&lt;WeatherStation&gt;(ws)));
155 *     }
156 * }
157 * </pre>
158 * 
159 * Where the resource bundle contains the entry <code>weather.message=Weather station reports that
160 * the temperature is ${currentTemperature} ${units}</code>
161 * <p>
162 * <b>Example 4 </b>
163 * <p>
164 * In this example, the use of substitution parameters is employed to format a quite complex message
165 * string. This is an example of the most complex and powerful use of the string resource model:
166 * 
167 * <pre>
168 * public class MyPage extends WebPage&lt;Void&gt;
169 * {
170 *     public MyPage(final PageParameters parameters)
171 *     {
172 *         WeatherStation ws = new WeatherStation();
173 *         IModel&lt;WeatherStation&gt; model = new Model&lt;WeatherStation&gt;(ws);
174 *         add(new Label(&quot;weatherMessage&quot;,
175 *             new StringResourceModel(&quot;weather.detail&quot;, this)
176 *                     .setParameters(
177 *                         new Date(),
178 *                         new PropertyModel&lt;?&gt;(model, &quot;currentStatus&quot;),
179 *                         new PropertyModel&lt;?&gt;(model, &quot;currentTemperature&quot;),
180 *                         new PropertyModel&lt;?&gt;(model, &quot;units&quot;)
181 *                      )
182 *         }));
183 *     }
184 * }
185 * </pre>
186 * 
187 * And where the resource bundle entry is:
188 * 
189 * <pre>
190 * weather.detail=The report for {0,date}, shows the temperature as {2,number,###.##} {3} \
191 *     and the weather to be {1}
192 * </pre>
193 * 
194 * @see ComponentStringResourceLoader for additional information especially on the component search
195 *      order
196 * 
197 * @author Chris Turner
198 */
199public class StringResourceModel extends LoadableDetachableModel<String>
200        implements
201                IComponentAssignedModel<String>
202{
203        private static final long serialVersionUID = 1L;
204
205        /** The key of message to get. */
206        private final String resourceKey;
207
208        /** The relative component used for lookups. */
209        private final Component component;
210
211        /** The wrapped model. */
212        private IModel<?> model;
213
214        /** Optional parameters. */
215        private Object[] parameters;
216
217        /** The default value of the message. */
218        private IModel<String> defaultValue;
219
220        @Override
221        public IWrapModel<String> wrapOnAssignment(Component component)
222        {
223                return new AssignmentWrapper(component);
224        }
225
226        private class AssignmentWrapper extends LoadableDetachableModel<String>
227                implements
228                        IWrapModel<String>
229        {
230                private static final long serialVersionUID = 1L;
231
232                private final Component component;
233
234                /**
235                 * Construct.
236                 * 
237                 * @param component
238                 */
239                public AssignmentWrapper(Component component)
240                {
241                        this.component = component;
242                }
243
244                @Override
245                public void detach()
246                {
247                        super.detach();
248
249                        StringResourceModel.this.detach();
250                }
251
252                @Override
253                protected void onDetach()
254                {
255                        if (StringResourceModel.this.component == null)
256                        {
257                                StringResourceModel.this.onDetach();
258                        }
259                }
260
261                @Override
262                protected String load()
263                {
264                        if (StringResourceModel.this.component != null)
265                        {
266                                // ignore assignment if component was specified explicitly
267                                return StringResourceModel.this.getObject();
268                        }
269                        else
270                        {
271                                return getString(component);
272                        }
273                }
274
275                @Override
276                public void setObject(String object)
277                {
278                        StringResourceModel.this.setObject(object);
279                }
280
281                @Override
282                public IModel<String> getWrappedModel()
283                {
284                        return StringResourceModel.this;
285                }
286        }
287
288        /**
289         * Creates a new string resource model using the supplied parameters.
290         * <p>
291         * The relative component parameter should generally be supplied, as without it resources can
292         * not be obtained from resource bundles that are held relative to a particular component or
293         * page. However, for application that use only global resources then this parameter may be
294         * null.
295         * 
296         * @param resourceKey
297         *            The resource key for this string resource
298         * @param component
299         *            The component that the resource is relative to
300         * @param model
301         *            The model to use for property substitutions
302         */
303        public StringResourceModel(final String resourceKey, final Component component, final IModel<?> model)
304        {
305                Args.notNull(resourceKey, "resource key");
306
307                this.resourceKey = resourceKey;
308                this.component = component;
309                this.model = model;
310        }
311
312        /**
313         * Creates a new string resource model using the supplied parameters.
314         * <p>
315         * The relative component parameter should generally be supplied, as without it resources can
316         * not be obtained from resource bundles that are held relative to a particular component or
317         * page. However, for application that use only global resources then this parameter may be
318         * null.
319         * 
320         * @param resourceKey
321         *            The resource key for this string resource
322         * @param component
323         *            The component that the resource is relative to
324         */
325        public StringResourceModel(final String resourceKey, final Component component)
326        {
327                this(resourceKey, component, null);
328        }
329        
330        /**
331         * Creates a new string resource model using the supplied parameter.
332         *
333         * @param resourceKey
334         *            The resource key for this string resource
335         * @param model
336         *            The model to use for property substitutions           
337         */
338        public StringResourceModel(final String resourceKey, final IModel<?> model)
339        {
340                this(resourceKey, null, model);
341        }
342        
343        /**
344         * Creates a new string resource model using the supplied parameter.
345         *
346         * @param resourceKey
347         *            The resource key for this string resource
348         */
349        public StringResourceModel(final String resourceKey)
350        {
351                this(resourceKey, null, null);
352        }
353
354        /**
355         * Sets the default value if the resource key is not found.
356         *
357         * @param defaultValue
358         *            The default value if the resource key is not found.
359         * @return this for chaining
360         */
361        public StringResourceModel setDefaultValue(final IModel<String> defaultValue)
362        {
363                this.defaultValue = defaultValue;
364                return this;
365        }
366        
367        /**
368         * Sets the default value if the resource key is not found.
369         *
370         * @param defaultValue
371         *            The default value as string if the resource key is not found.
372         * @return this for chaining
373         */
374        public StringResourceModel setDefaultValue(final String defaultValue)
375        {
376                return setDefaultValue(Model.of(defaultValue));
377        }
378
379        /**
380         * Sets the model used for property substitutions.
381         * 
382         * @param model
383         *            The model to use for property substitutions
384         * @return this for chaining
385         */
386        public StringResourceModel setModel(final IModel<?> model)
387        {
388                this.model = model;
389                return this;
390        }
391
392        /**
393         * Sets the parameters used for substitution.
394         * 
395         * @param parameters
396         *           The parameters to substitute using a Java MessageFormat object
397         * @return this for chaining
398         */
399        public StringResourceModel setParameters(Object... parameters)
400        {
401                this.parameters = parameters;
402                return this;
403        }
404
405        /**
406         * Gets the localizer that is being used by this string resource model.
407         * 
408         * @return The localizer
409         */
410        public Localizer getLocalizer()
411        {
412                return Application.get().getResourceSettings().getLocalizer();
413        }
414
415        /**
416         * Gets the string currently represented by this model. The string that is returned may vary for
417         * each call to this method depending on the values contained in the model and an the parameters
418         * that were passed when this string resource model was created.
419         * 
420         * @return The string
421         */
422        public final String getString()
423        {
424                return getString(component);
425        }
426
427        protected String getString(final Component component)
428        {
429                final Localizer localizer = getLocalizer();
430
431                String value;
432
433                // Substitute any parameters if necessary
434                Object[] parameters = getParameters();
435                if (parameters == null || parameters.length == 0)
436                {
437                        // Get the string resource, doing any property substitutions as part
438                        // of the get operation
439                        value = localizer.getString(getResourceKey(), component, model, null, null, defaultValue);
440                }
441                else
442                {
443                        // Get the string resource, doing not any property substitutions
444                        // that has to be done later after MessageFormat
445                        value = localizer.getString(getResourceKey(), component, null, null, null, defaultValue);
446                        if (value != null)
447                        {
448                                // Build the real parameters
449                                Object[] realParams = new Object[parameters.length];
450                                for (int i = 0; i < parameters.length; i++)
451                                {
452                                        if (parameters[i] instanceof IModel<?>)
453                                        {
454                                                realParams[i] = ((IModel<?>)parameters[i]).getObject();
455                                        }
456                                        else if (model != null && parameters[i] instanceof String)
457                                        {
458                                                realParams[i] = localizer.substitutePropertyExpressions(component,
459                                                        (String)parameters[i], model);
460                                        }
461                                        else
462                                        {
463                                                realParams[i] = parameters[i];
464                                        }
465                                }
466
467                                // Escape all single quotes outside {..}
468                                if (value.indexOf('\'') != -1)
469                                {
470                                        value = escapeQuotes(value);
471                                }
472
473                                if (model != null)
474                                {
475                                        // First escape all substitute properties so that message format doesn't try to
476                                        // parse that.
477                                        value = Strings.replaceAll(value, "${", "$'{'").toString();
478                                }
479
480                                // Apply the parameters
481                                final MessageFormat format = new MessageFormat(value, getLocale());
482                                value = format.format(realParams);
483
484                                if (model != null)
485                                {
486                                        // un escape the substitute properties
487                                        value = Strings.replaceAll(value, "$'{'", "${").toString();
488                                        // now substitute the properties
489                                        value = localizer.substitutePropertyExpressions(component, value, model);
490                                }
491                        }
492                }
493
494                // Return the string resource
495                return value;
496        }
497
498        /**
499         * @return The locale to use when formatting the resource value
500         */
501        protected Locale getLocale()
502        {
503                final Locale locale;
504                if (component != null)
505                {
506                        locale = component.getLocale();
507                }
508                else
509                {
510                        locale = Session.exists() ? Session.get().getLocale() : Locale.getDefault();
511                }
512                return locale;
513        }
514
515        /**
516         * Replace "'" with "''" outside of "{..}"
517         * 
518         * @param value
519         * @return escaped message format
520         */
521        private String escapeQuotes(final String value)
522        {
523                StringBuilder newValue = new StringBuilder(value.length() + 10);
524                int count = 0;
525                for (int i = 0; i < value.length(); i++)
526                {
527                        char ch = value.charAt(i);
528                        if (ch == '{')
529                        {
530                                count += 1;
531                        }
532                        else if (ch == '}')
533                        {
534                                count -= 1;
535                        }
536
537                        newValue.append(ch);
538                        if ((ch == '\'') && (count == 0))
539                        {
540                                // Escape "'"
541                                newValue.append(ch);
542                        }
543                }
544
545                return newValue.toString();
546        }
547
548        /**
549         * This method just returns debug information, so it won't return the localized string. Please
550         * use getString() for that.
551         * 
552         * @return The string for this model object
553         */
554        @Override
555        public String toString()
556        {
557                StringBuilder sb = new StringBuilder("StringResourceModel[");
558                sb.append("key:");
559                sb.append(resourceKey);
560                sb.append(",default:");
561                sb.append(defaultValue);
562                sb.append(",params:");
563                if (parameters != null)
564                {
565                        sb.append(Arrays.asList(parameters));
566                }
567                sb.append(']');
568                return sb.toString();
569        }
570
571        /**
572         * Gets the Java MessageFormat substitution parameters.
573         * 
574         * @return The substitution parameters
575         */
576        protected Object[] getParameters()
577        {
578                return parameters;
579        }
580
581        /**
582         * Gets the resource key for this string resource. If the resource key contains property
583         * expressions and the model is not null then the returned value is the actual resource key with
584         * all substitutions undertaken.
585         * 
586         * @return The (possibly substituted) resource key
587         */
588        protected final String getResourceKey()
589        {
590                if (model != null)
591                {
592                        return new PropertyVariableInterpolator(resourceKey, model.getObject()) {
593                                @Override
594                                protected String getValue(String variableName) {
595                                        String result = super.getValue(variableName);
596                                        
597                                        // WICKET-5820 interpolate null with 'null'
598                                        return result == null ? "null" : result;
599                                };
600                        }.toString();
601                }
602                else
603                {
604                        return resourceKey;
605                }
606        }
607
608        /**
609         * Gets the string that this string resource model currently represents.
610         * <p>
611         * Note: This method is used only if this model is used directly without assignment to a
612         * component, it is not called by the assignment wrapper returned from
613         * {@link #wrapOnAssignment(Component)}.
614         */
615        @Override
616        protected final String load()
617        {
618                return getString();
619        }
620
621        @Override
622        public final void detach()
623        {
624                super.detach();
625
626                // detach any model
627                if (model != null)
628                {
629                        model.detach();
630                }
631
632                // some parameters can be detachable
633                if (parameters != null)
634                {
635                        for (Object parameter : parameters)
636                        {
637                                if (parameter instanceof IDetachable)
638                                {
639                                        ((IDetachable)parameter).detach();
640                                }
641                        }
642                }
643
644                if (defaultValue != null)
645                {
646                        defaultValue.detach();
647                }
648        }
649
650        @Override
651        public void setObject(String object)
652        {
653                throw new UnsupportedOperationException();
654        }
655}