001/* 002 * Copyright 2012 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 */ 016 017package org.ametys.core.util; 018 019import java.util.Arrays; 020import java.util.HashMap; 021import java.util.Locale; 022import java.util.Map; 023 024import org.apache.avalon.framework.activity.Disposable; 025import org.apache.avalon.framework.activity.Initializable; 026import org.apache.avalon.framework.component.Component; 027import org.apache.avalon.framework.component.ComponentException; 028import org.apache.avalon.framework.context.Context; 029import org.apache.avalon.framework.context.ContextException; 030import org.apache.avalon.framework.context.Contextualizable; 031import org.apache.avalon.framework.logger.AbstractLogEnabled; 032import org.apache.avalon.framework.service.ServiceException; 033import org.apache.avalon.framework.service.ServiceManager; 034import org.apache.avalon.framework.service.Serviceable; 035import org.apache.cocoon.components.ContextHelper; 036import org.apache.cocoon.i18n.Bundle; 037import org.apache.cocoon.i18n.BundleFactory; 038import org.apache.cocoon.xml.ParamSaxBuffer; 039import org.apache.cocoon.xml.SaxBuffer; 040import org.apache.cocoon.xml.SaxBuffer.Characters; 041import org.xml.sax.SAXException; 042import org.xml.sax.helpers.DefaultHandler; 043 044import org.ametys.core.cocoon.XMLResourceBundle; 045import org.ametys.runtime.i18n.I18nizableText; 046import org.ametys.runtime.plugin.PluginsManager; 047import org.ametys.runtime.workspace.WorkspaceManager; 048 049/** 050 * Utils for i18n 051 */ 052public class I18nUtils extends AbstractLogEnabled implements Component, Serviceable, Contextualizable, Initializable, Disposable 053{ 054 /** The avalon role */ 055 public static final String ROLE = I18nUtils.class.getName(); 056 057 private static I18nUtils _instance; 058 059 /** I18n catalogues */ 060 protected Map<String, Location> _locations; 061 062 /** The avalon context */ 063 protected Context _context; 064 065 private BundleFactory _bundleFactory; 066 067 // Map<language, Map<text, translatedValue>> 068 private Map<String, Map<I18nizableText, String>> _cache; 069 070 071 @Override 072 public void contextualize(Context context) throws ContextException 073 { 074 _context = context; 075 } 076 077 @Override 078 public void service(ServiceManager manager) throws ServiceException 079 { 080 _bundleFactory = (BundleFactory) manager.lookup(BundleFactory.ROLE); 081 } 082 083 @Override 084 public void initialize() throws Exception 085 { 086 _instance = this; 087 _cache = new HashMap<>(); 088 089 _configure(); 090 } 091 092 /** 093 * Configure the i18n catalogue 094 */ 095 protected void _configure () 096 { 097 _locations = new HashMap<>(); 098 099 // initializes locations 100 101 _locations.put("application", new Location("application", new String[]{"context://WEB-INF/i18n"})); 102 103 PluginsManager pm = PluginsManager.getInstance(); 104 105 for (String pluginName : pm.getPluginNames()) 106 { 107 String id = "plugin." + pluginName; 108 109 String location2 = "plugin:" + pluginName + "://i18n"; 110 111 _locations.put(id, new Location("messages", new String[]{"context://WEB-INF/i18n/plugins/" + pluginName, location2})); 112 } 113 114 WorkspaceManager wm = WorkspaceManager.getInstance(); 115 116 for (String workspace : wm.getWorkspaceNames()) 117 { 118 String id = "workspace." + workspace; 119 String location2 = "workspace:" + workspace + "://i18n"; 120 121 _locations.put(id, new Location("messages", new String[]{"context://WEB-INF/i18n/workspaces/" + workspace, location2})); 122 } 123 } 124 125 /** 126 * Reload the i18n catalogues and clear cache. 127 * This method should be called as soon as the list of i18n catalogue was changed, when adding a new catalogue for example. 128 */ 129 public void reloadCatalogues () 130 { 131 clearCache(); 132 _configure(); 133 } 134 135 /** 136 * Get the unique instance 137 * @return the unique instance 138 */ 139 public static I18nUtils getInstance() 140 { 141 return _instance; 142 } 143 144 /** 145 * Get the translation of the key. 146 * This method is slow. 147 * Only use in very specific cases (send mail for example) 148 * @param text The i18n key to translate 149 * @return The translation or null if there's no available translation 150 * @throws IllegalStateException if an error occured 151 */ 152 public String translate(I18nizableText text) 153 { 154 return translate(text, null); 155 } 156 157 /** 158 * Get the translation of the key. 159 * Only use in very specific cases (send mail for example) 160 * @param text The i18n key to translate 161 * @param language The language code to use for translation. Can be null. 162 * @return The translation or null if there's no available translation 163 * @throws IllegalStateException if an error occurred 164 */ 165 public String translate(I18nizableText text, String language) throws IllegalStateException 166 { 167 return translate(text, language, false); 168 } 169 170 /** 171 * Get the translation of the key. 172 * Only use in very specific cases (send mail for example) 173 * @param text The i18n key to translate 174 * @param language The language code to use for translation. Can be null. 175 * @param rawValue Set true to get the value corresponding strictly to the specified Locale, without escalading to parent Locale if not found. Note that there is no cache for strict values. 176 * @return The translation or null if there's no available translation 177 * @throws IllegalStateException if an error occurred 178 */ 179 public String translate(I18nizableText text, String language, boolean rawValue) throws IllegalStateException 180 { 181 // Check language 182 String langCode = language; 183 if (langCode == null) 184 { 185 Map objectModel = ContextHelper.getObjectModel(_context); 186 Locale locale = org.apache.cocoon.i18n.I18nUtils.findLocale(objectModel, "locale", null, Locale.getDefault(), true); 187 langCode = locale.toString(); 188 } 189 190 Map<I18nizableText, String> values = getLangCache(langCode); 191 192 String value = null; 193 194 if (rawValue) 195 { 196 // No cache for strict values 197 value = _translate(text, langCode, true); 198 } 199 else if (values.containsKey(text)) 200 { 201 value = values.get(text); 202 } 203 else 204 { 205 value = _translate(text, langCode, false); 206 207 if (value != null) 208 { 209 // Put in cache 210 values.put(text, value); 211 } 212 } 213 214 return value; 215 } 216 217 /** 218 * Clear the i18n cache. 219 */ 220 public void clearCache() 221 { 222 _cache.clear(); 223 } 224 225 /** 226 * Get the translation cache for a language. 227 * @param language the language. 228 * @return the translation cache for the given language. 229 */ 230 protected Map<I18nizableText, String> getLangCache(String language) 231 { 232 Map<I18nizableText, String> langCache; 233 234 if (_cache.containsKey(language)) 235 { 236 langCache = _cache.get(language); 237 } 238 else 239 { 240 langCache = new HashMap<>(); 241 _cache.put(language, langCache); 242 } 243 244 return langCache; 245 } 246 247 /** 248 * Get the translation of the key. 249 * Only use in very specific cases (send mail for example) 250 * @param text The i18n key to translate 251 * @param language The language code to use for translation. Can be null. 252 * @param rawValue Set true to get the value corresponding strictly to the specified Locale, without escalading to parent Locale if not found 253 * @return The translation or null if there's no available translation 254 * @throws IllegalStateException if an error occured 255 */ 256 protected String _translate(I18nizableText text, String language, boolean rawValue) throws IllegalStateException 257 { 258 if (!text.isI18n()) 259 { 260 return text.getLabel(); 261 } 262 263 Location location = null; 264 if (text.getLocation() != null) 265 { 266 location = new Location(text.getBundleName(), new String[]{text.getLocation()}); 267 } 268 else 269 { 270 String catalogue = text.getCatalogue(); 271 location = _locations.get(catalogue); 272 } 273 274 if (location == null) 275 { 276 return null; 277 } 278 279 try 280 { 281 Bundle bundle = _bundleFactory.select(location.getLocations(), location.getName(), org.apache.cocoon.i18n.I18nUtils.parseLocale(language)); 282 283 // translated message 284 ParamSaxBuffer buffer = rawValue ? (ParamSaxBuffer) ((XMLResourceBundle) bundle).getRawObject(text.getKey()) : (ParamSaxBuffer) bundle.getObject(text.getKey()); 285 286 if (buffer == null) 287 { 288 return null; 289 } 290 291 // message parameters 292 Map<String, SaxBuffer> params = new HashMap<>(); 293 294 if (text.getParameters() != null) 295 { 296 int p = 0; 297 for (String param : text.getParameters()) 298 { 299 Characters characters = new Characters(param.toCharArray(), 0, param.length()); 300 params.put(String.valueOf(p++), new SaxBuffer(Arrays.asList(characters))); 301 } 302 } 303 304 if (text.getParameterMap() != null) 305 { 306 for (String name : text.getParameterMap().keySet()) 307 { 308 // named parameters are themselves I18nizableText, so translate them recursively 309 String param = translate(text.getParameterMap().get(name), language, rawValue); 310 if (param == null) 311 { 312 param = ""; 313 } 314 Characters characters = new Characters(param.toCharArray(), 0, param.length()); 315 params.put(name, new SaxBuffer(Arrays.asList(characters))); 316 } 317 } 318 319 StringBuilder result = new StringBuilder(); 320 buffer.toSAX(new BufferHandler(result), params); 321 322 return result.toString(); 323 } 324 catch (SAXException e) 325 { 326 throw new RuntimeException("Unable to get i18n translation", e); 327 } 328 catch (ComponentException e) 329 { 330 throw new RuntimeException("Unable to get i18n catalogue", e); 331 } 332 } 333 334 private class BufferHandler extends DefaultHandler 335 { 336 StringBuilder _builder; 337 338 public BufferHandler(StringBuilder builder) 339 { 340 _builder = builder; 341 } 342 343 @Override 344 public void characters(char[] ch, int start, int length) throws SAXException 345 { 346 _builder.append(ch, start, length); 347 } 348 } 349 350 public void dispose() 351 { 352 _instance = null; 353 } 354 355 /** 356 * Class representing an i18n location 357 */ 358 protected class Location 359 { 360 String[] _loc; 361 String _name; 362 363 /** 364 * Constructor. 365 * @param name the catalogue name 366 * @param locations the files locations. 367 */ 368 public Location (String name, String[] locations) 369 { 370 _name = name; 371 _loc = locations; 372 } 373 374 /** 375 * Get the name 376 * @return the name 377 */ 378 public String getName () 379 { 380 return _name; 381 } 382 383 /** 384 * Get the files location 385 * @return the files location 386 */ 387 public String[] getLocations() 388 { 389 return _loc; 390 } 391 } 392}