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.util.List;
019import java.util.Map;
020import java.util.Optional;
021
022import org.apache.avalon.framework.configuration.Configuration;
023import org.apache.avalon.framework.configuration.ConfigurationException;
024import org.apache.cocoon.transformation.I18nTransformer;
025import org.apache.cocoon.xml.AttributesImpl;
026import org.apache.commons.lang3.builder.EqualsBuilder;
027import org.apache.commons.lang3.builder.HashCodeBuilder;
028import org.xml.sax.ContentHandler;
029import org.xml.sax.SAXException;
030
031/**
032 * This class wraps a text that <i>may</i> be internationalized.
033 */
034public final class I18nizableText implements I18nizable, I18nizableTextParameter
035{
036    private final boolean _i18n;
037    private String _directLabel;
038    private String _key;
039    private String _catalogue;
040    private List<String> _parameters;
041    private Map<String, I18nizableTextParameter> _parameterMap;
042    private String _catalogueLocation;
043    private String _catalogueBundleName;
044
045    /**
046     * Create a simple international text
047     * @param label The text. Cannot be null.
048     */
049    public I18nizableText(String label)
050    {
051        _i18n = false;
052        _directLabel = label;
053    }
054
055    /**
056     * Create an i18nized text
057     * @param catalogue the catalogue where the key is defined. Can be null. Can be overloaded by the catalogue in the key.
058     * @param key the key of the text. Cannot be null. May include the catalogue using the character ':' as separator. CATALOG:KEY.
059     */
060    public I18nizableText(String catalogue, String key)
061    {
062        this(catalogue, key, (List<String>) null);
063    }
064
065    /**
066     * Create an i18nized text with ordered parameters.
067     * @param catalogue the catalogue where the key is defined. Can be null. Can be overloaded by the catalogue in the key.
068     * @param key the key of the text. Cannot be null. May include the catalogue using the character ':' as separator. CATALOG:KEY.
069     * @param parameters the parameters of the key if any. Can be null.
070     */
071    public I18nizableText(String catalogue, String key, List<String> parameters)
072    {
073        _i18n = true;
074
075        String i18nKey = key.substring(key.indexOf(":") + 1);
076        String i18nCatalogue = i18nKey.length() == key.length() ? catalogue : key.substring(0, key.length() - i18nKey.length() - 1);
077
078        _catalogue = i18nCatalogue;
079        _key = i18nKey;
080        _parameters = parameters;
081        _parameterMap = null;
082    }
083
084    /**
085     * Create an i18nized text with named parameters.
086     * @param catalogue the catalogue where the key is defined. Can be null. Can be overloaded by the catalogue in the key.
087     * @param key the key of the text. Cannot be null. May include the catalogue using the character ':' as separator. CATALOG:KEY.
088     * @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.
089     */
090    public I18nizableText(String catalogue, String key, Map<String, I18nizableTextParameter> parameters)
091    {
092        _i18n = true;
093
094        String i18nKey = key.substring(key.indexOf(":") + 1);
095        String i18nCatalogue = i18nKey.length() == key.length() ? catalogue : key.substring(0, key.length() - i18nKey.length() - 1);
096
097        _catalogue = i18nCatalogue;
098        _key = i18nKey;
099        _parameterMap = parameters;
100        _parameters = null;
101    }
102    
103    /**
104     * Create an i18nized text.
105     * Use this constructor only when the catalogue is an external catalogue, not managed by Ametys application
106     * @param catalogueLocation the file location URI of the catalogue where the key is defined.
107     * @param catalogueFilename the catalogue bundle name such as 'messages'
108     * @param key the key of the text. Cannot be null.
109     */
110    public I18nizableText(String catalogueLocation, String catalogueFilename, String key)
111    {
112        this(catalogueLocation, catalogueFilename, key, (List<String>) null);
113    }
114
115    /**
116     * Create an i18nized text with ordered parameters.
117     * Use this constructor only when the catalogue is an external catalogue, not managed by Ametys application
118     * @param catalogueLocation the file location URI of the catalogue where the key is defined.
119     * @param catalogueFilename the catalogue bundle name such as 'messages'
120     * @param key the key of the text. Cannot be null.
121     * @param parameters the parameters of the key if any. Can be null.
122     */
123    public I18nizableText(String catalogueLocation, String catalogueFilename, String key, List<String> parameters)
124    {
125        _i18n = true;
126
127        _catalogueLocation = catalogueLocation;
128        _catalogueBundleName = catalogueFilename;
129        _catalogue = null;
130        _key = key;
131        _parameters = parameters;
132        _parameterMap = null;
133    }
134
135    /**
136     * Create an i18nized text with named parameters.
137     * @param catalogueLocation the file location URI of the catalogue where the key is defined.
138     * @param catalogueFilename the catalogue bundle name such as 'messages'
139     * @param key the key of the text. Cannot be null.
140     * @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.
141     */
142    public I18nizableText(String catalogueLocation, String catalogueFilename, String key, Map<String, I18nizableTextParameter> parameters)
143    {
144        _i18n = true;
145
146        _catalogueLocation = catalogueLocation;
147        _catalogueBundleName = catalogueFilename;
148        _catalogue = null;
149        _key = key;
150        _parameterMap = parameters;
151        _parameters = null;
152    }
153
154    /**
155     * Determine whether the text is i18nized or a simple cross languages text.
156     * @return true if the text is i18nized and so defined by a catalogue, a key and optionally parameters.<br>false if the text is a simple label
157     */
158    public boolean isI18n()
159    {
160        return _i18n;
161    }
162
163    /**
164     * Get the catalogue of the i18nized text.
165     * @return The catalogue where the key is defined
166     */
167    public String getCatalogue()
168    {
169        if (_i18n)
170        {
171            return _catalogue;
172        }
173        else
174        {
175            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");
176        }
177    }
178    
179    /**
180     * Get the file location URI of the i18nized text.
181     * @return The catalogue location where the key is defined
182     */
183    public String getLocation()
184    {
185        if (_i18n)
186        {
187            return _catalogueLocation;
188        }
189        else
190        {
191            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");
192        }
193    }
194    
195    /**
196     * Get the files name of catalogue
197     * @return bundle name
198     */
199    public String getBundleName()
200    {
201        if (_i18n)
202        {
203            return _catalogueBundleName;
204        }
205        else
206        {
207            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");
208        }
209    }
210
211    /**
212     * Get the key of the i18nized text.
213     * @return The key in the catalogue
214     */
215    public String getKey()
216    {
217        if (_i18n)
218        {
219            return _key;
220        }
221        else
222        {
223            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");
224        }
225    }
226
227    /**
228     * Get the parameters of the key of the i18nized text.
229     * @return The list of parameters' values or null if there is no parameters
230     */
231    public List<String> getParameters()
232    {
233        if (_i18n)
234        {
235            return _parameters;
236        }
237        else
238        {
239            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");
240        }
241    }
242
243    /**
244     * Get the parameters of the key of the i18nized text.
245     * @return The list of parameters' values or null if there is no parameters
246     */
247    public Map<String, I18nizableTextParameter> getParameterMap()
248    {
249        if (_i18n)
250        {
251            return _parameterMap;
252        }
253        else
254        {
255            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");
256        }
257    }
258
259    /**
260     * Get the label if a text is not i18nized.
261     * @return The label
262     */
263    public String getLabel()
264    {
265        if (!_i18n)
266        {
267            return _directLabel;
268        }
269        else
270        {
271            throw new IllegalArgumentException("This text is i18nized and so do not have label. Use the 'isI18n' method to know whether a text is i18nized");
272        }
273    }
274
275    public void toSAX(ContentHandler handler) throws SAXException
276    {
277        if (isI18n())
278        {
279            List<String> parameters = getParameters();
280            Map<String, ? extends I18nizableTextParameter> parameterMap = getParameterMap();
281            boolean hasParameter = parameters != null && parameters.size() > 0 || parameterMap != null && !parameterMap.isEmpty();
282
283            handler.startPrefixMapping("i18n", I18nTransformer.I18N_NAMESPACE_URI);
284
285            if (hasParameter)
286            {
287                handler.startElement(I18nTransformer.I18N_NAMESPACE_URI, "translate", "i18n:translate", new AttributesImpl());
288            }
289
290            AttributesImpl atts = new AttributesImpl();
291            atts.addCDATAAttribute(I18nTransformer.I18N_NAMESPACE_URI, "key", "i18n:key", getKey());
292            if (getCatalogue() != null)
293            {
294                atts.addCDATAAttribute(I18nTransformer.I18N_NAMESPACE_URI, "catalogue", "i18n:catalogue", getCatalogue());
295            }
296
297            handler.startElement(I18nTransformer.I18N_NAMESPACE_URI, "text", "i18n:text", atts);
298            handler.endElement(I18nTransformer.I18N_NAMESPACE_URI, "text", "i18n:text");
299
300            if (hasParameter)
301            {
302                if (parameters != null)
303                {
304                    // Ordered parameters version.
305                    for (String parameter : parameters)
306                    {
307                        if (parameter != null)
308                        {
309                            handler.startElement(I18nTransformer.I18N_NAMESPACE_URI, "param", "i18n:param", new AttributesImpl());
310                            handler.characters(parameter.toCharArray(), 0, parameter.length());
311                            handler.endElement(I18nTransformer.I18N_NAMESPACE_URI, "param", "i18n:param");
312                        }
313                    }
314                }
315                else if (parameterMap != null)
316                {
317                    // Named parameters version.
318                    for (String parameterName : parameterMap.keySet())
319                    {
320                        I18nizableTextParameter value = parameterMap.get(parameterName);
321                        AttributesImpl attrs = new AttributesImpl();
322                        attrs.addCDATAAttribute("name", parameterName);
323                        handler.startElement(I18nTransformer.I18N_NAMESPACE_URI, "param", "i18n:param", attrs);
324                        value.toSAXAsParam(handler);
325                        handler.endElement(I18nTransformer.I18N_NAMESPACE_URI, "param", "i18n:param");
326                    }
327                }
328
329                handler.endElement(I18nTransformer.I18N_NAMESPACE_URI, "translate", "i18n:translate");
330            }
331
332            handler.endPrefixMapping("i18n");
333        }
334        else
335        {
336            handler.characters(getLabel().toCharArray(), 0, getLabel().length());
337        }
338    }
339
340    public void toSAXAsParam(ContentHandler handler) throws SAXException
341    {
342        if (isI18n())
343        {
344            AttributesImpl atts = new AttributesImpl();
345            if (getCatalogue() != null)
346            {
347                atts.addCDATAAttribute(I18nTransformer.I18N_NAMESPACE_URI, "catalogue", "i18n:catalogue", getCatalogue());
348            }
349    
350            handler.startElement(I18nTransformer.I18N_NAMESPACE_URI, "text", "i18n:text", atts);
351            handler.characters(_key.toCharArray(), 0, _key.length());
352            handler.endElement(I18nTransformer.I18N_NAMESPACE_URI, "text", "i18n:text");
353        }
354        else
355        {
356            handler.characters(getLabel().toCharArray(), 0, getLabel().length());
357        }
358    }
359
360    @Override
361    public String toString()
362    {
363        String result = "";
364        if (isI18n())
365        {
366            result += getCatalogue() + ":" + getKey();
367            List<String> parameters = getParameters();
368            if (parameters != null)
369            {
370                result += "[";
371                boolean isFirst = true;
372                for (String parameter : parameters)
373                {
374                    if (!isFirst)
375                    {
376                        result += "; param : " + parameter;
377                    }
378                    else
379                    {
380                        result += "param : " + parameter;
381                        isFirst = false;
382                    }
383                }
384                result += "]";
385            }
386        }
387        else
388        {
389            result = getLabel();
390        }
391        return result;
392    }
393
394    @Override
395    public int hashCode()
396    {
397        HashCodeBuilder hashCodeBuilder = new HashCodeBuilder();
398        hashCodeBuilder.append(_i18n);
399        hashCodeBuilder.append(_key);
400        hashCodeBuilder.append(_catalogueLocation);
401        hashCodeBuilder.append(_catalogueBundleName);
402        hashCodeBuilder.append(_catalogue);
403        hashCodeBuilder.append(_directLabel);
404
405        if (_parameters == null)
406        {
407            hashCodeBuilder.append((Object) null);
408        }
409        else
410        {
411            hashCodeBuilder.append(_parameters.toArray(new String[_parameters.size()]));
412        }
413
414        hashCodeBuilder.append(_parameterMap);
415
416        return hashCodeBuilder.toHashCode();
417    }
418
419    @Override
420    public boolean equals(Object obj)
421    {
422        if (obj == null)
423        {
424            return false;
425        }
426
427        if (!(obj instanceof I18nizableText))
428        {
429            return false;
430        }
431
432        if (this == obj)
433        {
434            return true;
435        }
436
437        I18nizableText i18nObj = (I18nizableText) obj;
438        if (_i18n != i18nObj._i18n)
439        {
440            return false;
441        }
442
443        EqualsBuilder equalsBuilder = new EqualsBuilder();
444        if (_i18n)
445        {
446            equalsBuilder.append(_key, i18nObj._key);
447            
448            if (_catalogue == null)
449            {
450                equalsBuilder.append(_catalogueLocation, i18nObj._catalogueLocation);
451                equalsBuilder.append(_catalogueBundleName, i18nObj._catalogueBundleName);
452            }
453            else
454            {
455                equalsBuilder.append(_catalogue, i18nObj._catalogue);
456            }
457
458            if (_parameters == null)
459            {
460                equalsBuilder.append(_parameters, i18nObj._parameters);
461            }
462            else
463            {
464                String[] otherParameters = null;
465
466                if (i18nObj._parameters != null)
467                {
468                    otherParameters = i18nObj._parameters.toArray(new String[i18nObj._parameters.size()]);
469                }
470
471                equalsBuilder.append(_parameters.toArray(new String[_parameters.size()]),
472                        otherParameters);
473            }
474
475            equalsBuilder.append(_parameterMap, i18nObj.getParameterMap());
476        }
477        else
478        {
479            equalsBuilder.append(_directLabel, i18nObj._directLabel);
480        }
481
482        return equalsBuilder.isEquals();
483    }
484
485    /**
486     * Check if configuration is an i18nizable text
487     * @param config the config to check
488     * @return true if the config is an I18n
489     */
490    public static boolean isI18n(Configuration config)
491    {
492        return config.getAttributeAsBoolean("i18n", false) || config.getAttribute("type", "").equals("i18n");
493    }
494
495    /**
496     * Get an i18n text configuration (can be a key or a "direct" string).
497     * @param config The configuration to parse.
498     * @param catalogueLocation The i18n catalogue location URI
499     * @param catalogueFilename The i18n catalogue bundle name
500     * @param value The i18n text, can be a key or a "direct" string.
501     * @return The i18nizable text
502     */
503    private static I18nizableText getI18nizableTextValue(Configuration config, String catalogueLocation, String catalogueFilename, String value)
504    {
505        return new I18nizableText(catalogueLocation, catalogueFilename, value);
506    }
507
508    /**
509     * Get an i18n text configuration (can be a key or a "direct" string).
510     * @param config The configuration to parse.
511     * @param defaultCatalogue The i18n catalogue to use when not specified.
512     * @param value The i18n text, can be a key or a "direct" string.
513     * @return The i18nizable text or null if the config is null
514     */
515    public static I18nizableText getI18nizableTextValue(Configuration config, String defaultCatalogue, String value)
516    {
517        if (config != null)
518        {
519            if (I18nizableText.isI18n(config))
520            {
521                String catalogue = config.getAttribute("catalogue", defaultCatalogue);
522                
523                return new I18nizableText(catalogue, value);
524            }
525            else
526            {
527                return new I18nizableText(value);
528            }
529        }
530        else
531        {
532            return null;
533        }
534    }
535
536    /**
537     * Parse a i18n text configuration.
538     * @param config the configuration to use.
539     * @param catalogueLocation The i18n catalogue location URI
540     * @param catalogueFilename The i18n catalogue bundle name
541     * @param defaultValue The default value key in configuration
542     * @return the i18n text or null if the config is null
543     */
544    public static I18nizableText parseI18nizableText(Configuration config, String catalogueLocation, String catalogueFilename, String defaultValue)
545    {
546        if (config != null)
547        {
548            String text = config.getValue(defaultValue);
549            return I18nizableText.getI18nizableTextValue(config, catalogueLocation, catalogueFilename, text);
550        }
551        else
552        {
553            return null;
554        }
555    }
556
557    /**
558     * Parse an optional i18n text configuration, with a default value.
559     * @param config the configuration to use.
560     * @param defaultCatalogue the i18n catalogue to use when not specified.
561     * @param defaultValue the default value key in configuration.
562     * @return the i18n text or null if the config is null
563     */
564    public static I18nizableText parseI18nizableText(Configuration config, String defaultCatalogue, String defaultValue)
565    {
566        return Optional.ofNullable(config)
567            .map(cfg -> cfg.getValue(defaultValue))
568            .map(text -> I18nizableText.getI18nizableTextValue(config, defaultCatalogue, text))
569            .orElse(new I18nizableText(defaultValue));
570    }
571
572    /**
573     * Parse an optional i18n text configuration, with a default i18n text value.
574     * @param config the configuration to use.
575     * @param defaultCatalogue the i18n catalogue to use when not specified.
576     * @param defaultValue the default i18n text value.
577     * @return the i18n text or null if the config is null
578     */
579    public static I18nizableText parseI18nizableText(Configuration config, String defaultCatalogue, I18nizableText defaultValue)
580    {
581        return Optional.ofNullable(config)
582            .map(cfg -> cfg.getValue(null))
583            .map(text -> I18nizableText.getI18nizableTextValue(config, defaultCatalogue, text))
584            .orElse(defaultValue);
585    }
586
587    /**
588     * Parse a mandatory i18n text configuration, throwing an exception if empty.
589     * @param config the configuration to use.
590     * @param defaultCatalogue the i18n catalogue to use when not specified.
591     * @return the i18n text or null if the config is null
592     * @throws ConfigurationException if the configuration is not valid.
593     */
594    public static I18nizableText parseI18nizableText(Configuration config, String defaultCatalogue) throws ConfigurationException
595    {
596        if (config != null)
597        {
598            String text = config.getValue();
599            return I18nizableText.getI18nizableTextValue(config, defaultCatalogue, text);
600        }
601        else
602        {
603            return null;
604        }
605    }
606}