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;
018
019import java.util.Iterator;
020import java.util.List;
021import java.util.Locale;
022import java.util.Map;
023import java.util.MissingResourceException;
024import java.util.concurrent.ConcurrentHashMap;
025import java.util.concurrent.ConcurrentMap;
026import java.util.concurrent.atomic.AtomicLong;
027
028import org.apache.wicket.core.util.string.interpolator.ConvertingPropertyVariableInterpolator;
029import org.apache.wicket.markup.repeater.AbstractRepeater;
030import org.apache.wicket.model.IModel;
031import org.apache.wicket.model.Model;
032import org.apache.wicket.resource.loader.IStringResourceLoader;
033import org.apache.wicket.settings.ResourceSettings;
034import org.apache.wicket.util.lang.Generics;
035import org.apache.wicket.util.string.AppendingStringBuffer;
036import org.slf4j.Logger;
037import org.slf4j.LoggerFactory;
038
039
040/**
041 * A utility class that encapsulates all of the localization related functionality in a way that it
042 * can be accessed by all areas of the framework in a consistent way. A singleton instance of this
043 * class is available via the <code>Application</code> object.
044 * <p>
045 * You may register additional IStringResourceLoader to extend or replace Wickets default search
046 * strategy for the properties. E.g. string resource loaders which load the properties from a
047 * database. There should be hardly any need to extend Localizer.
048 * 
049 * @see org.apache.wicket.settings.ResourceSettings#getLocalizer()
050 * @see org.apache.wicket.resource.loader.IStringResourceLoader
051 * @see org.apache.wicket.settings.ResourceSettings#getStringResourceLoaders()
052 * 
053 * @author Chris Turner
054 * @author Juergen Donnerstag
055 */
056public class Localizer
057{
058        private static final Logger log = LoggerFactory.getLogger(Localizer.class);
059
060        /** ConcurrentHashMap does not allow null values */
061        private static final String NULL_VALUE = "<null-value>";
062
063        /** Cache properties */
064        private Map<String, String> cache = newCache();
065
066        /** Database that maps class names to an integer id. */
067        private final ClassMetaDatabase metaDatabase = new ClassMetaDatabase();
068
069        /**
070         * @return Same as Application.get().getResourceSettings().getLocalizer()
071         */
072        public static Localizer get()
073        {
074                return Application.get().getResourceSettings().getLocalizer();
075        }
076
077        /**
078         * Create the utils instance class backed by the configuration information contained within the
079         * supplied application object.
080         */
081        public Localizer()
082        {
083        }
084
085        /**
086         * Clear all cache entries by instantiating a new cache object
087         * 
088         * @see #newCache()
089         */
090        public final void clearCache()
091        {
092                if (cache != null)
093                {
094                        cache = newCache();
095                }
096        }
097
098        /**
099         * @see #getString(String, Component, IModel, Locale, String, String)
100         * 
101         * @param key
102         *            The key to obtain the resource for
103         * @param component
104         *            The component to get the resource for
105         * @return The string resource
106         * @throws MissingResourceException
107         *             If resource not found and configuration dictates that exception should be thrown
108         */
109        public String getString(final String key, final Component component)
110                throws MissingResourceException
111        {
112                return getString(key, component, null, null, null, (String)null);
113        }
114
115        /**
116         * @see #getString(String, Component, IModel, Locale, String, String)
117         * 
118         * @param key
119         *            The key to obtain the resource for
120         * @param component
121         *            The component to get the resource for
122         * @param model
123         *            The model to use for property substitutions in the strings (optional)
124         * @return The string resource
125         * @throws MissingResourceException
126         *             If resource not found and configuration dictates that exception should be thrown
127         */
128        public String getString(final String key, final Component component, final IModel<?> model)
129                throws MissingResourceException
130        {
131                return getString(key, component, model, null, null, (String)null);
132        }
133
134        /**
135         * @see #getString(String, Component, IModel, Locale, String, String)
136         * 
137         * @param key
138         *            The key to obtain the resource for
139         * @param component
140         *            The component to get the resource for
141         * @param defaultValue
142         *            The default value (optional)
143         * @return The string resource
144         * @throws MissingResourceException
145         *             If resource not found and configuration dictates that exception should be thrown
146         */
147        public String getString(final String key, final Component component, final String defaultValue)
148                throws MissingResourceException
149        {
150                return getString(key, component, null, null, null, defaultValue);
151        }
152
153        /**
154         * @see #getString(String, Component, IModel, Locale, String, String)
155         * 
156         * @param key
157         *            The key to obtain the resource for
158         * @param component
159         *            The component to get the resource for
160         * @param model
161         *            The model to use for property substitutions in the strings (optional)
162         * @param defaultValue
163         *            The default value (optional)
164         * @return The string resource
165         * @throws MissingResourceException
166         *             If resource not found and configuration dictates that exception should be thrown
167         */
168        public String getString(final String key, final Component component, final IModel<?> model,
169                final String defaultValue) throws MissingResourceException
170        {
171                return getString(key, component, model, null, null, defaultValue);
172        }
173
174        /**
175         * Get the localized string using all of the supplied parameters. This method is left public to
176         * allow developers full control over string resource loading. However, it is recommended that
177         * one of the other convenience methods in the class are used as they handle all of the work
178         * related to obtaining the current user locale and style information.
179         * 
180         * @param key
181         *            The key to obtain the resource for
182         * @param component
183         *            The component to get the resource for (optional)
184         * @param model
185         *            The model to use for substitutions in the strings (optional)
186         * @param locale
187         *            If != null, it'll supersede the component's locale
188         * @param style
189         *            If != null, it'll supersede the component's style
190         * @param defaultValue
191         *            The default value (optional)
192         * @return The string resource
193         * @throws MissingResourceException
194         *             If resource not found and configuration dictates that exception should be thrown
195         */
196        public String getString(final String key, final Component component, final IModel<?> model,
197                final Locale locale, final String style, final String defaultValue)
198                throws MissingResourceException
199        {
200                IModel<String> defaultValueModel = defaultValue != null ? Model.of(defaultValue) : null;
201                return getString(key, component, model, locale, style, defaultValueModel);
202        }
203
204        /**
205         * Get the localized string using all of the supplied parameters. This method is left public to
206         * allow developers full control over string resource loading. However, it is recommended that
207         * one of the other convenience methods in the class are used as they handle all of the work
208         * related to obtaining the current user locale and style information.
209         *
210         * @param key
211         *            The key to obtain the resource for
212         * @param component
213         *            The component to get the resource for (optional)
214         * @param model
215         *            The model to use for substitutions in the strings (optional)
216         * @param locale
217         *            If != null, it'll supersede the component's locale
218         * @param style
219         *            If != null, it'll supersede the component's style
220         * @param defaultValue
221         *            The default value (optional)
222         * @return The string resource
223         * @throws MissingResourceException
224         *             If resource not found and configuration dictates that exception should be thrown
225         */
226        public String getString(final String key, final Component component, final IModel<?> model,
227                                final Locale locale, final String style, final IModel<String> defaultValue)
228                        throws MissingResourceException
229        {
230                final ResourceSettings resourceSettings = Application.get().getResourceSettings();
231
232                String value = getStringIgnoreSettings(key, component, model, locale, style, null);
233
234                // If a property value has been found, or a default value was given,
235                // then replace the placeholder and we are done
236                if (value != null)
237                {
238                        return value;
239                }
240                else if (defaultValue != null && resourceSettings.getUseDefaultOnMissingResource())
241                {
242                        // Resource not found, so handle missing resources based on
243                        // application configuration and try the default value
244                        value = defaultValue.getObject();
245
246                        if (value != null)
247                        {
248                                // If a property value has been found, or a default value was given,
249                                // then replace the placeholder and we are done
250                                return substitutePropertyExpressions(component, value, model);
251                        }
252                }
253
254                if (resourceSettings.getThrowExceptionOnMissingResource())
255                {
256                        AppendingStringBuffer message = new AppendingStringBuffer("Unable to find property: '");
257                        message.append(key);
258                        message.append('\'');
259
260                        if (component != null)
261                        {
262                                message.append(" for component: ");
263                                message.append(component.getPageRelativePath());
264                                message.append(" [class=").append(component.getClass().getName()).append(']');
265                        }
266                        message.append(". Locale: ").append(locale).append(", style: ").append(style);
267
268                        throw new MissingResourceException(message.toString(), (component != null
269                                ? component.getClass().getName() : ""), key);
270                }
271
272                return "[Warning: Property for '" + key + "' not found]";
273        }
274
275        /**
276         * @see #getStringIgnoreSettings(String, Component, IModel, Locale, String, String)
277         * 
278         * @param key
279         *            The key to obtain the resource for
280         * @param component
281         *            The component to get the resource for (optional)
282         * @param model
283         *            The model to use for substitutions in the strings (optional)
284         * @param defaultValue
285         *            The default value (optional)
286         * @return The string resource
287         */
288        public String getStringIgnoreSettings(final String key, final Component component,
289                final IModel<?> model, final String defaultValue)
290        {
291                return getStringIgnoreSettings(key, component, model, null, null, defaultValue);
292        }
293
294        /**
295         * This is similar to {@link #getString(String, Component, IModel, String)} except that the
296         * resource settings are ignored. This allows to to code something like
297         * 
298         * <pre>
299         * String option = getLocalizer().getStringIgnoreSettings(getId() + &quot;.null&quot;, this, &quot;&quot;);
300         * if (Strings.isEmpty(option))
301         * {
302         *      option = getLocalizer().getString(&quot;null&quot;, this, CHOOSE_ONE);
303         * }
304         * </pre>
305         * 
306         * @param key
307         *            The key to obtain the resource for
308         * @param component
309         *            The component to get the resource for (optional)
310         * @param model
311         *            The model to use for substitutions in the strings (optional)
312         * @param locale
313         *            If != null, it'll supersede the component's locale
314         * @param style
315         *            If != null, it'll supersede the component's style
316         * @param defaultValue
317         *            The default value (optional)
318         * @return The string resource
319         */
320        public String getStringIgnoreSettings(final String key, final Component component,
321                final IModel<?> model, Locale locale, String style, final String defaultValue)
322        {
323                boolean addedToPage = false;
324                if (component != null)
325                {
326                        if ((component instanceof Page) || (null != component.findParent(Page.class)))
327                        {
328                                addedToPage = true;
329                        }
330
331                        if (!addedToPage && log.isWarnEnabled())
332                        {
333                                log.warn(
334                                        "Tried to retrieve a localized string for a component that has not yet been added to the page. "
335                                                + "This can sometimes lead to an invalid or no localized resource returned. "
336                                                + "Make sure you are not calling Component#getString() inside your Component's constructor. "
337                                                + "Offending component: {} - Resource key: {}", component, key);
338                        }
339                }
340
341                String cacheKey = null;
342                String value;
343
344                // Make sure locale, style and variation have the right values
345                String variation = (component != null ? component.getVariation() : null);
346
347                if ((locale == null) && (component != null))
348                {
349                        locale = component.getLocale();
350                }
351                if (locale == null)
352                {
353                        locale = Session.exists() ? Session.get().getLocale() : Locale.getDefault();
354                }
355
356                if ((style == null) && (component != null))
357                {
358                        style = component.getStyle();
359                }
360                if (style == null)
361                {
362                        style = Session.exists() ? Session.get().getStyle() : null;
363                }
364
365                // If this component is not yet added to page we do not want to check
366                // cache as we can generate an invalid cache key
367                if ((cache != null) && ((component == null) || addedToPage))
368                {
369                        cacheKey = getCacheKey(key, component, locale, style, variation);
370                }
371
372                // Value not found are cached as well (value = null)
373                if ((cacheKey != null) && cache.containsKey(cacheKey))
374                {
375                        value = getFromCache(cacheKey);
376                        if (log.isDebugEnabled())
377                        {
378                                log.debug("Property found in cache: '" + key + "'; Component: '" +
379                                        (component != null ? component.toString(false) : null) + "'; value: '" + value +
380                                        '\'');
381                        }
382                }
383                else
384                {
385                        if (log.isDebugEnabled())
386                        {
387                                log.debug("Locate property: key: '" + key + "'; Component: '" +
388                                        (component != null ? component.toString(false) : null) + '\'');
389                        }
390
391                        // Iterate over all registered string resource loaders until the property has been found
392                        Iterator<IStringResourceLoader> iter = getStringResourceLoaders().iterator();
393                        value = null;
394                        while (iter.hasNext() && (value == null))
395                        {
396                                IStringResourceLoader loader = iter.next();
397                                value = loader.loadStringResource(component, key, locale, style, variation);
398                        }
399
400                        // Cache the result incl null if not found
401                        if (cacheKey != null)
402                        {
403                                putIntoCache(cacheKey, value);
404                        }
405
406                        if ((value == null) && log.isDebugEnabled())
407                        {
408                                log.debug("Property not found; key: '" + key + "'; Component: '" +
409                                        (component != null ? component.toString(false) : null) + '\'');
410                        }
411                }
412
413                if (value == null)
414                {
415                        value = defaultValue;
416                }
417
418                // If a property value has been found, or a default value was given,
419                // then replace the placeholder and we are done
420                if (value != null)
421                {
422                        return substitutePropertyExpressions(component, value, model);
423                }
424
425                return null;
426        }
427
428        /**
429         * In case you want to provide your own list of string resource loaders
430         * 
431         * @return List of string resource loaders
432         */
433        protected List<IStringResourceLoader> getStringResourceLoaders()
434        {
435                return Application.get().getResourceSettings().getStringResourceLoaders();
436        }
437
438        /**
439         * Put the value into the cache and associate it with the cache key
440         * 
441         * @param cacheKey
442         * @param string
443         */
444        protected void putIntoCache(final String cacheKey, final String string)
445        {
446                if (cache == null)
447                {
448                        return;
449                }
450
451                // ConcurrentHashMap does not allow null values
452                if (string == null)
453                {
454                        cache.put(cacheKey, NULL_VALUE);
455                }
456                else
457                {
458                        cache.put(cacheKey, string);
459                }
460        }
461
462        /**
463         * Get the value associated with the key from the cache.
464         * 
465         * @param cacheKey
466         * @return The value of the key
467         */
468        protected String getFromCache(final String cacheKey)
469        {
470                if (cache == null)
471                {
472                        return null;
473                }
474
475                final String value = cache.get(cacheKey);
476
477                // ConcurrentHashMap does not allow null values
478                if (NULL_VALUE == value)
479                {
480                        return null;
481                }
482                return value;
483        }
484
485        /**
486         * Gets the cache key
487         * 
488         * @param key
489         * @param component
490         * @param locale
491         *            Guaranteed to be != null
492         * @param style
493         * @param variation
494         * @return The value of the key
495         */
496        protected String getCacheKey(final String key, final Component component, final Locale locale,
497                final String style, final String variation)
498        {
499                if (component != null)
500                {
501                        StringBuilder buffer = new StringBuilder(200);
502                        buffer.append(key);
503
504                        Component cursor = component;
505
506                        while (cursor != null)
507                        {
508                                buffer.append('-').append(metaDatabase.id(cursor.getClass()));
509
510                                if (cursor instanceof Page)
511                                {
512                                        break;
513                                }
514
515                                /*
516                                 * only append component id if component is not a loop item because (a) these ids
517                                 * are irrelevant when generating resource cache keys (b) they cause a lot of
518                                 * redundant keys to be generated
519                                 * 
520                                 * also if the cursor component is an auto component we append a constant string
521                                 * instead of component's id because auto components have a newly generated id on
522                                 * every render.
523                                 */
524                                final Component parent = cursor.getParent();
525                                final boolean skip = parent instanceof AbstractRepeater;
526
527                                if (skip == false)
528                                {
529                                        String cursorKey = cursor.isAuto() ? "wicket-auto" : cursor.getId();
530                                        buffer.append(':').append(cursorKey);
531                                }
532
533                                cursor = parent;
534                        }
535
536                        buffer.append('-').append(locale);
537                        buffer.append('-').append(style);
538                        buffer.append('-').append(variation);
539
540                        return buffer.toString();
541                }
542                else
543                {
544                        // locale is guaranteed to be != null
545                        return key + '-' + locale.toString() + '-' + style;
546                }
547        }
548
549/**
550         * Helper method to handle property variable substitution in strings.
551         * 
552         * @param component
553         *            The component requesting a model value or {@code null}
554         * @param string
555         *            The string to substitute into
556         * @param model
557         *            The model
558         * @return The resulting string
559         */
560        public String substitutePropertyExpressions(final Component component, final String string,
561                final IModel<?> model)
562        {
563                if ((string != null) && (model != null))
564                {
565                        final IConverterLocator locator;
566                        final Locale locale;
567                        if (component == null)
568                        {
569                                locator = Application.get().getConverterLocator();
570
571                                if (Session.exists())
572                                {
573                                        locale = Session.get().getLocale();
574                                }
575                                else
576                                {
577                                        locale = Locale.getDefault();
578                                }
579                        }
580                        else
581                        {
582                                locator = component;
583                                locale = component.getLocale();
584                        }
585
586                        return new ConvertingPropertyVariableInterpolator(string, model.getObject(), locator,
587                                locale).toString();
588                }
589                return string;
590        }
591
592        /**
593         * By default the cache is enabled. Disabling the cache will disable it and clear the cache.
594         * This can be handy for example in development mode.
595         * 
596         * @param value
597         */
598        public final void setEnableCache(boolean value)
599        {
600                if (value == false)
601                {
602                        cache = null;
603                }
604                else if (cache == null)
605                {
606                        cache = newCache();
607                }
608        }
609
610        /**
611         * Create a new cache, override this method if you want a different map to store the cache keys,
612         * for example a map that hold only the last X number of elements..
613         * 
614         * By default it uses the {@link ConcurrentHashMap}
615         * 
616         * @return cache
617         */
618        protected Map<String, String> newCache()
619        {
620                return new ConcurrentHashMap<>();
621        }
622
623        /**
624         * Database that maps class names to an integer id. This is used to make localizer keys shorter
625         * because sometimes they can contain a large number of class names.
626         * 
627         * @author igor.vaynberg
628         */
629        private static class ClassMetaDatabase
630        {
631                private final ConcurrentMap<String, Long> nameToId = Generics.newConcurrentHashMap();
632                private final AtomicLong nameCounter = new AtomicLong();
633
634                /**
635                 * Returns a unique id that represents this class' name. This can be used for compressing
636                 * class names. Notice this id should not be used across cluster nodes.
637                 * 
638                 * @param clazz
639                 * @return long id of class name
640                 */
641                public long id(Class<?> clazz)
642                {
643                        final String name = clazz.getName();
644                        Long id = nameToId.get(name);
645                        if (id == null)
646                        {
647                                id = nameCounter.incrementAndGet();
648                                Long previousId = nameToId.putIfAbsent(name, id);
649                                if (previousId != null)
650                                {
651                                        id = previousId;
652                                }
653                        }
654                        return id;
655                }
656        }
657}