001/*
002 *  Copyright 2015 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 */
016package org.ametys.runtime.plugins.admin.system;
017
018import java.io.File;
019import java.io.FileInputStream;
020import java.io.FileOutputStream;
021import java.io.InputStream;
022import java.io.OutputStream;
023import java.util.HashMap;
024import java.util.Locale;
025import java.util.Map;
026import java.util.Properties;
027
028import javax.xml.transform.OutputKeys;
029import javax.xml.transform.TransformerFactory;
030import javax.xml.transform.sax.SAXTransformerFactory;
031import javax.xml.transform.sax.TransformerHandler;
032import javax.xml.transform.stream.StreamResult;
033
034import org.apache.avalon.framework.activity.Initializable;
035import org.apache.avalon.framework.component.Component;
036import org.apache.avalon.framework.configuration.Configuration;
037import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder;
038import org.apache.avalon.framework.context.Context;
039import org.apache.avalon.framework.context.ContextException;
040import org.apache.avalon.framework.context.Contextualizable;
041import org.apache.avalon.framework.logger.AbstractLogEnabled;
042import org.apache.avalon.framework.service.ServiceException;
043import org.apache.avalon.framework.service.ServiceManager;
044import org.apache.avalon.framework.service.Serviceable;
045import org.apache.cocoon.ProcessingException;
046import org.apache.cocoon.components.ContextHelper;
047import org.apache.cocoon.xml.XMLUtils;
048import org.apache.commons.io.FileUtils;
049import org.apache.commons.lang3.StringUtils;
050import org.xml.sax.helpers.AttributesImpl;
051
052import org.ametys.core.cache.AbstractCacheManager;
053import org.ametys.core.cache.Cache;
054import org.ametys.core.ui.Callable;
055import org.ametys.core.util.I18nUtils;
056import org.ametys.runtime.i18n.I18nizableText;
057import org.ametys.runtime.servlet.RuntimeConfig;
058import org.ametys.runtime.util.AmetysHomeHelper;
059
060/**
061 * Helper for manipulating system announcement 
062 */
063public class SystemHelper extends AbstractLogEnabled implements Component, Serviceable, Contextualizable, Initializable
064{
065    /** The relative path to the file where system information are saved (announcement, maintenance...) */
066    public static final String ADMINISTRATOR_SYSTEM_FILENAME = "system.xml";
067    /** Avalon role */
068    public static final String ROLE = SystemHelper.class.getName();
069    
070    private static final String SYSTEM_ANNOUNCEMENT_CACHE = SystemHelper.class.getName() + "$SystemAnnouncement";
071    private static final String SYSTEM_ANNOUNCEMENT_CACHE_KEY = SystemHelper.class.getName() + "$SystemAnnouncement";
072    
073    
074    private I18nUtils _i18nUtils;
075    private Context _context;
076    private AbstractCacheManager _cacheManager;
077    
078    @Override
079    public void service(ServiceManager serviceManager) throws ServiceException
080    {
081        _i18nUtils = (I18nUtils) serviceManager.lookup(I18nUtils.ROLE);
082        _cacheManager = (AbstractCacheManager) serviceManager.lookup(AbstractCacheManager.ROLE);
083    }
084    
085    public void initialize() throws Exception
086    {
087        _cacheManager.createMemoryCache(SYSTEM_ANNOUNCEMENT_CACHE,
088                new I18nizableText("plugin.admin", "PLUGINS_ADMIN_CACHE_SYSTEM_ANNOUNCEMENT_LABEL"),
089                new I18nizableText("plugin.admin", "PLUGINS_ADMIN_CACHE_SYSTEM_ANNOUNCEMENT_DESCRIPTION"),
090                true,
091                null);
092    }
093    
094    @Override
095    public void contextualize(Context context) throws ContextException
096    {
097        _context = context;
098    }
099    
100    /**
101     * Enables or disable system announcement
102     * @param available true to enable system announcement
103     * @throws ProcessingException if an error occurred
104     */
105    @Callable(right = "Runtime_Rights_Admin_Access", context = "/admin")    
106    public void setAnnouncementAvailable (boolean available) throws ProcessingException
107    {
108        SystemAnnouncement systemAnnouncement = readValues();
109        
110        _save(available, systemAnnouncement.getMessages());
111    }
112    
113    /**
114     * Add or edit a system announcement
115     * @param language the language typed in by the user or "*" if modifying the default message
116     * @param message the message to add nor edit
117     * @param override true to override the existing value if exists
118     * @return the result map
119     * @throws Exception if an exception occurs 
120     */
121    @Callable(right = "Runtime_Rights_Admin_Access", context = "/admin")    
122    public Map<String, Object> editAnnouncement(String language, String message, boolean override) throws Exception
123    {
124        Map<String, Object> result = new HashMap<> ();
125        
126        SystemAnnouncement sytemAnnouncement = readValues();
127        
128        Map<String, String> messages = sytemAnnouncement.getMessages();
129        if (messages.containsKey(language) && !override)
130        {
131            result.put("already-exists", true);
132            return result;
133        }
134        
135        // Add or edit message
136        messages.put(language, message);
137        
138        _save(sytemAnnouncement.isAvailable(), messages);
139        
140        return result;
141    }
142    
143    /**
144     * Delete a announcement 
145     * @param language the language of the announcement to delete
146     * @throws ProcessingException if an exception occurs
147     * @return an empty map
148     */
149    @Callable(right = "Runtime_Rights_Admin_Access", context = "/admin")    
150    public Map deleteAnnouncement(String language) throws ProcessingException
151    {
152        Map<String, Object> result = new HashMap<> ();
153        
154        SystemAnnouncement sytemAnnouncement = readValues();
155        
156        Map<String, String> messages = sytemAnnouncement.getMessages();
157        if (messages.containsKey(language))
158        {
159            messages.remove(language);
160            
161            _save(sytemAnnouncement.isAvailable(), messages);
162        }
163        
164        return result;
165    }
166    
167    private File _getSystemFile()
168    {
169        return FileUtils.getFile(RuntimeConfig.getInstance().getAmetysHome(), AmetysHomeHelper.AMETYS_HOME_ADMINISTRATOR_DIR, ADMINISTRATOR_SYSTEM_FILENAME);
170    }
171    
172    /**
173     * Saves the system announcement's values
174     * @param state true to enable system announcement
175     * @param messages the messages
176     * @throws ProcessingException if an error ocurred
177     */
178    private void _save (boolean state, Map<String, String> messages) throws ProcessingException
179    {
180        File systemFile = _getSystemFile();
181        
182        try
183        {
184            // Create file if not exists
185            if (!systemFile.exists())
186            {
187                systemFile.getParentFile().mkdirs();
188                systemFile.createNewFile();
189            }
190            
191            // create a transformer for saving sax into a file
192            TransformerHandler th = ((SAXTransformerFactory) TransformerFactory.newInstance()).newTransformerHandler();
193
194            // create the result where to write
195            try (OutputStream os = new FileOutputStream(systemFile))
196            {
197                StreamResult sResult = new StreamResult(os);
198                th.setResult(sResult);
199    
200                // create the format of result
201                Properties format = new Properties();
202                format.put(OutputKeys.METHOD, "xml");
203                format.put(OutputKeys.INDENT, "yes");
204                format.put(OutputKeys.ENCODING, "UTF-8");
205                th.getTransformer().setOutputProperties(format);
206    
207                // Send SAX events
208                th.startDocument();
209    
210                AttributesImpl announcementsAttrs = new AttributesImpl();
211                announcementsAttrs.addAttribute("", "state", "state", "CDATA", state ? "on" : "off");
212                XMLUtils.startElement(th, "announcements", announcementsAttrs);
213                
214                for (String id : messages.keySet())
215                {
216                    AttributesImpl announcementAttrs = new AttributesImpl();
217                    if (!"*".equals(id))
218                    {
219                        announcementAttrs.addAttribute("", "lang", "lang", "CDATA", id);
220                    }
221                    
222                    XMLUtils.createElement(th, "announcement", announcementAttrs, messages.get(id));
223                }
224                
225                XMLUtils.endElement(th, "announcements");
226                
227                th.endDocument();
228            }
229        }
230        catch (Exception e)
231        {
232            throw new ProcessingException("Unable to save system announcement values", e);
233        }
234        finally
235        {
236            // clear the cache
237            _cacheManager.get(SYSTEM_ANNOUNCEMENT_CACHE).invalidateAll();
238        }
239    }
240    
241    /**
242     * Tests if system announcements are active.
243     * @return true if system announcements are active.
244     */
245    @Callable
246    public boolean isSystemAnnouncementAvailable()
247    {
248        SystemAnnouncement systemAnnouncement = readValues();
249        return systemAnnouncement.isAvailable();
250    }
251    
252    /**
253     * Return the date of the last modification of the annonce
254     * @return The date of the last modification or 0 if there is no announce file
255     */
256    public long getSystemAnnoucementLastModificationDate()
257    {
258        try
259        {
260            File systemFile = _getSystemFile();
261            if (!systemFile.exists() || !systemFile.isFile())
262            {
263                return 0;
264            }
265            
266            return systemFile.lastModified();
267        }
268        catch (Exception e)
269        {
270            throw new RuntimeException("Unable to get system announcements", e);
271        }
272    }
273
274    /**
275     * Returns the system announcement for the given language code, or for the default language code if there is no specified announcement for the given language code.<br>
276     * Returns null if the system announcements are not activated.
277     * @param languageCode the desired language code of the system announcement
278     * @return the system announcement in the specified language code, or in the default language code, or null if announcements are not active.
279     */
280    public String getSystemAnnouncement(String languageCode)
281    {
282        SystemAnnouncement systemAnnouncement = readValues();
283        
284        if (!systemAnnouncement.isAvailable())
285        {
286            return null;
287        }
288        
289        Map<String, String> messages = systemAnnouncement.getMessages();
290        
291        String announcement = null;
292        if (messages.containsKey(languageCode))
293        {
294            announcement = messages.get(languageCode);
295        }
296        
297        if (StringUtils.isEmpty(announcement))
298        {
299            String defaultAnnouncement = messages.containsKey("*") ? messages.get("*") : null;
300            if (StringUtils.isEmpty(defaultAnnouncement))
301            {
302                throw new IllegalStateException("There must be a default announcement.");
303            }
304            
305            return defaultAnnouncement;
306        }
307        
308        return announcement;
309    }
310    
311    /**
312     * Read the system announcement's values
313     * @return The system announcement values;
314     */
315    public SystemAnnouncement readValues ()
316    {
317        Cache<String, SystemAnnouncement> cache = _cacheManager.get(SYSTEM_ANNOUNCEMENT_CACHE);
318        return cache.get(SYSTEM_ANNOUNCEMENT_CACHE_KEY, str -> _readValues());
319    }
320    
321    private SystemAnnouncement _readValues()
322    {
323        SystemAnnouncement announcement = new SystemAnnouncement();
324        
325        try 
326        {
327            File systemFile = _getSystemFile();
328            if (!systemFile.exists() || !systemFile.isFile())
329            {
330                _setDefaultValues();
331            }
332            
333            Configuration configuration;
334            try (InputStream is = new FileInputStream(systemFile))
335            {
336                configuration = new DefaultConfigurationBuilder().build(is);
337            }
338            
339            // State
340            String state = configuration.getAttribute("state", "off");
341            boolean isAvailable = "on".equals(state);
342            announcement.setAvailable(isAvailable);
343            
344            // Announcements
345            for (Configuration announcementConfiguration : configuration.getChildren("announcement"))
346            {
347                String lang = announcementConfiguration.getAttribute("lang", "*");
348                String message = announcementConfiguration.getValue();
349                
350                announcement.addMessage(lang, message);
351            }
352            
353            return announcement;
354        }
355        catch (Exception e)
356        {
357            throw new RuntimeException("Unable to get system announcements", e);
358        }
359    }
360    
361    private void _setDefaultValues () throws ProcessingException
362    {
363        Map objectModel = ContextHelper.getObjectModel(_context);
364        Locale locale = org.apache.cocoon.i18n.I18nUtils.findLocale(objectModel, "locale", null, Locale.getDefault(), true);
365        String defaultMessage = _i18nUtils.translate(new I18nizableText("plugin.admin", "PLUGINS_ADMIN_SYSTEM_DEFAULTMESSAGE"), locale.getLanguage());
366
367        Map<String, String> messages = new HashMap<> ();
368        messages.put("*", defaultMessage);
369        
370        _save(false, messages);
371    }
372    
373    /**
374     * Class representing the system announcement file
375     */
376    public static class SystemAnnouncement
377    {
378        private boolean _available;
379        private Map<String, String> _messages;
380        
381        /**
382         * Constructor
383         */
384        public SystemAnnouncement()
385        {
386            _available = false;
387            _messages = new HashMap<>();
388        }
389        
390        /**
391         * Is the system announcement available ?
392         * @return true if the system announcement is available, false otherwise
393         */
394        public boolean isAvailable()
395        {
396            return _available;
397        }
398        
399        /**
400         * Get the messages by language
401         * @return the messages by languaga
402         */
403        public Map<String, String> getMessages ()
404        {
405            return _messages;
406        }
407        
408        /**
409         * Set the availability of the system announcement
410         * @param available true to set the system announcement available, false otherwise
411         */
412        public void setAvailable (boolean available)
413        {
414            _available = available;
415        }
416        
417        /**
418         * Add a message to the list of announcements
419         * @param lang the language of the message
420         * @param message the message itself
421         */
422        public void addMessage (String lang, String message)
423        {
424            _messages.put(lang, message);
425        }
426        
427    }
428}