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