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() + ".null", this, ""); 300 * if (Strings.isEmpty(option)) 301 * { 302 * option = getLocalizer().getString("null", 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}