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        return scriptCondition != null ? executeScript() : super.execute();
082    }
083
084    private boolean executeScript() throws IOException {
085        final List<PathWithAttributes> selectedForDeletion = callScript();
086        if (selectedForDeletion == null) {
087            LOGGER.trace("Script returned null list (no files to delete)");
088            return true;
089        }
090        deleteSelectedFiles(selectedForDeletion);
091        return true;
092    }
093
094    private List<PathWithAttributes> callScript() throws IOException {
095        final List<PathWithAttributes> sortedPaths = getSortedPaths();
096        trace("Sorted paths:", sortedPaths);
097        final List<PathWithAttributes> result = scriptCondition.selectFilesToDelete(getBasePath(), sortedPaths);
098        return result;
099    }
100
101    private void deleteSelectedFiles(final List<PathWithAttributes> selectedForDeletion) throws IOException {
102        trace("Paths the script selected for deletion:", selectedForDeletion);
103        for (final PathWithAttributes pathWithAttributes : selectedForDeletion) {
104            final Path path = pathWithAttributes == null ? null : pathWithAttributes.getPath();
105            if (isTestMode()) {
106                LOGGER.info("Deleting {} (TEST MODE: file not actually deleted)", path);
107            } else {
108                delete(path);
109            }
110        }
111    }
112
113    /**
114     * Deletes the specified file.
115     * 
116     * @param path the file to delete
117     * @throws IOException if a problem occurred deleting the file
118     */
119    protected void delete(final Path path) throws IOException {
120        LOGGER.trace("Deleting {}", path);
121        Files.deleteIfExists(path);
122    }
123
124    /*
125     * (non-Javadoc)
126     * 
127     * @see org.apache.logging.log4j.core.appender.rolling.action.AbstractPathAction#execute(FileVisitor)
128     */
129    @Override
130    public boolean execute(final FileVisitor<Path> visitor) throws IOException {
131        final List<PathWithAttributes> sortedPaths = getSortedPaths();
132        trace("Sorted paths:", sortedPaths);
133
134        for (PathWithAttributes element : sortedPaths) {
135            try {
136                visitor.visitFile(element.getPath(), element.getAttributes());
137            } catch (final IOException ioex) {
138                LOGGER.error("Error in post-rollover Delete when visiting {}", element.getPath(), ioex);
139                visitor.visitFileFailed(element.getPath(), ioex);
140            }
141        }
142        // TODO return (visitor.success || ignoreProcessingFailure)
143        return true; // do not abort rollover even if processing failed
144    }
145
146    private void trace(final String label, final List<PathWithAttributes> sortedPaths) {
147        LOGGER.trace(label);
148        for (final PathWithAttributes pathWithAttributes : sortedPaths) {
149            LOGGER.trace(pathWithAttributes);
150        }
151    }
152
153    /**
154     * Returns a sorted list of all files up to maxDepth under the basePath.
155     * 
156     * @return a sorted list of files
157     * @throws IOException
158     */
159    List<PathWithAttributes> getSortedPaths() throws IOException {
160        final SortingVisitor sort = new SortingVisitor(pathSorter);
161        super.execute(sort);
162        final List<PathWithAttributes> sortedPaths = sort.getSortedPaths();
163        return sortedPaths;
164    }
165
166    /**
167     * Returns {@code true} if files are not deleted even when all conditions accept a path, {@code false} otherwise.
168     * 
169     * @return {@code true} if files are not deleted even when all conditions accept a path, {@code false} otherwise
170     */
171    public boolean isTestMode() {
172        return testMode;
173    }
174
175    @Override
176    protected FileVisitor<Path> createFileVisitor(final Path visitorBaseDir, final List<PathCondition> conditions) {
177        return new DeletingVisitor(visitorBaseDir, conditions, testMode);
178    }
179
180    /**
181     * Create a DeleteAction.
182     * 
183     * @param basePath base path from where to start scanning for files to delete.
184     * @param followLinks whether to follow symbolic links. Default is false.
185     * @param maxDepth The maxDepth parameter is the maximum number of levels of directories to visit. A value of 0
186     *            means that only the starting file is visited, unless denied by the security manager. A value of
187     *            MAX_VALUE may be used to indicate that all levels should be visited.
188     * @param testMode if true, files are not deleted but instead a message is printed to the <a
189     *            href="http://logging.apache.org/log4j/2.x/manual/configuration.html#StatusMessages">status logger</a>
190     *            at INFO level. Users can use this to do a dry run to test if their configuration works as expected.
191     *            Default is false.
192     * @param PathSorter a plugin implementing the {@link PathSorter} interface
193     * @param PathConditions an array of path conditions (if more than one, they all need to accept a path before it is
194     *            deleted).
195     * @param config The Configuration.
196     * @return A DeleteAction.
197     */
198    @PluginFactory
199    public static DeleteAction createDeleteAction(
200            // @formatter:off
201            @PluginAttribute("basePath") final String basePath, //
202            @PluginAttribute(value = "followLinks", defaultBoolean = false) final boolean followLinks,
203            @PluginAttribute(value = "maxDepth", defaultInt = 1) final int maxDepth,
204            @PluginAttribute(value = "testMode", defaultBoolean = false) final boolean testMode,
205            @PluginElement("PathSorter") final PathSorter sorterParameter,
206            @PluginElement("PathConditions") final PathCondition[] pathConditions,
207            @PluginElement("ScriptCondition") final ScriptCondition scriptCondition,
208            @PluginConfiguration final Configuration config) {
209            // @formatter:on
210        final PathSorter sorter = sorterParameter == null ? new PathSortByModificationTime(true) : sorterParameter;
211        return new DeleteAction(basePath, followLinks, maxDepth, testMode, sorter, pathConditions, scriptCondition,
212                config.getStrSubstitutor());
213    }
214}