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.hasValue(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.hasValue(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        if (locale == null)
228        {
229            // Given locale is null, sax all existing locales
230            XMLUtils.startElement(contentHandler, tagName, attributes);
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                attributes.addCDATAAttribute("lang", closestLocale.toString());
243                XMLUtils.createElement(contentHandler, tagName, attributes, 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     * @throws TransformerException if an error occurs while parsing the DOM node
253     */
254    public static MultilingualString fromXML(Element element) throws TransformerException
255    {
256        if (element != null)
257        {
258            String lang = element.getAttribute("lang");
259            if (StringUtils.isNotEmpty(lang))
260            {
261                MultilingualString multilingualString = new MultilingualString();
262                String value = element.getTextContent();
263                multilingualString.add(Locale.forLanguageTag(lang), value);
264                return multilingualString;
265            }
266            else
267            {
268                MultilingualString multilingualString = new MultilingualString(); 
269                for (Element entry : DOMUtils.getChildElements(element))
270                {
271                    String languageTag = entry.getNodeName();
272                    String value = entry.getTextContent();
273                    multilingualString.add(Locale.forLanguageTag(languageTag), value);
274                }
275                return multilingualString;
276            }
277        }
278        
279        return null;
280    }
281    
282    /**
283     * Get the JSON representation of a {@link MultilingualString}
284     * @param multilingualString The multilingual string. Cannot be null.
285     * @return A map with the locales and values.
286     * @throws AmetysRepositoryException if an error occurs
287     */
288    public static Map<String, Object> toJson(MultilingualString multilingualString) throws AmetysRepositoryException
289    {
290        Map<String, Object> json = new LinkedHashMap<>();
291        
292        for (Locale locale : multilingualString.getLocales())
293        {
294            json.put(locale.toString(), multilingualString.getValue(locale));
295        }
296        
297        return json;
298    }
299    
300    /**
301     * Get the {@link MultilingualString} object from its JSON representation 
302     * @param json the JSON representation of the multilingual string
303     * @return the {@link MultilingualString} object
304     */
305    public static MultilingualString fromJSON(Map<String, ? extends Object> json)
306    {
307        if (json == null)
308        {
309            return null;
310        }
311        
312        MultilingualString multilingualString = new MultilingualString();
313        
314        for (Map.Entry<String, ? extends Object> entry : json.entrySet())
315        {
316            Locale locale = LocaleUtils.toLocale(entry.getKey());
317            String value = entry.getValue().toString();
318            multilingualString.add(locale, value);
319        }
320        
321        return multilingualString;
322    }
323    
324    /**
325     * Retrieves a string representation of a {@link MultilingualString}
326     * @param multilingualString the multilingual string
327     * @return thestring representation of the multilingual string
328     */
329    public static String toString(MultilingualString multilingualString)
330    {
331        if (multilingualString != null)
332        {
333            StringBuilder asString = new StringBuilder();
334            Iterator<Locale> localesIterator = multilingualString.getLocales().iterator();
335            while (localesIterator.hasNext())
336            {
337                Locale locale = localesIterator.next();
338                asString.append(locale.toString()).append(__LOCALE_AND_VALUE_SEPARATOR).append(multilingualString.getValue(locale));
339                if (localesIterator.hasNext())
340                {
341                    asString.append(__ENTRIES_SEPARATOR);
342                }
343            }
344            return asString.toString();
345        }
346        else
347        {
348            return null;
349        }
350    }
351    
352    /**
353     * Retrieves the {@link MultilingualString} from its string representation
354     * @param string the string representation of the multilingual string
355     * @return the multilingual string from its string representation, or <code>null</code> if the given string value is null or empty
356     * @throws IllegalArgumentException if the given string value can't be cast to a multilingual string
357     */
358    public static MultilingualString fromString(String string) throws IllegalArgumentException
359    {
360        if (StringUtils.isEmpty(string))
361        {
362            return null;
363        }
364        
365        if (string.contains(__LOCALE_AND_VALUE_SEPARATOR))
366        {
367            MultilingualString multilingualString = new MultilingualString();
368            
369            String[] entries = string.split(__ENTRIES_SEPARATOR);
370            for (String entry : entries)
371            {
372                String localeAsString = StringUtils.substringBeforeLast(entry, __LOCALE_AND_VALUE_SEPARATOR);
373                String value = StringUtils.substringAfterLast(entry, __LOCALE_AND_VALUE_SEPARATOR);
374                multilingualString.add(LocaleUtils.toLocale(localeAsString), value);
375            }
376            
377            return multilingualString;
378        }
379        else
380        {
381            throw new IllegalArgumentException("Unable to cast '" + string + "' to a multilingual string");
382        }
383    }
384}