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