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