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