Skip to content

Commit 24a7107

Browse files
authored
Merge pull request #4847 from volodya-lombrozo/4846-new-cache
feat(#4846): implement a simple caching mechanism in the Maven plugin
2 parents cdc1cec + fb0e998 commit 24a7107

File tree

2 files changed

+193
-0
lines changed

2 files changed

+193
-0
lines changed
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*
2+
* SPDX-FileCopyrightText: Copyright (c) 2016-2026 Objectionary.com
3+
* SPDX-License-Identifier: MIT
4+
*/
5+
package org.eolang.maven;
6+
7+
import java.io.IOException;
8+
import java.nio.file.Files;
9+
import java.nio.file.Path;
10+
import java.security.MessageDigest;
11+
import java.security.NoSuchAlgorithmException;
12+
import java.util.Base64;
13+
import org.cactoos.Func;
14+
import org.cactoos.func.UncheckedFunc;
15+
16+
/**
17+
* Simple cache mechanism.
18+
* @since 0.60
19+
* @todo #4846:30min Make Cache thread-safe.
20+
* The Cache class lacks thread-safety mechanisms. If multiple threads call the apply method
21+
* concurrently with the same source file, there could be race conditions where both threads
22+
* determine the cache is stale and attempt to write simultaneously, potentially leading to
23+
* corrupted files or inconsistent state. Consider adding synchronization or using atomic file
24+
* operations. See ChCachedTest.java:63-86 for an example of how concurrency is tested in similar
25+
* caching classes in this codebase.
26+
* @todo #4846:30min Replace {@link FpDefault} with {@link Cache}.
27+
* The FpDefault class currently implements caching logic that is similar to the Cache class.
28+
* Refactor the codebase to use Cache instead of FpDefault for caching functionality to
29+
* improve code reuse and maintainability.
30+
*/
31+
final class Cache {
32+
33+
/**
34+
* Base cache directory.
35+
*/
36+
private final Path base;
37+
38+
/**
39+
* Compilation function.
40+
*/
41+
private final Func<Path, String> compilation;
42+
43+
/**
44+
* Ctor.
45+
* @param base Base cache directory
46+
* @param compilation Compilation function
47+
*/
48+
Cache(final Path base, final Func<Path, String> compilation) {
49+
this.base = base;
50+
this.compilation = compilation;
51+
}
52+
53+
/**
54+
* Check cache and apply compilation if needed.
55+
* @param source From file
56+
* @param target To file
57+
* @param tail Tail path in cache
58+
*/
59+
public void apply(final Path source, final Path target, final Path tail) {
60+
try {
61+
final String sha = Cache.sha(source);
62+
final Path hash = this.hash(tail);
63+
final Path cache = this.base.resolve(tail);
64+
if (Files.notExists(hash)
65+
|| Files.notExists(cache)
66+
|| !Files.readString(hash).equals(sha)) {
67+
final String content = new UncheckedFunc<>(this.compilation).apply(source);
68+
new Saved(sha, this.hash(tail)).value();
69+
new Saved(content, cache).value();
70+
new Saved(content, target).value();
71+
} else {
72+
new Saved(Files.readString(cache), target).value();
73+
}
74+
} catch (final IOException ioexception) {
75+
throw new IllegalStateException(
76+
"Failed to perform an IO operation with cache",
77+
ioexception
78+
);
79+
} catch (final NoSuchAlgorithmException exception) {
80+
throw new IllegalStateException("SHA-256 hashing algorithm isn't found", exception);
81+
}
82+
}
83+
84+
/**
85+
* Get hash file path for the given tail.
86+
* @param tail Tail path
87+
* @return Hash file path
88+
*/
89+
private Path hash(final Path tail) {
90+
final Path full = this.base.resolve(tail.normalize());
91+
return full.getParent().resolve(String.format("%s.sha256", full.getFileName().toString()));
92+
}
93+
94+
/**
95+
* Calculate SHA-256 hash of a file and return it as Base64 string.
96+
* @param file File path
97+
* @return Base64-encoded SHA-256 hash
98+
* @throws NoSuchAlgorithmException If SHA-256 algorithm is not available
99+
* @throws IOException If an I/O error occurs reading the file
100+
* @todo #4846:30min OutOfMemoryError for large files in cache.
101+
* The sha method reads the entire file into memory using Files.readAllBytes(file) which
102+
* could cause OutOfMemoryError for large files. Consider using a streaming approach with
103+
* MessageDigest.update() in a loop to hash the file in chunks, similar to how it's typically
104+
* done for large file hashing operations.
105+
*/
106+
private static String sha(final Path file) throws NoSuchAlgorithmException, IOException {
107+
final MessageDigest digest = MessageDigest.getInstance("SHA-256");
108+
final byte[] hash = digest.digest(Files.readAllBytes(file));
109+
return Base64.getEncoder().encodeToString(hash);
110+
}
111+
}
112+
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* SPDX-FileCopyrightText: Copyright (c) 2016-2026 Objectionary.com
3+
* SPDX-License-Identifier: MIT
4+
*/
5+
package org.eolang.maven;
6+
7+
import com.yegor256.Mktmp;
8+
import com.yegor256.MktmpResolver;
9+
import java.nio.file.Files;
10+
import java.nio.file.Path;
11+
import java.util.concurrent.atomic.AtomicInteger;
12+
import org.hamcrest.MatcherAssert;
13+
import org.hamcrest.Matchers;
14+
import org.junit.jupiter.api.Test;
15+
import org.junit.jupiter.api.extension.ExtendWith;
16+
17+
/**
18+
* Test for {@link Cache}.
19+
* @since 0.60
20+
* @todo #4846:30min Add more tests for Cache class.
21+
* Currently only two basic tests are implemented. More tests should be added to cover edge cases
22+
* and ensure robustness of the caching mechanism.
23+
* For example, you can add a test to verify the behavior when the source file is modified.
24+
*/
25+
@ExtendWith(MktmpResolver.class)
26+
final class CacheTest {
27+
28+
@Test
29+
void compilesSourceAndAddsToCache(@Mktmp final Path temp) throws Exception {
30+
final var base = temp.resolve("cache");
31+
Files.createDirectories(base);
32+
final var source = temp.resolve("source.eo");
33+
Files.writeString(source, "[] > main\n (stdout \"Hello, EO!\") > @\n");
34+
final var target = temp.resolve("target.xmir");
35+
final var tail = source.getFileName();
36+
final String content = "compiled";
37+
new Cache(base, p -> content).apply(source, target, tail);
38+
MatcherAssert.assertThat(
39+
"Target file must be created from source",
40+
Files.readString(target),
41+
Matchers.equalTo(content)
42+
);
43+
MatcherAssert.assertThat(
44+
"Cache file must be created",
45+
Files.exists(base.resolve(tail)),
46+
Matchers.is(true)
47+
);
48+
MatcherAssert.assertThat(
49+
"Hash file must be created",
50+
Files.exists(base.resolve(String.format("%s.sha256", tail))),
51+
Matchers.is(true)
52+
);
53+
}
54+
55+
@Test
56+
void readsFromCacheWhenUnchanged(@Mktmp final Path temp) throws Exception {
57+
final var base = temp.resolve("cache-directory");
58+
Files.createDirectories(base);
59+
final var source = temp.resolve("stdin.eo");
60+
Files.writeString(source, "[] > main\n (stdout \"Hello, EO!\") > @\n");
61+
final var target = temp.resolve("stdin.xmir");
62+
final var counter = new AtomicInteger(0);
63+
final var cache = new Cache(
64+
base,
65+
p -> String.format("stdin %d", counter.incrementAndGet())
66+
);
67+
final Path tail = source.getFileName();
68+
cache.apply(source, target, tail);
69+
MatcherAssert.assertThat(
70+
"Compilation should happen only once",
71+
counter.get(),
72+
Matchers.equalTo(1)
73+
);
74+
cache.apply(source, target, tail);
75+
MatcherAssert.assertThat(
76+
"Compilation should not happen again",
77+
counter.get(),
78+
Matchers.equalTo(1)
79+
);
80+
}
81+
}

0 commit comments

Comments
 (0)