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