diff --git a/src/main/java/org/jboss/logmanager/handlers/FileRotator.java b/src/main/java/org/jboss/logmanager/handlers/FileRotator.java new file mode 100644 index 00000000..bd7838af --- /dev/null +++ b/src/main/java/org/jboss/logmanager/handlers/FileRotator.java @@ -0,0 +1,252 @@ +/* + * JBoss, Home of Professional Open Source. + * + * Copyright 2017 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jboss.logmanager.handlers; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.zip.GZIPOutputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +/** + * Represents a suffix used for rotating files. + * + * @author James R. Perkins + */ +class FileRotator { + + /** + * The compression type for the rotation + */ + public enum CompressionType { + NONE, + GZIP, + ZIP + } + + /** + * An empty rotation suffix. + */ + static final FileRotator EMPTY = new FileRotator("", "", "", CompressionType.NONE); + + private final String originalSuffix; + private final String datePattern; + private final SimpleDateFormat formatter; + private final String compressionSuffix; + private final CompressionType compressionType; + + private FileRotator(final String originalSuffix, final String datePattern, final String compressionSuffix, final CompressionType compressionType) { + this.originalSuffix = originalSuffix; + this.datePattern = datePattern; + this.compressionSuffix = compressionSuffix; + this.compressionType = compressionType; + if (datePattern.isEmpty()) { + formatter = null; + } else { + formatter = new SimpleDateFormat(datePattern); + } + } + + /** + * Parses a suffix into possible parts. + * + * @param suffix the suffix to parse + * + * @return the rotation suffix representation + */ + static FileRotator parse(final String suffix) { + if (suffix == null || suffix.isEmpty()) { + return EMPTY; + } + // Check the if the suffix contains a compression suffix + String compressionSuffix = ""; + String datePattern = ""; + CompressionType compressionType = CompressionType.NONE; + final String lSuffix = suffix.toLowerCase(Locale.ROOT); + int compressionIndex = lSuffix.indexOf(".gz"); + if (compressionIndex != -1) { + compressionSuffix = suffix.substring(compressionIndex); + datePattern = suffix.substring(0, compressionIndex); + compressionType = FileRotator.CompressionType.GZIP; + } else { + compressionIndex = lSuffix.indexOf(".zip"); + if (compressionIndex != -1) { + compressionSuffix = suffix.substring(compressionIndex); + datePattern = suffix.substring(0, compressionIndex); + compressionType = FileRotator.CompressionType.ZIP; + } + } + if (compressionSuffix.isEmpty() && datePattern.isEmpty()) { + return new FileRotator(suffix, suffix, "", CompressionType.NONE); + } + return new FileRotator(suffix, datePattern, compressionSuffix, compressionType); + } + + /** + * The {@linkplain java.text.SimpleDateFormat date format pattern} for the suffix or an empty + * {@linkplain String string}. + * + * @return the date pattern or an empty string + */ + String getDatePattern() { + return datePattern; + } + + /** + * The compression suffix or an empty {@linkplain String string} + * + * @return the compression suffix or an empty string + */ + @SuppressWarnings("unused") + String getCompressionSuffix() { + return compressionSuffix; + } + + /** + * The compression type. + * + * @return the compression type + */ + @SuppressWarnings("unused") + CompressionType getCompressionType() { + return compressionType; + } + + /** + * Rotates the file to a new file appending the suffix to the target. + *
+ * The compression suffix will automatically be appended to target file if compression is being used. If compression + * is not being used the file is just moved replacing the target file if it already exists. + *
+ * + * @param source the file to be rotated + * @param suffix the suffix to append to the rotated file. + * + * @throws IOException if an error occurs rotating the file + */ + void rotate(final Path source, final String suffix) throws IOException { + final Path target = Paths.get(source + suffix + compressionSuffix); + if (compressionType == CompressionType.GZIP) { + archiveGzip(source, target); + } else if (compressionType == CompressionType.ZIP) { + archiveZip(source, target); + } else { + Files.move(source, target, StandardCopyOption.REPLACE_EXISTING); + } + } + + /** + * Rotates the file to a new file appending the suffix to the target. If a date suffix was specified the suffix + * will be added before the index or compression suffix. The current date will be used for the suffix. + *+ * If the {@code maxBackupIndex} is greater than 0 previously rotated files will be moved to an numerically + * incremented target. The compression suffix, if required, will be appended to this indexed file name. + *
+ * + * @param source the file to be rotated + * @param maxBackupIndex the number of backups to keep + * + * @throws IOException if an error occurs rotating the file + */ + void rotate(final Path source, final int maxBackupIndex) throws IOException { + if (formatter == null) { + rotate(source, "", maxBackupIndex); + } else { + final String suffix; + synchronized (formatter) { + suffix = formatter.format(new Date()); + } + rotate(source, suffix, maxBackupIndex); + } + } + + /** + * Rotates the file to a new file appending the suffix to the target. + *+ * If the {@code maxBackupIndex} is greater than 0 previously rotated files will be moved to an numerically + * incremented target. The compression suffix, if required, will be appended to this indexed file name. + *
+ * + * @param source the file to be rotated + * @param suffix the optional suffix to append to the file before the index and optional compression suffix + * @param maxBackupIndex the number of backups to keep + * + * @throws IOException if an error occurs rotating the file + */ + void rotate(final Path source, final String suffix, final int maxBackupIndex) throws IOException { + if (maxBackupIndex > 0) { + final String rotationSuffix = (suffix == null ? "" : suffix); + final String fileWithSuffix = source.toAbsolutePath() + rotationSuffix; + Files.deleteIfExists(Paths.get(fileWithSuffix + "." + maxBackupIndex + compressionSuffix)); + for (int i = maxBackupIndex - 1; i >= 1; i--) { + final Path src = Paths.get(fileWithSuffix + "." + i + compressionSuffix); + if (Files.exists(src)) { + final Path target = Paths.get(fileWithSuffix + "." + (i + 1) + compressionSuffix); + Files.move(src, target, StandardCopyOption.REPLACE_EXISTING); + } + } + rotate(source, rotationSuffix + ".1"); + } else if (suffix != null && !suffix.isEmpty()) { + rotate(source, suffix); + } + } + + @Override + public String toString() { + return originalSuffix; + } + + + private static void archiveGzip(final Path source, final Path target) throws IOException { + final byte[] buff = new byte[512]; + try (final GZIPOutputStream out = new GZIPOutputStream(Files.newOutputStream(target), true)) { + try (final InputStream in = Files.newInputStream(source)) { + int len; + while ((len = in.read(buff)) != -1) { + out.write(buff, 0, len); + } + } + out.finish(); + } + } + + private static void archiveZip(final Path source, final Path target) throws IOException { + final byte[] buff = new byte[512]; + try (final ZipOutputStream out = new ZipOutputStream(Files.newOutputStream(target), StandardCharsets.UTF_8)) { + final ZipEntry entry = new ZipEntry(source.getFileName().toString()); + out.putNextEntry(entry); + try (final InputStream in = Files.newInputStream(source)) { + int len; + while ((len = in.read(buff)) != -1) { + out.write(buff, 0, len); + } + } + out.closeEntry(); + } + } +} diff --git a/src/main/java/org/jboss/logmanager/handlers/PeriodicRotatingFileHandler.java b/src/main/java/org/jboss/logmanager/handlers/PeriodicRotatingFileHandler.java index 5d112344..73c80971 100644 --- a/src/main/java/org/jboss/logmanager/handlers/PeriodicRotatingFileHandler.java +++ b/src/main/java/org/jboss/logmanager/handlers/PeriodicRotatingFileHandler.java @@ -19,22 +19,17 @@ package org.jboss.logmanager.handlers; -import org.jboss.logmanager.ExtLogRecord; - +import java.io.File; +import java.io.FileNotFoundException; import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.StandardCopyOption; import java.text.SimpleDateFormat; -import java.util.Date; import java.util.Calendar; +import java.util.Date; import java.util.TimeZone; -import java.io.File; -import java.io.FileNotFoundException; - import java.util.logging.ErrorManager; +import org.jboss.logmanager.ExtLogRecord; + /** * A file handler which rotates the log at a preset time interval. The interval is determined by the content of the * suffix string which is passed in to {@link #setSuffix(String)}. @@ -46,6 +41,7 @@ public class PeriodicRotatingFileHandler extends FileHandler { private Period period = Period.NEVER; private long nextRollover = Long.MAX_VALUE; private TimeZone timeZone = TimeZone.getDefault(); + private FileRotator fileRotator = FileRotator.EMPTY; /** * Construct a new instance with no formatter and no output file. @@ -124,17 +120,22 @@ protected void preWrite(final ExtLogRecord record) { /** * Set the suffix string. The string is in a format which can be understood by {@link java.text.SimpleDateFormat}. * The period of the rotation is automatically calculated based on the suffix. + *+ * If the suffix ends with {@code .gz} or {@code .zip} the file will be compressed on rotation. + *
* * @param suffix the suffix * @throws IllegalArgumentException if the suffix is not valid */ public void setSuffix(String suffix) throws IllegalArgumentException { - final SimpleDateFormat format = new SimpleDateFormat(suffix); + final FileRotator fileRotator = FileRotator.parse(suffix); + final String dateSuffix = fileRotator.getDatePattern(); + final SimpleDateFormat format = new SimpleDateFormat(dateSuffix); format.setTimeZone(timeZone); - final int len = suffix.length(); + final int len = dateSuffix.length(); Period period = Period.NEVER; for (int i = 0; i < len; i ++) { - switch (suffix.charAt(i)) { + switch (dateSuffix.charAt(i)) { case 'y': period = min(period, Period.YEAR); break; case 'M': period = min(period, Period.MONTH); break; case 'w': @@ -149,7 +150,7 @@ public void setSuffix(String suffix) throws IllegalArgumentException { case 'K': case 'h': period = min(period, Period.HOUR); break; case 'm': period = min(period, Period.MINUTE); break; - case '\'': while (suffix.charAt(++i) != '\''); break; + case '\'': while (dateSuffix.charAt(++i) != '\''); break; case 's': case 'S': throw new IllegalArgumentException("Rotating by second or millisecond is not supported"); } @@ -157,6 +158,7 @@ public void setSuffix(String suffix) throws IllegalArgumentException { synchronized (outputLock) { this.format = format; this.period = period; + this.fileRotator = fileRotator; final long now; final File file = getFile(); if (file != null && file.lastModified() > 0) { @@ -177,14 +179,22 @@ protected final String getNextSuffix() { return nextSuffix; } + /** + * Returns the rotation suffix for this handler. + * + * @return the rotation suffix + */ + FileRotator getFileRotator() { + return fileRotator; + } + private void rollOver() { try { final File file = getFile(); // first, close the original file (some OSes won't let you move/rename a file that is open) setFile(null); // next, rotate it - final Path target = Paths.get(file.getAbsolutePath() + nextSuffix); - Files.move(file.toPath(), target, StandardCopyOption.REPLACE_EXISTING); + fileRotator.rotate(file.toPath(), nextSuffix); // start new file setFile(file); } catch (IOException e) { diff --git a/src/main/java/org/jboss/logmanager/handlers/PeriodicSizeRotatingFileHandler.java b/src/main/java/org/jboss/logmanager/handlers/PeriodicSizeRotatingFileHandler.java index 02fd5e69..644f42a9 100644 --- a/src/main/java/org/jboss/logmanager/handlers/PeriodicSizeRotatingFileHandler.java +++ b/src/main/java/org/jboss/logmanager/handlers/PeriodicSizeRotatingFileHandler.java @@ -23,10 +23,6 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.OutputStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.StandardCopyOption; import java.util.logging.ErrorManager; import org.jboss.logmanager.ExtLogRecord; @@ -156,7 +152,10 @@ public void setFile(final File file) throws FileNotFoundException { // Check for a rotate if (rotateOnBoot && maxBackupIndex > 0 && file != null && file.exists() && file.length() > 0L) { try { - rotate(file); + final FileRotator fileRotator = getFileRotator(); + if (fileRotator != null) { + fileRotator.rotate(file.toPath(), getNextSuffix(), maxBackupIndex); + } } catch (IOException e) { throw new RuntimeException(e); } @@ -229,7 +228,7 @@ protected void preWrite(final ExtLogRecord record) { } // close the old file. setFile(null); - rotate(file); + getFileRotator().rotate(file.toPath(), getNextSuffix(), maxBackupIndex); // start with new file. setFile(file); } catch (IOException e) { @@ -237,17 +236,4 @@ protected void preWrite(final ExtLogRecord record) { } } } - - private void rotate(final File file) throws IOException { - final Path fileWithSuffix = Paths.get(file.getAbsolutePath() + getNextSuffix()); - Files.deleteIfExists(Paths.get(fileWithSuffix + "." + maxBackupIndex)); - for (int i = maxBackupIndex - 1; i >= 1; i--) { - final Path src = Paths.get(fileWithSuffix + "." + i); - if (Files.exists(src)) { - final Path target = Paths.get(fileWithSuffix + "." + (i + 1)); - Files.move(src, target, StandardCopyOption.REPLACE_EXISTING); - } - } - Files.move(file.toPath(), Paths.get(fileWithSuffix + ".1"), StandardCopyOption.REPLACE_EXISTING); - } } diff --git a/src/main/java/org/jboss/logmanager/handlers/SizeRotatingFileHandler.java b/src/main/java/org/jboss/logmanager/handlers/SizeRotatingFileHandler.java index c70b6221..159cd68e 100644 --- a/src/main/java/org/jboss/logmanager/handlers/SizeRotatingFileHandler.java +++ b/src/main/java/org/jboss/logmanager/handlers/SizeRotatingFileHandler.java @@ -25,12 +25,6 @@ import java.io.FileNotFoundException; import org.jboss.logmanager.ExtLogRecord; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.StandardCopyOption; -import java.text.SimpleDateFormat; -import java.util.Date; import java.util.logging.ErrorManager; public class SizeRotatingFileHandler extends FileHandler { @@ -39,7 +33,7 @@ public class SizeRotatingFileHandler extends FileHandler { private int maxBackupIndex = 1; private CountingOutputStream outputStream; private boolean rotateOnBoot; - private String suffix; + private FileRotator suffix = FileRotator.EMPTY; /** * Construct a new instance with no formatter and no output file. @@ -146,7 +140,7 @@ public void setFile(final File file) throws FileNotFoundException { // Check for a rotate if (rotateOnBoot && maxBackupIndex > 0 && file != null && file.exists() && file.length() > 0L) { try { - rotate(file); + suffix.rotate(file.toPath(), maxBackupIndex); } catch (IOException e) { throw new RuntimeException(e); } @@ -211,15 +205,22 @@ public void setMaxBackupIndex(final int maxBackupIndex) { * @return the suffix or {@code null} if no suffix should be used */ public String getSuffix() { - return suffix; + if (suffix == FileRotator.EMPTY) { + return null; + } + return suffix.toString(); } /** * Sets the suffix to be appended to the file name during the file rotation. The suffix does not play a role in * determining when the file should be rotated. * - * The suffix must be a string understood by the {@link java.text.SimpleDateFormat}. + * The suffix must be a string understood by the {@link java.text.SimpleDateFormat}. Optionally the suffix can end + * with {@code .gz} or {@code .zip} which will compress the file on rotation. * + *+ * If the suffix ends with {@code .gz} or {@code .zip} the fill will be compressed on rotation. + *
* Note: Any files rotated with the suffix appended will not be deleted. The {@link #setMaxBackupIndex(int) * maxBackupIndex} is not used for files with a suffix. * @@ -228,7 +229,7 @@ public String getSuffix() { public void setSuffix(final String suffix) { checkAccess(this); synchronized (outputLock) { - this.suffix = suffix; + this.suffix = FileRotator.parse(suffix); } } @@ -245,7 +246,7 @@ protected void preWrite(final ExtLogRecord record) { } // close the old file. setFile(null); - rotate(file); + suffix.rotate(file.toPath(), maxBackupIndex); // start with new file. setFile(file); } catch (IOException e) { @@ -253,36 +254,4 @@ protected void preWrite(final ExtLogRecord record) { } } } - - private void rotate(final File file) throws IOException { - if (suffix == null) { - // rotate. First, drop the max file (if any), then move each file to the next higher slot. - Files.deleteIfExists(Paths.get(file.getAbsolutePath() + "." + maxBackupIndex)); - for (int i = maxBackupIndex - 1; i >= 1; i--) { - final Path src = Paths.get(file.getAbsolutePath() + "." + i); - if (Files.exists(src)) { - final Path target = Paths.get(file.getAbsolutePath() + "." + (i + 1)); - Files.move(src, target, StandardCopyOption.REPLACE_EXISTING); - } - } - Files.move(file.toPath(), Paths.get(file.getAbsolutePath() + ".1"), StandardCopyOption.REPLACE_EXISTING); - } else { - // This is not efficient, but performance risks were noted on the setSuffix() method - final String suffix = new SimpleDateFormat(this.suffix).format(new Date()); - // Create the file name - final String newBaseFilename = file.getAbsolutePath() + suffix; - - // rotate. First, drop the max file (if any), then move each file to the next higher slot. - Files.deleteIfExists(Paths.get(newBaseFilename + "." + maxBackupIndex)); - for (int i = maxBackupIndex - 1; i >= 1; i--) { - final Path src = Paths.get(newBaseFilename + "." + i); - if (Files.exists(src)) { - final Path target = Paths.get(newBaseFilename + "." + (i + 1)); - Files.move(src, target, StandardCopyOption.REPLACE_EXISTING); - } - } - // Rename the current file - Files.move(file.toPath(), Paths.get(newBaseFilename + ".1"), StandardCopyOption.REPLACE_EXISTING); - } - } } diff --git a/src/test/java/org/jboss/logmanager/handlers/AbstractHandlerTest.java b/src/test/java/org/jboss/logmanager/handlers/AbstractHandlerTest.java index b46b9cd0..7e8a5222 100644 --- a/src/test/java/org/jboss/logmanager/handlers/AbstractHandlerTest.java +++ b/src/test/java/org/jboss/logmanager/handlers/AbstractHandlerTest.java @@ -19,12 +19,25 @@ package org.jboss.logmanager.handlers; +import java.io.BufferedReader; import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; +import java.util.zip.GZIPInputStream; import org.jboss.logmanager.ExtHandler; import org.jboss.logmanager.ExtLogRecord; import org.jboss.logmanager.formatters.PatternFormatter; import org.junit.After; +import org.junit.Assert; import org.junit.Before; /** @@ -107,4 +120,44 @@ protected ExtLogRecord createLogRecord(final org.jboss.logmanager.Level level, f protected ExtLogRecord createLogRecord(final org.jboss.logmanager.Level level, final String format, final Object... args) { return new ExtLogRecord(level, String.format(format, args), getClass().getName()); } + + /** + * Validates that at least one line of the GZIP'd file contains the expected text. + * + * @param path the path to the GZIP file + * @param expectedContains the expected text + * + * @throws IOException if an error occurs while reading the GZIP file + */ + static void validateGzipContents(final Path path, final String expectedContains) throws IOException { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(new GZIPInputStream(Files.newInputStream(path))))) { + String line; + while ((line = reader.readLine()) != null) { + if (line.contains(expectedContains)) { + return; + } + } + } + Assert.fail(String.format("GZIP file %s missing contents: %s", path, expectedContains)); + } + + /** + * Validates that the ZIP file contains the expected file, the expected file is not empty and that the first line + * contains the expected text. + * + * @param path the path to the zip file + * @param expectedFileName the name of the file inside the zip file + * @param expectedContains the expected text + * + * @throws IOException if an error occurs reading the zip file + */ + static void validateZipContents(final Path path, final String expectedFileName, final String expectedContains) throws IOException { + try (final FileSystem zipFs = FileSystems.newFileSystem(URI.create("jar:" + path.toUri().toASCIIString()), Collections.singletonMap("create", "true"))) { + final Path file = zipFs.getPath(zipFs.getSeparator(), expectedFileName); + Assert.assertTrue(String.format("Expected file %s not found.", expectedFileName), Files.exists(file)); + final List