Convert config-loader to OSGi DS 39/89739/16
authorRobert Varga <robert.varga@pantheon.tech>
Wed, 13 May 2020 20:20:30 +0000 (22:20 +0200)
committerRobert Varga <nite@hq.sk>
Mon, 26 Oct 2020 12:14:14 +0000 (12:14 +0000)
The primary task here is getting rid of explicit blueprint while
retaining autowiring functionality.

This forces us to decompose ConfigLoaderImpl into multiple classes,
which ends up being a boon for tests, as they can completely forgo
threading.

First of all FileWatcherImpl was renamed to DefaultFileWatcher and
is a completely separate component, with proper lifecycle without
edge case failures.

Next we split out AbstractConfigLoader with all the loading logic
and AbstractWatchingConfigLoader with the addition of having notion
of a FileWatcher. This allows us to use synchronous loading in
test harness, while also providing the ability to have async
processing.

We then instantiate SimpleConfigLoader and OSGiConfigLoader to
handle the cases of static vs. dynamic codecs, as well as
TestConfigLoader to handle unit testing tasks.

We can therefore drop dependency on odl-controller-blueprint, in
favor of depending on YANG tools codecs instead.

JIRA: BGPCEP-905
Change-Id: I5549bdfeb372b2f9cb15a1690c399401432f4702
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
19 files changed:
bgp/rib-impl/src/test/java/org/opendaylight/protocol/bgp/rib/impl/AbstractAddPathTest.java
bgp/rib-impl/src/test/java/org/opendaylight/protocol/bgp/rib/impl/AbstractRIBTestSetup.java
bgp/rib-impl/src/test/java/org/opendaylight/protocol/bgp/rib/impl/ParserToSalTest.java
config-loader/bmp-monitors-config-loader/src/test/java/org/opendaylight/bgpcep/config/loader/bmp/BmpMonitorConfigFileProcessorTest.java
config-loader/config-loader-impl/pom.xml
config-loader/config-loader-impl/src/main/java/org/opendaylight/bgpcep/config/loader/impl/AbstractConfigLoader.java
config-loader/config-loader-impl/src/main/java/org/opendaylight/bgpcep/config/loader/impl/AbstractWatchingConfigLoader.java [new file with mode: 0644]
config-loader/config-loader-impl/src/main/java/org/opendaylight/bgpcep/config/loader/impl/DefaultFileWatcher.java
config-loader/config-loader-impl/src/main/java/org/opendaylight/bgpcep/config/loader/impl/OSGiConfigLoader.java [new file with mode: 0644]
config-loader/config-loader-impl/src/main/java/org/opendaylight/bgpcep/config/loader/impl/SimpleConfigLoader.java [new file with mode: 0644]
config-loader/config-loader-impl/src/main/resources/OSGI-INF/blueprint/config-loader-impl.xml [deleted file]
config-loader/config-loader-impl/src/test/java/org/opendaylight/bgpcep/config/loader/impl/AbstractConfigLoaderTest.java
config-loader/config-loader-impl/src/test/java/org/opendaylight/bgpcep/config/loader/impl/ConfigLoaderImplTest.java
config-loader/config-loader-impl/src/test/java/org/opendaylight/bgpcep/config/loader/impl/DefaultWatcherTest.java
config-loader/config-loader-impl/src/test/java/org/opendaylight/bgpcep/config/loader/impl/OSGiConfigLoaderTest.java [new file with mode: 0644]
config-loader/config-loader-impl/src/test/java/org/opendaylight/bgpcep/config/loader/impl/SimpleConfigLoaderTest.java [new file with mode: 0644]
config-loader/routing-policy-config-loader/src/test/java/org/opendaylight/bgpcep/config/loader/routing/policy/AbstractOpenconfigRoutingPolicyLoaderTest.java
features/config-loader/odl-bgpcep-config-loader-impl/pom.xml
features/config-loader/odl-bgpcep-config-loader-impl/src/main/feature/feature.xml

index 6ddb1c6ed1ba8e97bb3bed745db5d8a650545023..d4a1fbf51ba1da94d0572ea77615810e72774a13 100644 (file)
@@ -167,7 +167,6 @@ public abstract class AbstractAddPathTest extends DefaultRibPoliciesMockTest {
         this.clientDispatchers = new ArrayList<>();
     }
 
