DirectoryMicrophoneArrayProfileStore.java
package org.hammer.audio.acquisition;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Base64;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Stream;
/** Directory-backed profile store using the deterministic profile codec. */
public final class DirectoryMicrophoneArrayProfileStore implements MicrophoneArrayProfileStore {
private static final String FILE_SUFFIX = ".maprofile";
private final Path directory;
private final MicrophoneArrayProfileCodec codec;
/** Create a store rooted at one directory. */
public DirectoryMicrophoneArrayProfileStore(Path directory) {
this(directory, new MicrophoneArrayProfileCodec());
}
/** Create a store with an explicit codec. */
public DirectoryMicrophoneArrayProfileStore(Path directory, MicrophoneArrayProfileCodec codec) {
this.directory = Objects.requireNonNull(directory, "directory").toAbsolutePath().normalize();
this.codec = Objects.requireNonNull(codec, "codec");
}
@Override
public void save(MicrophoneArrayProfile profile) throws IOException {
Objects.requireNonNull(profile, "profile");
Files.createDirectories(directory);
Path target = pathFor(profile.profileId());
Path temporary = Files.createTempFile(directory, "profile-", ".tmp");
try {
Files.writeString(temporary, codec.encode(profile), StandardCharsets.UTF_8);
moveAtomicallyWhenSupported(temporary, target);
} finally {
Files.deleteIfExists(temporary);
}
}
@Override
public Optional<MicrophoneArrayProfile> find(String profileId) throws IOException {
Path path = pathFor(profileId);
if (!Files.isRegularFile(path)) {
return Optional.empty();
}
return Optional.of(codec.decode(Files.readString(path, StandardCharsets.UTF_8)));
}
@Override
public List<MicrophoneArrayProfile> list() throws IOException {
if (!Files.isDirectory(directory)) {
return List.of();
}
try (Stream<Path> paths = Files.list(directory)) {
return paths
.filter(DirectoryMicrophoneArrayProfileStore::isProfileFile)
.map(this::readUnchecked)
.sorted(Comparator.comparing(MicrophoneArrayProfile::profileId))
.toList();
} catch (UncheckedIOException exception) {
throw new IOException("Failed to read microphone-array profile directory", exception);
}
}
@Override
public boolean delete(String profileId) throws IOException {
return Files.deleteIfExists(pathFor(profileId));
}
private MicrophoneArrayProfile readUnchecked(Path path) {
try {
return codec.decode(Files.readString(path, StandardCharsets.UTF_8));
} catch (IOException exception) {
throw new UncheckedIOException(exception);
}
}
private Path pathFor(String profileId) {
if (profileId == null || profileId.isBlank()) {
throw new IllegalArgumentException("profileId must not be blank");
}
String encoded =
Base64.getUrlEncoder()
.withoutPadding()
.encodeToString(profileId.getBytes(StandardCharsets.UTF_8));
return directory.resolve(encoded + FILE_SUFFIX);
}
private static boolean isProfileFile(Path path) {
Path fileName = path.getFileName();
return fileName != null && fileName.toString().endsWith(FILE_SUFFIX);
}
private static void moveAtomicallyWhenSupported(Path source, Path target) throws IOException {
try {
Files.move(
source, target, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
} catch (AtomicMoveNotSupportedException exception) {
Files.move(source, target, StandardCopyOption.REPLACE_EXISTING);
}
}
}