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