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