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 */
017package org.apache.logging.log4j.core.appender.rolling;
018
019import java.io.File;
020import java.io.IOException;
021import java.nio.file.DirectoryStream;
022import java.nio.file.Files;
023import java.nio.file.Path;
024import java.util.ArrayList;
025import java.util.List;
026import java.util.SortedMap;
027import java.util.TreeMap;
028import java.util.regex.Matcher;
029import java.util.regex.Pattern;
030
031import org.apache.logging.log4j.Logger;
032import org.apache.logging.log4j.LoggingException;
033import org.apache.logging.log4j.core.appender.rolling.action.Action;
034import org.apache.logging.log4j.core.appender.rolling.action.CompositeAction;
035import org.apache.logging.log4j.core.lookup.StrSubstitutor;
036import org.apache.logging.log4j.core.pattern.NotANumber;
037import org.apache.logging.log4j.status.StatusLogger;
038
039/**
040 *
041 */
042public abstract class AbstractRolloverStrategy implements RolloverStrategy {
043
044    /**
045     * Allow subclasses access to the status logger without creating another instance.
046     */
047    protected static final Logger LOGGER = StatusLogger.getLogger();
048
049    public static final Pattern PATTERN_COUNTER= Pattern.compile(".*%((?<ZEROPAD>0)?(?<PADDING>\\d+))?i.*");
050
051    protected final StrSubstitutor strSubstitutor;
052
053    protected AbstractRolloverStrategy(final StrSubstitutor strSubstitutor) {
054        this.strSubstitutor = strSubstitutor;
055    }
056
057
058    public StrSubstitutor getStrSubstitutor() {
059        return strSubstitutor;
060    }
061
062    protected Action merge(final Action compressAction, final List<Action> custom, final boolean stopOnError) {
063        if (custom.isEmpty()) {
064            return compressAction;
065        }
066        if (compressAction == null) {
067            return new CompositeAction(custom, stopOnError);
068        }
069        final List<Action> all = new ArrayList<>();
070        all.add(compressAction);
071        all.addAll(custom);
072        return new CompositeAction(all, stopOnError);
073    }
074
075    protected int suffixLength(final String lowFilename) {
076        for (final FileExtension extension : FileExtension.values()) {
077            if (extension.isExtensionFor(lowFilename)) {
078                return extension.length();
079            }
080        }
081        return 0;
082    }
083
084
085    protected SortedMap<Integer, Path> getEligibleFiles(final RollingFileManager manager) {
086        return getEligibleFiles(manager, true);
087    }
088
089    protected SortedMap<Integer, Path> getEligibleFiles(final RollingFileManager manager,
090                                                        final boolean isAscending) {
091        final StringBuilder buf = new StringBuilder();
092        final String pattern = manager.getPatternProcessor().getPattern();
093        manager.getPatternProcessor().formatFileName(strSubstitutor, buf, NotANumber.NAN);
094        final String fileName = manager.isDirectWrite() ? "" : manager.getFileName();
095        return getEligibleFiles(fileName, buf.toString(), pattern, isAscending);
096    }
097
098    protected SortedMap<Integer, Path> getEligibleFiles(final String path, final String pattern) {
099        return getEligibleFiles("", path, pattern, true);
100    }
101
102    @Deprecated
103    protected SortedMap<Integer, Path> getEligibleFiles(final String path, final String logfilePattern,
104            final boolean isAscending) {
105        return getEligibleFiles("", path, logfilePattern, isAscending);
106    }
107
108    protected SortedMap<Integer, Path> getEligibleFiles(final String currentFile, final String path,
109            final String logfilePattern, final boolean isAscending) {
110        final TreeMap<Integer, Path> eligibleFiles = new TreeMap<>();
111        final File file = new File(path);
112        File parent = file.getParentFile();
113        if (parent == null) {
114            parent = new File(".");
115        } else {
116            parent.mkdirs();
117        }
118        if (!PATTERN_COUNTER.matcher(logfilePattern).matches()) {
119            return eligibleFiles;
120        }
121        final Path dir = parent.toPath();
122        String fileName = file.getName();
123        final int suffixLength = suffixLength(fileName);
124        // use Pattern.quote to treat all initial parts of the fileName as literal
125        // this fixes issues with filenames containing 'magic' regex characters
126        if (suffixLength > 0) {
127            fileName = Pattern.quote(fileName.substring(0, fileName.length() - suffixLength)) + ".*";
128        } else {
129            fileName = Pattern.quote(fileName);
130        }
131        // since we insert a pattern inside a regex escaped string,
132        // surround it with quote characters so that (\d) is treated as a pattern and not a literal
133        final String filePattern = fileName.replaceFirst("0?\\u0000", "\\\\E(0?\\\\d+)\\\\Q");
134        final Pattern pattern = Pattern.compile(filePattern);
135        final Path current = currentFile.length() > 0 ? new File(currentFile).toPath() : null;
136        LOGGER.debug("Current file: {}", currentFile);
137
138        try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) {
139            for (final Path entry: stream) {
140                final Matcher matcher = pattern.matcher(entry.toFile().getName());
141                if (matcher.matches() && !entry.equals(current)) {
142                    try {
143                        final Integer index = Integer.parseInt(matcher.group(1));
144                        eligibleFiles.put(index, entry);
145                    } catch (NumberFormatException ex) {
146                        LOGGER.debug("Ignoring file {} which matches pattern but the index is invalid.",
147                                entry.toFile().getName());
148                    }
149                }
150            }
151        } catch (final IOException ioe) {
152            throw new LoggingException("Error reading folder " + dir + " " + ioe.getMessage(), ioe);
153        }
154        return isAscending? eligibleFiles : eligibleFiles.descendingMap();
155    }
156}