001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements. See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache license, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License. You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the license for the specific language governing permissions and
015 * limitations under the license.
016 */
017
018package org.apache.logging.log4j.core.appender.rolling.action;
019
020import java.io.IOException;
021import java.nio.file.FileVisitor;
022import java.nio.file.Files;
023import java.nio.file.Path;
024import java.util.List;
025import java.util.Objects;
026
027import org.apache.logging.log4j.core.config.Configuration;
028import org.apache.logging.log4j.core.config.plugins.Plugin;
029import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
030import org.apache.logging.log4j.core.config.plugins.PluginConfiguration;
031import org.apache.logging.log4j.core.config.plugins.PluginElement;
032import org.apache.logging.log4j.core.config.plugins.PluginFactory;
033import org.apache.logging.log4j.core.lookup.StrSubstitutor;
034
035/**
036 * Rollover or scheduled action for deleting old log files that are accepted by the specified PathFilters.
037 */
038@Plugin(name = "Delete", category = "Core", printObject = true)
039public class DeleteAction extends AbstractPathAction {
040
041    private final PathSorter pathSorter;
042    private final boolean testMode;
043    private final ScriptCondition scriptCondition;
044
045    /**
046     * Creates a new DeleteAction that starts scanning for files to delete from the specified base path.
047     * 
048     * @param basePath base path from where to start scanning for files to delete.
049     * @param followSymbolicLinks whether to follow symbolic links. Default is false.
050     * @param maxDepth The maxDepth parameter is the maximum number of levels of directories to visit. A value of 0
051     *            means that only the starting file is visited, unless denied by the security manager. A value of
052     *            MAX_VALUE may be used to indicate that all levels should be visited.
053     * @param testMode if true, files are not deleted but instead a message is printed to the <a
054     *            href="http://logging.apache.org/log4j/2.x/manual/configuration.html#StatusMessages">status logger</a>
055     *            at INFO level. Users can use this to do a dry run to test if their configuration works as expected.
056     * @param sorter sorts
057     * @param pathConditions an array of path filters (if more than one, they all need to accept a path before it is
058     *            deleted).
059     * @param scriptCondition
060     */
061    DeleteAction(final String basePath, final boolean followSymbolicLinks, final int maxDepth, final boolean testMode,
062            final PathSorter sorter, final PathCondition[] pathConditions, final ScriptCondition scriptCondition,
063            final StrSubstitutor subst) {
064        super(basePath, followSymbolicLinks, maxDepth, pathConditions, subst);
065        this.testMode = testMode;
066        this.pathSorter = Objects.requireNonNull(sorter, "sorter");
067        this.scriptCondition = scriptCondition;
068        if (scriptCondition == null && (pathConditions == null || pathConditions.length == 0)) {
069            LOGGER.error("Missing Delete conditions: unconditional Delete not supported");
070            throw new IllegalArgumentException("Unconditional Delete not supported");
071        }
072    }
073
074    /*
075     * (non-Javadoc)
076     * 
077     * @see org.apache.logging.log4j.core.appender.rolling.action.AbstractPathAction#execute()
078     */
079    @Override
080    public boolean execute() throws IOException {
081        if (scriptCondition != null) {
082            return executeScript();
083        } else {
084            return super.execute();
085        }
086    }
087
088    private boolean executeScript() throws IOException {
089        final List<PathWithAttributes> selectedForDeletion = callScript();
090        if (selectedForDeletion == null) {
091            LOGGER.trace("Script returned null list (no files to delete)");
092            return true;
093        }
094        deleteSelectedFiles(selectedForDeletion);
095        return true;
096    }
097
098    private List<PathWithAttributes> callScript() throws IOException {
099        final List<PathWithAttributes> sortedPaths = getSortedPaths();
100        trace("Sorted paths:", sortedPaths);
101        final List<PathWithAttributes> result = scriptCondition.selectFilesToDelete(getBasePath(), sortedPaths);
102        return result;
103    }
104
105    private void deleteSelectedFiles(final List<PathWithAttributes> selectedForDeletion) throws IOException {
106        trace("Paths the script selected for deletion:", selectedForDeletion);
107        for (final PathWithAttributes pathWithAttributes : selectedForDeletion) {
108            final Path path = pathWithAttributes == null ? null : pathWithAttributes.getPath();
109            if (isTestMode()) {
110                LOGGER.info("Deleting {} (TEST MODE: file not actually deleted)", path);
111            } else {
112                delete(path);
113            }
114        }
115    }
116
117    /**
118     * Deletes the specified file.
119     * 
120     * @param path the file to delete
121     * @throws IOException if a problem occurred deleting the file
122     */
123    protected void delete(final Path path) throws IOException {
124        LOGGER.trace("Deleting {}", path);
125        Files.deleteIfExists(path);
126    }
127
128    /*
129     * (non-Javadoc)
130     * 
131     * @see org.apache.logging.log4j.core.appender.rolling.action.AbstractPathAction#execute(FileVisitor)
132     */
133    @Override
134    public boolean execute(final FileVisitor<Path> visitor) throws IOException {
135        final List<PathWithAttributes> sortedPaths = getSortedPaths();
136        trace("Sorted paths:", sortedPaths);
137
138        for (PathWithAttributes element : sortedPaths) {
139            try {
140                visitor.visitFile(element.getPath(), element.getAttributes());
141            } catch (final IOException ioex) {
142                LOGGER.error("Error in post-rollover Delete when visiting {}", element.getPath(), ioex);
143                visitor.visitFileFailed(element.getPath(), ioex);
144            }
145        }
146        // TODO return (visitor.success || ignoreProcessingFailure)
147        return true; // do not abort rollover even if processing failed
148    }
149
150    private void trace(final String label, final List<PathWithAttributes> sortedPaths) {
151        LOGGER.trace(label);
152        for (final PathWithAttributes pathWithAttributes : sortedPaths) {
153            LOGGER.trace(pathWithAttributes);
154        }
155    }
156
157    /**
158     * Returns a sorted list of all files up to maxDepth under the basePath.
159     * 
160     * @return a sorted list of files
161     * @throws IOException
162     */
163    List<PathWithAttributes> getSortedPaths() throws IOException {
164        final SortingVisitor sort = new SortingVisitor(pathSorter);
165        super.execute(sort);
166        final List<PathWithAttributes> sortedPaths = sort.getSortedPaths();
167        return sortedPaths;
168    }
169
170    /**
171     * Returns {@code true} if files are not deleted even when all conditions accept a path, {@code false} otherwise.
172     * 
173     * @return {@code true} if files are not deleted even when all conditions accept a path, {@code false} otherwise
174     */
175    public boolean isTestMode() {
176        return testMode;
177    }
178
179    @Override
180    protected FileVisitor<Path> createFileVisitor(final Path visitorBaseDir, final List<PathCondition> conditions) {
181        return new DeletingVisitor(visitorBaseDir, conditions, testMode);
182    }
183
184    /**
185     * Create a DeleteAction.
186     * 
187     * @param basePath base path from where to start scanning for files to delete.
188     * @param followLinks whether to follow symbolic links. Default is false.
189     * @param maxDepth The maxDepth parameter is the maximum number of levels of directories to visit. A value of 0
190     *            means that only the starting file is visited, unless denied by the security manager. A value of
191     *            MAX_VALUE may be used to indicate that all levels should be visited.
192     * @param testMode if true, files are not deleted but instead a message is printed to the <a
193     *            href="http://logging.apache.org/log4j/2.x/manual/configuration.html#StatusMessages">status logger</a>
194     *            at INFO level. Users can use this to do a dry run to test if their configuration works as expected.
195     *            Default is false.
196     * @param PathSorter a plugin implementing the {@link PathSorter} interface
197     * @param PathConditions an array of path conditions (if more than one, they all need to accept a path before it is
198     *            deleted).
199     * @param config The Configuration.
200     * @return A DeleteAction.
201     */
202    @PluginFactory
203    public static DeleteAction createDeleteAction(
204            // @formatter:off
205            @PluginAttribute("basePath") final String basePath, //
206            @PluginAttribute(value = "followLinks", defaultBoolean = false) final boolean followLinks,
207            @PluginAttribute(value = "maxDepth", defaultInt = 1) final int maxDepth,
208            @PluginAttribute(value = "testMode", defaultBoolean = false) final boolean testMode,
209            @PluginElement("PathSorter") final PathSorter sorterParameter,
210            @PluginElement("PathConditions") final PathCondition[] pathConditions,
211            @PluginElement("ScriptCondition") final ScriptCondition scriptCondition,
212            @PluginConfiguration final Configuration config) {
213            // @formatter:on
214        final PathSorter sorter = sorterParameter == null ? new PathSortByModificationTime(true) : sorterParameter;
215        return new DeleteAction(basePath, followLinks, maxDepth, testMode, sorter, pathConditions, scriptCondition,
216                config.getStrSubstitutor());
217    }
218}