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 catalog location where the key is defined
188     */
189    public String getLocation()
190    {
191        if (_i18n)
192        {
193            return _catalogueLocation;
194        }
195        else
196        {
197            throw new IllegalArgumentException("This text is not i18nized and so do not have location. Use the 'isI18n' method to know whether a text is i18nized");
198        }
199    }
200    
201    /**
202     * Get the files name of catalog
203     * @return bundle name
204     */
205    public String getBundleName()
206    {
207        if (_i18n)
208        {
209            return _catalogueBundleName;
210        }
211        else
212        {
213            throw new IllegalArgumentException("This text is not i18nized and so do not have location. Use the 'isI18n' method to know whether a text is i18nized");
214        }
215    }
216
217    /**
218     * Get the key of the i18nized text.
219     * @return The key in the catalogue
220     */
221    public String getKey()
222    {
223        if (_i18n)
224        {
225            return _key;
226        }
227        else
228        {
229            throw new IllegalArgumentException("This text is not i18nized and so do not have key. Use the 'isI18n' method to know whether a text is i18nized");
230        }
231    }
232
233    /**
234     * Get the parameters of the key of the i18nized text.
235     * @return The list of parameters' values or null if there is no parameters
236     */
237    public List<String> getParameters()
238    {
239        if (_i18n)
240        {
241            return _parameters;
242        }
243        else
244        {
245            throw new IllegalArgumentException("This text is not i18nized and so do not have parameters. Use the 'isI18n' method to know whether a text is i18nized");
246        }
247    }
248
249    /**
250     * Get the parameters of the key of the i18nized text.
251     * @return The list of parameters' values or null if there is no parameters
252     */
253    public Map<String, I18nizableTextParameter> getParameterMap()
254    {
255        if (_i18n)
256        {
257            return _parameterMap;
258        }
259        else
260        {
261            throw new IllegalArgumentException("This text is not i18nized and so do not have parameters. Use the 'isI18n' method to know whether a text is i18nized");
262        }
263    }
264
265    /**
266     * Get the label if a text is not i18nized.
267     * @return The label
268     */
269    public String getLabel()
270    {
271        if (!_i18n)
272        {
273            return _directLabel;
274        }
275        else
276        {
277            throw new IllegalArgumentException("This text is i18nized and so do not have label. Use the 'isI18n' method to know whether a text is i18nized");
278        }
279    }
280
281    public void toSAX(ContentHandler handler) throws SAXException
282    {
283        if (isI18n())
284        {
285            List<String> parameters = getParameters();
286            Map<String, ? extends I18nizableTextParameter> parameterMap = getParameterMap();
287            boolean hasParameter = parameters != null && parameters.size() > 0 || parameterMap != null && !parameterMap.isEmpty();
288
289            handler.startPrefixMapping("i18n", I18nTransformer.I18N_NAMESPACE_URI);
290
291            if (hasParameter)
292            {
293                handler.startElement(I18nTransformer.I18N_NAMESPACE_URI, "translate", "i18n:translate", new AttributesImpl());
294            }
295
296            AttributesImpl atts = new AttributesImpl();
297            atts.addCDATAAttribute(I18nTransformer.I18N_NAMESPACE_URI, "key", "i18n:key", getKey());
298            if (getCatalogue() != null)
299            {
300                atts.addCDATAAttribute(I18nTransformer.I18N_NAMESPACE_URI, "catalogue", "i18n:catalogue", getCatalogue());
301            }
302
303            handler.startElement(I18nTransformer.I18N_NAMESPACE_URI, "text", "i18n:text", atts);
304            handler.endElement(I18nTransformer.I18N_NAMESPACE_URI, "text", "i18n:text");
305
306            if (hasParameter)
307            {
308                if (parameters != null)
309                {
310                    // Ordered parameters version.
311                    for (String parameter : parameters)
312                    {
313                        if (parameter != null)
314                        {
315                            handler.startElement(I18nTransformer.I18N_NAMESPACE_URI, "param", "i18n:param", new AttributesImpl());
316                            handler.characters(parameter.toCharArray(), 0, parameter.length());
317                            handler.endElement(I18nTransformer.I18N_NAMESPACE_URI, "param", "i18n:param");
318                        }
319                    }
320                }
321                else if (parameterMap != null)
322                {
323                    // Named parameters version.
324                    for (String parameterName : parameterMap.keySet())
325                    {
326                        I18nizableTextParameter value = parameterMap.get(parameterName);
327                        AttributesImpl attrs = new AttributesImpl();
328                        attrs.addCDATAAttribute("name", parameterName);
329                        handler.startElement(I18nTransformer.I18N_NAMESPACE_URI, "param", "i18n:param", attrs);
330                        value.toSAXAsParam(handler);
331                        handler.endElement(I18nTransformer.I18N_NAMESPACE_URI, "param", "i18n:param");
332                    }
333                }
334
335                handler.endElement(I18nTransformer.I18N_NAMESPACE_URI, "translate", "i18n:translate");
336            }
337
338            handler.endPrefixMapping("i18n");
339        }
340        else
341        {
342            handler.characters(getLabel().toCharArray(), 0, getLabel().length());
343        }
344    }
345
346    public void toSAXAsParam(ContentHandler handler) throws SAXException
347    {
348        if (isI18n())
349        {
350            AttributesImpl atts = new AttributesImpl();
351            if (getCatalogue() != null)
352            {
353                atts.addCDATAAttribute(I18nTransformer.I18N_NAMESPACE_URI, "catalogue", "i18n:catalogue", getCatalogue());
354            }
355    
356            handler.startElement(I18nTransformer.I18N_NAMESPACE_URI, "text", "i18n:text", atts);
357            handler.characters(_key.toCharArray(), 0, _key.length());
358            handler.endElement(I18nTransformer.I18N_NAMESPACE_URI, "text", "i18n:text");
359        }
360        else
361        {
362            handler.characters(getLabel().toCharArray(), 0, getLabel().length());
363        }
364    }
365
366    @Override
367    public String toString()
368    {
369        String result = "";
370        if (isI18n())
371        {
372            result += getCatalogue() + ":" + getKey();
373            List<String> parameters = getParameters();
374            if (parameters != null)
375            {
376                result += "[";
377                boolean isFirst = true;
378                for (String parameter : parameters)
379                {
380                    if (!isFirst)
381                    {                        
382                        result += "; param : " + parameter;
383                    }
384                    else
385                    {                        
386                        result += "param : " + parameter;
387                        isFirst = false;
388                    }
389                }
390                result += "]";
391            }
392        }
393        else
394        {
395            result = getLabel();
396        }
397        return result;
398    }
399
400    @Override
401    public int hashCode()
402    {
403        HashCodeBuilder hashCodeBuilder = new HashCodeBuilder();
404        hashCodeBuilder.append(_i18n);
405        hashCodeBuilder.append(_key);
406        hashCodeBuilder.append(_catalogueLocation);
407        hashCodeBuilder.append(_catalogueBundleName);
408        hashCodeBuilder.append(_catalogue);
409        hashCodeBuilder.append(_directLabel);
410
411        if (_parameters == null)
412        {
413            hashCodeBuilder.append((Object) null);
414        }
415        else
416        {
417            hashCodeBuilder.append(_parameters.toArray(new String[_parameters.size()]));
418        }
419
420        hashCodeBuilder.append(_parameterMap);
421
422        return hashCodeBuilder.toHashCode();
423    }
424
425    @Override
426    public boolean equals(Object obj)
427    { 
428        if (obj == null)
429        {
430            return false;
431        }
432
433        if (!(obj instanceof I18nizableText))
434        {
435            return false;
436        }
437
438        if (this == obj)
439        {
440            return true;
441        }
442
443        I18nizableText i18nObj = (I18nizableText) obj;
444        if (_i18n != i18nObj._i18n)
445        {
446            return false;
447        }
448
449        EqualsBuilder equalsBuilder = new EqualsBuilder();
450        if (_i18n)
451        {
452            equalsBuilder.append(_key, i18nObj._key);
453            
454            if (_catalogue == null)
455            {
456                equalsBuilder.append(_catalogueLocation, i18nObj._catalogueLocation);
457                equalsBuilder.append(_catalogueBundleName, i18nObj._catalogueBundleName);
458            }
459            else
460            {
461                equalsBuilder.append(_catalogue, i18nObj._catalogue);
462            }
463
464            if (_parameters == null)
465            {
466                equalsBuilder.append(_parameters, i18nObj._parameters);
467            }
468            else
469            {
470                String[] otherParameters = null;
471
472                if (i18nObj._parameters != null)
473                {
474                    otherParameters = i18nObj._parameters.toArray(new String[i18nObj._parameters.size()]);
475                }
476
477                equalsBuilder.append(_parameters.toArray(new String[_parameters.size()]),
478                        otherParameters);
479            }
480
481            equalsBuilder.append(_parameterMap, i18nObj.getParameterMap());
482        }
483        else
484        {
485            equalsBuilder.append(_directLabel, i18nObj._directLabel);
486        }
487
488        return equalsBuilder.isEquals();
489    }
490
491    private static boolean isI18n(Configuration config)
492    {
493        return config.getAttributeAsBoolean("i18n", false) || config.getAttribute("type", "").equals("i18n");
494    }
495
496    /**
497     * Get an i18n text configuration (can be a key or a "direct" string).
498     * @param config The configuration to parse.
499     * @param catalogueLocation The i18n catalogue location URI
500     * @param catalogueFilename The i18n catalogue bundle name
501     * @param value The i18n text, can be a key or a "direct" string.
502     * @return The i18nizable text
503     */
504    private static I18nizableText getI18nizableTextValue(Configuration config, String catalogueLocation, String catalogueFilename, String value)
505    {
506        return new I18nizableText(catalogueLocation, catalogueFilename, value);
507    }
508
509    /**
510     * Get an i18n text configuration (can be a key or a "direct" string).
511     * @param config The configuration to parse.
512     * @param defaultCatalogue The i18n catalogue to use when not specified.  
513     * @param value The i18n text, can be a key or a "direct" string.
514     * @return The i18nizable text or null if the config is null
515     */
516    public static I18nizableText getI18nizableTextValue(Configuration config, String defaultCatalogue, String value)
517    {
518        if (config != null)
519        {
520            if (I18nizableText.isI18n(config))
521            {
522                String catalogue = config.getAttribute("catalogue", defaultCatalogue);
523                
524                return new I18nizableText(catalogue, value);
525            }
526            else
527            {
528                return new I18nizableText(value);
529            }
530        }
531        else
532        {
533            return null;
534        }
535    }
536
537    /**
538     * Parse a i18n text configuration.
539     * @param config the configuration to use.
540     * @param catalogueLocation The i18n catalog location URI
541     * @param catalogueFilename The i18n catalog bundle name
542     * @param defaultValue The default value key in configuration
543     * @return the i18n text or null if the config is null
544     * @throws ConfigurationException if the configuration is not valid.
545     */
546    public static I18nizableText parseI18nizableText(Configuration config, String catalogueLocation, String catalogueFilename, String defaultValue) throws ConfigurationException
547    {
548        if (config != null)
549        {
550            String text = config.getValue(defaultValue);
551            return I18nizableText.getI18nizableTextValue(config, catalogueLocation, catalogueFilename, text);
552        }
553        else
554        {
555            return null;
556        }
557    }
558
559    /**
560     * Parse an optional i18n text configuration, with a default value.
561     * @param config the configuration to use.
562     * @param defaultCatalogue the i18n catalogue to use when not specified. 
563     * @param defaultValue the default value key in configuration.
564     * @return the i18n text or null if the config is null
565     */
566    public static I18nizableText parseI18nizableText(Configuration config, String defaultCatalogue, String defaultValue)
567    {
568        if (config != null)
569        {
570            String text = config.getValue(defaultValue);
571            return I18nizableText.getI18nizableTextValue(config, defaultCatalogue, text);
572        }
573        else
574        {
575            return null;
576        }
577    }
578
579    /**
580     * Parse an optional i18n text configuration, with a default i18n text value.
581     * @param config the configuration to use.
582     * @param defaultCatalog the i18n catalog to use when not specified. 
583     * @param defaultValue the default i18n text value.
584     * @return the i18n text or null if the config is null
585     */
586    public static I18nizableText parseI18nizableText(Configuration config, String defaultCatalog, I18nizableText defaultValue)
587    {
588        if (config != null)
589        {
590            return Optional.ofNullable(config.getValue(null))
591                           .map(text -> I18nizableText.getI18nizableTextValue(config, defaultCatalog, text))
592                           .orElse(defaultValue);
593        }
594        else
595        {
596            return null;
597        }
598    }
599
600    /**
601     * Parse a mandatory i18n text configuration, throwing an exception if empty.
602     * @param config the configuration to use.
603     * @param defaultCatalogue the i18n catalogue to use when not specified.
604     * @return the i18n text or null if the config is null
605     * @throws ConfigurationException if the configuration is not valid.
606     */
607    public static I18nizableText parseI18nizableText(Configuration config, String defaultCatalogue) throws ConfigurationException
608    {
609        if (config != null)
610        {
611            String text = config.getValue();
612            return I18nizableText.getI18nizableTextValue(config, defaultCatalogue, text);
613        }
614        else
615        {
616            return null;
617        }
618    }
619    
620    /**
621     * Gets a string representation of a i18n text
622     * @param i18nizableText The i18 text
623     * @return The string representation of the i18n text
624     */
625    public static String i18nizableTextToString(I18nizableText i18nizableText)
626    {
627        Map<String, Object> map = new HashMap<>();
628        if (i18nizableText.isI18n())
629        {
630            map.put("key", i18nizableText.getKey());
631            map.put("catalogue", i18nizableText.getCatalogue());
632            map.put("parameters", i18nizableText.getParameters());
633        }
634        else
635        {
636            map.put("label", i18nizableText.getLabel());
637        }
638        
639        // Use Java Bean XMLEncoder
640        ByteArrayOutputStream bos = new ByteArrayOutputStream();
641        try (XMLEncoder xmlEncoder = new XMLEncoder(bos))
642        {
643            xmlEncoder.writeObject(map);
644            xmlEncoder.flush();
645        }
646        
647        try
648        {
649            return bos.toString("UTF-8");
650        }
651        catch (UnsupportedEncodingException e)
652        {
653            // should not happen
654            throw new IllegalStateException(e);
655        }
656    }
657    
658    /**
659     * Returns the i18n text from its string representation
660     * @param str The string representation of the i18n text
661     * @return The i18n text
662     */
663    @SuppressWarnings("unchecked")
664    public static I18nizableText stringToI18nizableText(String str)
665    {
666        Map<String, Object> map;
667        // Use Java Bean XMLDecoder
668        try (XMLDecoder xmlDecoder = new XMLDecoder(new ByteArrayInputStream(str.getBytes("UTF-8"))))
669        {
670            map = (Map<String, Object>) xmlDecoder.readObject();
671        }
672        catch (UnsupportedEncodingException e)
673        {
674            // should not happen
675            throw new IllegalStateException(e);
676        }
677        
678        if (map.get("label") != null)
679        {
680            return new I18nizableText((String) map.get("label"));
681        }
682        else
683        {
684            String key = (String) map.get("key");
685            String catalogue = (String) map.get("catalogue");
686            List<String> parameters = (List) map.get("parameters");
687            return new I18nizableText(catalogue, key, parameters);
688        }
689    }
690}