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