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