Skip to content

Commit

Permalink
Refactor script dependency tracking (openhab#3168)
Browse files Browse the repository at this point in the history
Signed-off-by: Jan N. Klug <github@klug.nrw>
  • Loading branch information
J-N-K authored Nov 29, 2022
1 parent e556fdf commit 4bcc15d
Show file tree
Hide file tree
Showing 16 changed files with 690 additions and 340 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.automation.module.script.ScriptEngineManager;
import org.openhab.core.automation.module.script.rulesupport.loader.ScriptFileWatcher;
import org.openhab.core.automation.module.script.rulesupport.loader.AbstractScriptFileWatcher;
import org.openhab.core.service.ReadyService;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
Expand All @@ -31,14 +31,14 @@
*/
@NonNullByDefault
@Component(immediate = true)
public class DefaultScriptFileWatcher extends ScriptFileWatcher {
public class DefaultScriptFileWatcher extends AbstractScriptFileWatcher {

private static final String FILE_DIRECTORY = "automation" + File.separator + "jsr223";

@Activate
public DefaultScriptFileWatcher(final @Reference ScriptEngineManager manager,
final @Reference ReadyService readyService) {
super(manager, null, readyService, FILE_DIRECTORY);
super(manager, readyService, FILE_DIRECTORY);
}

@Activate
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import org.eclipse.jdt.annotation.NonNullByDefault;

Expand All @@ -25,61 +26,84 @@
* Provides optimized lookup of values for a key, as well as keys referencing a value.
*
* @author Jonathan Gilbert - Initial contribution
* @author Jan N. Klug - Make implementation thread-safe
* @param <K> Type of Key
* @param <V> Type of Value
*/
@NonNullByDefault
public class BidiSetBag<K, V> {
private Map<K, Set<V>> keyToValues = new HashMap<>();
private Map<V, Set<K>> valueToKeys = new HashMap<>();

private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private final Map<K, Set<V>> keyToValues = new HashMap<>();
private final Map<V, Set<K>> valueToKeys = new HashMap<>();

public void put(K key, V value) {
addElement(keyToValues, key, value);
addElement(valueToKeys, value, key);
lock.writeLock().lock();
try {
keyToValues.computeIfAbsent(key, k -> new HashSet<>()).add(value);
valueToKeys.computeIfAbsent(value, v -> new HashSet<>()).add(key);
} finally {
lock.writeLock().unlock();
}
}

public Set<V> getValues(K key) {
Set<V> existing = keyToValues.get(key);
return existing == null ? Collections.emptySet() : Collections.unmodifiableSet(existing);
lock.readLock().lock();
try {
Set<V> values = keyToValues.getOrDefault(key, Set.of());
return Collections.unmodifiableSet(values);
} finally {
lock.readLock().unlock();
}
}

public Set<K> getKeys(V value) {
Set<K> existing = valueToKeys.get(value);
return existing == null ? Collections.emptySet() : Collections.unmodifiableSet(existing);
lock.readLock().lock();
try {
Set<K> keys = valueToKeys.getOrDefault(value, Set.of());
return Collections.unmodifiableSet(keys);
} finally {
lock.readLock().unlock();
}
}

public Set<V> removeKey(K key) {
Set<V> values = keyToValues.remove(key);
if (values != null) {
for (V value : values) {
valueToKeys.computeIfPresent(value, (k, v) -> {
v.remove(key);
return v;
});
lock.writeLock().lock();
try {
Set<V> values = keyToValues.remove(key);
if (values != null) {
for (V value : values) {
valueToKeys.computeIfPresent(value, (k, v) -> {
v.remove(key);
return v;
});
}
return values;
} else {
return Set.of();
}
return values;
} else {
return Collections.emptySet();
} finally {
lock.writeLock().unlock();
}
}

public Set<K> removeValue(V value) {
Set<K> keys = valueToKeys.remove(value);
if (keys != null) {
for (K key : keys) {
keyToValues.computeIfPresent(key, (k, v) -> {
v.remove(value);
return v;
});
lock.writeLock().lock();
try {
Set<K> keys = valueToKeys.remove(value);
if (keys != null) {
for (K key : keys) {
keyToValues.computeIfPresent(key, (k, v) -> {
v.remove(value);
return v;
});
}
return keys;
} else {
return Set.of();
}
return keys;
} else {
return Collections.emptySet();
} finally {
lock.writeLock().unlock();
}
}

private static <T, U> void addElement(Map<T, Set<U>> map, T key, U value) {
Set<U> elements = map.compute(key, (k, l) -> l == null ? new HashSet<>() : l);
elements.add(value);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.core.automation.module.script.rulesupport.loader;

import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;

import java.io.File;
import java.nio.file.Path;
import java.nio.file.WatchEvent;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.automation.module.script.ScriptDependencyTracker;
import org.openhab.core.automation.module.script.rulesupport.internal.loader.collection.BidiSetBag;
import org.openhab.core.service.AbstractWatchService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* The {@link AbstractScriptDependencyTracker} tracks dependencies between scripts and reloads dependees
* It needs to be sub-classed for each {@link org.openhab.core.automation.module.script.ScriptEngineFactory}
* that wants to support dependency tracking
*
* @author Jonathan Gilbert - Initial contribution
* @author Jan N. Klug - Refactored to OSGi service
*/
@NonNullByDefault
public abstract class AbstractScriptDependencyTracker implements ScriptDependencyTracker {
private final Logger logger = LoggerFactory.getLogger(AbstractScriptDependencyTracker.class);

protected final String libraryPath;

private final Set<ScriptDependencyTracker.Listener> dependencyChangeListeners = ConcurrentHashMap.newKeySet();

private final BidiSetBag<String, String> scriptToLibs = new BidiSetBag<>();
private @Nullable AbstractWatchService dependencyWatchService;

public AbstractScriptDependencyTracker(final String libraryPath) {
this.libraryPath = libraryPath;
}

public void activate() {
AbstractWatchService dependencyWatchService = createDependencyWatchService();
dependencyWatchService.activate();
this.dependencyWatchService = dependencyWatchService;
}

public void deactivate() {
AbstractWatchService dependencyWatchService = this.dependencyWatchService;
if (dependencyWatchService != null) {
dependencyWatchService.deactivate();
}
}

protected AbstractWatchService createDependencyWatchService() {
return new AbstractWatchService(libraryPath) {
@Override
protected boolean watchSubDirectories() {
return true;
}

@Override
protected WatchEvent.Kind<?> @Nullable [] getWatchEventKinds(Path path) {
return new WatchEvent.Kind<?>[] { ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY };
}

@Override
protected void processWatchEvent(WatchEvent<?> watchEvent, WatchEvent.Kind<?> kind, Path path) {
File file = path.toFile();
if (!file.isHidden() && (kind.equals(ENTRY_DELETE)
|| (file.canRead() && (kind.equals(ENTRY_CREATE) || kind.equals(ENTRY_MODIFY))))) {
dependencyChanged(file.getPath());
}
}
};
}

protected void dependencyChanged(String dependency) {
Set<String> scripts = new HashSet<>(scriptToLibs.getKeys(dependency)); // take a copy as it will change as we
logger.debug("Library {} changed; reimporting {} scripts...", libraryPath, scripts.size());
for (String scriptUrl : scripts) {
for (ScriptDependencyTracker.Listener listener : dependencyChangeListeners) {
try {
listener.onDependencyChange(scriptUrl);
} catch (Exception e) {
logger.warn("Failed to notify tracker of dependency change: {}: {}", e.getClass(), e.getMessage());
}
}
}
}

@Override
public Consumer<String> getTracker(String scriptId) {
return dependencyPath -> startTracking(scriptId, dependencyPath);
}

@Override
public void removeTracking(String scriptId) {
scriptToLibs.removeKey(scriptId);
}

protected void startTracking(String scriptId, String libPath) {
scriptToLibs.put(scriptId, libPath);
}

/**
* Add a dependency change listener
*
* Since this is done via service injection and OSGi annotations are not inherited it is required that subclasses
* expose this method with proper annotation
*
* @param listener the dependency change listener
*/
public void addChangeTracker(ScriptDependencyTracker.Listener listener) {
dependencyChangeListeners.add(listener);
}

public void removeChangeTracker(ScriptDependencyTracker.Listener listener) {
dependencyChangeListeners.remove(listener);
}
}
Loading

0 comments on commit 4bcc15d

Please sign in to comment.