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.hasNonEmptyValue(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.hasNonEmptyValue(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 AttributesImpl localAttributes = new AttributesImpl(attributes); 228 if (locale == null) 229 { 230 // Given locale is null, sax all existing locales 231 XMLUtils.startElement(contentHandler, tagName, localAttributes); 232 for (Locale valueLocale : multilingualString.getLocales()) 233 { 234 XMLUtils.createElement(contentHandler, valueLocale.toString(), multilingualString.getValue(valueLocale)); 235 } 236 XMLUtils.endElement(contentHandler, tagName); 237 } 238 else 239 { 240 Locale closestLocale = getClosestNonEmptyLocale(multilingualString, locale); 241 if (closestLocale != null) 242 { 243 localAttributes.addCDATAAttribute("lang", closestLocale.toString()); 244 XMLUtils.createElement(contentHandler, tagName, localAttributes, multilingualString.getValue(closestLocale)); 245 } 246 } 247 } 248 249 /** 250 * Get the {@link MultilingualString} object from the given {@link Node} 251 * @param element the DOM element containing the multilingual string data 252 * @return the {@link MultilingualString} object 253 * @throws TransformerException if an error occurs while parsing the DOM node 254 */ 255 public static MultilingualString fromXML(Element element) throws TransformerException 256 { 257 if (element != null) 258 { 259 String lang = element.getAttribute("lang"); 260 if (StringUtils.isNotEmpty(lang)) 261 { 262 MultilingualString multilingualString = new MultilingualString(); 263 String value = element.getTextContent(); 264 multilingualString.add(Locale.forLanguageTag(lang), value); 265 return multilingualString; 266 } 267 else 268 { 269 MultilingualString multilingualString = new MultilingualString(); 270 for (Element entry : DOMUtils.getChildElements(element)) 271 { 272 String languageTag = entry.getNodeName(); 273 String value = entry.getTextContent(); 274 multilingualString.add(Locale.forLanguageTag(languageTag), value); 275 } 276 return multilingualString; 277 } 278 } 279 280 return null; 281 } 282 283 /** 284 * Get the JSON representation of a {@link MultilingualString} 285 * @param multilingualString The multilingual string. Cannot be null. 286 * @return A map with the locales and values. 287 * @throws AmetysRepositoryException if an error occurs 288 */ 289 public static Map<String, Object> toJson(MultilingualString multilingualString) throws AmetysRepositoryException 290 { 291 Map<String, Object> json = new LinkedHashMap<>(); 292 293 for (Locale locale : multilingualString.getLocales()) 294 { 295 json.put(locale.toString(), multilingualString.getValue(locale)); 296 } 297 298 return json; 299 } 300 301 /** 302 * Get the {@link MultilingualString} object from its JSON representation 303 * @param json the JSON representation of the multilingual string 304 * @return the {@link MultilingualString} object 305 */ 306 public static MultilingualString fromJSON(Map<String, ? extends Object> json) 307 { 308 if (json == null) 309 { 310 return null; 311 } 312 313 MultilingualString multilingualString = new MultilingualString(); 314 315 for (Map.Entry<String, ? extends Object> entry : json.entrySet()) 316 { 317 Locale locale = LocaleUtils.toLocale(entry.getKey()); 318 String value = entry.getValue().toString(); 319 multilingualString.add(locale, value); 320 } 321 322 return multilingualString; 323 } 324 325 /** 326 * Retrieves a string representation of a {@link MultilingualString} 327 * @param multilingualString the multilingual string 328 * @return thestring representation of the multilingual string 329 */ 330 public static String toString(MultilingualString multilingualString) 331 { 332 if (multilingualString != null) 333 { 334 StringBuilder asString = new StringBuilder(); 335 Iterator<Locale> localesIterator = multilingualString.getLocales().iterator(); 336 while (localesIterator.hasNext()) 337 { 338 Locale locale = localesIterator.next(); 339 asString.append(locale.toString()).append(__LOCALE_AND_VALUE_SEPARATOR).append(multilingualString.getValue(locale)); 340 if (localesIterator.hasNext()) 341 { 342 asString.append(__ENTRIES_SEPARATOR); 343 } 344 } 345 return asString.toString(); 346 } 347 else 348 { 349 return null; 350 } 351 } 352 353 /** 354 * Retrieves the {@link MultilingualString} from its string representation 355 * @param string the string representation of the multilingual string 356 * @return the multilingual string from its string representation, or <code>null</code> if the given string value is null or empty 357 * @throws IllegalArgumentException if the given string value can't be cast to a multilingual string 358 */ 359 public static MultilingualString fromString(String string) throws IllegalArgumentException 360 { 361 if (StringUtils.isEmpty(string)) 362 { 363 return null; 364 } 365 366 if (string.contains(__LOCALE_AND_VALUE_SEPARATOR)) 367 { 368 MultilingualString multilingualString = new MultilingualString(); 369 370 String[] entries = string.split(__ENTRIES_SEPARATOR); 371 for (String entry : entries) 372 { 373 String localeAsString = StringUtils.substringBeforeLast(entry, __LOCALE_AND_VALUE_SEPARATOR); 374 String value = StringUtils.substringAfterLast(entry, __LOCALE_AND_VALUE_SEPARATOR); 375 multilingualString.add(LocaleUtils.toLocale(localeAsString), value); 376 } 377 378 return multilingualString; 379 } 380 else 381 { 382 throw new IllegalArgumentException("Unable to cast '" + string + "' to a multilingual string"); 383 } 384 } 385}