001/*
002 *  Copyright 2017 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.plugins.repository.metadata;
017
018import java.util.Collections;
019import java.util.Iterator;
020import java.util.LinkedHashMap;
021import java.util.List;
022import java.util.Locale;
023import java.util.Map;
024import java.util.Set;
025
026import org.apache.cocoon.xml.AttributesImpl;
027import org.apache.cocoon.xml.XMLUtils;
028import org.apache.commons.lang3.LocaleUtils;
029import org.apache.commons.lang3.StringUtils;
030import org.w3c.dom.Element;
031import org.w3c.dom.Node;
032import org.xml.sax.ContentHandler;
033import org.xml.sax.SAXException;
034
035import org.ametys.core.util.dom.DOMUtils;
036import org.ametys.plugins.repository.AmetysRepositoryException;
037import org.ametys.plugins.repository.data.holder.DataHolder;
038
039/**
040 * Helper methods for {@link MultilingualString} metadata
041 */
042public final class MultilingualStringHelper
043{
044    /** The default locale */
045    public static final Locale DEFAULT_LOCALE = Locale.ENGLISH;
046    
047    /** The separator between the locale and the value for the string representation of a multilingual string */
048    private static final String __LOCALE_AND_VALUE_SEPARATOR = ":";
049    /** The separator between the entries for the string representation of a multilingual string */
050    private static final String __ENTRIES_SEPARATOR = "#";
051 
052    private MultilingualStringHelper()
053    {
054        // Hide default constructor.
055    }
056    
057    /**
058     * Returns the closest non-empty value for a {@link MultilingualString} metadata in a given locale.
059     * If no close locale is found, return the value for {@link Locale#ENGLISH} if exists.<br>
060     * Otherwise, the value of first stored locale will be returned.<br>
061     * @param parentMetadata The metadata holder
062     * @param metadataName The metadata name
063     * @param locale The requested locale
064     * @return the closest non-empty localized value or <code>null</code> if not found.
065     * @throws AmetysRepositoryException if an errors occurs.
066     * @deprecated Use {@link #getValue(DataHolder, String, Locale)} instead
067     */
068    @Deprecated
069    public static String getValue(CompositeMetadata parentMetadata, String metadataName, Locale locale) throws AmetysRepositoryException
070    {
071        if (parentMetadata.hasMetadata(metadataName))
072        {
073            MultilingualString multilingualString = parentMetadata.getMultilingualString(metadataName);
074            
075            return getValue(multilingualString, locale);
076        }
077        
078        return null;
079    }
080    
081    /**
082     * Returns the closest non-empty value for a {@link MultilingualString} element in a given locale.
083     * If no close locale is found, return the value for {@link Locale#ENGLISH} if exists.<br>
084     * Otherwise, the value of first stored locale will be returned.<br>
085     * @param dataHolder data holder
086     * @param dataName The data name
087     * @param locale The requested locale
088     * @return the closest non-empty localized value or <code>null</code> if not found.
089     * @throws AmetysRepositoryException if an errors occurs.
090     */
091    public static String getValue(DataHolder dataHolder, String dataName, Locale locale) throws AmetysRepositoryException
092    {
093        if (dataHolder.hasValue(dataName))
094        {
095            MultilingualString multilingualString = dataHolder.getValue(dataName);
096            return getValue(multilingualString, locale);
097        }
098        return null;
099    }
100    
101    /**
102     * Returns the closest non-empty value for a {@link MultilingualString} in a given locale.
103     * If no close locale is found, return the value for {@link Locale#ENGLISH} if exists.<br>
104     * Otherwise, the value of first stored locale will be returned.<br>
105     * @param multilingualString The multilingual string
106     * @param locale The requested locale. Can be null.
107     * @return the closest non-empty localized value or <code>null</code> if not found.
108     * @throws AmetysRepositoryException if an errors occurs.
109     */
110    public static String getValue(MultilingualString multilingualString, Locale locale) throws AmetysRepositoryException
111    {
112        Locale closestLocale = getClosestNonEmptyLocale(multilingualString, locale);
113        if (closestLocale != null)
114        {
115            return multilingualString.getValue(closestLocale);
116        }
117        else
118        {
119            return null;
120        }
121    }
122    
123    /**
124     * Returns the closest non-empty locale for a {@link MultilingualString} in a given locale.
125     * If no close locale is found, return the {@link Locale#ENGLISH} if a values exists.<br>
126     * Otherwise, the first stored locale will be returned.<br>
127     * @param multilingualString The multilingual string
128     * @param locale The requested locale. Can be null.
129     * @return the closest non-empty locale or <code>null</code> if not found.
130     * @throws AmetysRepositoryException if an errors occurs.
131     */
132    public static Locale getClosestNonEmptyLocale(MultilingualString multilingualString, Locale locale) throws AmetysRepositoryException
133    {
134        if (locale != null && multilingualString.hasLocale(locale))
135        {
136            return locale;
137        }
138        else
139        {
140            // Try to find the closest locale
141            
142            List<Locale> closedLocales = localeLookupList(locale);
143            for (Locale closedLocale : closedLocales)
144            {
145                if (multilingualString.hasLocale(closedLocale))
146                {
147                    return closedLocale;
148                }
149            }
150            
151            // No locale found, get the first stored locale
152            Set<Locale> allLocales = multilingualString.getLocales();
153            if (!allLocales.isEmpty())
154            {
155                return allLocales.iterator().next();
156            }
157            
158            return null;
159        }
160    }
161    
162    /**
163     * Return the list of closest locales to search
164     * @param locale the locale to start from. If null, returns the default locale
165     * @return the unmodifiable list of Locale objects, 0 being locale, not null
166     */
167    public static List<Locale> localeLookupList(Locale locale)
168    {
169        if (locale != null)
170        {
171            return org.apache.commons.lang3.LocaleUtils.localeLookupList(locale, DEFAULT_LOCALE);
172        }
173        else
174        {
175            return Collections.singletonList(DEFAULT_LOCALE);
176        }
177    }
178    
179    /**
180     * Saxes the given multilingual string
181     * @param contentHandler the content handler where to SAX into.
182     * @param tagName the name of the tag to sax the multilingual string
183     * @param multilingualString the multilingual string to sax
184     * @param locale the requested locale. Can be null.
185     * @throws SAXException if an errors occurs during the value saxing
186     * @deprecated Use {@link #sax(ContentHandler, String, MultilingualString, AttributesImpl, Locale)} instead
187     */
188    @Deprecated
189    public static void sax(ContentHandler contentHandler, String tagName, MultilingualString multilingualString, Locale locale) throws SAXException
190    {
191        sax(contentHandler, tagName, multilingualString, new AttributesImpl(), locale);
192    }
193    
194    /**
195     * Saxes the given multilingual string
196     * @param contentHandler the content handler where to SAX into.
197     * @param tagName the name of the tag to sax the multilingual string
198     * @param multilingualString the multilingual string to sax
199     * @param attributes the attributes to sax the multilingual string
200     * @param locale the requested locale. Can be null.
201     * @throws SAXException if an errors occurs during the value saxing
202     */
203    public static void sax(ContentHandler contentHandler, String tagName, MultilingualString multilingualString, AttributesImpl attributes, Locale locale) throws SAXException
204    {
205        AttributesImpl localAttributes = new AttributesImpl(attributes);
206        if (locale == null)
207        {
208            // Given locale is null, sax all existing locales
209            XMLUtils.startElement(contentHandler, tagName, localAttributes);
210            for (Locale valueLocale : multilingualString.getLocales())
211            {
212                XMLUtils.createElement(contentHandler, valueLocale.toString(), multilingualString.getValue(valueLocale));
213            }
214            XMLUtils.endElement(contentHandler, tagName);
215        }
216        else
217        {
218            Locale closestLocale = getClosestNonEmptyLocale(multilingualString, locale);
219            if (closestLocale != null)
220            {
221                localAttributes.addCDATAAttribute("lang", closestLocale.toString());
222                XMLUtils.createElement(contentHandler, tagName, localAttributes, multilingualString.getValue(closestLocale));
223            }
224        }
225    }
226    
227    /**
228     * Get the {@link MultilingualString} object from the given {@link Node}
229     * @param element the DOM element containing the multilingual string data
230     * @return the {@link MultilingualString} object
231     */
232    public static MultilingualString fromXML(Element element)
233    {
234        if (element != null)
235        {
236            String lang = element.getAttribute("lang");
237            if (StringUtils.isNotEmpty(lang))
238            {
239                MultilingualString multilingualString = new MultilingualString();
240                String value = element.getTextContent();
241                multilingualString.add(LocaleUtils.toLocale(lang), value);
242                return multilingualString;
243            }
244            else
245            {
246                MultilingualString multilingualString = new MultilingualString();
247                for (Element entry : DOMUtils.getChildElements(element))
248                {
249                    String languageTag = entry.getNodeName();
250                    String value = entry.getTextContent();
251                    multilingualString.add(LocaleUtils.toLocale(languageTag), value);
252                }
253                return multilingualString;
254            }
255        }
256        
257        return null;
258    }
259    
260    /**
261     * Get the JSON representation of a {@link MultilingualString}
262     * @param multilingualString The multilingual string. Cannot be null.
263     * @return A map with the locales and values.
264     * @throws AmetysRepositoryException if an error occurs
265     */
266    public static Map<String, Object> toJson(MultilingualString multilingualString) throws AmetysRepositoryException
267    {
268        Map<String, Object> json = new LinkedHashMap<>();
269        
270        for (Locale locale : multilingualString.getLocales())
271        {
272            json.put(locale.toString(), multilingualString.getValue(locale));
273        }
274        
275        return json;
276    }
277    
278    /**
279     * Get the {@link MultilingualString} object from its JSON representation
280     * @param json the JSON representation of the multilingual string
281     * @return the {@link MultilingualString} object
282     */
283    public static MultilingualString fromJSON(Map<String, ? extends Object> json)
284    {
285        if (json == null)
286        {
287            return null;
288        }
289        
290        MultilingualString multilingualString = new MultilingualString();
291        
292        for (Map.Entry<String, ? extends Object> entry : json.entrySet())
293        {
294            Locale locale = LocaleUtils.toLocale(entry.getKey());
295            String value = entry.getValue().toString();
296            multilingualString.add(locale, value);
297        }
298        
299        return multilingualString;
300    }
301    
302    /**
303     * Retrieves a string representation of a {@link MultilingualString}
304     * @param multilingualString the multilingual string
305     * @return thestring representation of the multilingual string
306     */
307    public static String toString(MultilingualString multilingualString)
308    {
309        if (multilingualString != null)
310        {
311            StringBuilder asString = new StringBuilder();
312            Iterator<Locale> localesIterator = multilingualString.getLocales().iterator();
313            while (localesIterator.hasNext())
314            {
315                Locale locale = localesIterator.next();
316                asString.append(locale.toString()).append(__LOCALE_AND_VALUE_SEPARATOR).append(multilingualString.getValue(locale));
317                if (localesIterator.hasNext())
318                {
319                    asString.append(__ENTRIES_SEPARATOR);
320                }
321            }
322            return asString.toString();
323        }
324        else
325        {
326            return null;
327        }
328    }
329    
330    /**
331     * Retrieves the {@link MultilingualString} from its string representation
332     * @param string the string representation of the multilingual string
333     * @return the multilingual string from its string representation, or <code>null</code> if the given string value is null or empty
334     * @throws IllegalArgumentException if the given string value can't be cast to a multilingual string
335     */
336    public static MultilingualString fromString(String string) throws IllegalArgumentException
337    {
338        if (StringUtils.isEmpty(string))
339        {
340            return null;
341        }
342        
343        if (string.contains(__LOCALE_AND_VALUE_SEPARATOR))
344        {
345            MultilingualString multilingualString = new MultilingualString();
346            
347            String[] entries = string.split(__ENTRIES_SEPARATOR);
348            for (String entry : entries)
349            {
350                String localeAsString = StringUtils.substringBeforeLast(entry, __LOCALE_AND_VALUE_SEPARATOR);
351                String value = StringUtils.substringAfterLast(entry, __LOCALE_AND_VALUE_SEPARATOR);
352                multilingualString.add(LocaleUtils.toLocale(localeAsString), value);
353            }
354            
355            return multilingualString;
356        }
357        else
358        {
359            throw new IllegalArgumentException("Unable to cast '" + string + "' to a multilingual string");
360        }
361    }
362    
363    /**
364     * Check if a given string matches the Multilingual string pattern
365     * @param string the string representation of the multilingual string to check
366     * @return <code>true</code> if the string have a correct pattern, <code>false</code> otherwise
367     */
368    public static boolean matchesMultilingualStringPattern(String string)
369    {
370        if (string.contains(__LOCALE_AND_VALUE_SEPARATOR))
371        {
372            boolean localeExists = true;
373            
374            String[] entries = string.split(__ENTRIES_SEPARATOR);
375            for (String entry : entries)
376            {
377                String localeAsString = StringUtils.substringBeforeLast(entry, __LOCALE_AND_VALUE_SEPARATOR);
378                
379                try
380                {
381                    // Try to convert the string part to a Locale
382                    LocaleUtils.toLocale(localeAsString);
383                }
384                catch (IllegalArgumentException e)
385                {
386                    // If at least one string part that should represent a locale is not a locale,
387                    // then the string is not considered as a multilingual string
388                    return false;
389                }
390            }
391            return localeExists;
392        }
393        return false;
394    }
395}