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 EqualsBuilder equalsBuilder = new EqualsBuilder(); 463 equalsBuilder.append(_i18n, i18nObj._i18n); 464 465 if (_i18n) 466 { 467 equalsBuilder.append(_key, i18nObj._key); 468 469 if (_catalogue == null) 470 { 471 equalsBuilder.append(_catalogueLocation, i18nObj._catalogueLocation); 472 equalsBuilder.append(_catalogueBundleName, i18nObj._catalogueBundleName); 473 } 474 else 475 { 476 equalsBuilder.append(_catalogue, i18nObj._catalogue); 477 } 478 479 if (_parameters == null) 480 { 481 equalsBuilder.append(_parameters, i18nObj._parameters); 482 } 483 else 484 { 485 String[] otherParameters = null; 486 487 if (i18nObj._parameters != null) 488 { 489 otherParameters = i18nObj._parameters.toArray(new String[i18nObj._parameters.size()]); 490 } 491 492 equalsBuilder.append(_parameters.toArray(new String[_parameters.size()]), 493 otherParameters); 494 } 495 496 equalsBuilder.append(_parameterMap, i18nObj.getParameterMap()); 497 } 498 else 499 { 500 equalsBuilder.append(_directLabel, i18nObj._directLabel); 501 } 502 503 return equalsBuilder.isEquals(); 504 } 505 506 private static boolean isI18n(Configuration config) 507 { 508 return config.getAttributeAsBoolean("i18n", false) || config.getAttribute("type", "").equals("i18n"); 509 } 510 511 /** 512 * Get an i18n text configuration (can be a key or a "direct" string). 513 * @param config The configuration to parse. 514 * @param catalogueLocation The i18n catalogue location URI 515 * @param catalogueFilename The i18n catalogue bundle name 516 * @param value The i18n text, can be a key or a "direct" string. 517 * @return The i18nizable text 518 */ 519 private static I18nizableText getI18nizableTextValue(Configuration config, String catalogueLocation, String catalogueFilename, String value) 520 { 521 return new I18nizableText(catalogueLocation, catalogueFilename, value); 522 } 523 524 /** 525 * Get an i18n text configuration (can be a key or a "direct" string). 526 * @param config The configuration to parse. 527 * @param defaultCatalogue The i18n catalogue to use when not specified. 528 * @param value The i18n text, can be a key or a "direct" string. 529 * @return The i18nizable text 530 */ 531 public static I18nizableText getI18nizableTextValue(Configuration config, String defaultCatalogue, String value) 532 { 533 if (I18nizableText.isI18n(config)) 534 { 535 String catalogue = config.getAttribute("catalogue", defaultCatalogue); 536 537 return new I18nizableText(catalogue, value); 538 } 539 else 540 { 541 return new I18nizableText(value); 542 } 543 } 544 545 /** 546 * Parse a i18n text configuration. 547 * @param config the configuration to use. 548 * @param catalogueLocation The i18n catalogue location URI 549 * @param catalogueFilename The i18n catalogue bundle name 550 * @param defaultValue The default value key in configuration 551 * @return the i18n text. 552 * @throws ConfigurationException if the configuration is not valid. 553 */ 554 public static I18nizableText parseI18nizableText(Configuration config, String catalogueLocation, String catalogueFilename, String defaultValue) throws ConfigurationException 555 { 556 String text = config.getValue(defaultValue); 557 return I18nizableText.getI18nizableTextValue(config, catalogueLocation, catalogueFilename, text); 558 } 559 560 /** 561 * Parse an optional i18n text configuration, with a default value. 562 * @param config the configuration to use. 563 * @param defaultCatalogue the i18n catalogue to use when not specified. 564 * @param defaultValue the default value key in configuration. 565 * @return the i18n text. 566 */ 567 public static I18nizableText parseI18nizableText(Configuration config, String defaultCatalogue, String defaultValue) 568 { 569 String text = config.getValue(defaultValue); 570 return I18nizableText.getI18nizableTextValue(config, defaultCatalogue, text); 571 } 572 573 /** 574 * Parse a mandatory i18n text configuration, throwing an exception if empty. 575 * @param config the configuration to use. 576 * @param defaultCatalogue the i18n catalogue to use when not specified. 577 * @return the i18n text. 578 * @throws ConfigurationException if the configuration is not valid. 579 */ 580 public static I18nizableText parseI18nizableText(Configuration config, String defaultCatalogue) throws ConfigurationException 581 { 582 String text = config.getValue(); 583 return I18nizableText.getI18nizableTextValue(config, defaultCatalogue, text); 584 } 585 586 /** 587 * Gets a string representation of a i18n text 588 * @param i18nizableText The i18 text 589 * @return The string representation of the i18n text 590 */ 591 public static String i18nizableTextToString(I18nizableText i18nizableText) 592 { 593 Map<String, Object> map = new HashMap<>(); 594 if (i18nizableText.isI18n()) 595 { 596 map.put("key", i18nizableText.getKey()); 597 map.put("catalogue", i18nizableText.getCatalogue()); 598 map.put("parameters", i18nizableText.getParameters()); 599 } 600 else 601 { 602 map.put("label", i18nizableText.getLabel()); 603 } 604 605 // Use Java Bean XMLEncoder 606 ByteArrayOutputStream bos = new ByteArrayOutputStream(); 607 try (XMLEncoder xmlEncoder = new XMLEncoder(bos)) 608 { 609 xmlEncoder.writeObject(map); 610 xmlEncoder.flush(); 611 } 612 613 try 614 { 615 return bos.toString("UTF-8"); 616 } 617 catch (UnsupportedEncodingException e) 618 { 619 // should not happen 620 throw new IllegalStateException(e); 621 } 622 } 623 624 /** 625 * Returns the i18n text from its string representation 626 * @param str The string representation of the i18n text 627 * @return The i18n text 628 */ 629 @SuppressWarnings("unchecked") 630 public static I18nizableText stringToI18nizableText(String str) 631 { 632 Map<String, Object> map; 633 // Use Java Bean XMLDecoder 634 try (XMLDecoder xmlDecoder = new XMLDecoder(new ByteArrayInputStream(str.getBytes("UTF-8")))) 635 { 636 map = (Map<String, Object>) xmlDecoder.readObject(); 637 } 638 catch (UnsupportedEncodingException e) 639 { 640 // should not happen 641 throw new IllegalStateException(e); 642 } 643 644 if (map.get("label") != null) 645 { 646 return new I18nizableText((String) map.get("label")); 647 } 648 else 649 { 650 String key = (String) map.get("key"); 651 String catalogue = (String) map.get("catalogue"); 652 List<String> parameters = (List) map.get("parameters"); 653 return new I18nizableText(catalogue, key, parameters); 654 } 655 } 656}