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}