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 -&gt; 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 -&gt; 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 catalogue 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 catalogue
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 catalogue location URI
541     * @param catalogueFilename The i18n catalogue bundle name
542     * @param defaultValue The default value key in configuration
543     * @return the i18n text or null if the config is null
544     */
545    public static I18nizableText parseI18nizableText(Configuration config, String catalogueLocation, String catalogueFilename, String defaultValue)
546    {
547        if (config != null)
548        {
549            String text = config.getValue(defaultValue);
550            return I18nizableText.getI18nizableTextValue(config, catalogueLocation, catalogueFilename, text);
551        }
552        else
553        {
554            return null;
555        }
556    }
557
558    /**
559     * Parse an optional i18n text configuration, with a default value.
560     * @param config the configuration to use.
561     * @param defaultCatalogue the i18n catalogue to use when not specified. 
562     * @param defaultValue the default value key in configuration.
563     * @return the i18n text or null if the config is null
564     */
565    public static I18nizableText parseI18nizableText(Configuration config, String defaultCatalogue, String defaultValue)
566    {
567        return Optional.ofNullable(config)
568            .map(cfg -> cfg.getValue(defaultValue))
569            .map(text -> I18nizableText.getI18nizableTextValue(config, defaultCatalogue, text))
570            .orElse(new I18nizableText(defaultValue));
571    }
572
573    /**
574     * Parse an optional i18n text configuration, with a default i18n text value.
575     * @param config the configuration to use.
576     * @param defaultCatalogue the i18n catalogue to use when not specified. 
577     * @param defaultValue the default i18n text value.
578     * @return the i18n text or null if the config is null
579     */
580    public static I18nizableText parseI18nizableText(Configuration config, String defaultCatalogue, I18nizableText defaultValue)
581    {
582        return Optional.ofNullable(config)
583            .map(cfg -> cfg.getValue(null))
584            .map(text -> I18nizableText.getI18nizableTextValue(config, defaultCatalogue, text))
585            .orElse(defaultValue);
586    }
587
588    /**
589     * Parse a mandatory i18n text configuration, throwing an exception if empty.
590     * @param config the configuration to use.
591     * @param defaultCatalogue the i18n catalogue to use when not specified.
592     * @return the i18n text or null if the config is null
593     * @throws ConfigurationException if the configuration is not valid.
594     */
595    public static I18nizableText parseI18nizableText(Configuration config, String defaultCatalogue) throws ConfigurationException
596    {
597        if (config != null)
598        {
599            String text = config.getValue();
600            return I18nizableText.getI18nizableTextValue(config, defaultCatalogue, text);
601        }
602        else
603        {
604            return null;
605        }
606    }
607    
608    /**
609     * Gets a string representation of a i18n text
610     * @param i18nizableText The i18 text
611     * @return The string representation of the i18n text
612     */
613    public static String i18nizableTextToString(I18nizableText i18nizableText)
614    {
615        Map<String, Object> map = new HashMap<>();
616        if (i18nizableText.isI18n())
617        {
618            map.put("key", i18nizableText.getKey());
619            map.put("catalogue", i18nizableText.getCatalogue());
620            map.put("parameters", i18nizableText.getParameters());
621        }
622        else
623        {
624            map.put("label", i18nizableText.getLabel());
625        }
626        
627        // Use Java Bean XMLEncoder
628        ByteArrayOutputStream bos = new ByteArrayOutputStream();
629        try (XMLEncoder xmlEncoder = new XMLEncoder(bos))
630        {
631            xmlEncoder.writeObject(map);
632            xmlEncoder.flush();
633        }
634        
635        try
636        {
637            return bos.toString("UTF-8");
638        }
639        catch (UnsupportedEncodingException e)
640        {
641            // should not happen
642            throw new IllegalStateException(e);
643        }
644    }
645    
646    /**
647     * Returns the i18n text from its string representation
648     * @param str The string representation of the i18n text
649     * @return The i18n text
650     */
651    @SuppressWarnings("unchecked")
652    public static I18nizableText stringToI18nizableText(String str)
653    {
654        Map<String, Object> map;
655        // Use Java Bean XMLDecoder
656        try (XMLDecoder xmlDecoder = new XMLDecoder(new ByteArrayInputStream(str.getBytes("UTF-8"))))
657        {
658            map = (Map<String, Object>) xmlDecoder.readObject();
659        }
660        catch (UnsupportedEncodingException e)
661        {
662            // should not happen
663            throw new IllegalStateException(e);
664        }
665        
666        if (map.get("label") != null)
667        {
668            return new I18nizableText((String) map.get("label"));
669        }
670        else
671        {
672            String key = (String) map.get("key");
673            String catalogue = (String) map.get("catalogue");
674            List<String> parameters = (List) map.get("parameters");
675            return new I18nizableText(catalogue, key, parameters);
676        }
677    }
678}