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