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 -&gt; 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 -&gt; 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}