Fix file processing in FileTransformationProvider (#3457)

Signed-off-by: Jan N. Klug <github@klug.nrw>
pull/3467/head
J-N-K 2023-03-16 08:52:30 +01:00 committed by GitHub
parent f529a7b77f
commit 4390d515d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 52 additions and 71 deletions

View File

@ -12,22 +12,23 @@
*/
package org.openhab.core.transform;
import static org.openhab.core.service.WatchService.Kind.CREATE;
import static org.openhab.core.service.WatchService.Kind.DELETE;
import static org.openhab.core.service.WatchService.Kind.MODIFY;
import static org.openhab.core.transform.Transformation.FUNCTION;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.OpenHAB;
import org.openhab.core.common.registry.ProviderChangeListener;
import org.openhab.core.service.WatchService;
import org.osgi.service.component.annotations.Activate;
@ -46,13 +47,9 @@ import org.slf4j.LoggerFactory;
@NonNullByDefault
@Component(service = TransformationProvider.class, immediate = true)
public class FileTransformationProvider implements WatchService.WatchEventListener, TransformationProvider {
private static final WatchEvent.Kind<?>[] WATCH_EVENTS = { StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_DELETE, StandardWatchEventKinds.ENTRY_MODIFY };
private static final Set<String> IGNORED_EXTENSIONS = Set.of("txt", "swp");
private static final Pattern FILENAME_PATTERN = Pattern
.compile("(?<filename>.+?)(_(?<language>[a-z]{2}))?\\.(?<extension>[^.]*)$");
private static final Path TRANSFORMATION_PATH = Path.of(OpenHAB.getConfigFolder(),
TransformationService.TRANSFORM_FOLDER_NAME);
private final Logger logger = LoggerFactory.getLogger(FileTransformationProvider.class);
@ -64,19 +61,13 @@ public class FileTransformationProvider implements WatchService.WatchEventListen
@Activate
public FileTransformationProvider(
@Reference(target = WatchService.CONFIG_WATCHER_FILTER) WatchService watchService) {
this(watchService, TRANSFORMATION_PATH);
}
// constructor package private used for testing
FileTransformationProvider(WatchService watchService, Path transformationPath) {
this.transformationPath = transformationPath;
this.watchService = watchService;
this.transformationPath = watchService.getWatchPath().resolve(TransformationService.TRANSFORM_FOLDER_NAME);
watchService.registerListener(this, transformationPath);
// read initial contents
try {
Files.walk(transformationPath).filter(Files::isRegularFile)
.forEach(f -> processPath(WatchService.Kind.CREATE, f));
try (Stream<Path> files = Files.walk(transformationPath)) {
files.filter(Files::isRegularFile).map(transformationPath::relativize).forEach(f -> processPath(CREATE, f));
} catch (IOException e) {
logger.warn("Could not list files in '{}', transformation configurations might be missing: {}",
transformationPath, e.getMessage());
@ -104,14 +95,14 @@ public class FileTransformationProvider implements WatchService.WatchEventListen
}
private void processPath(WatchService.Kind kind, Path path) {
if (kind == WatchService.Kind.DELETE) {
Path finalPath = transformationPath.resolve(path);
if (kind == DELETE) {
Transformation oldElement = transformationConfigurations.remove(path);
if (oldElement != null) {
logger.trace("Removed configuration from file '{}", path);
listeners.forEach(listener -> listener.removed(this, oldElement));
}
} else if (Files.isRegularFile(path)
&& (kind == WatchService.Kind.CREATE || kind == WatchService.Kind.MODIFY)) {
} else if (Files.isRegularFile(finalPath) && ((kind == CREATE) || (kind == MODIFY))) {
try {
String fileName = path.getFileName().toString();
Matcher m = FILENAME_PATTERN.matcher(fileName);
@ -128,8 +119,8 @@ public class FileTransformationProvider implements WatchService.WatchEventListen
return;
}
String content = new String(Files.readAllBytes(path));
String uid = transformationPath.relativize(path).toString();
String content = new String(Files.readAllBytes(finalPath));
String uid = path.toString();
Transformation newElement = new Transformation(uid, uid, fileExtension, Map.of(FUNCTION, content));
Transformation oldElement = transformationConfigurations.put(path, newElement);

View File

@ -18,22 +18,23 @@ import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.not;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.when;
import static org.openhab.core.service.WatchService.Kind.CREATE;
import static org.openhab.core.service.WatchService.Kind.DELETE;
import static org.openhab.core.service.WatchService.Kind.MODIFY;
import static org.openhab.core.transform.Transformation.FUNCTION;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
@ -54,38 +55,32 @@ import org.openhab.core.service.WatchService;
public class FileTransformationProviderTest {
private static final String FOO_TYPE = "foo";
private static final String INITIAL_CONTENT = "initial";
private static final String INITIAL_FILENAME = INITIAL_CONTENT + "." + FOO_TYPE;
private static final Transformation INITIAL_CONFIGURATION = new Transformation(INITIAL_FILENAME, INITIAL_FILENAME,
FOO_TYPE, Map.of(FUNCTION, INITIAL_CONTENT));
private static final Path INITIAL_FILENAME = Path.of(INITIAL_CONTENT + "." + FOO_TYPE);
private static final Transformation INITIAL_CONFIGURATION = new Transformation(INITIAL_FILENAME.toString(),
INITIAL_FILENAME.toString(), FOO_TYPE, Map.of(FUNCTION, INITIAL_CONTENT));
private static final String ADDED_CONTENT = "added";
private static final String ADDED_FILENAME = ADDED_CONTENT + "." + FOO_TYPE;
private static final Path ADDED_FILENAME = Path.of(ADDED_CONTENT + "." + FOO_TYPE);
private @Mock @NonNullByDefault({}) WatchService watchService;
private @Mock @NonNullByDefault({}) WatchService watchServiceMock;
private @Mock @NonNullByDefault({}) ProviderChangeListener<@NonNull Transformation> listenerMock;
private @NonNullByDefault({}) FileTransformationProvider provider;
private @NonNullByDefault({}) Path targetPath;
private @NonNullByDefault({}) @TempDir Path configPath;
private @NonNullByDefault({}) Path transformationPath;
@BeforeEach
public void setup() throws IOException {
// create directory
targetPath = Files.createTempDirectory("fileTest");
when(watchServiceMock.getWatchPath()).thenReturn(configPath);
transformationPath = configPath.resolve(TransformationService.TRANSFORM_FOLDER_NAME);
// set initial content
Files.write(targetPath.resolve(INITIAL_FILENAME), INITIAL_CONTENT.getBytes(StandardCharsets.UTF_8));
// create transformation directory and set initial content
Files.createDirectories(transformationPath);
Files.writeString(transformationPath.resolve(INITIAL_FILENAME), INITIAL_CONTENT);
provider = new FileTransformationProvider(watchService, targetPath);
provider = new FileTransformationProvider(watchServiceMock);
provider.addProviderChangeListener(listenerMock);
}
@AfterEach
public void tearDown() throws IOException {
try (Stream<Path> walk = Files.walk(targetPath)) {
walk.map(Path::toFile).forEach(File::delete);
}
Files.deleteIfExists(targetPath);
}
@Test
public void testInitialConfigurationIsPresent() {
// assert that initial configuration is present
@ -94,13 +89,10 @@ public class FileTransformationProviderTest {
@Test
public void testAddingConfigurationIsPropagated() throws IOException {
Path path = targetPath.resolve(ADDED_FILENAME);
Files.write(path, ADDED_CONTENT.getBytes(StandardCharsets.UTF_8));
Transformation addedConfiguration = new Transformation(ADDED_FILENAME, ADDED_FILENAME, FOO_TYPE,
Map.of(FUNCTION, ADDED_CONTENT));
provider.processWatchEvent(WatchService.Kind.CREATE, path);
Files.writeString(transformationPath.resolve(ADDED_FILENAME), ADDED_CONTENT);
Transformation addedConfiguration = new Transformation(ADDED_FILENAME.toString(), ADDED_FILENAME.toString(),
FOO_TYPE, Map.of(FUNCTION, ADDED_CONTENT));
provider.processWatchEvent(CREATE, ADDED_FILENAME);
// assert registry is notified and internal cache updated
Mockito.verify(listenerMock).added(provider, addedConfiguration);
@ -109,12 +101,10 @@ public class FileTransformationProviderTest {
@Test
public void testUpdatingConfigurationIsPropagated() throws IOException {
Path path = targetPath.resolve(INITIAL_FILENAME);
Files.write(path, "updated".getBytes(StandardCharsets.UTF_8));
Transformation updatedConfiguration = new Transformation(INITIAL_FILENAME, INITIAL_FILENAME, FOO_TYPE,
Map.of(FUNCTION, "updated"));
provider.processWatchEvent(WatchService.Kind.MODIFY, path);
Files.writeString(transformationPath.resolve(INITIAL_FILENAME), "updated");
Transformation updatedConfiguration = new Transformation(INITIAL_FILENAME.toString(),
INITIAL_FILENAME.toString(), FOO_TYPE, Map.of(FUNCTION, "updated"));
provider.processWatchEvent(MODIFY, INITIAL_FILENAME);
Mockito.verify(listenerMock).updated(provider, INITIAL_CONFIGURATION, updatedConfiguration);
assertThat(provider.getAll(), contains(updatedConfiguration));
@ -123,9 +113,7 @@ public class FileTransformationProviderTest {
@Test
public void testDeletingConfigurationIsPropagated() {
Path path = targetPath.resolve(INITIAL_FILENAME);
provider.processWatchEvent(WatchService.Kind.DELETE, path);
provider.processWatchEvent(DELETE, INITIAL_FILENAME);
Mockito.verify(listenerMock).removed(provider, INITIAL_CONFIGURATION);
assertThat(provider.getAll(), not(contains(INITIAL_CONFIGURATION)));
@ -134,22 +122,23 @@ public class FileTransformationProviderTest {
@Test
public void testLanguageIsProperlyParsed() throws IOException {
String fileName = "test_de." + FOO_TYPE;
Path path = targetPath.resolve(fileName);
Path path = transformationPath.resolve(fileName);
Files.write(path, INITIAL_CONTENT.getBytes(StandardCharsets.UTF_8));
Files.writeString(path, INITIAL_CONTENT);
Transformation expected = new Transformation(fileName, fileName, FOO_TYPE, Map.of(FUNCTION, INITIAL_CONTENT));
provider.processWatchEvent(WatchService.Kind.CREATE, path);
provider.processWatchEvent(CREATE, Path.of(fileName));
assertThat(provider.getAll(), hasItem(expected));
}
@Test
public void testMissingExtensionIsIgnored() throws IOException {
Path path = targetPath.resolve("extensionMissing");
Files.write(path, INITIAL_CONTENT.getBytes(StandardCharsets.UTF_8));
provider.processWatchEvent(WatchService.Kind.CREATE, path);
provider.processWatchEvent(WatchService.Kind.MODIFY, path);
Path extensionMissing = Path.of("extensionMissing");
Path path = transformationPath.resolve(extensionMissing);
Files.writeString(path, INITIAL_CONTENT);
provider.processWatchEvent(CREATE, extensionMissing);
provider.processWatchEvent(MODIFY, extensionMissing);
Mockito.verify(listenerMock, never()).added(any(), any());
Mockito.verify(listenerMock, never()).updated(any(), any(), any());
@ -157,10 +146,11 @@ public class FileTransformationProviderTest {
@Test
public void testIgnoredExtensionIsIgnored() throws IOException {
Path path = targetPath.resolve("extensionIgnore.txt");
Files.write(path, INITIAL_CONTENT.getBytes(StandardCharsets.UTF_8));
provider.processWatchEvent(WatchService.Kind.CREATE, path);
provider.processWatchEvent(WatchService.Kind.MODIFY, path);
Path extensionIgnored = Path.of("extensionIgnore.txt");
Path path = transformationPath.resolve(extensionIgnored);
Files.writeString(path, INITIAL_CONTENT);
provider.processWatchEvent(CREATE, extensionIgnored);
provider.processWatchEvent(MODIFY, extensionIgnored);
Mockito.verify(listenerMock, never()).added(any(), any());
Mockito.verify(listenerMock, never()).updated(any(), any(), any());