Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
Gerald Unterrainer committed Jun 28, 2021
2 parents 9dba803 + 135d79e commit 3d53227
Show file tree
Hide file tree
Showing 5 changed files with 342 additions and 1 deletion.
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

<modelVersion>4.0.0</modelVersion>
<artifactId>jre-utils</artifactId>
<version>0.1.10</version>
<version>0.1.11</version>
<name>JreUtils</name>
<packaging>jar</packaging>

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package info.unterrainer.commons.jreutils;

import java.nio.file.attribute.FileTime;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
Expand Down Expand Up @@ -27,6 +28,10 @@ public static LocalDateTime epochToUtcLocalDateTime(final Long epoch) {
return Instant.ofEpochMilli(epoch).atZone(ZoneId.of("UTC")).toLocalDateTime();
}

public static LocalDateTime fileTimeToUtcLocalDateTime(final FileTime time) {
return time.toInstant().atZone(ZoneId.of("UTC")).toLocalDateTime();
}

public static int getWeekOf(final LocalDateTime dateTime) {
return dateTime.get(WeekFields.ISO.weekOfWeekBasedYear());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
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.<br>
* The files are named pathToFile/file1.fileExtension and
* pathToFile/file2.fileExtension
* <p>
* 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.
* <p>
* You create this with a path to a virtual file that should not exist, since it
* misses the numbers.<br>
* Use the methods to retrieve a write-handle to the correct (older) file and a
* read-handle as well (newer).
* <p>
* 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<T> {
/**
* 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<T> andThen(final ConsumerWithIoException<? super T> 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 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;
}

public void write(final ConsumerWithIoException<BufferedWriter> 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());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package info.unterrainer.commons.jreutils;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIOException;

import java.io.IOException;
import java.nio.file.Path;
import java.time.LocalDateTime;

import org.junit.jupiter.api.Test;

public class DoubleBufferedFileTests {

@Test
public void readFromPreparedFileWorks() throws IOException {
DoubleBufferedFile dbf = new DoubleBufferedFile(Path.of("test"), "txt");
assertThat(dbf.read()).isEqualTo("test");
}

@Test
public void readThrowsExceptionWhenNoFilePresent() throws IOException {
DoubleBufferedFile dbf = new DoubleBufferedFile(Path.of("new"), "txt");
try {
dbf.delete();
assertThatIOException().isThrownBy(() -> dbf.read());
} finally {
dbf.delete();
}
}

@Test
public void readAfterWriteReturnsSameValue() throws IOException {
DoubleBufferedFile dbf = new DoubleBufferedFile(Path.of("new"), "txt");
try {
dbf.write(w -> w.write("test"));
assertThat(dbf.read()).isEqualTo("test");
} finally {
dbf.delete();
}
}

@Test
public void readAfterWriteAfterWriteReturnsNewValue() throws IOException {
DoubleBufferedFile dbf = new DoubleBufferedFile(Path.of("new"), "txt");
try {
dbf.write(w -> w.write("test_old"));
dbf.write(w -> w.write("test_new"));
assertThat(dbf.read()).isEqualTo("test_new");
} finally {
dbf.delete();
}
}

@Test
public void modifiedNewIsNullWithoutAnyValueWritten() throws IOException {
DoubleBufferedFile dbf = new DoubleBufferedFile(Path.of("new"), "txt");
LocalDateTime newest = dbf.getNewestModifiedTime();
assertThat(newest).isNull();
}

@Test
public void modifiedOldIsNullWithoutAnyValueWritten() throws IOException {
DoubleBufferedFile dbf = new DoubleBufferedFile(Path.of("new"), "txt");
LocalDateTime oldest = dbf.getOldestModifiedTime();
assertThat(oldest).isNull();
}

@Test
public void modifiedNewAndOldAreEqualWithOneValueWritten() throws IOException {
DoubleBufferedFile dbf = new DoubleBufferedFile(Path.of("new"), "txt");
try {
dbf.write(w -> w.write("test_old"));
LocalDateTime newest = dbf.getNewestModifiedTime();
LocalDateTime oldest = dbf.getOldestModifiedTime();
assertThat(newest).isEqualTo(oldest);
} finally {
dbf.delete();
}
}

@Test
public void modifiedNewAndOldDifferWithTwoValuesWritten() throws IOException {
DoubleBufferedFile dbf = new DoubleBufferedFile(Path.of("new"), "txt");
try {
dbf.write(w -> w.write("test_old"));
dbf.write(w -> w.write("test_new"));
LocalDateTime newest = dbf.getNewestModifiedTime();
LocalDateTime oldest = dbf.getOldestModifiedTime();
assertThat(newest).isAfter(oldest);
} finally {
dbf.delete();
}
}

@Test
public void modifiedAfterThreeWritesReturnsNewestValue() throws IOException {
DoubleBufferedFile dbf = new DoubleBufferedFile(Path.of("new"), "txt");
try {
dbf.write(w -> w.write("test_oldest"));
LocalDateTime oldest = dbf.getNewestModifiedTime();
dbf.write(w -> w.write("test_older"));
LocalDateTime older = dbf.getNewestModifiedTime();
dbf.write(w -> w.write("test_new"));
assertThat(dbf.getNewestModifiedTime()).isAfter(older);
assertThat(dbf.getNewestModifiedTime()).isAfter(oldest);
} finally {
dbf.delete();
}
}
}
1 change: 1 addition & 0 deletions test1.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
test

0 comments on commit 3d53227

Please sign in to comment.