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