001/* 002 * Copyright 2012 Anyware Services 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.ametys.runtime.i18n; 017 018import java.beans.XMLDecoder; 019import java.beans.XMLEncoder; 020import java.io.ByteArrayInputStream; 021import java.io.ByteArrayOutputStream; 022import java.io.UnsupportedEncodingException; 023import java.util.HashMap; 024import java.util.List; 025import java.util.Map; 026import java.util.Optional; 027 028import org.apache.avalon.framework.configuration.Configuration; 029import org.apache.avalon.framework.configuration.ConfigurationException; 030import org.apache.cocoon.transformation.I18nTransformer; 031import org.apache.cocoon.xml.AttributesImpl; 032import org.apache.commons.lang3.builder.EqualsBuilder; 033import org.apache.commons.lang3.builder.HashCodeBuilder; 034import org.xml.sax.ContentHandler; 035import org.xml.sax.SAXException; 036 037/** 038 * This class wraps a text that <i>may</i> be internationalized. 039 */ 040public final class I18nizableText implements I18nizable, I18nizableTextParameter 041{ 042 private final boolean _i18n; 043 private String _directLabel; 044 private String _key; 045 private String _catalogue; 046 private List<String> _parameters; 047 private Map<String, I18nizableTextParameter> _parameterMap; 048 private String _catalogueLocation; 049 private String _catalogueBundleName; 050 051 /** 052 * Create a simple international text 053 * @param label The text. Cannot be null. 054 */ 055 public I18nizableText(String label) 056 { 057 _i18n = false; 058 _directLabel = label; 059 } 060 061 /** 062 * Create an i18nized text 063 * @param catalogue the catalogue where the key is defined. Can be null. Can be overloaded by the catalogue in the key. 064 * @param key the key of the text. Cannot be null. May include the catalogue using the character ':' as separator. CATALOG:KEY. 065 */ 066 public I18nizableText(String catalogue, String key) 067 { 068 this(catalogue, key, (List<String>) null); 069 } 070 071 /** 072 * Create an i18nized text with ordered parameters. 073 * @param catalogue the catalogue where the key is defined. Can be null. Can be overloaded by the catalogue in the key. 074 * @param key the key of the text. Cannot be null. May include the catalogue using the character ':' as separator. CATALOG:KEY. 075 * @param parameters the parameters of the key if any. Can be null. 076 */ 077 public I18nizableText(String catalogue, String key, List<String> parameters) 078 { 079 _i18n = true; 080 081 String i18nKey = key.substring(key.indexOf(":") + 1); 082 String i18nCatalogue = i18nKey.length() == key.length() ? catalogue : key.substring(0, key.length() - i18nKey.length() - 1); 083 084 _catalogue = i18nCatalogue; 085 _key = i18nKey; 086 _parameters = parameters; 087 _parameterMap = null; 088 } 089 090 /** 091 * Create an i18nized text with named parameters. 092 * @param catalogue the catalogue where the key is defined. Can be null. Can be overloaded by the catalogue in the key. 093 * @param key the key of the text. Cannot be null. May include the catalogue using the character ':' as separator. CATALOG:KEY. 094 * @param parameters the named parameters of the message, as a Map of name -> value. Value can itself be an i18n key but must not have parameters. 095 */ 096 public I18nizableText(String catalogue, String key, Map<String, I18nizableTextParameter> parameters) 097 { 098 _i18n = true; 099 100 String i18nKey = key.substring(key.indexOf(":") + 1); 101 String i18nCatalogue = i18nKey.length() == key.length() ? catalogue : key.substring(0, key.length() - i18nKey.length() - 1); 102 103 _catalogue = i18nCatalogue; 104 _key = i18nKey; 105 _parameterMap = parameters; 106 _parameters = null; 107 } 108 109 /** 110 * Create an i18nized text. 111 * Use this constructor only when the catalogue is an external catalogue, not managed by Ametys application 112 * @param catalogueLocation the file location URI of the catalogue where the key is defined. 113 * @param catalogueFilename the catalogue bundle name such as 'messages' 114 * @param key the key of the text. Cannot be null. 115 */ 116 public I18nizableText(String catalogueLocation, String catalogueFilename, String key) 117 { 118 this(catalogueLocation, catalogueFilename, key, (List<String>) null); 119 } 120 121 /** 122 * Create an i18nized text with ordered parameters. 123 * Use this constructor only when the catalogue is an external catalogue, not managed by Ametys application 124 * @param catalogueLocation the file location URI of the catalogue where the key is defined. 125 * @param catalogueFilename the catalogue bundle name such as 'messages' 126 * @param key the key of the text. Cannot be null. 127 * @param parameters the parameters of the key if any. Can be null. 128 */ 129 public I18nizableText(String catalogueLocation, String catalogueFilename, String key, List<String> parameters) 130 { 131 _i18n = true; 132 133 _catalogueLocation = catalogueLocation; 134 _catalogueBundleName = catalogueFilename; 135 _catalogue = null; 136 _key = key; 137 _parameters = parameters; 138 _parameterMap = null; 139 } 140 141 /** 142 * Create an i18nized text with named parameters. 143 * @param catalogueLocation the file location URI of the catalogue where the key is defined. 144 * @param catalogueFilename the catalogue bundle name such as 'messages' 145 * @param key the key of the text. Cannot be null. 146 * @param parameters the named parameters of the message, as a Map of name -> value. Value can itself be an i18n key but must not have parameters. 147 */ 148 public I18nizableText(String catalogueLocation, String catalogueFilename, String key, Map<String, I18nizableTextParameter> parameters) 149 { 150 _i18n = true; 151 152 _catalogueLocation = catalogueLocation; 153 _catalogueBundleName = catalogueFilename; 154 _catalogue = null; 155 _key = key; 156 _parameterMap = parameters; 157 _parameters = null; 158 } 159 160 /** 161 * Determine whether the text is i18nized or a simple cross languages text. 162 * @return true if the text is i18nized and so defined by a catalogue, a key and optionaly parameters.<br>false if the text is a simple label 163 */ 164 public boolean isI18n() 165 { 166 return _i18n; 167 } 168 169 /** 170 * Get the catalogue of the i18nized text. 171 * @return The catalogue where the key is defined 172 */ 173 public String getCatalogue() 174 { 175 if (_i18n) 176 { 177 return _catalogue; 178 } 179 else 180 { 181 throw new IllegalArgumentException("This text is not i18nized and so do not have catalogue. Use the 'isI18n' method to know whether a text is i18nized"); 182 } 183 } 184 185 /** 186 * Get the file location URI of the i18nized text. 187 * @return The catalogue location where the key is defined 188 */ 189 public String getLocation() 190 { 191 if (_i18n) 192 { 193 return _catalogueLocation; 194 } 195 else 196 { 197 throw new IllegalArgumentException("This text is not i18nized and so do not have location. Use the 'isI18n' method to know whether a text is i18nized"); 198 } 199 } 200 201 /** 202 * Get the files name of catalogue 203 * @return bundle name 204 */ 205 public String getBundleName() 206 { 207 if (_i18n) 208 { 209 return _catalogueBundleName; 210 } 211 else 212 { 213 throw new IllegalArgumentException("This text is not i18nized and so do not have location. Use the 'isI18n' method to know whether a text is i18nized"); 214 } 215 } 216 217 /** 218 * Get the key of the i18nized text. 219 * @return The key in the catalogue 220 */ 221 public String getKey() 222 { 223 if (_i18n) 224 { 225 return _key; 226 } 227 else 228 { 229 throw new IllegalArgumentException("This text is not i18nized and so do not have key. Use the 'isI18n' method to know whether a text is i18nized"); 230 } 231 } 232 233 /** 234 * Get the parameters of the key of the i18nized text. 235 * @return The list of parameters' values or null if there is no parameters 236 */ 237 public List<String> getParameters() 238 { 239 if (_i18n) 240 { 241 return _parameters; 242 } 243 else 244 { 245 throw new IllegalArgumentException("This text is not i18nized and so do not have parameters. Use the 'isI18n' method to know whether a text is i18nized"); 246 } 247 } 248 249 /** 250 * Get the parameters of the key of the i18nized text. 251 * @return The list of parameters' values or null if there is no parameters 252 */ 253 public Map<String, I18nizableTextParameter> getParameterMap() 254 { 255 if (_i18n) 256 { 257 return _parameterMap; 258 } 259 else 260 { 261 throw new IllegalArgumentException("This text is not i18nized and so do not have parameters. Use the 'isI18n' method to know whether a text is i18nized"); 262 } 263 } 264 265 /** 266 * Get the label if a text is not i18nized. 267 * @return The label 268 */ 269 public String getLabel() 270 { 271 if (!_i18n) 272 { 273 return _directLabel; 274 } 275 else 276 { 277 throw new IllegalArgumentException("This text is i18nized and so do not have label. Use the 'isI18n' method to know whether a text is i18nized"); 278 } 279 } 280 281 public void toSAX(ContentHandler handler) throws SAXException 282 { 283 if (isI18n()) 284 { 285 List<String> parameters = getParameters(); 286 Map<String, ? extends I18nizableTextParameter> parameterMap = getParameterMap(); 287 boolean hasParameter = parameters != null && parameters.size() > 0 || parameterMap != null && !parameterMap.isEmpty(); 288 289 handler.startPrefixMapping("i18n", I18nTransformer.I18N_NAMESPACE_URI); 290 291 if (hasParameter) 292 { 293 handler.startElement(I18nTransformer.I18N_NAMESPACE_URI, "translate", "i18n:translate", new AttributesImpl()); 294 } 295 296 AttributesImpl atts = new AttributesImpl(); 297 atts.addCDATAAttribute(I18nTransformer.I18N_NAMESPACE_URI, "key", "i18n:key", getKey()); 298 if (getCatalogue() != null) 299 { 300 atts.addCDATAAttribute(I18nTransformer.I18N_NAMESPACE_URI, "catalogue", "i18n:catalogue", getCatalogue()); 301 } 302 303 handler.startElement(I18nTransformer.I18N_NAMESPACE_URI, "text", "i18n:text", atts); 304 handler.endElement(I18nTransformer.I18N_NAMESPACE_URI, "text", "i18n:text"); 305 306 if (hasParameter) 307 { 308 if (parameters != null) 309 { 310 // Ordered parameters version. 311 for (String parameter : parameters) 312 { 313 if (parameter != null) 314 { 315 handler.startElement(I18nTransformer.I18N_NAMESPACE_URI, "param", "i18n:param", new AttributesImpl()); 316 handler.characters(parameter.toCharArray(), 0, parameter.length()); 317 handler.endElement(I18nTransformer.I18N_NAMESPACE_URI, "param", "i18n:param"); 318 } 319 } 320 } 321 else if (parameterMap != null) 322 { 323 // Named parameters version. 324 for (String parameterName : parameterMap.keySet()) 325 { 326 I18nizableTextParameter value = parameterMap.get(parameterName); 327 AttributesImpl attrs = new AttributesImpl(); 328 attrs.addCDATAAttribute("name", parameterName); 329 handler.startElement(I18nTransformer.I18N_NAMESPACE_URI, "param", "i18n:param", attrs); 330 value.toSAXAsParam(handler); 331 handler.endElement(I18nTransformer.I18N_NAMESPACE_URI, "param", "i18n:param"); 332 } 333 } 334 335 handler.endElement(I18nTransformer.I18N_NAMESPACE_URI, "translate", "i18n:translate"); 336 } 337 338 handler.endPrefixMapping("i18n"); 339 } 340 else 341 { 342 handler.characters(getLabel().toCharArray(), 0, getLabel().length()); 343 } 344 } 345 346 public void toSAXAsParam(ContentHandler handler) throws SAXException 347 { 348 if (isI18n()) 349 { 350 AttributesImpl atts = new AttributesImpl(); 351 if (getCatalogue() != null) 352 { 353 atts.addCDATAAttribute(I18nTransformer.I18N_NAMESPACE_URI, "catalogue", "i18n:catalogue", getCatalogue()); 354 } 355 356 handler.startElement(I18nTransformer.I18N_NAMESPACE_URI, "text", "i18n:text", atts); 357 handler.characters(_key.toCharArray(), 0, _key.length()); 358 handler.endElement(I18nTransformer.I18N_NAMESPACE_URI, "text", "i18n:text"); 359 } 360 else 361 { 362 handler.characters(getLabel().toCharArray(), 0, getLabel().length()); 363 } 364 } 365 366 @Override 367 public String toString() 368 { 369 String result = ""; 370 if (isI18n()) 371 { 372 result += getCatalogue() + ":" + getKey(); 373 List<String> parameters = getParameters(); 374 if (parameters != null) 375 { 376 result += "["; 377 boolean isFirst = true; 378 for (String parameter : parameters) 379 { 380 if (!isFirst) 381 { 382 result += "; param : " + parameter; 383 } 384 else 385 { 386 result += "param : " + parameter; 387 isFirst = false; 388 } 389 } 390 result += "]"; 391 } 392 } 393 else 394 { 395 result = getLabel(); 396 } 397 return result; 398 } 399 400 @Override 401 public int hashCode() 402 { 403 HashCodeBuilder hashCodeBuilder = new HashCodeBuilder(); 404 hashCodeBuilder.append(_i18n); 405 hashCodeBuilder.append(_key); 406 hashCodeBuilder.append(_catalogueLocation); 407 hashCodeBuilder.append(_catalogueBundleName); 408 hashCodeBuilder.append(_catalogue); 409 hashCodeBuilder.append(_directLabel); 410 411 if (_parameters == null) 412 { 413 hashCodeBuilder.append((Object) null); 414 } 415 else 416 { 417 hashCodeBuilder.append(_parameters.toArray(new String[_parameters.size()])); 418 } 419 420 hashCodeBuilder.append(_parameterMap); 421 422 return hashCodeBuilder.toHashCode(); 423 } 424 425 @Override 426 public boolean equals(Object obj) 427 { 428 if (obj == null) 429 { 430 return false; 431 } 432 433 if (!(obj instanceof I18nizableText)) 434 { 435 return false; 436 } 437 438 if (this == obj) 439 { 440 return true; 441 } 442 443 I18nizableText i18nObj = (I18nizableText) obj; 444 if (_i18n != i18nObj._i18n) 445 { 446 return false; 447 } 448 449 EqualsBuilder equalsBuilder = new EqualsBuilder(); 450 if (_i18n) 451 { 452 equalsBuilder.append(_key, i18nObj._key); 453 454 if (_catalogue == null) 455 { 456 equalsBuilder.append(_catalogueLocation, i18nObj._catalogueLocation); 457 equalsBuilder.append(_catalogueBundleName, i18nObj._catalogueBundleName); 458 } 459 else 460 { 461 equalsBuilder.append(_catalogue, i18nObj._catalogue); 462 } 463 464 if (_parameters == null) 465 { 466 equalsBuilder.append(_parameters, i18nObj._parameters); 467 } 468 else 469 { 470 String[] otherParameters = null; 471 472 if (i18nObj._parameters != null) 473 { 474 otherParameters = i18nObj._parameters.toArray(new String[i18nObj._parameters.size()]); 475 } 476 477 equalsBuilder.append(_parameters.toArray(new String[_parameters.size()]), 478 otherParameters); 479 } 480 481 equalsBuilder.append(_parameterMap, i18nObj.getParameterMap()); 482 } 483 else 484 { 485 equalsBuilder.append(_directLabel, i18nObj._directLabel); 486 } 487 488 return equalsBuilder.isEquals(); 489 } 490 491 /** 492 * Check if configuration is an i18nizable text 493 * @param config the config to check 494 * @return true if the config is an I18n 495 */ 496 public static boolean isI18n(Configuration config) 497 { 498 return config.getAttributeAsBoolean("i18n", false) || config.getAttribute("type", "").equals("i18n"); 499 } 500 501 /** 502 * Get an i18n text configuration (can be a key or a "direct" string). 503 * @param config The configuration to parse. 504 * @param catalogueLocation The i18n catalogue location URI 505 * @param catalogueFilename The i18n catalogue bundle name 506 * @param value The i18n text, can be a key or a "direct" string. 507 * @return The i18nizable text 508 */ 509 private static I18nizableText getI18nizableTextValue(Configuration config, String catalogueLocation, String catalogueFilename, String value) 510 { 511 return new I18nizableText(catalogueLocation, catalogueFilename, value); 512 } 513 514 /** 515 * Get an i18n text configuration (can be a key or a "direct" string). 516 * @param config The configuration to parse. 517 * @param defaultCatalogue The i18n catalogue to use when not specified. 518 * @param value The i18n text, can be a key or a "direct" string. 519 * @return The i18nizable text or null if the config is null 520 */ 521 public static I18nizableText getI18nizableTextValue(Configuration config, String defaultCatalogue, String value) 522 { 523 if (config != null) 524 { 525 if (I18nizableText.isI18n(config)) 526 { 527 String catalogue = config.getAttribute("catalogue", defaultCatalogue); 528 529 return new I18nizableText(catalogue, value); 530 } 531 else 532 { 533 return new I18nizableText(value); 534 } 535 } 536 else 537 { 538 return null; 539 } 540 } 541 542 /** 543 * Parse a i18n text configuration. 544 * @param config the configuration to use. 545 * @param catalogueLocation The i18n catalogue location URI 546 * @param catalogueFilename The i18n catalogue bundle name 547 * @param defaultValue The default value key in configuration 548 * @return the i18n text or null if the config is null 549 */ 550 public static I18nizableText parseI18nizableText(Configuration config, String catalogueLocation, String catalogueFilename, String defaultValue) 551 { 552 if (config != null) 553 { 554 String text = config.getValue(defaultValue); 555 return I18nizableText.getI18nizableTextValue(config, catalogueLocation, catalogueFilename, text); 556 } 557 else 558 { 559 return null; 560 } 561 } 562 563 /** 564 * Parse an optional i18n text configuration, with a default value. 565 * @param config the configuration to use. 566 * @param defaultCatalogue the i18n catalogue to use when not specified. 567 * @param defaultValue the default value key in configuration. 568 * @return the i18n text or null if the config is null 569 */ 570 public static I18nizableText parseI18nizableText(Configuration config, String defaultCatalogue, String defaultValue) 571 { 572 return Optional.ofNullable(config) 573 .map(cfg -> cfg.getValue(defaultValue)) 574 .map(text -> I18nizableText.getI18nizableTextValue(config, defaultCatalogue, text)) 575 .orElse(new I18nizableText(defaultValue)); 576 } 577 578 /** 579 * Parse an optional i18n text configuration, with a default i18n text value. 580 * @param config the configuration to use. 581 * @param defaultCatalogue the i18n catalogue to use when not specified. 582 * @param defaultValue the default i18n text value. 583 * @return the i18n text or null if the config is null 584 */ 585 public static I18nizableText parseI18nizableText(Configuration config, String defaultCatalogue, I18nizableText defaultValue) 586 { 587 return Optional.ofNullable(config) 588 .map(cfg -> cfg.getValue(null)) 589 .map(text -> I18nizableText.getI18nizableTextValue(config, defaultCatalogue, text)) 590 .orElse(defaultValue); 591 } 592 593 /** 594 * Parse a mandatory i18n text configuration, throwing an exception if empty. 595 * @param config the configuration to use. 596 * @param defaultCatalogue the i18n catalogue to use when not specified. 597 * @return the i18n text or null if the config is null 598 * @throws ConfigurationException if the configuration is not valid. 599 */ 600 public static I18nizableText parseI18nizableText(Configuration config, String defaultCatalogue) throws ConfigurationException 601 { 602 if (config != null) 603 { 604 String text = config.getValue(); 605 return I18nizableText.getI18nizableTextValue(config, defaultCatalogue, text); 606 } 607 else 608 { 609 return null; 610 } 611 } 612 613 /** 614 * Gets a string representation of a i18n text 615 * @param i18nizableText The i18 text 616 * @return The string representation of the i18n text 617 */ 618 public static String i18nizableTextToString(I18nizableText i18nizableText) 619 { 620 Map<String, Object> map = new HashMap<>(); 621 if (i18nizableText.isI18n()) 622 { 623 map.put("key", i18nizableText.getKey()); 624 map.put("catalogue", i18nizableText.getCatalogue()); 625 map.put("parameters", i18nizableText.getParameters()); 626 } 627 else 628 { 629 map.put("label", i18nizableText.getLabel()); 630 } 631 632 // Use Java Bean XMLEncoder 633 ByteArrayOutputStream bos = new ByteArrayOutputStream(); 634 try (XMLEncoder xmlEncoder = new XMLEncoder(bos)) 635 { 636 xmlEncoder.writeObject(map); 637 xmlEncoder.flush(); 638 } 639 640 try 641 { 642 return bos.toString("UTF-8"); 643 } 644 catch (UnsupportedEncodingException e) 645 { 646 // should not happen 647 throw new IllegalStateException(e); 648 } 649 } 650 651 /** 652 * Returns the i18n text from its string representation 653 * @param str The string representation of the i18n text 654 * @return The i18n text 655 */ 656 @SuppressWarnings("unchecked") 657 public static I18nizableText stringToI18nizableText(String str) 658 { 659 Map<String, Object> map; 660 // Use Java Bean XMLDecoder 661 try (XMLDecoder xmlDecoder = new XMLDecoder(new ByteArrayInputStream(str.getBytes("UTF-8")))) 662 { 663 map = (Map<String, Object>) xmlDecoder.readObject(); 664 } 665 catch (UnsupportedEncodingException e) 666 { 667 // should not happen 668 throw new IllegalStateException(e); 669 } 670 671 if (map.get("label") != null) 672 { 673 return new I18nizableText((String) map.get("label")); 674 } 675 else 676 { 677 String key = (String) map.get("key"); 678 String catalogue = (String) map.get("catalogue"); 679 List<String> parameters = (List) map.get("parameters"); 680 return new I18nizableText(catalogue, key, parameters); 681 } 682 } 683}