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}