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.resource.loader;
018
019import java.util.ArrayList;
020import java.util.List;
021import java.util.Locale;
022
023import org.apache.wicket.Application;
024import org.apache.wicket.Component;
025import org.apache.wicket.MarkupContainer;
026import org.apache.wicket.Page;
027import org.apache.wicket.core.util.resource.locator.IResourceNameIterator;
028import org.apache.wicket.core.util.resource.locator.IResourceStreamLocator;
029import org.apache.wicket.markup.html.WebComponent;
030import org.apache.wicket.markup.html.WebMarkupContainer;
031import org.apache.wicket.markup.html.WebPage;
032import org.apache.wicket.markup.repeater.AbstractRepeater;
033import org.apache.wicket.resource.IPropertiesFactory;
034import org.apache.wicket.resource.Properties;
035import org.apache.wicket.util.lang.Args;
036import org.apache.wicket.util.string.Strings;
037import org.slf4j.Logger;
038import org.slf4j.LoggerFactory;
039
040
041/**
042 * This is one of Wicket's default string resource loaders.
043 * <p>
044 * The component based string resource loader attempts to find the resource from a bundle that
045 * corresponds to the supplied component object or one of its parent containers.
046 * <p>
047 * The search order for resources is built around the containers that hold the component (if it is
048 * not a page). Consider a Page that contains a Panel that contains a Label. If we pass the Label as
049 * the component then resource loading will first look for the resource against the page, then
050 * against the panel and finally against the label.
051 * <p>
052 * The above search order may seem slightly odd at first, but can be explained thus: Team A writes a
053 * new component X and packages it as a reusable Wicket component along with all required resources.
054 * Team B then creates a new container component Y that holds a instance of an X. However, Team B
055 * wishes the text to be different to that which was provided with X so rather than needing to
056 * change X, they include override values in the resources for Y. Finally, Team C makes use of
057 * component Y in a page they are writing. Initially they are happy with the text for Y so they do
058 * not include any override values in the resources for the page. However, after demonstrating to
059 * the customer, the customer requests the text for Y to be different. Team C need only provide
060 * override values against their page and thus do not need to change Y.
061 * <p>
062 * This implementation is fully aware of both locale and style values when trying to obtain the
063 * appropriate resources.
064 * <p>
065 * In addition to the above search order, each key will be pre-pended with the relative path of the
066 * current component related to the component that is being searched. E.g. assume a component
067 * hierarchy like page1.form1.input1 and your are requesting a key named 'Required'. Wicket will
068 * search the property in the following order:
069 * 
070 * <pre>
071 *        page1.properties =&gt; form1.input1.Required
072 *        page1.properties =&gt; Required
073 *        form1.properties =&gt; input1.Required
074 *        form1.properties =&gt; Required
075 *        input1.properties =&gt; Required
076 *        myApplication.properties =&gt; form1.input1.Required
077 *        myApplication.properties =&gt; Required
078 * </pre>
079 * 
080 * Note that the latter two property files are only checked if the ClassStringResourceLoader has
081 * been registered with Application as well, which is the default.
082 * <p>
083 * In addition to the above search order, each component that is being searched for a resource also
084 * includes the resources from any parent classes that it inherits from. For example, PageA extends
085 * CommonBasePage which in turn extends WebPage When a resource lookup is requested on PageA, the
086 * resource bundle for PageA is first checked. If the resource is not found in this bundle then the
087 * resource bundle for CommonBasePage is checked. This allows designers of base pages and components
088 * to define default sets of string resources and then developers implementing subclasses to either
089 * override or extend these in their own resource bundle.
090 * <p>
091 * This implementation can be subclassed to implement modified behavior. The new implementation must
092 * be registered with the Application (ResourceSettings) though.
093 * <p>
094 * You may enable log debug messages for this class to fully understand the search order.
095 * 
096 * @author Chris Turner
097 * @author Juergen Donnerstag
098 */
099public class ComponentStringResourceLoader implements IStringResourceLoader
100{
101        /** Log. */
102        private static final Logger log = LoggerFactory.getLogger(ComponentStringResourceLoader.class);
103
104        /**
105         * Create and initialize the resource loader.
106         */
107        public ComponentStringResourceLoader()
108        {
109        }
110
111        @Override
112        public String loadStringResource(Class<?> clazz, final String key, final Locale locale,
113                final String style, final String variation)
114        {
115                if (clazz == null)
116                {
117                        return null;
118                }
119
120                if (log.isDebugEnabled())
121                {
122                        log.debug("key: '" + key + "'; class: '" + clazz.getName() + "'; locale: '" + locale +
123                                "'; Style: '" + style + "'; Variation: '" + variation + '\'');
124                }
125
126                // Load the properties associated with the path
127                IPropertiesFactory propertiesFactory = getPropertiesFactory();
128                while (true)
129                {
130                        // Create the base path
131                        String path = clazz.getName().replace('.', '/');
132
133                        // Iterator over all the combinations
134                        IResourceNameIterator iter = newResourceNameIterator(path, locale, style, variation);
135                        while (iter.hasNext())
136                        {
137                                String newPath = iter.next();
138
139                                Properties props = propertiesFactory.load(clazz, newPath);
140                                if (props != null)
141                                {
142                                        // Lookup the value
143                                        String value = props.getString(key);
144                                        if (value != null)
145                                        {
146                                                return value;
147                                        }
148                                }
149                        }
150
151                        // Didn't find the key yet, continue searching if possible
152                        if (isStopResourceSearch(clazz))
153                        {
154                                break;
155                        }
156
157                        // Move to the next superclass
158                        clazz = clazz.getSuperclass();
159
160                        if (clazz == null)
161                        {
162                                // nothing more to search, done
163                                break;
164                        }
165                }
166
167                // not found
168                return null;
169        }
170
171        /**
172         * @see IResourceStreamLocator#newResourceNameIterator(String, Locale, String, String, String,
173         *      boolean)
174         * 
175         * @param path
176         * @param locale
177         * @param style
178         * @param variation
179         * @return resource name iterator
180         */
181        protected IResourceNameIterator newResourceNameIterator(final String path, final Locale locale,
182                final String style, final String variation)
183        {
184                return Application.get()
185                        .getResourceSettings()
186                        .getResourceStreamLocator()
187                        .newResourceNameIterator(path, locale, style, variation, null, false);
188        }
189
190        /**
191         * Get the properties file factory which loads the properties based on locale and style from
192         * *.properties and *.xml files
193         * 
194         * @return properties factory
195         */
196        protected IPropertiesFactory getPropertiesFactory()
197        {
198                return Application.get().getResourceSettings().getPropertiesFactory();
199        }
200
201        /**
202         * @see org.apache.wicket.resource.loader.IStringResourceLoader#loadStringResource(org.apache.wicket.Component,
203         *      java.lang.String, java.util.Locale, java.lang.String, java.lang.String)
204         */
205        @Override
206        public String loadStringResource(final Component component, final String key,
207                final Locale locale, final String style, final String variation)
208        {
209                if (component == null)
210                {
211                        return null;
212                }
213
214                if (log.isDebugEnabled())
215                {
216                        log.debug("component: '" + component.toString(false) + "'; key: '" + key + '\'');
217                }
218
219                // The return value
220                String string = null;
221
222                // The key prefix is equal to the component path relative to the
223                // current component on the top of the stack.
224                String prefix = getResourcePath(component);
225
226                // walk downwards starting with page going down to component
227                for (Component current : getComponentTrail(component))
228                {
229                        // get current component class
230                        final Class<?> clazz = current.getClass();
231
232                        // first, try the fully qualified resource name relative to the
233                        // component on the path from page down.
234                        if (Strings.isEmpty(prefix) == false)
235                        {
236                                // lookup fully qualified path
237                                string = loadStringResource(clazz, prefix + '.' + key, locale, style, variation);
238
239                                // return string if we found it
240                                if (string != null)
241                                {
242                                        return string;
243                                }
244
245                                // shorten resource key prefix when going downwards (skip for repeaters)
246                                if ((current instanceof AbstractRepeater) == false)
247                                {
248                                        prefix = Strings.afterFirst(prefix, '.');
249                                }
250                        }
251                        // If not found, than check if a property with the 'key' provided by
252                        // the user can be found.
253                        string = loadStringResource(clazz, key, locale, style, variation);
254
255                        // return string if we found it
256                        if (string != null)
257                        {
258                                return string;
259                        }
260                }
261
262                return string;
263        }
264
265        /**
266         * get path for resource lookup
267         * 
268         * @param component
269         * @return path
270         */
271        protected String getResourcePath(final Component component)
272        {
273                Component current = Args.notNull(component, "component");
274
275                final StringBuilder buffer = new StringBuilder();
276
277                while (current.getParent() != null)
278                {
279                        final boolean skip = current.getParent() instanceof AbstractRepeater;
280
281                        if (skip == false)
282                        {
283                                if (buffer.length() > 0)
284                                {
285                                        buffer.insert(0, '.');
286                                }
287                                buffer.insert(0, current.getId());
288                        }
289                        current = current.getParent();
290                }
291                return buffer.toString();
292        }
293
294        /**
295         * return the trail of components from page to specified component
296         * 
297         * @param component
298         *            The component to retrieve path for
299         * @return The list of components starting from top going down to component
300         */
301        private List<Component> getComponentTrail(Component component)
302        {
303                final List<Component> path = new ArrayList<Component>();
304
305                while (component != null)
306                {
307                        path.add(0, component);
308                        if (isStopResourceSearch(component))
309                        {
310                                break;
311                        }
312                        component = component.getParent();
313                }
314                return path;
315        }
316
317        /**
318         * Check the supplied component to see if it is one that we shouldn't bother further searches up
319         * the component hierarchy for properties.
320         * 
321         * @param component
322         *            The component to check
323         * @return Whether to stop the search
324         */
325        protected boolean isStopResourceSearch(Component component)
326        {
327                return false;
328        }
329
330        /**
331         * Check the supplied class to see if it is one that we shouldn't bother further searches up the
332         * class hierarchy for properties.
333         * 
334         * @param clazz
335         *            The class to check
336         * @return Whether to stop the search
337         */
338        protected boolean isStopResourceSearch(final Class<?> clazz)
339        {
340                if ((clazz == null) || clazz.equals(Object.class) || clazz.equals(Application.class))
341                {
342                        return true;
343                }
344
345                // Stop at all html markup base classes
346                if (clazz.equals(WebPage.class) || clazz.equals(WebMarkupContainer.class) ||
347                        clazz.equals(WebComponent.class))
348                {
349                        return true;
350                }
351
352                // Stop at all wicket base classes
353                return clazz.equals(Page.class) || clazz.equals(MarkupContainer.class) ||
354                        clazz.equals(Component.class);
355        }
356}