Skip to content

Commit 3d53227

Browse files
author
Gerald Unterrainer
committed
Merge branch 'develop'
2 parents 9dba803 + 135d79e commit 3d53227

File tree

5 files changed

+342
-1
lines changed

5 files changed

+342
-1
lines changed

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
<modelVersion>4.0.0</modelVersion>
1212
<artifactId>jre-utils</artifactId>
13-
<version>0.1.10</version>
13+
<version>0.1.11</version>
1414
<name>JreUtils</name>
1515
<packaging>jar</packaging>
1616

src/main/java/info/unterrainer/commons/jreutils/DateUtils.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package info.unterrainer.commons.jreutils;
22

3+
import java.nio.file.attribute.FileTime;
34
import java.time.Instant;
45
import java.time.LocalDateTime;
56
import java.time.ZoneId;
@@ -27,6 +28,10 @@ public static LocalDateTime epochToUtcLocalDateTime(final Long epoch) {
2728
return Instant.ofEpochMilli(epoch).atZone(ZoneId.of("UTC")).toLocalDateTime();
2829
}
2930

31+
public static LocalDateTime fileTimeToUtcLocalDateTime(final FileTime time) {
32+
return time.toInstant().atZone(ZoneId.of("UTC")).toLocalDateTime();
33+
}
34+
3035
public static int getWeekOf(final LocalDateTime dateTime) {
3136
return dateTime.get(WeekFields.ISO.weekOfWeekBasedYear());
3237
}
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
package info.unterrainer.commons.jreutils;
2+
3+
import java.io.BufferedWriter;
4+
import java.io.IOException;
5+
import java.nio.charset.Charset;
6+
import java.nio.file.Files;
7+
import java.nio.file.LinkOption;
8+
import java.nio.file.Path;
9+
import java.nio.file.attribute.BasicFileAttributes;
10+
import java.time.LocalDateTime;
11+
import java.util.Objects;
12+
13+
import lombok.Data;
14+
import lombok.experimental.Accessors;
15+
16+
/**
17+
* Allows to access (read/write) a 'doubleBuffered file', meaning that there are
18+
* two files which get written to alternately.<br>
19+
* The files are named pathToFile/file1.fileExtension and
20+
* pathToFile/file2.fileExtension
21+
* <p>
22+
* This allows for some fault-tolerance when it comes to corrupted files, since
23+
* you always have, at least, the other, albeit older file at your disposal.
24+
* <p>
25+
* You create this with a path to a virtual file that should not exist, since it
26+
* misses the numbers.<br>
27+
* Use the methods to retrieve a write-handle to the correct (older) file and a
28+
* read-handle as well (newer).
29+
* <p>
30+
* Throws an IOException if something happens it cannot handle any longer (both
31+
* files are locked for write-access and you're requesting write-access for
32+
* example).
33+
*/
34+
@Accessors(fluent = true)
35+
public class DoubleBufferedFile {
36+
37+
@FunctionalInterface
38+
public interface ConsumerWithIoException<T> {
39+
/**
40+
* Performs this operation on the given argument.
41+
*
42+
* @param t the input argument
43+
* @throws IOException if one occurs
44+
*/
45+
void accept(T t) throws IOException;
46+
47+
/**
48+
* Returns a composed {@code Consumer} that performs, in sequence, this
49+
* operation followed by the {@code after} operation. If performing either
50+
* operation throws an exception, it is relayed to the caller of the composed
51+
* operation. If performing this operation throws an exception, the
52+
* {@code after} operation will not be performed.
53+
*
54+
* @param after the operation to perform after this operation
55+
* @return a composed {@code Consumer} that performs in sequence this operation
56+
* followed by the {@code after} operation
57+
* @throws NullPointerException if {@code after} is null
58+
* @throws IOException if one occurs
59+
*/
60+
default ConsumerWithIoException<T> andThen(final ConsumerWithIoException<? super T> after) throws IOException {
61+
Objects.requireNonNull(after);
62+
return (final T t) -> {
63+
accept(t);
64+
after.accept(t);
65+
};
66+
}
67+
}
68+
69+
@Data
70+
class DoubleBufferedFileData {
71+
private final Path path;
72+
private boolean exists;
73+
private LocalDateTime modified;
74+
private boolean readable;
75+
private boolean writable;
76+
77+
DoubleBufferedFileData(final Path path) {
78+
super();
79+
this.path = path;
80+
probe();
81+
}
82+
83+
void delete() throws IOException {
84+
if (Files.exists(path, LinkOption.NOFOLLOW_LINKS))
85+
Files.delete(path);
86+
}
87+
88+
void probe() {
89+
exists = Files.exists(path, LinkOption.NOFOLLOW_LINKS);
90+
91+
writable = Files.isWritable(path);
92+
readable = Files.isReadable(path);
93+
94+
modified = null;
95+
if (exists)
96+
try {
97+
modified = DateUtils.fileTimeToUtcLocalDateTime(
98+
Files.readAttributes(path, BasicFileAttributes.class).lastModifiedTime());
99+
} catch (IOException e) {
100+
modified = null;
101+
readable = false;
102+
writable = false;
103+
}
104+
}
105+
106+
DoubleBufferedFileData withCheckedWrite() throws IOException {
107+
if (!writable)
108+
throw new IOException(String.format("There is no write-access for the given path [%s].", path));
109+
return this;
110+
}
111+
112+
DoubleBufferedFileData withCheckedRead() throws IOException {
113+
if (!readable)
114+
throw new IOException(String.format("There is no read-access for the given path [%s].", path));
115+
return this;
116+
}
117+
118+
BufferedWriter getBufferedWriter() throws IOException {
119+
return Files.newBufferedWriter(path, Charset.forName("UTF-8"));
120+
}
121+
}
122+
123+
protected DoubleBufferedFileData path1;
124+
protected DoubleBufferedFileData path2;
125+
126+
public DoubleBufferedFile(final Path pathWithoutNumber, final String fileExtension) {
127+
path1 = new DoubleBufferedFileData(Path.of(pathWithoutNumber.toString() + "1." + fileExtension));
128+
path2 = new DoubleBufferedFileData(Path.of(pathWithoutNumber.toString() + "2." + fileExtension));
129+
}
130+
131+
public void delete() throws IOException {
132+
path1.delete();
133+
path2.delete();
134+
}
135+
136+
public LocalDateTime getNewestModifiedTime() {
137+
if (path1.modified() == null && path2.modified() == null)
138+
return null;
139+
if (path1.modified() == null)
140+
return path2.modified();
141+
if (path2.modified() == null)
142+
return path1.modified();
143+
144+
if (path1.modified().compareTo(path2.modified()) > 0)
145+
return path1.modified();
146+
return path2.modified();
147+
}
148+
149+
public LocalDateTime getOldestModifiedTime() {
150+
if (path1.modified() == null && path2.modified() == null)
151+
return null;
152+
if (path1.modified() == null)
153+
return path2.modified();
154+
if (path2.modified() == null)
155+
return path1.modified();
156+
157+
if (path1.modified().compareTo(path2.modified()) <= 0)
158+
return path1.modified();
159+
return path2.modified();
160+
}
161+
162+
private DoubleBufferedFileData getOldestForWriteAccess() throws IOException {
163+
path1.probe();
164+
path2.probe();
165+
if (!path1.exists() && !path2.exists())
166+
return path1;
167+
if (!path1.exists())
168+
return path1;
169+
if (!path2.exists())
170+
return path2;
171+
172+
if (!path1.writable() && !path2.writable())
173+
throw new IOException("Both files are locked for write-access.");
174+
if (!path1.writable())
175+
throw new IOException("File1 is locked for write-access.");
176+
if (!path2.writable())
177+
throw new IOException("File2 is locked for write-access.");
178+
179+
if (path1.modified() == null || path2.modified() == null)
180+
throw new IOException("Could not read the modified-date from one of the files.");
181+
if (path1.modified().compareTo(path2.modified()) <= 0)
182+
return path1;
183+
return path2;
184+
}
185+
186+
private DoubleBufferedFileData getNewestForReadAccess() throws IOException {
187+
path1.probe();
188+
path2.probe();
189+
if (!path1.exists() && !path2.exists())
190+
throw new IOException("There is no file to read from, because both files are missing.");
191+
if (!path1.exists())
192+
return path2.withCheckedRead();
193+
if (!path2.exists())
194+
return path1.withCheckedRead();
195+
196+
if (!path1.readable() && !path2.readable())
197+
throw new IOException("Both files are locked for read-access.");
198+
if (!path1.readable())
199+
throw new IOException("File1 is locked for read-access.");
200+
if (!path2.readable())
201+
throw new IOException("File2 is locked for read-access.");
202+
203+
if (path1.modified() == null || path2.modified() == null)
204+
throw new IOException("Could not read the modified-date from one of the files.");
205+
if (path1.modified().compareTo(path2.modified()) > 0)
206+
return path1;
207+
return path2;
208+
}
209+
210+
public void write(final ConsumerWithIoException<BufferedWriter> writeContentDelegate) throws IOException {
211+
DoubleBufferedFileData p = getOldestForWriteAccess();
212+
if (p.exists())
213+
Files.delete(p.path());
214+
215+
try (BufferedWriter writer = p.getBufferedWriter()) {
216+
writeContentDelegate.accept(writer);
217+
}
218+
p.probe();
219+
}
220+
221+
public String read() throws IOException {
222+
DoubleBufferedFileData p = getNewestForReadAccess();
223+
return Files.readString(p.path());
224+
}
225+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package info.unterrainer.commons.jreutils;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
import static org.assertj.core.api.Assertions.assertThatIOException;
5+
6+
import java.io.IOException;
7+
import java.nio.file.Path;
8+
import java.time.LocalDateTime;
9+
10+
import org.junit.jupiter.api.Test;
11+
12+
public class DoubleBufferedFileTests {
13+
14+
@Test
15+
public void readFromPreparedFileWorks() throws IOException {
16+
DoubleBufferedFile dbf = new DoubleBufferedFile(Path.of("test"), "txt");
17+
assertThat(dbf.read()).isEqualTo("test");
18+
}
19+
20+
@Test
21+
public void readThrowsExceptionWhenNoFilePresent() throws IOException {
22+
DoubleBufferedFile dbf = new DoubleBufferedFile(Path.of("new"), "txt");
23+
try {
24+
dbf.delete();
25+
assertThatIOException().isThrownBy(() -> dbf.read());
26+
} finally {
27+
dbf.delete();
28+
}
29+
}
30+
31+
@Test
32+
public void readAfterWriteReturnsSameValue() throws IOException {
33+
DoubleBufferedFile dbf = new DoubleBufferedFile(Path.of("new"), "txt");
34+
try {
35+
dbf.write(w -> w.write("test"));
36+
assertThat(dbf.read()).isEqualTo("test");
37+
} finally {
38+
dbf.delete();
39+
}
40+
}
41+
42+
@Test
43+
public void readAfterWriteAfterWriteReturnsNewValue() throws IOException {
44+
DoubleBufferedFile dbf = new DoubleBufferedFile(Path.of("new"), "txt");
45+
try {
46+
dbf.write(w -> w.write("test_old"));
47+
dbf.write(w -> w.write("test_new"));
48+
assertThat(dbf.read()).isEqualTo("test_new");
49+
} finally {
50+
dbf.delete();
51+
}
52+
}
53+
54+
@Test
55+
public void modifiedNewIsNullWithoutAnyValueWritten() throws IOException {
56+
DoubleBufferedFile dbf = new DoubleBufferedFile(Path.of("new"), "txt");
57+
LocalDateTime newest = dbf.getNewestModifiedTime();
58+
assertThat(newest).isNull();
59+
}
60+
61+
@Test
62+
public void modifiedOldIsNullWithoutAnyValueWritten() throws IOException {
63+
DoubleBufferedFile dbf = new DoubleBufferedFile(Path.of("new"), "txt");
64+
LocalDateTime oldest = dbf.getOldestModifiedTime();
65+
assertThat(oldest).isNull();
66+
}
67+
68+
@Test
69+
public void modifiedNewAndOldAreEqualWithOneValueWritten() throws IOException {
70+
DoubleBufferedFile dbf = new DoubleBufferedFile(Path.of("new"), "txt");
71+
try {
72+
dbf.write(w -> w.write("test_old"));
73+
LocalDateTime newest = dbf.getNewestModifiedTime();
74+
LocalDateTime oldest = dbf.getOldestModifiedTime();
75+
assertThat(newest).isEqualTo(oldest);
76+
} finally {
77+
dbf.delete();
78+
}
79+
}
80+
81+
@Test
82+
public void modifiedNewAndOldDifferWithTwoValuesWritten() throws IOException {
83+
DoubleBufferedFile dbf = new DoubleBufferedFile(Path.of("new"), "txt");
84+
try {
85+
dbf.write(w -> w.write("test_old"));
86+
dbf.write(w -> w.write("test_new"));
87+
LocalDateTime newest = dbf.getNewestModifiedTime();
88+
LocalDateTime oldest = dbf.getOldestModifiedTime();
89+
assertThat(newest).isAfter(oldest);
90+
} finally {
91+
dbf.delete();
92+
}
93+
}
94+
95+
@Test
96+
public void modifiedAfterThreeWritesReturnsNewestValue() throws IOException {
97+
DoubleBufferedFile dbf = new DoubleBufferedFile(Path.of("new"), "txt");
98+
try {
99+
dbf.write(w -> w.write("test_oldest"));
100+
LocalDateTime oldest = dbf.getNewestModifiedTime();
101+
dbf.write(w -> w.write("test_older"));
102+
LocalDateTime older = dbf.getNewestModifiedTime();
103+
dbf.write(w -> w.write("test_new"));
104+
assertThat(dbf.getNewestModifiedTime()).isAfter(older);
105+
assertThat(dbf.getNewestModifiedTime()).isAfter(oldest);
106+
} finally {
107+
dbf.delete();
108+
}
109+
}
110+
}

test1.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
test

0 commit comments

Comments
 (0)