import java.io.File;
import java.io.IOException;
import java.io.Writer;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
自動的にファイルのバックアップを行うライタ。<br/>
ファイルのサイズが閾値を超えた場合または前回出力時と日付が異なる場合にバックアップを行う。<br/>
指定された世代数を超えるバックアップファイルは自動的に削除されるが、VMの起動前から存在していたファイルはその限りではない。<br/>
public class BackupFileWriter extends Writer {
private static final String FILE_SEPARATOR = System.getProperty("file.separator");
private static final SimpleDateFormat DATE_FORMATTER = new SimpleDateFormat("yyyyMMdd");
private static final Calendar c1 = Calendar.getInstance();
private static final Calendar c2 = Calendar.getInstance();
ファイルを作成日時と対で保持するクラス
public static class BackupFile {
private long creationTime;
private File file;
public void setCreationTime(long creationTime) {
this.creationTime = creationTime;
}
public long getCreationTime() {
return creationTime;
}
public void setFile(File file) {
this.file = file;
}
public File getFile() {
return file;
}
}
新しくバックアップファイルを作成した時に、古いバックアップファイルの処理を定義するクラス。<br/>
public abstract static class BackupPolicy {
protected int capacity;
protected List<BackupFile> files = new ArrayList<BackupFile>();
@param capacity
public BackupPolicy(int capacity) {
this.capacity = capacity;
}
バックアップ済みのファイルを追加
@param file
public void add(BackupFile file) {
files.add(file);
}
保持しているバックアップファイルの数を返す。
@return
public int size() {
return files.size();
}
public boolean isFull() {
return files.size() == capacity;
}
古いバックアップファイルの処理内容を記述する。
@param 現在書き込み中のファイルの情報
@throws IOException
public abstract void processOldFile(CyclicFileWriter.FileContext context) throws IOException;
新しいバックアップファイルの名前を決める
@param context
@return
@throws IOException
public abstract String createBackupFileName(CyclicFileWriter.FileContext context) throws IOException;
}
最も古いファイルを削除し、それ以降のファイルを順次一つ前のファイルにリネームするクラス。<br/>
public static class RotateBackupPolicy extends BackupPolicy {
public RotateBackupPolicy(int capacity) {
super(capacity);
}
@Override
public void processOldFile(CyclicFileWriter.FileContext context) throws IOException {
if (!isFull()) {
return;
}
if (!files.get(0).getFile().delete()) {
throw new IOException("failed delete file '" + files.get(0).getFile().getCanonicalPath() + "'");
}
if (files.get(0).getFile().exists()) {
throw new IOException("failed delete file '" + files.get(0).getFile().getCanonicalPath() + "'");
}
for (int i = 1; i < files.size(); i++) {
if (!files.get(i).getFile().renameTo(files.get(i - 1).getFile())) {
throw new IOException("failed rename file '" + files.get(i).getFile().getCanonicalPath() + "' to '" + files.get(i - 1).getFile().getCanonicalPath() + "'");
}
if (files.get(i).getFile().exists()) {
throw new IOException("failed rename file '" + files.get(i).getFile().getCanonicalPath() + "' to '" + files.get(i - 1).getFile().getCanonicalPath() + "'");
}
files.get(i - 1).setCreationTime(files.get(i).getCreationTime());
}
files.remove(files.size() - 1);
}
ファイル名の最後(拡張子の前)にバックアップ数を付加する。<br/>
例:/work/test1.txt => /work/test_1.txt,/work/test_2.txt・・・ <br/>
付加されるバックアップ数は{@link #size()}+1。
@Override
public String createBackupFileName(CyclicFileWriter.FileContext context) throws IOException {
File file = context.getFile();
String fileName = file.getName();
String ext = "";
int pos = fileName.indexOf('.');
if (pos > 0) {
ext = fileName.substring(pos + 1);
fileName = fileName.substring(0, pos);
}
int generation = size() + 1;
if (ext.length() > 0) {
return String.format("%s%s%s_%s.%s", file.getCanonicalFile().getParent(), FILE_SEPARATOR, fileName, generation, ext);
} else {
return String.format("%s%s%s_%s", file.getCanonicalFile().getParent(), FILE_SEPARATOR, fileName, generation);
}
}
}
最も古いファイルを削除するだけのクラス。<br/>
public static class DeleteBackupPolicy extends BackupPolicy {
public DeleteBackupPolicy(int capacity) {
super(capacity);
}
@Override
public void processOldFile(CyclicFileWriter.FileContext context) throws IOException {
if (!isFull()) {
return;
}
if (!files.get(0).getFile().delete()) {
throw new IOException("failed delete file '" + files.get(0).getFile().getCanonicalPath() + "'");
}
if (files.get(0).getFile().exists()) {
throw new IOException("failed delete file '" + files.get(0).getFile().getCanonicalPath() + "'");
}
files.remove(0);
}
ファイル名の最後(拡張子の前)に作成日を付加する。<br/>
例:/work/test1.txt => /work/test_20100101.txt,/work/test_20100102.txt・・・ <br/>
public String createBackupFileName(CyclicFileWriter.FileContext context) throws IOException {
File file = context.getFile();
String fileName = file.getName();
String ext = "";
int pos = fileName.indexOf('.');
if (pos > 0) {
ext = fileName.substring(pos + 1);
fileName = fileName.substring(0, pos);
}
fileName += "_" + dateFormat(DATE_FORMATTER, new Date(context.getCreationTime()));
if (ext.length() > 0) {
return String.format("%s%s%s.%s", file.getCanonicalFile().getParent(), FILE_SEPARATOR, fileName, ext);
} else {
return String.format("%s%s%s", file.getCanonicalFile().getParent(), FILE_SEPARATOR, fileName);
}
}
}
条件によって古いファイルを削除するだけかリネームを行うかを決めるクラス。<br/>
rotateCondition()がfalseを返すと削除するだけになり、trueを返すとリネームを行うようになる。
public abstract static class ConditionalRotateBackupPolicy extends BackupPolicy {
public ConditionalRotateBackupPolicy(int capacity) {
super(capacity);
}
@Override
public void processOldFile(CyclicFileWriter.FileContext context) throws IOException {
if (!isFull()) {
return;
}
boolean isRotate = rotateCondition(context);
if (!files.get(0).getFile().delete()) {
throw new IOException("failed delete file '" + files.get(0).getFile().getCanonicalPath() + "'");
}
if (files.get(0).getFile().exists()) {
throw new IOException("failed delete file '" + files.get(0).getFile().getCanonicalPath() + "'");
}
if (isRotate) {
for (int i = 1; i < files.size(); i++) {
if (!files.get(i).getFile().renameTo(files.get(i - 1).getFile())) {
throw new IOException("failed rename file '" + files.get(i).getFile().getCanonicalPath() + "' to '" + files.get(i - 1).getFile().getCanonicalPath() + "'");
}
if (files.get(i).getFile().exists()) {
throw new IOException("failed rename file '" + files.get(i).getFile().getCanonicalPath() + "' to '" + files.get(i - 1).getFile().getCanonicalPath() + "'");
}
files.get(i - 1).setCreationTime(files.get(i).getCreationTime());
}
files.remove(files.size() - 1);
} else {
files.remove(0);
}
}
public abstract boolean rotateCondition(CyclicFileWriter.FileContext context) throws IOException;
}
日付ごと、一定サイズごとでバックアップするクラス
public static class DailyRotateBackupPolicy extends ConditionalRotateBackupPolicy {
protected int dailyBackupCount = 1;
public DailyRotateBackupPolicy(int capacity) {
super(capacity);
}
@Override
public boolean rotateCondition(FileContext context) throws IOException {
return compareDate(files.get(0).getCreationTime(), context.getCreationTime());
}
ファイル名の最後(拡張子の前)に作成日とバックアップ数を付加する。<br/>
例:/work/test1.txt => /work/test_20100101_1.txt,/work/test_20100101_2.txt, /work/test_20100102_1.txt・・・ <br/>
@Override
public String createBackupFileName(FileContext context) throws IOException {
File file = context.getFile();
String fileName = file.getName();
String ext = "";
int pos = fileName.indexOf('.');
if (pos > 0) {
ext = fileName.substring(pos + 1);
fileName = fileName.substring(0, pos);
}
fileName += "_" + dateFormat(DATE_FORMATTER, new Date(context.getCreationTime()));
int generation = dailyBackupCount++;
dailyBackupCount = Math.min(dailyBackupCount, capacity);
if (ext.length() > 0) {
return String.format("%s%s%s_%s.%s", file.getCanonicalFile().getParent(), FILE_SEPARATOR, fileName, generation, ext);
} else {
return String.format("%s%s%s_%s", file.getCanonicalFile().getParent(), FILE_SEPARATOR, fileName, generation);
}
}
@Override
public void add(BackupFile file) {
super.add(file);
if (!compareDate(file.getCreationTime(), SystemUtils.currentTimeMillis())) {
dailyBackupCount = 1;
}
}
}
2つの日付が同じであればtrueを返す。<br/>
@return
private synchronized static boolean compareDate(long d1, long d2) {
c1.setTimeInMillis(d1);
c2.setTimeInMillis(d2);
return c1.get(Calendar.DATE) == c2.get(Calendar.DATE) && c1.get(Calendar.MONTH) == c2.get(Calendar.MONTH) && c1.get(Calendar.YEAR) == c2.get(Calendar.YEAR);
}
日付の文字列化。<br/>
DateFormatがスレッドセーフではないためsynchronizedで使用。
@param df
@param date
@return
private synchronized static String dateFormat(DateFormat df, Date date) {
return df.format(date);
}
private CyclicFileWriter writer;
private long maxLength = -1;
private boolean changeDate;
private int backupCount = 3;
protected BackupPolicy backupPolicy;
ファイルの上限サイズを指定してライタを作成する。<br/>
ただし、サイズのチェックは改行のタイミングで行っているので、maxLengthを若干超えることは有りうる。<br/>
@param file
@param maxLength
@param backupCount
@throws IOException
public BackupFileWriter(File file, long maxLength, int backupCount) throws IOException {
this(file, maxLength, false, backupCount);
}
日付が変わるたびにバックアップを行うライタを作成する。<br/>
@param file
@param changeDate
@param backupCount
@throws IOException
public BackupFileWriter(File file, boolean changeDate, int backupCount) throws IOException {
this(file, -1, changeDate, backupCount);
}
ファイルサイズが閾値を超えるか、日付が変わった場合にバックアップを行うライタを作成する。<br/>
ただし、サイズのチェックは改行のタイミングで行っているので、maxLengthを若干超えることは有りうる。<br/>
@param file
@param maxLength
@param changeDate
@param backupCount
@throws IOException
public BackupFileWriter(File file, long maxLength, boolean changeDate, int backupCount) throws IOException {
if (maxLength <= 0 && !changeDate) {
throw new IllegalArgumentException("must be maxLength > 0 or changeDate is true");
}
if (backupCount <= 0) {
throw new IllegalArgumentException("must be backupCount > 0");
}
this.maxLength = maxLength;
this.changeDate = changeDate;
this.backupCount = backupCount;
if (maxLength <= 0) {
backupPolicy = new DeleteBackupPolicy(backupCount);
} else {
if (changeDate) {
backupPolicy = new DailyRotateBackupPolicy(backupCount);
} else {
backupPolicy = new RotateBackupPolicy(backupCount);
}
}
writer = new CyclicFileWriter(new CyclicFileWriter.CyclicPolicy() {
@Override
public boolean checkCyclic(CyclicFileWriter.FileContext context) {
if (getMaxLength() > 0 && context.getLength() > getMaxLength()) {
return true;
}
if (isChangeDate() && !compareDate(context.getCreationTime(), SystemUtils.currentTimeMillis())) {
return true;
}
return false;
}
@Override
public void cyclic(CyclicFileWriter.FileContext context) throws IOException {
String newFileName = "";
BackupFile newFile = null;
while (newFile == null) {
if (backupPolicy.isFull()) {
backupPolicy.processOldFile(context);
}
newFileName = backupPolicy.createBackupFileName(context);
newFile = new BackupFile();
newFile.setFile(new File(newFileName));
if (newFile.getFile().exists()) {
newFile.setCreationTime(newFile.getFile().lastModified() + SystemUtils.getOffsetCurrentTimeMillis());
backupPolicy.add(newFile);
newFile = null;
}
}
File file = context.getFile();
if (!file.renameTo(newFile.getFile())) {
throw new IOException("failed rename file '" + file.getCanonicalPath() + "' to '" + newFileName + "'");
}
if (file.exists()) {
throw new IOException("failed rename file '" + file.getCanonicalPath() + "' to '" + newFileName + "'");
}
newFile.setCreationTime(context.getCreationTime());
backupPolicy.add(newFile);
}
}, file);
}
ファイルの上限サイズを指定してライタを作成する。<br/>
ただし、サイズのチェックは改行のタイミングで行っているので、maxLengthを若干超えることは有りうる。<br/>
@param name
@param maxLength
@param backupCount
@throws IOException
public BackupFileWriter(String name, long maxLength, int backupCount) throws IOException {
this(new File(name), maxLength, false, backupCount);
}
日付が変わるたびにバックアップを行うライタを作成する。<br/>
@param name
@param changeDate
@param backupCount
@throws IOException
public BackupFileWriter(String name, boolean changeDate, int backupCount) throws IOException {
this(new File(name), -1, changeDate, backupCount);
}
ファイルサイズが閾値を超えるか、日付が変わった場合にバックアップを行うライタを作成する。<br/>
ただし、サイズのチェックは改行のタイミングで行っているので、maxLengthを若干超えることは有りうる。<br/>
@param name
@param maxLength
@param changeDate
@param backupCount
@throws IOException
public BackupFileWriter(String name, int maxLength, boolean changeDate, int backupCount) throws IOException {
this(new File(name), maxLength, changeDate, backupCount);
}
ファイルの上限サイズを返す。<br/>
@return
public long getMaxLength() {
return maxLength;
}
日付ごとのバックアップを行うか否かを返す。<br/>
@return
public boolean isChangeDate() {
return changeDate;
}
バックアップを保持する世代数
@return
public int getBackupCount() {
return backupCount;
}
@Override
public void close() throws IOException {
writer.close();
}
@Override
public void flush() throws IOException {
writer.flush();
}
@Override
public void write(char[] cbuf, int off, int len) throws IOException {
writer.write(cbuf, off, len);
}
}