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}