Skip to content

Commit

Permalink
[LOGMGR-30] Add ability to compress files when rotated if the suffix …
Browse files Browse the repository at this point in the history
…ends with .zip or .gz.
  • Loading branch information
jamezp committed May 5, 2017
1 parent bb5cae2 commit 1c0c3ea
Show file tree
Hide file tree
Showing 8 changed files with 529 additions and 83 deletions.
252 changes: 252 additions & 0 deletions src/main/java/org/jboss/logmanager/handlers/FileRotator.java
Original file line number Diff line number Diff line change
@@ -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 <a href="mailto:jperkins@redhat.com">James R. Perkins</a>
*/
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.
* <p>
* 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.
* </p>
*
* @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.
* <p>
* 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.
* </p>
*
* @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.
* <p>
* 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.
* </p>
*
* @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();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)}.
Expand All @@ -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.
Expand Down Expand Up @@ -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.
* <p>
* If the suffix ends with {@code .gz} or {@code .zip} the file will be compressed on rotation.
* </p>
*
* @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':
Expand All @@ -149,14 +150,15 @@ 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");
}
}
synchronized (outputLock) {
this.format = format;
this.period = period;
this.fileRotator = fileRotator;
final long now;
final File file = getFile();
if (file != null && file.lastModified() > 0) {
Expand All @@ -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) {
Expand Down

0 comments on commit 1c0c3ea

Please sign in to comment.