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}