All Downloads are FREE. Search and download functionalities are using the official Maven repository.

info.unterrainer.commons.jreutils.DoubleBufferedFile Maven / Gradle / Ivy

There is a newer version: 0.3.15
Show newest version
package info.unterrainer.commons.jreutils;

import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.time.LocalDateTime;
import java.util.Objects;

import lombok.Data;
import lombok.experimental.Accessors;

/**
 * Allows to access (read/write) a 'doubleBuffered file', meaning that there are
 * two files which get written to alternately.
* The files are named pathToFile/file1.fileExtension and * pathToFile/file2.fileExtension *

* This allows for some fault-tolerance when it comes to corrupted files, since * you always have, at least, the other, albeit older file at your disposal. *

* You create this with a path to a virtual file that should not exist, since it * misses the numbers.
* Use the methods to retrieve a write-handle to the correct (older) file and a * read-handle as well (newer). *

* Throws an IOException if something happens it cannot handle any longer (both * files are locked for write-access and you're requesting write-access for * example). */ @Accessors(fluent = true) public class DoubleBufferedFile { @FunctionalInterface public interface ConsumerWithIoException { /** * Performs this operation on the given argument. * * @param t the input argument * @throws IOException if one occurs */ void accept(T t) throws IOException; /** * Returns a composed {@code Consumer} that performs, in sequence, this * operation followed by the {@code after} operation. If performing either * operation throws an exception, it is relayed to the caller of the composed * operation. If performing this operation throws an exception, the * {@code after} operation will not be performed. * * @param after the operation to perform after this operation * @return a composed {@code Consumer} that performs in sequence this operation * followed by the {@code after} operation * @throws NullPointerException if {@code after} is null * @throws IOException if one occurs */ default ConsumerWithIoException andThen(final ConsumerWithIoException after) throws IOException { Objects.requireNonNull(after); return (final T t) -> { accept(t); after.accept(t); }; } } @Data class DoubleBufferedFileData { private final Path path; private boolean exists; private LocalDateTime modified; private boolean readable; private boolean writable; DoubleBufferedFileData(final Path path) { super(); this.path = path; probe(); } void delete() throws IOException { if (Files.exists(path, LinkOption.NOFOLLOW_LINKS)) Files.delete(path); } void probe() { exists = Files.exists(path, LinkOption.NOFOLLOW_LINKS); writable = Files.isWritable(path); readable = Files.isReadable(path); modified = null; if (exists) try { modified = DateUtils.fileTimeToUtcLocalDateTime( Files.readAttributes(path, BasicFileAttributes.class).lastModifiedTime()); } catch (IOException e) { modified = null; readable = false; writable = false; } } DoubleBufferedFileData withCheckedWrite() throws IOException { if (!writable) throw new IOException(String.format("There is no write-access for the given path [%s].", path)); return this; } DoubleBufferedFileData withCheckedRead() throws IOException { if (!readable) throw new IOException(String.format("There is no read-access for the given path [%s].", path)); return this; } BufferedWriter getBufferedWriter() throws IOException { return Files.newBufferedWriter(path, Charset.forName("UTF-8")); } } protected DoubleBufferedFileData path1; protected DoubleBufferedFileData path2; public DoubleBufferedFile(final Path pathWithoutNumber, final String fileExtension) { path1 = new DoubleBufferedFileData(Path.of(pathWithoutNumber.toString() + "1." + fileExtension)); path2 = new DoubleBufferedFileData(Path.of(pathWithoutNumber.toString() + "2." + fileExtension)); } public void delete() throws IOException { path1.delete(); path2.delete(); } public boolean exists() { return path1.exists() || path2.exists(); } public LocalDateTime getNewestModifiedTime() { if (path1.modified() == null && path2.modified() == null) return null; if (path1.modified() == null) return path2.modified(); if (path2.modified() == null) return path1.modified(); if (path1.modified().compareTo(path2.modified()) > 0) return path1.modified(); return path2.modified(); } public LocalDateTime getOldestModifiedTime() { if (path1.modified() == null && path2.modified() == null) return null; if (path1.modified() == null) return path2.modified(); if (path2.modified() == null) return path1.modified(); if (path1.modified().compareTo(path2.modified()) <= 0) return path1.modified(); return path2.modified(); } private DoubleBufferedFileData getOldestForWriteAccess() throws IOException { path1.probe(); path2.probe(); if (!path1.exists() && !path2.exists()) return path1; if (!path1.exists()) return path1; if (!path2.exists()) return path2; if (!path1.writable() && !path2.writable()) throw new IOException("Both files are locked for write-access."); if (!path1.writable()) throw new IOException("File1 is locked for write-access."); if (!path2.writable()) throw new IOException("File2 is locked for write-access."); if (path1.modified() == null || path2.modified() == null) throw new IOException("Could not read the modified-date from one of the files."); if (path1.modified().compareTo(path2.modified()) <= 0) return path1; return path2; } private DoubleBufferedFileData getNewestForReadAccess() throws IOException { path1.probe(); path2.probe(); if (!path1.exists() && !path2.exists()) throw new IOException("There is no file to read from, because both files are missing."); if (!path1.exists()) return path2.withCheckedRead(); if (!path2.exists()) return path1.withCheckedRead(); if (!path1.readable() && !path2.readable()) throw new IOException("Both files are locked for read-access."); if (!path1.readable()) throw new IOException("File1 is locked for read-access."); if (!path2.readable()) throw new IOException("File2 is locked for read-access."); if (path1.modified() == null || path2.modified() == null) throw new IOException("Could not read the modified-date from one of the files."); if (path1.modified().compareTo(path2.modified()) > 0) return path1; return path2; } private DoubleBufferedFileData getOldestForReadAccess() throws IOException { path1.probe(); path2.probe(); if (!path1.exists() && !path2.exists()) throw new IOException("There is no file to read from, because both files are missing."); if (!path1.exists || !path2.exists()) throw new IOException("There is no file to read from, because the second file is missing."); if (!path1.readable() && !path2.readable()) throw new IOException("Both files are locked for read-access."); if (!path1.readable() || !path2.readable()) throw new IOException("The second file is locked for read-access."); if (path1.modified() == null || path2.modified() == null) throw new IOException("Could not read the modified-date from one of the files."); if (path1.modified().compareTo(path2.modified()) > 0) return path2; return path1; } public void write(final ConsumerWithIoException writeContentDelegate) throws IOException { DoubleBufferedFileData p = getOldestForWriteAccess(); if (p.exists()) Files.delete(p.path()); try (BufferedWriter writer = p.getBufferedWriter()) { writeContentDelegate.accept(writer); } p.probe(); } public String read() throws IOException { DoubleBufferedFileData p = getNewestForReadAccess(); return Files.readString(p.path()); } public String readOther() throws IOException { DoubleBufferedFileData p = getOldestForReadAccess(); return Files.readString(p.path()); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy