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