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.logs;
017
018import java.io.IOException;
019import java.util.ArrayList;
020import java.util.Calendar;
021import java.util.Date;
022import java.util.Enumeration;
023import java.util.GregorianCalendar;
024import java.util.HashMap;
025import java.util.List;
026import java.util.Map;
027
028import org.apache.avalon.framework.service.ServiceException;
029import org.apache.avalon.framework.service.ServiceManager;
030import org.apache.cocoon.ProcessingException;
031import org.apache.excalibur.source.ModifiableSource;
032import org.apache.excalibur.source.ModifiableTraversableSource;
033import org.apache.excalibur.source.SourceException;
034import org.apache.excalibur.source.SourceResolver;
035import org.apache.excalibur.source.TraversableSource;
036import org.apache.log4j.Level;
037import org.apache.log4j.LogManager;
038import org.apache.log4j.Logger;
039import org.apache.log4j.spi.LoggerRepository;
040
041import org.ametys.core.ui.Callable;
042import org.ametys.core.ui.StaticClientSideElement;
043
044/**
045 * Client side element for log actions
046 */
047public class LogsClientSideElement extends StaticClientSideElement
048{
049    /** The path to the application's logs folder */
050    private static final String __LOGS_BASE = "ametys-home://logs/";
051    
052    /** The Excalibur source resolver */
053    private SourceResolver _sourceResolver;
054    
055    @Override
056    public void service(ServiceManager serviceManager) throws ServiceException
057    {
058        super.service(serviceManager);
059        _sourceResolver = (SourceResolver) serviceManager.lookup(SourceResolver.ROLE);
060    }
061    
062    /**
063     * Delete the log files
064     * @param files the files to delete
065     * @return the result map with the successes and failures files. 
066     * @throws IOException if an error occurred during deletion
067     * @throws ProcessingException if an error occurred during deletion
068     */
069    @Callable (rights = "Runtime_Rights_Admin_Access", context = "/admin")
070    public Map<String, Object> deleteLogs(List<String> files) throws ProcessingException, IOException
071    {
072        Map<String, Object> result = new HashMap<>();
073        List<String> failures = new ArrayList<> ();
074        List<String> successes = new ArrayList<> ();
075        
076        if (getLogger().isInfoEnabled())
077        {
078            getLogger().info("Administrator starts a deletion of " + files.size() + " logged file");
079        }
080        
081        // Delete the selected files one by one
082        for (String location : files)
083        {
084            if (_deleteFile(location))
085            {
086                successes.add(location);
087            }
088            else
089            {
090                failures.add(location);
091            }
092        }
093        
094        if (getLogger().isInfoEnabled())
095        {
096            getLogger().info("Process terminated with following results : failure '" + failures.toString() + " successes '" + successes.toString());
097        }
098        
099        result.put("failures", failures);
100        result.put("successes", successes);
101        return result;
102    }
103    
104    /**
105     * Delete the log entries that are at least 12 days old
106     * @return the result 
107     * @throws ProcessingException if an exception occurs while processing the deletion
108     */
109    @Callable (rights = "Runtime_Rights_Admin_Access", context = "/admin")
110    public Map<String, Object> purgeLogs() throws ProcessingException
111    {
112        Calendar purgeCalendar = new GregorianCalendar();
113        purgeCalendar.add(Calendar.DAY_OF_MONTH, -12);
114        Date purgeDate = new Date(purgeCalendar.getTimeInMillis());
115
116        if (getLogger().isDebugEnabled())
117        {
118            getLogger().debug("Starting purge...");
119            getLogger().debug("Purge date is " + purgeDate);
120        }
121
122        List<String> filesList = new ArrayList<>();
123        TraversableSource logsSources = null;
124        try
125        {
126            logsSources = (TraversableSource) _sourceResolver.resolveURI(__LOGS_BASE);
127            for (Object log : logsSources.getChildren())
128            {
129                ModifiableTraversableSource logSource = (ModifiableTraversableSource) log;
130                if (!logSource.isCollection() && logSource.getURI().endsWith(".log") && new Date(logSource.getLastModified()).before(purgeDate))
131                {
132                    String location = logSource.getURI();
133                    String name = location.substring(location.lastIndexOf('/') + 1);
134                    filesList.add(name);
135                    if (getLogger().isDebugEnabled())
136                    {
137                        getLogger().debug("Adding file to purge : " + name);
138                    }
139                }
140            }
141            
142            return deleteLogs(filesList);
143        }
144        catch (SourceException e)
145        {
146            String message = "The purge of old log files failed";
147            getLogger().error(message, e);
148            throw new ProcessingException(message, e);
149        }
150        catch (IOException e)
151        {
152            String message = "The log directory was not found";
153            getLogger().error(message, e);
154            throw new ProcessingException(message, e);
155        }
156        finally
157        {
158            if (logsSources != null)
159            {
160                _sourceResolver.release(logsSources);
161            }
162        }  
163    }
164    
165    private boolean _deleteFile(String fileLocation) throws IOException, ProcessingException
166    {
167        if (fileLocation.indexOf("/") != -1 || fileLocation.indexOf('\\') != -1)
168        {
169            String message = "The LogsDeleteAction has been call with the forbiden parameter '" + fileLocation + "'";
170            getLogger().error(message);
171            throw new ProcessingException(message);
172        }
173        
174        ModifiableSource logsource = null;
175        try
176        {
177            if (fileLocation.endsWith(".log"))
178            {
179                logsource = (ModifiableSource) _sourceResolver.resolveURI(__LOGS_BASE + fileLocation);
180                if (getLogger().isInfoEnabled())
181                {
182                    getLogger().info("Removing log file " + logsource.getURI());
183                }
184                logsource.delete();
185                return true;
186            }
187            else if (getLogger().isWarnEnabled())
188            {
189                getLogger().warn("Ignoring this file during deletion '" + "' because it does not ends with .log");
190            }
191            return false;
192        }
193        catch (SourceException e)
194        {
195            getLogger().error("The administrator tried unsuccessfully to remove the following log file '" + (logsource != null ? logsource.getURI() : fileLocation) + "'.", e);
196            return false;
197        }
198        finally
199        {
200            if (logsource != null)
201            {
202                _sourceResolver.release(logsource);
203            }
204        }
205    }
206    
207    /**
208     * Change the log level of the selected category to the selected level
209     * @param level the selected level
210     * @param category the selected category
211     * @return a map
212     */
213    @Callable (rights = "Runtime_Rights_Admin_Access", context = "/admin")
214    public Map<String, Object> changeLogLevel(String level, String category)
215    {
216        Map<String, Object> result = new HashMap<>();
217        
218        if (getLogger().isInfoEnabled())
219        {
220            getLogger().info("Administrator change log level");
221        }
222
223        LoggerRepository loggerRepository = LogManager.getLoggerRepository();
224        
225        if (getLogger().isInfoEnabled())
226        {
227            getLogger().info("Log level changing category '" + category + "' " + level);
228        }
229
230        try
231        {
232            _changeLogLevel(loggerRepository, category, level);
233        }
234        catch (Throwable t)
235        {
236            String errorMessage = "Cannot change log level correctly : changing category '" + category + "'";
237            getLogger().error(errorMessage, t);
238            result.put("error", "error");
239            return result;
240        }
241        
242        return result;
243    }
244    
245    private void _changeLogLevel(LoggerRepository loggerRepository, String category, String mode)
246    {
247        boolean inherited = "INHERIT".equals(mode) || "INHERITFORCED".equals(mode);
248        boolean force = "FORCE".equals(mode) || "INHERITFORCED".equals(mode);
249
250        Logger logger;
251        boolean isRoot = false;
252        
253        if ("root".equals(category))
254        {
255            isRoot = true;
256            logger = loggerRepository.getRootLogger();
257        }
258        else
259        {
260            logger = loggerRepository.getLogger(category);
261        }
262    
263        if (inherited && !isRoot)
264        {
265            logger.setLevel(null);
266        }
267        
268        if (force)
269        {
270            Enumeration<Logger> e = loggerRepository.getCurrentLoggers();
271            while (e.hasMoreElements())
272            {
273                Logger l = e.nextElement();
274                if (l.getParent() == logger)
275                {
276                    _changeLogLevel(loggerRepository, l.getName(), "INHERITFORCED");
277                }
278            }
279        }
280
281        if (!inherited && !force)
282        {
283            Level level = Level.toLevel(mode);
284            logger.setLevel(level);
285        }
286    }
287}