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 * "label.username". 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 "product.${product.id}" 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<Void> 105 * { 106 * public MyPage(final PageParameters parameters) 107 * { 108 * add(new Label("username", new StringResourceModel("label.username", 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<Void> 121 * { 122 * public MyPage(final PageParameters parameters) 123 * { 124 * WeatherStation ws = new WeatherStation(); 125 * add(new Label("weatherMessage", 126 * new StringResourceModel("weather.${currentStatus}", this, new Model<WeatherStation>(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<Void> 149 * { 150 * public MyPage(final PageParameters parameters) 151 * { 152 * WeatherStation ws = new WeatherStation(); 153 * add(new Label("weatherMessage", 154 * new StringResourceModel("weather.message", this, new Model<WeatherStation>(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<Void> 169 * { 170 * public MyPage(final PageParameters parameters) 171 * { 172 * WeatherStation ws = new WeatherStation(); 173 * IModel<WeatherStation> model = new Model<WeatherStation>(ws); 174 * add(new Label("weatherMessage", 175 * new StringResourceModel("weather.detail", this) 176 * .setParameters( 177 * new Date(), 178 * new PropertyModel<?>(model, "currentStatus"), 179 * new PropertyModel<?>(model, "currentTemperature"), 180 * new PropertyModel<?>(model, "units") 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}