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; 026 027import org.apache.avalon.framework.configuration.Configuration; 028import org.apache.avalon.framework.configuration.ConfigurationException; 029import org.apache.cocoon.transformation.I18nTransformer; 030import org.apache.cocoon.xml.AttributesImpl; 031import org.apache.cocoon.xml.XMLUtils; 032import org.apache.commons.lang.builder.EqualsBuilder; 033import org.apache.commons.lang.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 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, I18nizableText> _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, I18nizableText> 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, I18nizableText> 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, I18nizableText> 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 /** 282 * SAX a text 283 * @param handler The sax content handler 284 * @throws SAXException if an error occurs 285 */ 286 public void toSAX(ContentHandler handler) throws SAXException 287 { 288 if (isI18n()) 289 { 290 List<String> parameters = getParameters(); 291 Map<String, I18nizableText> parameterMap = getParameterMap(); 292 boolean hasParameter = parameters != null && parameters.size() > 0 || parameterMap != null && !parameterMap.isEmpty(); 293 294 handler.startPrefixMapping("i18n", I18nTransformer.I18N_NAMESPACE_URI); 295 296 if (hasParameter) 297 { 298 handler.startElement(I18nTransformer.I18N_NAMESPACE_URI, "translate", "i18n:translate", new AttributesImpl()); 299 } 300 301 AttributesImpl atts = new AttributesImpl(); 302 atts.addCDATAAttribute(I18nTransformer.I18N_NAMESPACE_URI, "key", "i18n:key", getKey()); 303 if (getCatalogue() != null) 304 { 305 atts.addCDATAAttribute(I18nTransformer.I18N_NAMESPACE_URI, "catalogue", "i18n:catalogue", getCatalogue()); 306 } 307 308 handler.startElement(I18nTransformer.I18N_NAMESPACE_URI, "text", "i18n:text", atts); 309 handler.endElement(I18nTransformer.I18N_NAMESPACE_URI, "text", "i18n:text"); 310 311 if (hasParameter) 312 { 313 if (parameters != null) 314 { 315 // Ordered parameters version. 316 for (String parameter : parameters) 317 { 318 if (parameter != null) 319 { 320 handler.startElement(I18nTransformer.I18N_NAMESPACE_URI, "param", "i18n:param", new AttributesImpl()); 321 handler.characters(parameter.toCharArray(), 0, parameter.length()); 322 handler.endElement(I18nTransformer.I18N_NAMESPACE_URI, "param", "i18n:param"); 323 } 324 } 325 } 326 else if (parameterMap != null) 327 { 328 // Named parameters version. 329 for (String parameterName : parameterMap.keySet()) 330 { 331 I18nizableText value = parameterMap.get(parameterName); 332 AttributesImpl attrs = new AttributesImpl(); 333 attrs.addCDATAAttribute("name", parameterName); 334 handler.startElement(I18nTransformer.I18N_NAMESPACE_URI, "param", "i18n:param", attrs); 335 value._toSAXAsParam(handler); 336 handler.endElement(I18nTransformer.I18N_NAMESPACE_URI, "param", "i18n:param"); 337 } 338 } 339 340 handler.endElement(I18nTransformer.I18N_NAMESPACE_URI, "translate", "i18n:translate"); 341 } 342 343 handler.endPrefixMapping("i18n"); 344 } 345 else 346 { 347 handler.characters(getLabel().toCharArray(), 0, getLabel().length()); 348 } 349 } 350 351 /** 352 * SAX a text 353 * @param handler The sax content handler 354 * @param tagName The tag name 355 * @throws SAXException if an error occurs 356 */ 357 public void toSAX(ContentHandler handler, String tagName) throws SAXException 358 { 359 XMLUtils.startElement(handler, tagName); 360 toSAX(handler); 361 XMLUtils.endElement(handler, tagName); 362 } 363 364 private void _toSAXAsParam(ContentHandler handler) throws SAXException 365 { 366 if (isI18n()) 367 { 368 AttributesImpl atts = new AttributesImpl(); 369 if (getCatalogue() != null) 370 { 371 atts.addCDATAAttribute(I18nTransformer.I18N_NAMESPACE_URI, "catalogue", "i18n:catalogue", getCatalogue()); 372 } 373 374 handler.startElement(I18nTransformer.I18N_NAMESPACE_URI, "text", "i18n:text", atts); 375 handler.characters(_key.toCharArray(), 0, _key.length()); 376 handler.endElement(I18nTransformer.I18N_NAMESPACE_URI, "text", "i18n:text"); 377 } 378 else 379 { 380 handler.characters(getLabel().toCharArray(), 0, getLabel().length()); 381 } 382 } 383 384 @Override 385 public String toString() 386 { 387 String result = ""; 388 if (isI18n()) 389 { 390 result += getCatalogue() + ":" + getKey(); 391 List<String> parameters = getParameters(); 392 if (parameters != null) 393 { 394 result += "["; 395 boolean isFirst = true; 396 for (String parameter : parameters) 397 { 398 if (!isFirst) 399 { 400 result += "; param : " + parameter; 401 } 402 else 403 { 404 result += "param : " + parameter; 405 isFirst = false; 406 } 407 } 408 result += "]"; 409 } 410 } 411 else 412 { 413 result = getLabel(); 414 } 415 return result; 416 } 417 418 @Override 419 public int hashCode() 420 { 421 HashCodeBuilder hashCodeBuilder = new HashCodeBuilder(); 422 hashCodeBuilder.append(_i18n); 423 hashCodeBuilder.append(_key); 424 hashCodeBuilder.append(_catalogueLocation); 425 hashCodeBuilder.append(_catalogueBundleName); 426 hashCodeBuilder.append(_catalogue); 427 hashCodeBuilder.append(_directLabel); 428 429 if (_parameters == null) 430 { 431 hashCodeBuilder.append((Object) null); 432 } 433 else 434 { 435 hashCodeBuilder.append(_parameters.toArray(new String[_parameters.size()])); 436 } 437 438 hashCodeBuilder.append(_parameterMap); 439 440 return hashCodeBuilder.toHashCode(); 441 } 442 443 @Override 444 public boolean equals(Object obj) 445 { 446 if (obj == null) 447 { 448 return false; 449 } 450 451 if (!(obj instanceof I18nizableText)) 452 { 453 return false; 454 } 455 456 if (this == obj) 457 { 458 return true; 459 } 460 461 I18nizableText i18nObj = (I18nizableText) obj; 462 if (_i18n != i18nObj._i18n) 463 { 464 return false; 465 } 466 467 EqualsBuilder equalsBuilder = new EqualsBuilder(); 468 if (_i18n) 469 { 470 equalsBuilder.append(_key, i18nObj._key); 471 472 if (_catalogue == null) 473 { 474 equalsBuilder.append(_catalogueLocation, i18nObj._catalogueLocation); 475 equalsBuilder.append(_catalogueBundleName, i18nObj._catalogueBundleName); 476 } 477 else 478 { 479 equalsBuilder.append(_catalogue, i18nObj._catalogue); 480 } 481 482 if (_parameters == null) 483 { 484 equalsBuilder.append(_parameters, i18nObj._parameters); 485 } 486 else 487 { 488 String[] otherParameters = null; 489 490 if (i18nObj._parameters != null) 491 { 492 otherParameters = i18nObj._parameters.toArray(new String[i18nObj._parameters.size()]); 493 } 494 495 equalsBuilder.append(_parameters.toArray(new String[_parameters.size()]), 496 otherParameters); 497 } 498 499 equalsBuilder.append(_parameterMap, i18nObj.getParameterMap()); 500 } 501 else 502 { 503 equalsBuilder.append(_directLabel, i18nObj._directLabel); 504 } 505 506 return equalsBuilder.isEquals(); 507 } 508 509 private static boolean isI18n(Configuration config) 510 { 511 return config.getAttributeAsBoolean("i18n", false) || config.getAttribute("type", "").equals("i18n"); 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 catalogueLocation The i18n catalogue location URI 518 * @param catalogueFilename The i18n catalogue bundle name 519 * @param value The i18n text, can be a key or a "direct" string. 520 * @return The i18nizable text 521 */ 522 private static I18nizableText getI18nizableTextValue(Configuration config, String catalogueLocation, String catalogueFilename, String value) 523 { 524 return new I18nizableText(catalogueLocation, catalogueFilename, value); 525 } 526 527 /** 528 * Get an i18n text configuration (can be a key or a "direct" string). 529 * @param config The configuration to parse. 530 * @param defaultCatalogue The i18n catalogue to use when not specified. 531 * @param value The i18n text, can be a key or a "direct" string. 532 * @return The i18nizable text 533 */ 534 public static I18nizableText getI18nizableTextValue(Configuration config, String defaultCatalogue, String value) 535 { 536 if (I18nizableText.isI18n(config)) 537 { 538 String catalogue = config.getAttribute("catalogue", defaultCatalogue); 539 540 return new I18nizableText(catalogue, value); 541 } 542 else 543 { 544 return new I18nizableText(value); 545 } 546 } 547 548 /** 549 * Parse a i18n text configuration. 550 * @param config the configuration to use. 551 * @param catalogueLocation The i18n catalogue location URI 552 * @param catalogueFilename The i18n catalogue bundle name 553 * @param defaultValue The default value key in configuration 554 * @return the i18n text. 555 * @throws ConfigurationException if the configuration is not valid. 556 */ 557 public static I18nizableText parseI18nizableText(Configuration config, String catalogueLocation, String catalogueFilename, String defaultValue) throws ConfigurationException 558 { 559 String text = config.getValue(defaultValue); 560 return I18nizableText.getI18nizableTextValue(config, catalogueLocation, catalogueFilename, text); 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. 569 */ 570 public static I18nizableText parseI18nizableText(Configuration config, String defaultCatalogue, String defaultValue) 571 { 572 String text = config.getValue(defaultValue); 573 return I18nizableText.getI18nizableTextValue(config, defaultCatalogue, text); 574 } 575 576 /** 577 * Parse a mandatory i18n text configuration, throwing an exception if empty. 578 * @param config the configuration to use. 579 * @param defaultCatalogue the i18n catalogue to use when not specified. 580 * @return the i18n text. 581 * @throws ConfigurationException if the configuration is not valid. 582 */ 583 public static I18nizableText parseI18nizableText(Configuration config, String defaultCatalogue) throws ConfigurationException 584 { 585 String text = config.getValue(); 586 return I18nizableText.getI18nizableTextValue(config, defaultCatalogue, text); 587 } 588 589 /** 590 * Gets a string representation of a i18n text 591 * @param i18nizableText The i18 text 592 * @return The string representation of the i18n text 593 */ 594 public static String i18nizableTextToString(I18nizableText i18nizableText) 595 { 596 Map<String, Object> map = new HashMap<>(); 597 if (i18nizableText.isI18n()) 598 { 599 map.put("key", i18nizableText.getKey()); 600 map.put("catalogue", i18nizableText.getCatalogue()); 601 map.put("parameters", i18nizableText.getParameters()); 602 } 603 else 604 { 605 map.put("label", i18nizableText.getLabel()); 606 } 607 608 // Use Java Bean XMLEncoder 609 ByteArrayOutputStream bos = new ByteArrayOutputStream(); 610 try (XMLEncoder xmlEncoder = new XMLEncoder(bos)) 611 { 612 xmlEncoder.writeObject(map); 613 xmlEncoder.flush(); 614 } 615 616 try 617 { 618 return bos.toString("UTF-8"); 619 } 620 catch (UnsupportedEncodingException e) 621 { 622 // should not happen 623 throw new IllegalStateException(e); 624 } 625 } 626 627 /** 628 * Returns the i18n text from its string representation 629 * @param str The string representation of the i18n text 630 * @return The i18n text 631 */ 632 @SuppressWarnings("unchecked") 633 public static I18nizableText stringToI18nizableText(String str) 634 { 635 Map<String, Object> map; 636 // Use Java Bean XMLDecoder 637 try (XMLDecoder xmlDecoder = new XMLDecoder(new ByteArrayInputStream(str.getBytes("UTF-8")))) 638 { 639 map = (Map<String, Object>) xmlDecoder.readObject(); 640 } 641 catch (UnsupportedEncodingException e) 642 { 643 // should not happen 644 throw new IllegalStateException(e); 645 } 646 647 if (map.get("label") != null) 648 { 649 return new I18nizableText((String) map.get("label")); 650 } 651 else 652 { 653 String key = (String) map.get("key"); 654 String catalogue = (String) map.get("catalogue"); 655 List<String> parameters = (List) map.get("parameters"); 656 return new I18nizableText(catalogue, key, parameters); 657 } 658 } 659}