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;
}