-    @Override
     @After
     public void tearDown() throws Exception {
         this.serverDispatcher.close();
@@ -180,7 +179,6 @@ public abstract class AbstractAddPathTest extends DefaultRibPoliciesMockTest {
         this.bgpActivator.close();
         this.clientDispatchers.forEach(BGPDispatcherImpl::close);
         this.clientDispatchers = null;
-        super.tearDown();
     }
 
     void sendRouteAndCheckIsOnLocRib(final BGPSessionImpl session, final Ipv4Prefix prefix, final long localPreference,
index 1eb44500d45599d1bea940e1dceac85f56a3292b..a41dda02a5ca4665e410be66aacac11d616524b1 100644 (file)
@@ -223,7 +223,6 @@ public class AbstractRIBTestSetup extends DefaultRibPoliciesMockTest {
         return this.domTransWrite;
     }
 
-    @Override
     @After
     public void tearDown() {
         this.a1.close();
index d4022dc1c5e41ff1ebc1bb420bedb630461c74f9..ed9a0151444d0af778888b3da8631285c41f87f5 100644 (file)
@@ -101,7 +101,6 @@ public class ParserToSalTest extends DefaultRibPoliciesMockTest {
         this.codecsRegistry = new ConstantCodecsRegistry(serializer);
     }
 
-    @Override
     @After
     public void tearDown() {
         this.lsact.close();
index 9c8013c843a1f4007191fa53d842c4fa2fedaed4..af8c86de1036d052a70a34cd8b264d43916b21e1 100644 (file)
@@ -5,7 +5,6 @@
  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
  * and is available at http://www.eclipse.org/legal/epl-v10.html
  */
-
 package org.opendaylight.bgpcep.config.loader.bmp;
 
 import static org.junit.Assert.assertEquals;
index 66157cf36450121e9a8df384ca2aaac7abbafafd..762e0abb891117c42527b2c3c787dffa4663f4fb 100644 (file)
             <groupId>org.opendaylight.mdsal</groupId>
             <artifactId>mdsal-binding-dom-codec-api</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.opendaylight.mdsal</groupId>
+            <artifactId>mdsal-binding-dom-codec-spi</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.opendaylight.mdsal</groupId>
+            <artifactId>mdsal-binding-runtime-api</artifactId>
+        </dependency>
         <dependency>
             <groupId>com.google.guava</groupId>
             <artifactId>guava</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>osgi.cmpn</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>javax.annotation</groupId>
+            <artifactId>javax.annotation-api</artifactId>
+            <scope>provided</scope>
+            <optional>true</optional>
+        </dependency>
+        <dependency>
+            <groupId>javax.inject</groupId>
+            <artifactId>javax.inject</artifactId>
+            <scope>provided</scope>
+            <optional>true</optional>
+        </dependency>
 
         <!-- test scope dependencies -->
         <dependency>
index feb0a1c4d839594d3f011e8f64b06e42c980aeb1..eaad18aa05a14ff1e6650c7d521bb6bbc764fdaa 100644 (file)
@@ -7,9 +7,8 @@
  */
 package org.opendaylight.bgpcep.config.loader.impl;
 
-import static java.util.Objects.requireNonNull;
-
 import com.google.common.base.Stopwatch;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.IOException;
@@ -18,23 +17,20 @@ import java.io.RandomAccessFile;
 import java.net.URISyntaxException;
 import java.nio.channels.FileChannel;
 import java.nio.channels.FileLock;
-import java.nio.file.ClosedWatchServiceException;
-import java.nio.file.WatchKey;
-import java.nio.file.WatchService;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
 import java.util.regex.Pattern;
-import java.util.stream.Collectors;
 import javax.xml.stream.XMLInputFactory;
 import javax.xml.stream.XMLStreamException;
 import javax.xml.stream.XMLStreamReader;
 import org.checkerframework.checker.lock.qual.GuardedBy;
+import org.eclipse.jdt.annotation.NonNull;
 import org.opendaylight.bgpcep.config.loader.spi.ConfigFileProcessor;
 import org.opendaylight.bgpcep.config.loader.spi.ConfigLoader;
-import org.opendaylight.mdsal.binding.dom.codec.api.BindingNormalizedNodeSerializer;
+import org.opendaylight.yangtools.concepts.AbstractObjectRegistration;
 import org.opendaylight.yangtools.concepts.AbstractRegistration;
 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
@@ -48,105 +44,45 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.xml.sax.SAXException;
 
-public final class AbstractConfigLoader implements ConfigLoader, AutoCloseable {
-    private static final Logger LOG = LoggerFactory.getLogger(AbstractConfigLoader.class);
-    private static final String INTERRUPTED = "InterruptedException";
-    private static final String EXTENSION = "-.*\\.xml";
-    private static final String INITIAL = "^";
-    private static final String READ = "rw";
-    private static final long TIMEOUT_SECONDS = 5;
-    @GuardedBy("this")
-    private final Map<String, ConfigFileProcessor> configServices = new HashMap<>();
-    private final EffectiveModelContext schemaContext;
-    private final BindingNormalizedNodeSerializer bindingSerializer;
-    private final String path;
-    private final Thread watcherThread;
-    private final File file;
-    @GuardedBy("this")
-    private boolean closed = false;
-
-    public AbstractConfigLoader(final EffectiveModelContext schemaContext,
-            final BindingNormalizedNodeSerializer bindingSerializer, final FileWatcher fileWatcher) {
-        this.schemaContext = requireNonNull(schemaContext);
-        this.bindingSerializer = requireNonNull(bindingSerializer);
-        this.path = requireNonNull(fileWatcher.getPathFile());
-        this.file = new File(this.path);
-        this.watcherThread = new Thread(new ConfigLoaderImplRunnable(requireNonNull(fileWatcher.getWatchService())));
-    }
-
-    public void init() {
-        this.watcherThread.start();
-        LOG.info("Config Loader service initiated");
-    }
-
-    private synchronized void handleConfigFile(final ConfigFileProcessor config, final String filename) {
-        final NormalizedNode<?, ?> dto;
-        try {
-            dto = parseDefaultConfigFile(config, filename);
-        } catch (final IOException | XMLStreamException e) {
-            LOG.warn("Failed to parse config file {}", filename, e);
-            return;
+/**
+ * Reference implementation of configuration loading bits, without worrying where files are actually coming from.
+ */
+abstract class AbstractConfigLoader implements ConfigLoader {
+    private final class PatternRegistration extends AbstractObjectRegistration<Pattern> {
+        PatternRegistration(final Pattern pattern) {
+            super(pattern);
         }
-        LOG.info("Loading initial config {}", filename);
-        config.loadConfiguration(dto);
-    }
-
-    private NormalizedNode<?, ?> parseDefaultConfigFile(final ConfigFileProcessor config, final String filename)
-            throws IOException, XMLStreamException {
-        final NormalizedNodeResult result = new NormalizedNodeResult();
-        final NormalizedNodeStreamWriter streamWriter = ImmutableNormalizedNodeStreamWriter.from(result);
-
-        final File newFile = new File(this.path, filename);
-        FileChannel channel = new RandomAccessFile(newFile, READ).getChannel();
 
-        FileLock lock = null;
-        final Stopwatch stopwatch = Stopwatch.createStarted();
-        while (lock == null || stopwatch.elapsed(TimeUnit.SECONDS) > TIMEOUT_SECONDS) {
-            try {
-                lock = channel.tryLock();
-            } catch (final IllegalStateException e) {
-                //Ignore
-            }
-            if (lock == null) {
-                try {
-                    Thread.sleep(100L);
-                } catch (InterruptedException e) {
-                    LOG.warn("Failed to lock xml", e);
-                }
-            }
+        @Override
+        protected void removeRegistration() {
+            unregister(this);
         }
+    }
 
-        try (InputStream resourceAsStream = new FileInputStream(newFile)) {
-            final XMLInputFactory factory = XMLInputFactory.newInstance();
-            final XMLStreamReader reader = factory.createXMLStreamReader(resourceAsStream);
-
-            final SchemaNode schemaNode = SchemaContextUtil
-                    .findDataSchemaNode(this.schemaContext, config.getSchemaPath());
-            try (XmlParserStream xmlParser = XmlParserStream.create(streamWriter, this.schemaContext, schemaNode)) {
-                xmlParser.parse(reader);
-            } catch (final URISyntaxException | XMLStreamException | IOException | SAXException e) {
-                LOG.warn("Failed to parse xml", e);
-            } finally {
-                reader.close();
-                channel.close();
-            }
-        }
+    private static final Logger LOG = LoggerFactory.getLogger(AbstractConfigLoader.class);
+    private static final String EXTENSION = "-.*\\.xml";
+    private static final String INITIAL = "^";
+    private static final String READ = "rw";
+    private static final long TIMEOUT_NANOS = TimeUnit.SECONDS.toNanos(5);
 
-        return result.getResult();
-    }
+    @GuardedBy("this")
+    private final Map<PatternRegistration, ConfigFileProcessor> configServices = new HashMap<>();
 
     @Override
-    public synchronized AbstractRegistration registerConfigFile(final ConfigFileProcessor config) {
-        final String pattern = INITIAL + config.getSchemaPath().getLastComponent().getLocalName() + EXTENSION;
-        this.configServices.put(pattern, config);
+    public final synchronized AbstractRegistration registerConfigFile(final ConfigFileProcessor config) {
+        final String patternStr = INITIAL + config.getSchemaPath().getLastComponent().getLocalName() + EXTENSION;
+        final Pattern pattern = Pattern.compile(patternStr);
+        final PatternRegistration reg = new PatternRegistration(pattern);
+
+        this.configServices.put(reg, config);
 
-        final File[] fList = this.file.listFiles();
+        final File[] fList = directory().listFiles();
         final List<String> newFiles = new ArrayList<>();
         if (fList != null) {
             for (final File newFile : fList) {
                 if (newFile.isFile()) {
                     final String filename = newFile.getName();
-                    if (Pattern.matches(pattern, filename)) {
+                    if (pattern.matcher(filename).matches()) {
                         newFiles.add(filename);
                     }
                 }
@@ -155,73 +91,80 @@ public final class AbstractConfigLoader implements ConfigLoader, AutoCloseable {
         for (final String filename : newFiles) {
             handleConfigFile(config, filename);
         }
-        return new AbstractRegistration() {
-            @Override
-            protected void removeRegistration() {
-                synchronized (AbstractConfigLoader.this) {
-                    AbstractConfigLoader.this.configServices.remove(pattern);
-                }
-            }
-        };
+        return reg;
     }
 
-    @Override
-    public BindingNormalizedNodeSerializer getBindingNormalizedNodeSerializer() {
-        return this.bindingSerializer;
+    final synchronized void handleEvent(final String filename) {
+        configServices.entrySet().stream()
+                .filter(entry -> entry.getKey().getInstance().matcher(filename).matches())
+                .forEach(entry -> handleConfigFile(entry.getValue(), filename));
     }
 
+    abstract @NonNull File directory();
 
-    @Override
-    public synchronized void close() {
-        LOG.info("Config Loader service closed");
-        this.closed = true;
-        this.watcherThread.interrupt();
-    }
+    abstract @NonNull EffectiveModelContext modelContext();
 
-    private class ConfigLoaderImplRunnable implements Runnable {
-        @GuardedBy("this")
-        private final WatchService watchService;
+    @SuppressFBWarnings(value = "UPM_UNCALLED_PRIVATE_METHOD",
+        justification = "https://github.com/spotbugs/spotbugs/issues/811")
+    private synchronized void unregister(final PatternRegistration reg) {
+        configServices.remove(reg);
+    }
 
-        ConfigLoaderImplRunnable(final WatchService watchService) {
-            this.watchService = watchService;
+    private synchronized void handleConfigFile(final ConfigFileProcessor config, final String filename) {
+        final NormalizedNode<?, ?> dto;
+        try {
+            dto = parseDefaultConfigFile(config, filename);
+        } catch (final IOException | XMLStreamException e) {
+            LOG.warn("Failed to parse config file {}", filename, e);
+            return;
         }
+        LOG.info("Loading initial config {}", filename);
+        config.loadConfiguration(dto);
+    }
 
-        @Override
-        public void run() {
-            while (!Thread.currentThread().isInterrupted()) {
-                handleChanges();
-            }
-        }
+    private NormalizedNode<?, ?> parseDefaultConfigFile(final ConfigFileProcessor config, final String filename)
+            throws IOException, XMLStreamException {
+        final NormalizedNodeResult result = new NormalizedNodeResult();
+        final NormalizedNodeStreamWriter streamWriter = ImmutableNormalizedNodeStreamWriter.from(result);
+
+        final File newFile = new File(directory(), filename);
+        try (RandomAccessFile raf = new RandomAccessFile(newFile, READ)) {
+            final FileChannel channel = raf.getChannel();
 
-        private synchronized void handleChanges() {
-            final WatchKey key;
-            try {
-                key = this.watchService.take();
-            } catch (final InterruptedException | ClosedWatchServiceException e) {
-                if (!AbstractConfigLoader.this.closed) {
-                    LOG.warn(INTERRUPTED, e);
-                    Thread.currentThread().interrupt();
+            FileLock lock = null;
+            final Stopwatch stopwatch = Stopwatch.createStarted();
+            while (lock == null || stopwatch.elapsed(TimeUnit.NANOSECONDS) > TIMEOUT_NANOS) {
+                try {
+                    lock = channel.tryLock();
+                } catch (final IllegalStateException e) {
+                    //Ignore
+                }
+                if (lock == null) {
+                    try {
+                        Thread.sleep(100);
+                    } catch (InterruptedException e) {
+                        LOG.warn("Failed to lock xml", e);
+                    }
                 }
-                return;
             }
 
-            if (key != null) {
-                final List<String> fileNames = key.pollEvents()
-                        .stream().map(event -> event.context().toString())
-                        .collect(Collectors.toList());
-                fileNames.forEach(this::handleEvent);
-
-                final boolean reset = key.reset();
-                if (!reset) {
-                    LOG.warn("Could not reset the watch key.");
+            try (InputStream resourceAsStream = new FileInputStream(newFile)) {
+                final XMLInputFactory factory = XMLInputFactory.newInstance();
+                final XMLStreamReader reader = factory.createXMLStreamReader(resourceAsStream);
+
+                final EffectiveModelContext modelContext = modelContext();
+                final SchemaNode schemaNode = SchemaContextUtil.findDataSchemaNode(modelContext(),
+                    config.getSchemaPath());
+                try (XmlParserStream xmlParser = XmlParserStream.create(streamWriter, modelContext, schemaNode)) {
+                    xmlParser.parse(reader);
+                } catch (final URISyntaxException | XMLStreamException | IOException | SAXException e) {
+                    LOG.warn("Failed to parse xml", e);
+                } finally {
+                    reader.close();
                 }
             }
         }
 
-        private synchronized void handleEvent(final String filename) {
-            AbstractConfigLoader.this.configServices.entrySet().stream()
-                    .filter(entry -> Pattern.matches(entry.getKey(), filename))
-                    .forEach(entry -> handleConfigFile(entry.getValue(), filename));
-        }
+        return result.getResult();
     }
 }
diff --git a/config-loader/config-loader-impl/src/main/java/org/opendaylight/bgpcep/config/loader/impl/AbstractWatchingConfigLoader.java b/config-loader/config-loader-impl/src/main/java/org/opendaylight/bgpcep/config/loader/impl/AbstractWatchingConfigLoader.java
new file mode 100644 (file)
index 0000000..e5c3404
--- /dev/null
@@ -0,0 +1,73 @@
+/*
+ * Copyright (c) 2020 PANTHEON.tech, s.r.o. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.bgpcep.config.loader.impl;
+
+import java.nio.file.ClosedWatchServiceException;
+import java.nio.file.WatchKey;
+import java.nio.file.WatchService;
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * An {@link AbstractConfigLoader} which additionally talks to a {@link WatchService}.
+ */
+abstract class AbstractWatchingConfigLoader extends AbstractConfigLoader {
+    private static final Logger LOG = LoggerFactory.getLogger(AbstractWatchingConfigLoader.class);
+
+    private final AtomicBoolean closed = new AtomicBoolean();
+    private final Thread watcherThread;
+
+    AbstractWatchingConfigLoader() {
+        watcherThread = new Thread(this::dispatchEvents, "Config Loader Watcher Thread");
+        watcherThread.setDaemon(true);
+    }
+
+    final void start() {
+        watcherThread.start();
+        LOG.info("Config Loader service started");
+    }
+
+    final void stop() {
+        LOG.info("Config Loader service stopping");
+
+        closed.set(true);
+        watcherThread.interrupt();
+
+        try {
+            this.watcherThread.join();
+        } catch (InterruptedException e) {
+            LOG.warn("Interrupted while waiting for watcher thread to terminate", e);
+        }
+        LOG.info("Config Loader service stopped");
+    }
+
+    abstract WatchKey takeEvent() throws InterruptedException;
+
+    private void dispatchEvents() {
+        while (!closed.get()) {
+            final WatchKey key;
+            try {
+                key = takeEvent();
+            } catch (final InterruptedException | ClosedWatchServiceException e) {
+                if (!closed.get()) {
+                    LOG.warn("Exception while waiting for events, exiting", e);
+                    Thread.currentThread().interrupt();
+                }
+                return;
+            }
+
+            if (key != null) {
+                key.pollEvents().stream().map(event -> event.context().toString()).forEach(this::handleEvent);
+                if (!key.reset()) {
+                    LOG.warn("Could not reset the watch key.");
+                }
+            }
+        }
+    }
+}
index f88b37493bc864d31eadc50d8f7f05ad5b988b97..4fda85d42a15605c620b443d8df9d64f771e3ecf 100644 (file)
@@ -16,18 +16,43 @@ import java.nio.file.FileSystems;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.nio.file.WatchService;
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import javax.inject.Singleton;
+import org.opendaylight.yangtools.concepts.AbstractRegistration;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-public final class DefaultFileWatcher implements FileWatcher, AutoCloseable {
+@Singleton
+@Component(immediate = true, service = FileWatcher.class)
+public final class DefaultFileWatcher extends AbstractRegistration implements FileWatcher {
     private static final Logger LOG = LoggerFactory.getLogger(DefaultFileWatcher.class);
-    private static final String INTERRUPTED = "InterruptedException";
     //BGPCEP config folder OS agnostic path
     private static final Path PATH = Paths.get("etc","opendaylight","bgpcep");
+
     private final WatchService watchService;
 
     public DefaultFileWatcher() throws IOException {
-        this.watchService = FileSystems.getDefault().newWatchService();
+        watchService = FileSystems.getDefault().newWatchService();
+        Runtime.getRuntime().addShutdownHook(new Thread(this::close));
+    }
+
+    @Override
+    public String getPathFile() {
+        return PATH.toString();
+    }
+
+    @Override
+    public WatchService getWatchService() {
+        return watchService;
+    }
+
+    @Activate
+    @PostConstruct
+    public void activate() throws IOException {
         final File file = new File(PATH.toString());
         if (!file.exists()) {
             if (!file.mkdirs()) {
@@ -36,31 +61,24 @@ public final class DefaultFileWatcher implements FileWatcher, AutoCloseable {
             }
         }
 
-        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
-            try {
-                DefaultFileWatcher.this.watchService.close();
-            } catch (final IOException e) {
-                LOG.warn(INTERRUPTED, e);
-            }
-        }));
         PATH.register(this.watchService, OVERFLOW, ENTRY_CREATE);
-        LOG.info("File Watcher service initiated");
+        LOG.info("File Watcher service started");
     }
 
-    @Override
-    public String getPathFile() {
-        return PATH.toString();
-    }
-
-    @Override
-    public synchronized WatchService getWatchService() {
-        return this.watchService;
+    @Deactivate
+    @PreDestroy
+    public void deactivate() {
+        // Just route to close(), it will do the right thing
+        close();
     }
 
     @Override
-    public synchronized void close() throws Exception {
-        if (this.watchService != null) {
-            this.watchService.close();
+    protected void removeRegistration() {
+        try {
+            watchService.close();
+        } catch (IOException e) {
+            LOG.warn("Failed to close watch service", e);
         }
+        LOG.info("File Watcher service stopped");
     }
 }
diff --git a/config-loader/config-loader-impl/src/main/java/org/opendaylight/bgpcep/config/loader/impl/OSGiConfigLoader.java b/config-loader/config-loader-impl/src/main/java/org/opendaylight/bgpcep/config/loader/impl/OSGiConfigLoader.java
new file mode 100644 (file)
index 0000000..e714ffe
--- /dev/null
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2020 PANTHEON.tech, s.r.o. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.bgpcep.config.loader.impl;
+
+import static com.google.common.base.Verify.verifyNotNull;
+
+import com.google.common.annotations.Beta;
+import java.io.File;
+import java.nio.file.WatchKey;
+import org.opendaylight.bgpcep.config.loader.spi.ConfigLoader;
+import org.opendaylight.mdsal.binding.dom.codec.api.BindingNormalizedNodeSerializer;
+import org.opendaylight.mdsal.binding.dom.codec.spi.BindingDOMCodecServices;
+import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Reference;
+
+@Beta
+@Component(immediate = true, service = ConfigLoader.class)
+public final class OSGiConfigLoader extends AbstractWatchingConfigLoader {
+    @Reference
+    FileWatcher watcher;
+
+    @Reference
+    volatile BindingDOMCodecServices codecServices;
+
+    private File directory;
+
+    @Override
+    public BindingNormalizedNodeSerializer getBindingNormalizedNodeSerializer() {
+        return verifyNotNull(codecServices);
+    }
+
+    @Override
+    EffectiveModelContext modelContext() {
+        return codecServices.getRuntimeContext().getEffectiveModelContext();
+    }
+
+    @Activate
+    void activate() {
+        directory = new File(watcher.getPathFile());
+        start();
+    }
+
+    @Deactivate
+    void deactivate() {
+        try {
+            stop();
+        } finally {
+            directory = null;
+        }
+    }
+
+    @Override
+    File directory() {
+        return verifyNotNull(directory);
+    }
+
+    @Override
+    WatchKey takeEvent() throws InterruptedException {
+        return watcher.getWatchService().take();
+    }
+}
diff --git a/config-loader/config-loader-impl/src/main/java/org/opendaylight/bgpcep/config/loader/impl/SimpleConfigLoader.java b/config-loader/config-loader-impl/src/main/java/org/opendaylight/bgpcep/config/loader/impl/SimpleConfigLoader.java
new file mode 100644 (file)
index 0000000..1756d9b
--- /dev/null
@@ -0,0 +1,71 @@
+/*
+ * Copyright (c) 2020 PANTHEON.tech, s.r.o. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.bgpcep.config.loader.impl;
+
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.annotations.Beta;
+import java.io.File;
+import java.nio.file.WatchKey;
+import java.nio.file.WatchService;
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import org.eclipse.jdt.annotation.NonNull;
+import org.opendaylight.mdsal.binding.dom.codec.api.BindingNormalizedNodeSerializer;
+import org.opendaylight.mdsal.binding.dom.codec.spi.BindingDOMCodecServices;
+import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
+
+@Beta
+@Singleton
+public final class SimpleConfigLoader extends AbstractWatchingConfigLoader implements AutoCloseable {
+    private final @NonNull BindingNormalizedNodeSerializer serializer;
+    private final @NonNull EffectiveModelContext modelContext;
+    private final @NonNull WatchService watchService;
+    private final @NonNull File directory;
+
+    @Inject
+    public SimpleConfigLoader(final FileWatcher fileWatcher, final BindingDOMCodecServices codec) {
+        this.serializer = requireNonNull(codec);
+        this.modelContext = codec.getRuntimeContext().getEffectiveModelContext();
+        this.watchService = fileWatcher.getWatchService();
+        this.directory = new File(fileWatcher.getPathFile());
+    }
+
+    @Override
+    public BindingNormalizedNodeSerializer getBindingNormalizedNodeSerializer() {
+        return serializer;
+    }
+
+    @PostConstruct
+    public void init() {
+        start();
+    }
+
+    @Override
+    @PreDestroy
+    public void close() {
+        stop();
+    }
+
+    @Override
+    EffectiveModelContext modelContext() {
+        return modelContext;
+    }
+
+    @Override
+    File directory() {
+        return directory;
+    }
+
+    @Override
+    WatchKey takeEvent() throws InterruptedException {
+        return watchService.take();
+    }
+}
diff --git a/config-loader/config-loader-impl/src/main/resources/OSGI-INF/blueprint/config-loader-impl.xml b/config-loader/config-loader-impl/src/main/resources/OSGI-INF/blueprint/config-loader-impl.xml
deleted file mode 100644 (file)
index 337f4e7..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-  ~ Copyright (c) 2017. AT&T Intellectual Property. All rights reserved.
-  ~
-  ~  This program and the accompanying materials are made available under the
-  ~  terms of the Eclipse Public License v1.0 which accompanies this distribution,
-  ~  and is available at http://www.eclipse.org/legal/epl-v10.html
-  -->
-<blueprint xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0"
-           xmlns:odl="http://opendaylight.org/xmlns/blueprint/v1.0.0">
-    <odl:static-reference id="domSchemaService" interface="org.opendaylight.mdsal.dom.api.DOMSchemaService"/>
-    <odl:static-reference id="mappingCodec"
-                          interface="org.opendaylight.mdsal.binding.dom.codec.api.BindingNormalizedNodeSerializer"/>
-
-    <bean id="filewatcher" class="org.opendaylight.bgpcep.config.loader.impl.DefaultFileWatcher"
-          destroy-method="close"/>
-
-    <bean id="configLoader" class="org.opendaylight.bgpcep.config.loader.impl.AbstractConfigLoader"
-          init-method="init"
-          destroy-method="close">
-        <argument>
-            <bean factory-ref="domSchemaService" factory-method="getGlobalContext"/>
-        </argument>
-        <argument ref="mappingCodec"/>
-        <argument ref="filewatcher"/>
-    </bean>
-
-    <service ref="configLoader" interface="org.opendaylight.bgpcep.config.loader.spi.ConfigLoader"/>
-</blueprint>
index e7e683836c844e1f7beb07eb4c38285931757534..ebcb3b95bc8867924283a1ac717723ef3052ad0d 100644 (file)
@@ -8,16 +8,9 @@
 package org.opendaylight.bgpcep.config.loader.impl;
 
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doNothing;
 
-import java.nio.file.WatchEvent;
-import java.nio.file.WatchKey;
-import java.nio.file.WatchService;
-import java.util.List;
-import java.util.concurrent.CopyOnWriteArrayList;
-import org.checkerframework.checker.lock.qual.GuardedBy;
-import org.junit.After;
+import java.io.File;
 import org.junit.Before;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
@@ -25,22 +18,40 @@ import org.opendaylight.bgpcep.config.loader.spi.ConfigFileProcessor;
 import org.opendaylight.mdsal.binding.dom.adapter.AdapterContext;
 import org.opendaylight.mdsal.binding.dom.adapter.test.AbstractConcurrentDataBrokerTest;
 import org.opendaylight.mdsal.binding.dom.adapter.test.AbstractDataBrokerTestCustomizer;
+import org.opendaylight.mdsal.binding.dom.codec.api.BindingNormalizedNodeSerializer;
 import org.opendaylight.mdsal.dom.api.DOMSchemaService;
+import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
 
 public abstract class AbstractConfigLoaderTest extends AbstractConcurrentDataBrokerTest {
-    @GuardedBy("this")
-    private final List<WatchEvent<?>> eventList = new CopyOnWriteArrayList<>();
-    protected AbstractConfigLoader configLoader;
-    @Mock
-    private WatchService watchService;
+    protected final class TestConfigLoader extends AbstractConfigLoader {
+        @Override
+        public BindingNormalizedNodeSerializer getBindingNormalizedNodeSerializer() {
+            return mappingService.currentSerializer();
+        }
+
+        @Override
+        File directory() {
+            return new File(getResourceFolder());
+        }
+
+        @Override
+        @SuppressWarnings("checkstyle:illegalCatch")
+        EffectiveModelContext modelContext() {
+            try {
+                return getSchemaContext();
+            } catch (Exception e) {
+                throw new IllegalStateException("Failed to acquire schema context", e);
+            }
+        }
+
+        public void triggerEvent(final String filename) {
+            handleEvent(filename);
+        }
+    }
+
+    protected final TestConfigLoader configLoader = new TestConfigLoader();
     @Mock
     ConfigFileProcessor processor;
-    @Mock
-    private WatchKey watchKey;
-    @Mock
-    private WatchEvent<?> watchEvent;
-    @Mock
-    private FileWatcher fileWatcher;
     protected AdapterContext mappingService;
     protected DOMSchemaService schemaService;
 
@@ -51,46 +62,18 @@ public abstract class AbstractConfigLoaderTest extends AbstractConcurrentDataBro
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
-        doAnswer(invocation -> true).when(this.watchKey).reset();
-        doReturn(this.eventList).when(this.watchKey).pollEvents();
-        doReturn(this.watchKey).when(this.watchService).take();
-        doReturn("watchKey").when(this.watchKey).toString();
-        doReturn("watchService").when(this.watchService).toString();
-        doReturn("watchEvent").when(this.watchEvent).toString();
-        doReturn(getResourceFolder()).when(this.fileWatcher).getPathFile();
-        doReturn(this.watchService).when(this.fileWatcher).getWatchService();
-        doAnswer(invocation -> {
-            clearEvent();
-            return null;
-        }).when(this.processor).loadConfiguration(any());
-        this.configLoader = new AbstractConfigLoader(getSchemaContext(), this.mappingService.currentSerializer(),
-            this.fileWatcher);
-        this.configLoader.init();
+        doNothing().when(processor).loadConfiguration(any());
     }
 
     @Override
-    protected final AbstractDataBrokerTestCustomizer createDataBrokerTestCustomizer() {
+    protected AbstractDataBrokerTestCustomizer createDataBrokerTestCustomizer() {
         final AbstractDataBrokerTestCustomizer customizer = super.createDataBrokerTestCustomizer();
         this.mappingService = customizer.getAdapterContext();
         this.schemaService = customizer.getSchemaService();
         return customizer;
     }
 
-    private synchronized void clearEvent() {
-        this.eventList.clear();
-    }
-
     protected String getResourceFolder() {
         return ClassLoader.getSystemClassLoader().getResource("initial").getPath();
     }
-
-    protected synchronized void triggerEvent(final String filename) {
-        doReturn(filename).when(this.watchEvent).context();
-        this.eventList.add(this.watchEvent);
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        this.configLoader.close();
-    }
 }
index 4ccee171c1620f108745f767b6845f5035d7733a..233093acf9cb8f755dfd6c6e4ab6361072d7c9ed 100644 (file)
@@ -10,7 +10,7 @@ package org.opendaylight.bgpcep.config.loader.impl;
 import static org.junit.Assert.assertNotNull;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
 import org.junit.Before;
@@ -44,11 +44,11 @@ public class ConfigLoaderImplTest extends AbstractConfigLoaderTest {
         final AbstractRegistration ticket = this.configLoader.registerConfigFile(this.processor);
         verify(this.processor).loadConfiguration(any());
 
-        triggerEvent("protocols-config.xml");
-        verify(this.processor, timeout(20000).times(2)).loadConfiguration(any());
+        configLoader.triggerEvent("protocols-config.xml");
+        verify(this.processor, times(2)).loadConfiguration(any());
 
         ticket.close();
-        triggerEvent("protocols-config.xml");
-        verify(this.processor, timeout(20000).times(2)).loadConfiguration(any());
+        configLoader.triggerEvent("protocols-config.xml");
+        verify(this.processor, times(2)).loadConfiguration(any());
     }
 }
index 5475761b15b131f0ced0ae5a5044066beb0877ba..7c8c79f691336607ee56e2257026be84566fda9a 100644 (file)
@@ -18,9 +18,11 @@ public class DefaultWatcherTest {
 
     @Test
     public void bgpFileWatcherTest() throws Exception {
-        final DefaultFileWatcher bgpFileWatcher = new DefaultFileWatcher();
-        assertEquals(PATH, bgpFileWatcher.getPathFile());
-        assertNotNull(bgpFileWatcher.getWatchService());
-        bgpFileWatcher.close();
+        try (DefaultFileWatcher bgpFileWatcher = new DefaultFileWatcher()) {
+            bgpFileWatcher.activate();
+
+            assertEquals(PATH, bgpFileWatcher.getPathFile());
+            assertNotNull(bgpFileWatcher.getWatchService());
+        }
     }
 }
diff --git a/config-loader/config-loader-impl/src/test/java/org/opendaylight/bgpcep/config/loader/impl/OSGiConfigLoaderTest.java b/config-loader/config-loader-impl/src/test/java/org/opendaylight/bgpcep/config/loader/impl/OSGiConfigLoaderTest.java
new file mode 100644 (file)
index 0000000..2d4725f
--- /dev/null
@@ -0,0 +1,70 @@
+/*
+ * Copyright (c) 2020 PANTHEON.tech, s.r.o. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.bgpcep.config.loader.impl;
+
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
+import java.nio.file.WatchEvent;
+import java.nio.file.WatchKey;
+import java.nio.file.WatchService;
+import java.util.List;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.opendaylight.mdsal.binding.dom.codec.spi.BindingDOMCodecServices;
+
+@RunWith(MockitoJUnitRunner.StrictStubs.class)
+public class OSGiConfigLoaderTest {
+    @Mock
+    private FileWatcher watcher;
+    @Mock
+    private WatchService watchService;
+    @Mock
+    private WatchKey watchKey;
+    @Mock
+    private WatchEvent<?> watchEvent;
+    @Mock
+    private BindingDOMCodecServices codec;
+
+    private OSGiConfigLoader loader;
+
+    @Before
+    public void before() throws InterruptedException {
+        doReturn(watchService).when(watcher).getWatchService();
+        doReturn("foo").when(watcher).getPathFile();
+        doReturn(watchKey).when(watchService).take();
+        doAnswer(inv -> {
+            doThrow(new RuntimeException("enough!")).when(watchKey).pollEvents();
+            return List.of(watchEvent);
+        }).when(watchKey).pollEvents();
+        doReturn("watchEvent").when(watchEvent).context();
+        doReturn(true).when(watchKey).reset();
+
+        loader = new OSGiConfigLoader();
+        loader.watcher = watcher;
+        loader.codecServices = codec;
+    }
+
+    @After
+    public void after() {
+        loader.deactivate();
+    }
+
+    @Test
+    public void testSimpleConfigLoader() {
+        loader.activate();
+        verify(watchKey, timeout(10000)).reset();
+    }
+}
diff --git a/config-loader/config-loader-impl/src/test/java/org/opendaylight/bgpcep/config/loader/impl/SimpleConfigLoaderTest.java b/config-loader/config-loader-impl/src/test/java/org/opendaylight/bgpcep/config/loader/impl/SimpleConfigLoaderTest.java
new file mode 100644 (file)
index 0000000..2711034
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+ * Copyright (c) 2020 PANTHEON.tech, s.r.o. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.bgpcep.config.loader.impl;
+
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
+import java.nio.file.WatchEvent;
+import java.nio.file.WatchKey;
+import java.nio.file.WatchService;
+import java.util.List;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.opendaylight.mdsal.binding.dom.codec.spi.BindingDOMCodecServices;
+import org.opendaylight.mdsal.binding.runtime.api.BindingRuntimeContext;
+import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
+
+@RunWith(MockitoJUnitRunner.StrictStubs.class)
+public class SimpleConfigLoaderTest {
+    @Mock
+    private FileWatcher watcher;
+    @Mock
+    private WatchService watchService;
+    @Mock
+    private WatchKey watchKey;
+    @Mock
+    private WatchEvent<?> watchEvent;
+    @Mock
+    private BindingDOMCodecServices codec;
+    @Mock
+    private BindingRuntimeContext bindingContext;
+    @Mock
+    private EffectiveModelContext domContext;
+
+    private SimpleConfigLoader loader;
+
+    @Before
+    public void before() throws InterruptedException {
+        doReturn(bindingContext).when(codec).getRuntimeContext();
+        doReturn(domContext).when(bindingContext).getEffectiveModelContext();
+        doReturn(watchService).when(watcher).getWatchService();
+        doReturn("foo").when(watcher).getPathFile();
+        doReturn(watchKey).when(watchService).take();
+        doAnswer(inv -> {
+            doThrow(new RuntimeException("enough!")).when(watchKey).pollEvents();
+            return List.of(watchEvent);
+        }).when(watchKey).pollEvents();
+        doReturn("watchEvent").when(watchEvent).context();
+        doReturn(true).when(watchKey).reset();
+
+        loader = new SimpleConfigLoader(watcher, codec);
+    }
+
+    @After
+    public void after() {
+        loader.close();
+    }
+
+    @Test
+    public void testSimpleConfigLoader() {
+        loader.init();
+        verify(watchKey, timeout(10000)).reset();
+    }
+}
index 441921693c326d06fc4cfb8e3d9c6ef1144e1077..a0135f3684f2ea09f3807172932374615e8d3383 100644 (file)
@@ -12,7 +12,6 @@ import static org.opendaylight.bgpcep.config.loader.routing.policy.OpenconfigRou
 import static org.opendaylight.protocol.util.CheckUtil.checkNotPresentConfiguration;
 import static org.opendaylight.protocol.util.CheckUtil.checkPresentConfiguration;
 
-import org.junit.After;
 import org.junit.Before;
 import org.opendaylight.bgpcep.config.loader.impl.AbstractConfigLoaderTest;
 
@@ -29,11 +28,5 @@ public class AbstractOpenconfigRoutingPolicyLoaderTest extends AbstractConfigLoa
         this.policyLoader.init();
         checkPresentConfiguration(getDataBroker(), ROUTING_POLICY_IID);
         this.policyLoader.close();
-        this.configLoader.close();
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        super.tearDown();
     }
 }
index d3537e845bd21582443e9564f9556828c6951349..2f2a4ea79c5483681e9005c964b6a25a86dad15f 100644 (file)
@@ -34,8 +34,8 @@
             <type>xml</type>
         </dependency>
         <dependency>
-            <groupId>org.opendaylight.controller</groupId>
-            <artifactId>odl-controller-blueprint</artifactId>
+            <groupId>org.opendaylight.yangtools</groupId>
+            <artifactId>odl-yangtools-codec</artifactId>
             <classifier>features</classifier>
             <type>xml</type>
         </dependency>
index e7f1eaf2b624b9bcdb3a94b6ec0490c20918cc64..80719eff219573d507f825e23b587bdf6c528496 100644 (file)
@@ -11,6 +11,6 @@
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://karaf.apache.org/xmlns/features/v1.2.0 http://karaf.apache.org/xmlns/features/v1.2.0">
     <feature name="odl-bgpcep-config-loader-impl" version="${project.version}">
-        <feature version="[3,4)">odl-controller-blueprint</feature>
+        <feature version="[6,7)">odl-yangtools-codec</feature>
     </feature>
 </features>