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 => form1.input1.Required 072 * page1.properties => Required 073 * form1.properties => input1.Required 074 * form1.properties => Required 075 * input1.properties => Required 076 * myApplication.properties => form1.input1.Required 077 * myApplication.properties => 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}