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            List<Locale> closedLocales = localeLookupList(locale);
163            for (Locale closedLocale : closedLocales)
164            {
165                if (multilingualString.hasLocale(closedLocale))
166                {
167                    return closedLocale;
168                }
169            }
170            
171            // No locale found, get the first stored locale
172            Set<Locale> allLocales = multilingualString.getLocales();
173            if (!allLocales.isEmpty())
174            {
175                return allLocales.iterator().next();
176            }
177            
178            return null;
179        }
180    }
181    
182    /**
183     * Return the list of closest locales to search 
184     * @param locale  the locale to start from. If null, returns the default locale
185     * @return the unmodifiable list of Locale objects, 0 being locale, not null
186     */
187    public static List<Locale> localeLookupList(Locale locale)
188    {
189        if (locale != null)
190        {
191            return LocaleUtils.localeLookupList(locale, DEFAULT_LOCALE);
192        }
193        else
194        {
195            return Collections.singletonList(DEFAULT_LOCALE);
196        }
197    }
198    
199    /**
200     * Saxes the given multilingual string
201     * @param contentHandler the content handler where to SAX into.
202     * @param tagName the name of the tag to sax the multilingual string 
203     * @param multilingualString the multilingual string to sax
204     * @param locale the requested locale. Can be null.
205     * @throws SAXException if an errors occurs during the value saxing
206     * @deprecated Use {@link #sax(ContentHandler, String, MultilingualString, AttributesImpl, Locale)} instead
207     */
208    @Deprecated
209    public static void sax(ContentHandler contentHandler, String tagName, MultilingualString multilingualString, Locale locale) throws SAXException
210    {
211        sax(contentHandler, tagName, multilingualString, new AttributesImpl(), locale);
212    }
213    
214    /**
215     * Saxes the given multilingual string
216     * @param contentHandler the content handler where to SAX into.
217     * @param tagName the name of the tag to sax the multilingual string 
218     * @param multilingualString the multilingual string to sax
219     * @param attributes the attributes to sax the multilingual string
220     * @param locale the requested locale. Can be null.
221     * @throws SAXException if an errors occurs during the value saxing
222     */
223    public static void sax(ContentHandler contentHandler, String tagName, MultilingualString multilingualString, AttributesImpl attributes, Locale locale) throws SAXException
224    {
225        AttributesImpl localAttributes = new AttributesImpl(attributes);
226        if (locale == null)
227        {
228            // Given locale is null, sax all existing locales
229            XMLUtils.startElement(contentHandler, tagName, localAttributes);
230            for (Locale valueLocale : multilingualString.getLocales())
231            {
232                XMLUtils.createElement(contentHandler, valueLocale.toString(), multilingualString.getValue(valueLocale));
233            }
234            XMLUtils.endElement(contentHandler, tagName);
235        }
236        else
237        {
238            Locale closestLocale = getClosestNonEmptyLocale(multilingualString, locale);
239            if (closestLocale != null)
240            {
241                localAttributes.addCDATAAttribute("lang", closestLocale.toString());
242                XMLUtils.createElement(contentHandler, tagName, localAttributes, multilingualString.getValue(closestLocale));
243            }
244        }
245    }
246    
247    /**
248     * Get the {@link MultilingualString} object from the given {@link Node}
249     * @param element the DOM element containing the multilingual string data
250     * @return the {@link MultilingualString} object
251     */
252    public static MultilingualString fromXML(Element element)
253    {
254        if (element != null)
255        {
256            String lang = element.getAttribute("lang");
257            if (StringUtils.isNotEmpty(lang))
258            {
259                MultilingualString multilingualString = new MultilingualString();
260                String value = element.getTextContent();
261                multilingualString.add(Locale.forLanguageTag(lang), value);
262                return multilingualString;
263            }
264            else
265            {
266                MultilingualString multilingualString = new MultilingualString(); 
267                for (Element entry : DOMUtils.getChildElements(element))
268                {
269                    String languageTag = entry.getNodeName();
270                    String value = entry.getTextContent();
271                    multilingualString.add(Locale.forLanguageTag(languageTag), value);
272                }
273                return multilingualString;
274            }
275        }
276        
277        return null;
278    }
279    
280    /**
281     * Get the JSON representation of a {@link MultilingualString}
282     * @param multilingualString The multilingual string. Cannot be null.
283     * @return A map with the locales and values.
284     * @throws AmetysRepositoryException if an error occurs
285     */
286    public static Map<String, Object> toJson(MultilingualString multilingualString) throws AmetysRepositoryException
287    {
288        Map<String, Object> json = new LinkedHashMap<>();
289        
290        for (Locale locale : multilingualString.getLocales())
291        {
292            json.put(locale.toString(), multilingualString.getValue(locale));
293        }
294        
295        return json;
296    }
297    
298    /**
299     * Get the {@link MultilingualString} object from its JSON representation 
300     * @param json the JSON representation of the multilingual string
301     * @return the {@link MultilingualString} object
302     */
303    public static MultilingualString fromJSON(Map<String, ? extends Object> json)
304    {
305        if (json == null)
306        {
307            return null;
308        }
309        
310        MultilingualString multilingualString = new MultilingualString();
311        
312        for (Map.Entry<String, ? extends Object> entry : json.entrySet())
313        {
314            Locale locale = LocaleUtils.toLocale(entry.getKey());
315            String value = entry.getValue().toString();
316            multilingualString.add(locale, value);
317        }
318        
319        return multilingualString;
320    }
321    
322    /**
323     * Retrieves a string representation of a {@link MultilingualString}
324     * @param multilingualString the multilingual string
325     * @return thestring representation of the multilingual string
326     */
327    public static String toString(MultilingualString multilingualString)
328    {
329        if (multilingualString != null)
330        {
331            StringBuilder asString = new StringBuilder();
332            Iterator<Locale> localesIterator = multilingualString.getLocales().iterator();
333            while (localesIterator.hasNext())
334            {
335                Locale locale = localesIterator.next();
336                asString.append(locale.toString()).append(__LOCALE_AND_VALUE_SEPARATOR).append(multilingualString.getValue(locale));
337                if (localesIterator.hasNext())
338                {
339                    asString.append(__ENTRIES_SEPARATOR);
340                }
341            }
342            return asString.toString();
343        }
344        else
345        {
346            return null;
347        }
348    }
349    
350    /**
351     * Retrieves the {@link MultilingualString} from its string representation
352     * @param string the string representation of the multilingual string
353     * @return the multilingual string from its string representation, or <code>null</code> if the given string value is null or empty
354     * @throws IllegalArgumentException if the given string value can't be cast to a multilingual string
355     */
356    public static MultilingualString fromString(String string) throws IllegalArgumentException
357    {
358        if (StringUtils.isEmpty(string))
359        {
360            return null;
361        }
362        
363        if (string.contains(__LOCALE_AND_VALUE_SEPARATOR))
364        {
365            MultilingualString multilingualString = new MultilingualString();
366            
367            String[] entries = string.split(__ENTRIES_SEPARATOR);
368            for (String entry : entries)
369            {
370                String localeAsString = StringUtils.substringBeforeLast(entry, __LOCALE_AND_VALUE_SEPARATOR);
371                String value = StringUtils.substringAfterLast(entry, __LOCALE_AND_VALUE_SEPARATOR);
372                multilingualString.add(LocaleUtils.toLocale(localeAsString), value);
373            }
374            
375            return multilingualString;
376        }
377        else
378        {
379            throw new IllegalArgumentException("Unable to cast '" + string + "' to a multilingual string");
380        }
381    }
382    
383    /**
384     * Check if a given string matches the Multilingual string pattern
385     * @param string the string representation of the multilingual string to check
386     * @return <code>true</code> if the string have a correct pattern, <code>false</code> otherwise
387     */
388    public static boolean matchesMultilingualStringPattern(String string)
389    {
390        if (string.contains(__LOCALE_AND_VALUE_SEPARATOR))
391        {
392            boolean localeExists = true;
393            
394            String[] entries = string.split(__ENTRIES_SEPARATOR);
395            for (String entry : entries)
396            {
397                String localeAsString = StringUtils.substringBeforeLast(entry, __LOCALE_AND_VALUE_SEPARATOR);
398                
399                try
400                {
401                    // Try to convert the string part to a Locale
402                    LocaleUtils.toLocale(localeAsString);
403                }
404                catch (IllegalArgumentException e)
405                {
406                    // If at least one string part that should represent a locale is not a locale,
407                    // then the string is not considered as a multilingual string
408                    return false;
409                }
410            }
411            return localeExists;
412        }
413        return false;
414    }
415}