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