killbill-aplcache

util: Implement notifyPluginChanged api Also add an integration

11/18/2015 10:40:51 PM

Details

diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestPublicBus.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestPublicBus.java
index ad64469..6443bda 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestPublicBus.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestPublicBus.java
@@ -91,7 +91,6 @@ public class TestPublicBus extends TestIntegrationBase {
         of the publicBus event;
         TODO modify sequence to allow optional registration of publicListener
          */
-        //super.beforeMethod();
 
         try {
             DBTestingHelper.get().getInstance().cleanupAllTables();
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithFakeKPMPlugin.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithFakeKPMPlugin.java
new file mode 100644
index 0000000..5f885a4
--- /dev/null
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithFakeKPMPlugin.java
@@ -0,0 +1,263 @@
+/*
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 The Billing Project, LLC
+ *
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.beatrix.integration;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.Callable;
+
+import javax.annotation.Nullable;
+import javax.inject.Inject;
+
+import org.killbill.billing.DBTestingHelper;
+import org.killbill.billing.api.TestApiListener.NextEvent;
+import org.killbill.billing.notification.plugin.api.BroadcastMetadata;
+import org.killbill.billing.notification.plugin.api.ExtBusEvent;
+import org.killbill.billing.notification.plugin.api.ExtBusEventType;
+import org.killbill.billing.osgi.BundleRegistry;
+import org.killbill.billing.osgi.BundleWithConfig;
+import org.killbill.billing.osgi.PureOSGIBundleFinder;
+import org.killbill.billing.osgi.api.PluginInfo;
+import org.killbill.billing.osgi.api.PluginStateChange;
+import org.killbill.billing.osgi.api.PluginsInfoApi;
+import org.killbill.billing.osgi.api.config.PluginConfig;
+import org.killbill.billing.osgi.api.config.PluginConfig.PluginLanguage;
+import org.killbill.billing.osgi.api.config.PluginConfigServiceApi;
+import org.killbill.billing.osgi.pluginconf.PluginFinder;
+import org.killbill.billing.platform.api.KillbillConfigSource;
+import org.killbill.billing.util.jackson.ObjectMapper;
+import org.killbill.billing.util.nodes.NodeCommand;
+import org.killbill.billing.util.nodes.NodeCommandMetadata;
+import org.killbill.billing.util.nodes.NodeCommandProperty;
+import org.killbill.billing.util.nodes.NodeInfo;
+import org.killbill.billing.util.nodes.NodeInfoMapper;
+import org.killbill.billing.util.nodes.PluginNodeCommandMetadata;
+import org.killbill.billing.util.nodes.SystemNodeCommandType;
+import org.mockito.Mockito;
+import org.osgi.framework.Bundle;
+import org.testng.Assert;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.datatype.joda.JodaModule;
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.common.eventbus.Subscribe;
+import com.google.inject.Binder;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Module;
+import com.google.inject.Stage;
+import com.google.inject.util.Modules;
+
+import static com.jayway.awaitility.Awaitility.await;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+public class TestWithFakeKPMPlugin extends TestIntegrationBase {
+
+    private static final String NEW_PLUGIN_NAME = "Foo";
+    private static final String NEW_PLUGIN_VERSION = "2.5.7";
+
+    @Inject
+    private PluginsInfoApi pluginsInfoApi;
+
+    @Override
+    protected KillbillConfigSource getConfigSource() {
+        ImmutableMap additionalProperties = new ImmutableMap.Builder()
+                .put("org.killbill.billing.util.broadcast.rate", "100ms")
+                .build();
+        return getConfigSource("/beatrix.properties", additionalProperties);
+    }
+
+    public class FakeKPMPlugin {
+
+        private final NodeInfoMapper nodeInfoMapper;
+        private final ObjectMapper objectMapper;
+
+        FakeKPMPlugin() {
+            this.nodeInfoMapper = new NodeInfoMapper();
+            this.objectMapper = new ObjectMapper();
+            objectMapper.registerModule(new JodaModule());
+            objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
+        }
+
+        @Subscribe
+        public void handleExternalEvents(final ExtBusEvent extBusEvent) {
+            if (extBusEvent.getEventType().equals(ExtBusEventType.BROADCAST_SERVICE)) {
+                final String metadata = extBusEvent.getMetaData();
+                try {
+                    final BroadcastMetadata broadcastMetadata = objectMapper.readValue(metadata, BroadcastMetadata.class);
+
+                    final PluginNodeCommandMetadata nodeCommandMetadata = (PluginNodeCommandMetadata) nodeInfoMapper.deserializeNodeCommand(broadcastMetadata.getEventJson(), broadcastMetadata.getCommandType());
+
+                    pluginsInfoApi.notifyOfStateChanged(PluginStateChange.NEW_VERSION, nodeCommandMetadata.getPluginName(), nodeCommandMetadata.getPluginVersion(), PluginLanguage.JAVA);
+
+                } catch (IOException e) {
+                    throw new RuntimeException(e);
+                }
+            }
+        }
+    }
+
+    // We override the  BundleRegistry to bypass the bundle installation and yet return our new bundle as being installed.
+    private static class FakeBundleRegistry extends BundleRegistry {
+
+        private final List<BundleWithMetadata> bundles;
+
+        @Inject
+        public FakeBundleRegistry(final PureOSGIBundleFinder osgiBundleFinder, final PluginFinder pluginFinder, final PluginConfigServiceApi pluginConfigServiceApi) {
+            super(osgiBundleFinder, pluginFinder, pluginConfigServiceApi);
+            bundles = new ArrayList<BundleWithMetadata>();
+        }
+
+        public void installNewBundle(final String pluginName, @Nullable final String version, final PluginLanguage pluginLanguage) {
+            final Bundle bundle = Mockito.mock(Bundle.class);
+            Mockito.when(bundle.getSymbolicName()).thenReturn(pluginName);
+
+            final BundleWithConfig config = new BundleWithConfig(bundle, new PluginConfig() {
+                @Override
+                public String getPluginName() {
+                    return pluginName;
+                }
+                @Override
+                public PluginType getPluginType() {
+                    return PluginType.NOTIFICATION;
+                }
+                @Override
+                public String getVersion() {
+                    return version;
+                }
+                @Override
+                public String getPluginVersionnedName() {
+                    return null;
+                }
+                @Override
+                public File getPluginVersionRoot() {
+                    return null;
+                }
+                @Override
+                public PluginLanguage getPluginLanguage() {
+                    return pluginLanguage;
+                }
+            });
+            bundles.add(new BundleWithMetadata(config));
+        }
+
+        public BundleWithMetadata getBundle(final String pluginName) {
+            return Iterables.tryFind(bundles, new Predicate<BundleWithMetadata>() {
+                @Override
+                public boolean apply(@Nullable final BundleWithMetadata input) {
+                    return input.getPluginName().equals(pluginName);
+                }
+            }).orNull();
+        }
+
+        public Collection<BundleWithMetadata> getBundles() {
+            return bundles;
+        }
+    }
+
+    public static class OverrideModuleForOSGI implements Module {
+
+        @Override
+        public void configure(final Binder binder) {
+            binder.bind(BundleRegistry.class).to(FakeBundleRegistry.class).asEagerSingleton();
+        }
+    }
+
+    @BeforeClass(groups = "slow")
+    public void beforeClass() throws Exception {
+        final Injector g = Guice.createInjector(Stage.PRODUCTION, Modules.override(new BeatrixIntegrationModule(configSource)).with(new OverrideModuleForOSGI()));
+        g.injectMembers(this);
+    }
+
+    @BeforeClass(groups = "slow")
+    public void beforeMethod() throws Exception {
+
+        try {
+            DBTestingHelper.get().getInstance().cleanupAllTables();
+        } catch (final Exception ignored) {
+        }
+
+        log.debug("RESET TEST FRAMEWORK");
+
+        clock.resetDeltaFromReality();
+        busHandler.reset();
+
+        lifecycle.fireStartupSequencePriorEventRegistration();
+        busService.getBus().register(busHandler);
+        externalBus.register(new FakeKPMPlugin());
+
+        lifecycle.fireStartupSequencePostEventRegistration();
+
+        // Make sure we start with a clean state
+        assertListenerStatus();
+    }
+
+    @Test(groups = "slow")
+    public void testPluginInstallMechanism() throws Exception {
+
+        final NodeCommand nodeCommand = new NodeCommand() {
+            @Override
+            public boolean isSystemCommandType() {
+                return true;
+            }
+
+            @Override
+            public String getNodeCommandType() {
+                return SystemNodeCommandType.INSTALL_PLUGIN.name();
+            }
+
+            @Override
+            public NodeCommandMetadata getNodeCommandMetadata() {
+                return new PluginNodeCommandMetadata(NEW_PLUGIN_NAME, NEW_PLUGIN_VERSION, ImmutableList.<NodeCommandProperty>of());
+            }
+        };
+        busHandler.pushExpectedEvent(NextEvent.BROADCAST_SERVICE);
+        nodesApi.triggerNodeCommand(nodeCommand);
+        assertListenerStatus();
+
+        // Exit condition is based on the new config being updated on disk
+        await().atMost(3, SECONDS).until(new Callable<Boolean>() {
+            @Override
+            public Boolean call() throws Exception {
+                final Iterable<NodeInfo> rawNodeInfos = nodesApi.getNodesInfo();
+
+                final List<NodeInfo> nodeInfos = ImmutableList.<NodeInfo>copyOf(rawNodeInfos);
+                Assert.assertEquals(nodeInfos.size(), 1);
+
+                final NodeInfo nodeInfo = nodeInfos.get(0);
+                final Iterable<PluginInfo> rawPluginInfos = nodeInfo.getPluginInfo();
+                final List<PluginInfo> pluginsInfo = ImmutableList.copyOf(rawPluginInfos);
+
+                if (pluginsInfo.size() == 1) {
+                    final PluginInfo pluginInfo = pluginsInfo.get(0);
+                    Assert.assertEquals(pluginInfo.getPluginName(), NEW_PLUGIN_NAME);
+                    Assert.assertEquals(pluginInfo.getVersion(), NEW_PLUGIN_VERSION);
+                }
+                return pluginsInfo.size() == 1;
+            }
+        });
+    }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/nodes/dao/DefaultNodeInfoDao.java b/util/src/main/java/org/killbill/billing/util/nodes/dao/DefaultNodeInfoDao.java
index f4c3fbb..8d1c264 100644
--- a/util/src/main/java/org/killbill/billing/util/nodes/dao/DefaultNodeInfoDao.java
+++ b/util/src/main/java/org/killbill/billing/util/nodes/dao/DefaultNodeInfoDao.java
@@ -96,4 +96,15 @@ public class DefaultNodeInfoDao implements NodeInfoDao {
         });
     }
 
+    @Override
+    public NodeInfoModelDao getByNodeName(final String nodeName) {
+        return dbi.inTransaction(new TransactionCallback<NodeInfoModelDao>() {
+            @Override
+            public NodeInfoModelDao inTransaction(final Handle handle, final TransactionStatus status) throws Exception {
+                final NodeInfoSqlDao sqlDao = handle.attach(NodeInfoSqlDao.class);
+                return sqlDao.getByNodeName(nodeName);
+            }
+        });
+    }
+
 }
diff --git a/util/src/main/java/org/killbill/billing/util/nodes/dao/NodeInfoDao.java b/util/src/main/java/org/killbill/billing/util/nodes/dao/NodeInfoDao.java
index 0d19523..13ae125 100644
--- a/util/src/main/java/org/killbill/billing/util/nodes/dao/NodeInfoDao.java
+++ b/util/src/main/java/org/killbill/billing/util/nodes/dao/NodeInfoDao.java
@@ -28,4 +28,6 @@ public interface NodeInfoDao {
     public void delete(final String nodeName);
 
     public List<NodeInfoModelDao> getAll();
+
+    public NodeInfoModelDao getByNodeName(final String nodeName);
 }
diff --git a/util/src/main/java/org/killbill/billing/util/nodes/DefaultKillbillNodesApi.java b/util/src/main/java/org/killbill/billing/util/nodes/DefaultKillbillNodesApi.java
index f83f3a1..8e14ee2 100644
--- a/util/src/main/java/org/killbill/billing/util/nodes/DefaultKillbillNodesApi.java
+++ b/util/src/main/java/org/killbill/billing/util/nodes/DefaultKillbillNodesApi.java
@@ -20,29 +20,39 @@ package org.killbill.billing.util.nodes;
 import java.io.IOException;
 import java.util.List;
 
+import org.killbill.CreatorName;
 import org.killbill.billing.broadcast.BroadcastApi;
 import org.killbill.billing.osgi.api.PluginInfo;
+import org.killbill.billing.osgi.api.PluginsInfoApi;
 import org.killbill.billing.util.nodes.dao.NodeInfoDao;
 import org.killbill.billing.util.nodes.dao.NodeInfoModelDao;
 import org.killbill.billing.util.nodes.json.NodeInfoModelJson;
+import org.killbill.billing.util.nodes.json.PluginInfoModelJson;
 import org.killbill.clock.Clock;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.google.common.base.Function;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.inject.Inject;
 
 public class DefaultKillbillNodesApi implements KillbillNodesApi {
 
+    private final Logger logger = LoggerFactory.getLogger(DefaultKillbillNodesApi.class);
+
     private final NodeInfoDao nodeInfoDao;
     private final BroadcastApi broadcastApi;
     private final NodeInfoMapper mapper;
     private final Clock clock;
+    private final PluginsInfoApi pluginInfoApi;
 
     @Inject
-    public DefaultKillbillNodesApi(final NodeInfoDao nodeInfoDao, final BroadcastApi broadcastApi, final NodeInfoMapper mapper, final Clock clock) {
+    public DefaultKillbillNodesApi(final NodeInfoDao nodeInfoDao, final BroadcastApi broadcastApi, final NodeInfoMapper mapper, final Clock clock, final PluginsInfoApi pluginInfoApi) {
         this.nodeInfoDao = nodeInfoDao;
         this.broadcastApi = broadcastApi;
+        this.pluginInfoApi = pluginInfoApi;
         this.clock = clock;
         this.mapper = mapper;
     }
@@ -84,7 +94,41 @@ public class DefaultKillbillNodesApi implements KillbillNodesApi {
     }
 
     @Override
-    public void notifyPluginChanged(final Iterable<PluginInfo> iterable) {
+    public void notifyPluginChanged(final PluginInfo plugin) {
+        final String updatedNodeInfoJson;
+        try {
+            updatedNodeInfoJson = computeLatestNodeInfo();
+            nodeInfoDao.updateNodeInfo(CreatorName.get(), updatedNodeInfoJson);
+        } catch (final IOException e) {
+            logger.warn("Failed to update nodeInfo after plugin change", e);
+        }
+    }
+
+
+    private String computeLatestNodeInfo() throws IOException {
+
+        final NodeInfoModelDao nodeInfo = nodeInfoDao.getByNodeName(CreatorName.get());
+        NodeInfoModelJson nodeInfoJson = mapper.deserializeNodeInfo(nodeInfo.getNodeInfo());
+
+        final Iterable<PluginInfo> rawPluginInfo = pluginInfoApi.getPluginsInfo();
+        final List<PluginInfo> pluginInfo = rawPluginInfo.iterator().hasNext() ? ImmutableList.<PluginInfo>copyOf(rawPluginInfo) : ImmutableList.<PluginInfo>of();
+
+        final NodeInfoModelJson updatedNodeInfoJson = new NodeInfoModelJson(CreatorName.get(),
+                                                                            nodeInfoJson.getBootTime(),
+                                                                            clock.getUTCNow(),
+                                                                            nodeInfoJson.getKillbillVersion(),
+                                                                            nodeInfoJson.getApiVersion(),
+                                                                            nodeInfoJson.getPluginApiVersion(),
+                                                                            nodeInfoJson.getCommonVersion(),
+                                                                            nodeInfoJson.getPlatformVersion(),
+                                                                            ImmutableList.copyOf(Iterables.transform(pluginInfo, new Function<PluginInfo, PluginInfoModelJson>() {
+                                                                                @Override
+                                                                                public PluginInfoModelJson apply(final PluginInfo input) {
+                                                                                    return new PluginInfoModelJson(input);
+                                                                                }
+                                                                            })));
 
+        final String nodeInfoValue = mapper.serializeNodeInfo(updatedNodeInfoJson);
+        return nodeInfoValue;
     }
 }
diff --git a/util/src/main/java/org/killbill/billing/util/nodes/DefaultKillbillNodesService.java b/util/src/main/java/org/killbill/billing/util/nodes/DefaultKillbillNodesService.java
index 0a548df..a5b2a74 100644
--- a/util/src/main/java/org/killbill/billing/util/nodes/DefaultKillbillNodesService.java
+++ b/util/src/main/java/org/killbill/billing/util/nodes/DefaultKillbillNodesService.java
@@ -50,11 +50,13 @@ public class DefaultKillbillNodesService implements KillbillNodesService {
     private final PluginsInfoApi pluginInfoApi;
     private final Clock clock;
     private final NodeInfoMapper mapper;
+    private final KillbillNodesApi nodesApi;
 
     @Inject
-    public DefaultKillbillNodesService(final NodeInfoDao nodeInfoDao, final PluginsInfoApi pluginInfoApi, final Clock clock, final NodeInfoMapper mapper) {
+    public DefaultKillbillNodesService(final NodeInfoDao nodeInfoDao, final PluginsInfoApi pluginInfoApi, final KillbillNodesApi nodesApi, final Clock clock, final NodeInfoMapper mapper) {
         this.nodeInfoDao = nodeInfoDao;
         this.pluginInfoApi = pluginInfoApi;
+        this.nodesApi = nodesApi;
         this.clock = clock;
         this.mapper = mapper;
     }