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 catalog 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 catalog 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 private static boolean isI18n(Configuration config) 492 { 493 return config.getAttributeAsBoolean("i18n", false) || config.getAttribute("type", "").equals("i18n"); 494 } 495 496 /** 497 * Get an i18n text configuration (can be a key or a "direct" string). 498 * @param config The configuration to parse. 499 * @param catalogueLocation The i18n catalogue location URI 500 * @param catalogueFilename The i18n catalogue bundle name 501 * @param value The i18n text, can be a key or a "direct" string. 502 * @return The i18nizable text 503 */ 504 private static I18nizableText getI18nizableTextValue(Configuration config, String catalogueLocation, String catalogueFilename, String value) 505 { 506 return new I18nizableText(catalogueLocation, catalogueFilename, value); 507 } 508 509 /** 510 * Get an i18n text configuration (can be a key or a "direct" string). 511 * @param config The configuration to parse. 512 * @param defaultCatalogue The i18n catalogue to use when not specified. 513 * @param value The i18n text, can be a key or a "direct" string. 514 * @return The i18nizable text or null if the config is null 515 */ 516 public static I18nizableText getI18nizableTextValue(Configuration config, String defaultCatalogue, String value) 517 { 518 if (config != null) 519 { 520 if (I18nizableText.isI18n(config)) 521 { 522 String catalogue = config.getAttribute("catalogue", defaultCatalogue); 523 524 return new I18nizableText(catalogue, value); 525 } 526 else 527 { 528 return new I18nizableText(value); 529 } 530 } 531 else 532 { 533 return null; 534 } 535 } 536 537 /** 538 * Parse a i18n text configuration. 539 * @param config the configuration to use. 540 * @param catalogueLocation The i18n catalog location URI 541 * @param catalogueFilename The i18n catalog bundle name 542 * @param defaultValue The default value key in configuration 543 * @return the i18n text or null if the config is null 544 * @throws ConfigurationException if the configuration is not valid. 545 */ 546 public static I18nizableText parseI18nizableText(Configuration config, String catalogueLocation, String catalogueFilename, String defaultValue) throws ConfigurationException 547 { 548 if (config != null) 549 { 550 String text = config.getValue(defaultValue); 551 return I18nizableText.getI18nizableTextValue(config, catalogueLocation, catalogueFilename, text); 552 } 553 else 554 { 555 return null; 556 } 557 } 558 559 /** 560 * Parse an optional i18n text configuration, with a default value. 561 * @param config the configuration to use. 562 * @param defaultCatalogue the i18n catalogue to use when not specified. 563 * @param defaultValue the default value key in configuration. 564 * @return the i18n text or null if the config is null 565 */ 566 public static I18nizableText parseI18nizableText(Configuration config, String defaultCatalogue, String defaultValue) 567 { 568 if (config != null) 569 { 570 String text = config.getValue(defaultValue); 571 return I18nizableText.getI18nizableTextValue(config, defaultCatalogue, text); 572 } 573 else 574 { 575 return null; 576 } 577 } 578 579 /** 580 * Parse an optional i18n text configuration, with a default i18n text value. 581 * @param config the configuration to use. 582 * @param defaultCatalog the i18n catalog to use when not specified. 583 * @param defaultValue the default i18n text value. 584 * @return the i18n text or null if the config is null 585 */ 586 public static I18nizableText parseI18nizableText(Configuration config, String defaultCatalog, I18nizableText defaultValue) 587 { 588 if (config != null) 589 { 590 return Optional.ofNullable(config.getValue(null)) 591 .map(text -> I18nizableText.getI18nizableTextValue(config, defaultCatalog, text)) 592 .orElse(defaultValue); 593 } 594 else 595 { 596 return null; 597 } 598 } 599 600 /** 601 * Parse a mandatory i18n text configuration, throwing an exception if empty. 602 * @param config the configuration to use. 603 * @param defaultCatalogue the i18n catalogue to use when not specified. 604 * @return the i18n text or null if the config is null 605 * @throws ConfigurationException if the configuration is not valid. 606 */ 607 public static I18nizableText parseI18nizableText(Configuration config, String defaultCatalogue) throws ConfigurationException 608 { 609 if (config != null) 610 { 611 String text = config.getValue(); 612 return I18nizableText.getI18nizableTextValue(config, defaultCatalogue, text); 613 } 614 else 615 { 616 return null; 617 } 618 } 619 620 /** 621 * Gets a string representation of a i18n text 622 * @param i18nizableText The i18 text 623 * @return The string representation of the i18n text 624 */ 625 public static String i18nizableTextToString(I18nizableText i18nizableText) 626 { 627 Map<String, Object> map = new HashMap<>(); 628 if (i18nizableText.isI18n()) 629 { 630 map.put("key", i18nizableText.getKey()); 631 map.put("catalogue", i18nizableText.getCatalogue()); 632 map.put("parameters", i18nizableText.getParameters()); 633 } 634 else 635 { 636 map.put("label", i18nizableText.getLabel()); 637 } 638 639 // Use Java Bean XMLEncoder 640 ByteArrayOutputStream bos = new ByteArrayOutputStream(); 641 try (XMLEncoder xmlEncoder = new XMLEncoder(bos)) 642 { 643 xmlEncoder.writeObject(map); 644 xmlEncoder.flush(); 645 } 646 647 try 648 { 649 return bos.toString("UTF-8"); 650 } 651 catch (UnsupportedEncodingException e) 652 { 653 // should not happen 654 throw new IllegalStateException(e); 655 } 656 } 657 658 /** 659 * Returns the i18n text from its string representation 660 * @param str The string representation of the i18n text 661 * @return The i18n text 662 */ 663 @SuppressWarnings("unchecked") 664 public static I18nizableText stringToI18nizableText(String str) 665 { 666 Map<String, Object> map; 667 // Use Java Bean XMLDecoder 668 try (XMLDecoder xmlDecoder = new XMLDecoder(new ByteArrayInputStream(str.getBytes("UTF-8")))) 669 { 670 map = (Map<String, Object>) xmlDecoder.readObject(); 671 } 672 catch (UnsupportedEncodingException e) 673 { 674 // should not happen 675 throw new IllegalStateException(e); 676 } 677 678 if (map.get("label") != null) 679 { 680 return new I18nizableText((String) map.get("label")); 681 } 682 else 683 { 684 String key = (String) map.get("key"); 685 String catalogue = (String) map.get("catalogue"); 686 List<String> parameters = (List) map.get("parameters"); 687 return new I18nizableText(catalogue, key, parameters); 688 } 689 } 690}