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