You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by ro...@apache.org on 2017/11/07 09:25:31 UTC
[sling-org-apache-sling-discovery-base] 01/09: SLING-5173 :
introducing discovery.base which is the sharable parts of discovery.impl
for discovery.oak - eg it includes topology connectors and base classes -
plus it also includes many it-kind tests of discovery.impl
This is an automated email from the ASF dual-hosted git repository.
rombert pushed a commit to annotated tag org.apache.sling.discovery.base-1.0.0
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-discovery-base.git
commit 976d955d1b9d7678e19ff3cddd1b0c7d90b60322
Author: Stefan Egli <st...@apache.org>
AuthorDate: Tue Oct 20 14:12:31 2015 +0000
SLING-5173 : introducing discovery.base which is the sharable parts of discovery.impl for discovery.oak - eg it includes topology connectors and base classes - plus it also includes many it-kind tests of discovery.impl
git-svn-id: https://svn.apache.org/repos/asf/sling/trunk/bundles/extensions/discovery/base@1709601 13f79535-47bb-0310-9956-ffa450edef68
---
pom.xml | 295 ++++
.../base/commons/BaseDiscoveryService.java | 104 ++
.../discovery/base/commons/BaseViewChecker.java | 335 +++++
.../discovery/base/commons/ClusterViewHelper.java | 55 +
.../discovery/base/commons/ClusterViewService.java | 47 +
.../base/commons/DefaultTopologyView.java | 252 ++++
.../commons/UndefinedClusterViewException.java | 64 +
.../sling/discovery/base/commons/ViewChecker.java | 40 +
.../sling/discovery/base/commons/package-info.java | 30 +
.../discovery/base/connectors/BaseConfig.java | 129 ++
.../base/connectors/announcement/Announcement.java | 460 ++++++
.../announcement/AnnouncementFilter.java | 32 +
.../announcement/AnnouncementRegistry.java | 66 +
.../announcement/AnnouncementRegistryImpl.java | 593 ++++++++
.../announcement/CachedAnnouncement.java | 122 ++
.../base/connectors/announcement/package-info.java | 30 +
.../discovery/base/connectors/package-info.java | 30 +
.../base/connectors/ping/ConnectorRegistry.java | 44 +
.../connectors/ping/ConnectorRegistryImpl.java | 157 ++
.../connectors/ping/TopologyConnectorClient.java | 496 +++++++
.../ping/TopologyConnectorClientInformation.java | 63 +
.../connectors/ping/TopologyConnectorServlet.java | 360 +++++
.../connectors/ping/TopologyRequestValidator.java | 585 ++++++++
.../base/connectors/ping/package-info.java | 30 +
.../connectors/ping/wl/SubnetWhitelistEntry.java | 47 +
.../base/connectors/ping/wl/WhitelistEntry.java | 35 +
.../base/connectors/ping/wl/WildcardHelper.java | 52 +
.../connectors/ping/wl/WildcardWhitelistEntry.java | 45 +
.../base/connectors/ping/wl/package-info.java | 30 +
.../base/commons/DefaultTopologyViewTest.java | 209 +++
.../base/commons/DummyDiscoveryService.java | 71 +
.../connectors/DummyVirtualInstanceBuilder.java | 85 ++
.../base/connectors/LargeTopologyWithHubTest.java | 123 ++
.../announcement/AnnouncementRegistryImplTest.java | 391 +++++
.../connectors/ping/ConnectorRegistryImplTest.java | 118 ++
.../ping/TopologyConnectorServletTest.java | 115 ++
.../ping/TopologyRequestValidatorTest.java | 180 +++
.../connectors/ping/wl/WildcardHelperTest.java | 117 ++
.../base/its/AbstractClusterLoadTest.java | 287 ++++
.../discovery/base/its/AbstractClusterTest.java | 1540 ++++++++++++++++++++
.../base/its/AbstractSingleInstanceTest.java | 293 ++++
.../base/its/AbstractTopologyEventTest.java | 252 ++++
.../sling/discovery/base/its/TopologyTest.java | 150 ++
.../base/its/setup/ModifiableTestBaseConfig.java | 37 +
.../sling/discovery/base/its/setup/OSGiMock.java | 124 ++
.../discovery/base/its/setup/TopologyHelper.java | 145 ++
.../discovery/base/its/setup/VirtualConnector.java | 39 +
.../discovery/base/its/setup/VirtualInstance.java | 374 +++++
.../base/its/setup/VirtualInstanceBuilder.java | 238 +++
.../base/its/setup/VirtualInstanceHelper.java | 88 ++
.../base/its/setup/VirtualRepository.java | 27 +
.../base/its/setup/WithholdingAppender.java | 82 ++
.../base/its/setup/mock/AcceptsMultiple.java | 65 +
.../setup/mock/AcceptsParticularTopologyEvent.java | 50 +
.../setup/mock/AssertingTopologyEventListener.java | 160 ++
.../base/its/setup/mock/DummyViewChecker.java | 48 +
.../base/its/setup/mock/FailingScheduler.java | 72 +
.../discovery/base/its/setup/mock/MockFactory.java | 126 ++
.../base/its/setup/mock/MockedResource.java | 297 ++++
.../its/setup/mock/MockedResourceResolver.java | 335 +++++
.../base/its/setup/mock/PropertyProviderImpl.java | 58 +
.../its/setup/mock/SimpleClusterViewService.java | 59 +
.../base/its/setup/mock/SimpleConnectorConfig.java | 215 +++
.../base/its/setup/mock/TopologyEventAsserter.java | 25 +
src/test/resources/log4j.properties | 26 +
65 files changed, 11249 insertions(+)
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..764b776
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,295 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF 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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>sling</artifactId>
+ <version>25</version>
+ <relativePath />
+ </parent>
+
+ <artifactId>org.apache.sling.discovery.base</artifactId>
+ <packaging>bundle</packaging>
+ <version>1.0.0-SNAPSHOT</version>
+
+ <name>Apache Sling Discovery Base</name>
+ <description>Contains Connector and Properties support that some implementations might choose to build upon</description>
+
+ <scm>
+ <connection>scm:svn:http://svn.apache.org/repos/asf/sling/trunk/bundles/extensions/discovery/base</connection>
+ <developerConnection>scm:svn:https://svn.apache.org/repos/asf/sling/trunk/bundles/extensions/discovery/base</developerConnection>
+ <url>http://svn.apache.org/viewvc/sling/trunk/bundles/extensions/discovery/base</url>
+ </scm>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <configuration>
+ <redirectTestOutputToFile>false</redirectTestOutputToFile>
+ <argLine>-Xmx2048m</argLine>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>maven-scr-plugin</artifactId>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>maven-bundle-plugin</artifactId>
+ <extensions>true</extensions>
+ <configuration>
+ <instructions>
+ <Embed-Dependency>
+ commons-net;inline=org/apache/commons/net/util/SubnetUtils*
+ </Embed-Dependency>
+ </instructions>
+ </configuration>
+ </plugin>
+ <!-- discovery.base exports a few test classes for reuse.
+ In order for others to use these, the test-jar must be built/installed too.
+ Note that 'mvn -Dmaven.test.skip=true' does NOT build the test-jar,
+ however 'mvn -DskipTests' does. -->
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-jar-plugin</artifactId>
+ <executions>
+ <execution>
+ <goals>
+ <goal>test-jar</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>org.apache.felix.scr.annotations</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>biz.aQute</groupId>
+ <artifactId>bndlib</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ <version>1.6.1</version>
+ </dependency>
+ <dependency>
+ <groupId>org.osgi</groupId>
+ <artifactId>org.osgi.core</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.osgi</groupId>
+ <artifactId>org.osgi.compendium</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>org.apache.sling.jcr.api</artifactId>
+ <version>2.1.0</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>javax.jcr</groupId>
+ <artifactId>jcr</artifactId>
+ <version>2.0</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.jackrabbit</groupId>
+ <artifactId>jackrabbit-api</artifactId>
+ <version>2.2.4</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>org.apache.sling.commons.osgi</artifactId>
+ <version>2.1.0</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>org.apache.sling.settings</artifactId>
+ <version>1.2.2</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>org.apache.sling.discovery.api</artifactId>
+ <version>1.0.0</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>org.apache.sling.discovery.commons</artifactId>
+ <version>1.0.0-SNAPSHOT</version>
+ </dependency>
+ <!-- besides including discovery.commons' normal jar above,
+ for testing a few test helper classes are also reused.
+ in order to achieve that, also adding a test/test-jar dependency: -->
+ <dependency>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>org.apache.sling.discovery.commons</artifactId>
+ <version>1.0.0-SNAPSHOT</version>
+ <scope>test</scope>
+ <type>test-jar</type>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>org.apache.sling.api</artifactId>
+ <version>2.4.0</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>org.apache.sling.commons.scheduler</artifactId>
+ <version>2.3.4</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>org.apache.felix.webconsole</artifactId>
+ <version>3.0.0</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-servlet</artifactId>
+ <version>8.1.2.v20120308</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>javax.servlet</groupId>
+ <artifactId>servlet-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.httpcomponents</groupId>
+ <artifactId>httpclient-osgi</artifactId>
+ <version>4.3.5</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>org.apache.sling.commons.json</artifactId>
+ <version>2.0.6</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>org.apache.sling.launchpad.api</artifactId>
+ <version>1.1.0</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>commons-io</groupId>
+ <artifactId>commons-io</artifactId>
+ <version>2.4</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>commons-net</groupId>
+ <artifactId>commons-net</artifactId>
+ <version>3.3</version>
+ <scope>provided</scope>
+ </dependency>
+ <!-- Testing -->
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>junit-addons</groupId>
+ <artifactId>junit-addons</artifactId>
+ <version>1.4</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.jmock</groupId>
+ <artifactId>jmock-junit4</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-all</artifactId>
+ <version>1.9.5</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>org.apache.sling.commons.testing</artifactId>
+ <version>2.0.16</version>
+ <scope>test</scope>
+ <exclusions>
+ <!-- slf4j simple implementation logs INFO + higher to stdout (we don't want that behaviour) -->
+ <exclusion>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-simple</artifactId>
+ </exclusion>
+ <!-- also excluding jcl-over-slf4j as we need a newer vesion of this which is compatible with slf4j 1.6 -->
+ <exclusion>
+ <groupId>org.slf4j</groupId>
+ <artifactId>jcl-over-slf4j</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+
+ <!-- using log4j under slf4j to allow fine-grained logging config (see src/test/resources/log4j.properties) -->
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-log4j12</artifactId>
+ <version>1.7.5</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>org.apache.sling.jcr.resource</artifactId>
+ <version>2.3.8</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>log4j</groupId>
+ <artifactId>log4j</artifactId>
+ <version>1.2.13</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>org.apache.sling.commons.threads</artifactId>
+ <version>3.1.0</version>
+ <type>bundle</type>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>org.apache.sling.testing.tools</artifactId>
+ <version>1.0.2</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>org.apache.sling.testing.sling-mock</artifactId>
+ <version>1.2.0</version>
+ </dependency>
+ </dependencies>
+</project>
diff --git a/src/main/java/org/apache/sling/discovery/base/commons/BaseDiscoveryService.java b/src/main/java/org/apache/sling/discovery/base/commons/BaseDiscoveryService.java
new file mode 100644
index 0000000..cf9563d
--- /dev/null
+++ b/src/main/java/org/apache/sling/discovery/base/commons/BaseDiscoveryService.java
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.sling.discovery.base.commons;
+
+import java.util.Collection;
+
+import org.apache.sling.discovery.DiscoveryService;
+import org.apache.sling.discovery.InstanceDescription;
+import org.apache.sling.discovery.TopologyView;
+import org.apache.sling.discovery.base.commons.UndefinedClusterViewException.Reason;
+import org.apache.sling.discovery.base.connectors.announcement.AnnouncementRegistry;
+import org.apache.sling.discovery.commons.providers.spi.LocalClusterView;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Abstract base class for DiscoveryService implementations which uses the
+ * ClusterViewService plus Topology Connectors to calculate
+ * the current TopologyView
+ */
+public abstract class BaseDiscoveryService implements DiscoveryService {
+
+ private final static Logger logger = LoggerFactory.getLogger(BaseDiscoveryService.class);
+
+ /** the old view previously valid and sent to the TopologyEventListeners **/
+ private DefaultTopologyView oldView;
+
+ protected abstract ClusterViewService getClusterViewService();
+
+ protected abstract AnnouncementRegistry getAnnouncementRegistry();
+
+ protected abstract void handleIsolatedFromTopology();
+
+ public abstract void updateProperties();
+
+ public abstract void handlePotentialTopologyChange();
+
+ public abstract void handleTopologyChanging();
+
+ protected DefaultTopologyView getOldView() {
+ return oldView;
+ }
+
+ protected void setOldView(DefaultTopologyView view) {
+ if (view==null) {
+ throw new IllegalArgumentException("view must not be null");
+ }
+ logger.debug("setOldView: oldView is now: {}", oldView);
+ oldView = view;
+ }
+
+ /**
+ * @see DiscoveryService#getTopology()
+ */
+ public TopologyView getTopology() {
+ ClusterViewService clusterViewService = getClusterViewService();
+ if (clusterViewService == null) {
+ throw new IllegalStateException(
+ "DiscoveryService not yet initialized with IClusterViewService");
+ }
+ // create a new topology view
+ final DefaultTopologyView topology = new DefaultTopologyView();
+
+ LocalClusterView localClusterView = null;
+ try {
+ localClusterView = clusterViewService.getLocalClusterView();
+ topology.setLocalClusterView(localClusterView);
+ } catch (UndefinedClusterViewException e) {
+ // SLING-5030 : when we're cut off from the local cluster we also
+ // treat it as being cut off from the entire topology, ie we don't
+ // update the announcements but just return
+ // the previous oldView marked as !current
+ logger.info("getTopology: undefined cluster view: "+e.getReason()+"] "+e);
+ oldView.setNotCurrent();
+ if (e.getReason()==Reason.ISOLATED_FROM_TOPOLOGY) {
+ handleIsolatedFromTopology();
+ }
+ return oldView;
+ }
+
+ Collection<InstanceDescription> attachedInstances = getAnnouncementRegistry()
+ .listInstances(localClusterView);
+ topology.addInstances(attachedInstances);
+
+ return topology;
+ }
+
+}
diff --git a/src/main/java/org/apache/sling/discovery/base/commons/BaseViewChecker.java b/src/main/java/org/apache/sling/discovery/base/commons/BaseViewChecker.java
new file mode 100644
index 0000000..516e8ec
--- /dev/null
+++ b/src/main/java/org/apache/sling/discovery/base/commons/BaseViewChecker.java
@@ -0,0 +1,335 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.sling.discovery.base.commons;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Deactivate;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.sling.api.resource.ResourceResolverFactory;
+import org.apache.sling.commons.scheduler.Scheduler;
+import org.apache.sling.discovery.base.connectors.BaseConfig;
+import org.apache.sling.discovery.base.connectors.announcement.AnnouncementRegistry;
+import org.apache.sling.discovery.base.connectors.ping.ConnectorRegistry;
+import org.apache.sling.launchpad.api.StartupListener;
+import org.apache.sling.launchpad.api.StartupMode;
+import org.apache.sling.settings.SlingSettingsService;
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.component.ComponentContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The heartbeat handler is responsible and capable of issuing both local and
+ * remote heartbeats and registers a periodic job with the scheduler for doing so.
+ * <p>
+ * Local heartbeats are stored in the repository. Remote heartbeats are POSTs to
+ * remote TopologyConnectorServlets.
+ */
+public abstract class BaseViewChecker implements ViewChecker, Runnable, StartupListener {
+
+ protected final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+ /** Endpoint service registration property from RFC 189 */
+ private static final String REG_PROPERTY_ENDPOINTS = "osgi.http.service.endpoints";
+
+ protected static final String PROPERTY_ID_ENDPOINTS = "endpoints";
+
+ protected static final String PROPERTY_ID_SLING_HOME_PATH = "slingHomePath";
+
+ protected static final String PROPERTY_ID_RUNTIME = "runtimeId";
+
+ /** the name used for the period job with the scheduler **/
+ protected String NAME = "discovery.impl.heartbeat.runner.";
+
+ @Reference
+ protected SlingSettingsService slingSettingsService;
+
+ @Reference
+ protected ResourceResolverFactory resourceResolverFactory;
+
+ @Reference
+ protected ConnectorRegistry connectorRegistry;
+
+ @Reference
+ protected AnnouncementRegistry announcementRegistry;
+
+ @Reference
+ protected Scheduler scheduler;
+
+ @Reference
+ protected BaseConfig connectorConfig;
+
+ /** the discovery service reference is used to get properties updated before heartbeats are sent **/
+ protected BaseDiscoveryService discoveryService;
+
+ /** the sling id of the local instance **/
+ protected String slingId;
+
+ /** SLING-2901: the runtimeId is a unique id, set on activation, used for robust duplicate sling.id detection **/
+ protected String runtimeId;
+
+ /** lock object for synchronizing the run method **/
+ protected final Object lock = new Object();
+
+ /** SLING-2895: avoid heartbeats after deactivation **/
+ protected volatile boolean activated = false;
+
+ /** keep a reference to the component context **/
+ protected ComponentContext context;
+
+ /** SLING-2968 : start issuing remote heartbeats only after startup finished **/
+ protected boolean startupFinished = false;
+
+ /** SLING-3382 : force ping instructs the servlet to start the backoff from scratch again **/
+ private boolean forcePing;
+
+ /** SLING-4765 : store endpoints to /clusterInstances for more verbose duplicate slingId/ghost detection **/
+ protected final Map<Long, String[]> endpoints = new HashMap<Long, String[]>();
+
+ public void inform(StartupMode mode, boolean finished) {
+ if (finished) {
+ startupFinished(mode);
+ }
+ }
+
+ public void startupFinished(StartupMode mode) {
+ synchronized(lock) {
+ startupFinished = true;
+ issueHeartbeat();
+ }
+ }
+
+ public void startupProgress(float ratio) {
+ // we dont care
+ }
+
+ @Activate
+ protected void activate(ComponentContext context) {
+ synchronized(lock) {
+ this.context = context;
+
+ slingId = slingSettingsService.getSlingId();
+ NAME = "discovery.connectors.common.runner." + slingId;
+
+ doActivate();
+ activated = true;
+ }
+ }
+
+ protected void doActivate() {
+ try {
+ final long interval = connectorConfig.getConnectorPingInterval();
+ logger.info("doActivate: starting periodic connectorPing job for "+slingId+" with interval "+interval+" sec.");
+ scheduler.addPeriodicJob(NAME, this,
+ null, interval, false);
+ } catch (Exception e) {
+ logger.error("doActivate: Could not start connectorPing runner: " + e, e);
+ }
+ logger.info("doActivate: activated with slingId: {}, this: {}", slingId, this);
+ }
+
+ @Deactivate
+ protected void deactivate() {
+ // SLING-3365 : dont synchronize on deactivate
+ activated = false;
+ logger.info("deactivate: deactivated slingId: {}, this: {}", slingId, this);
+ scheduler.removeJob(NAME);
+ }
+
+ /** for testing only **/
+ @Override
+ public void checkView() {
+ synchronized(lock) {
+ doCheckView();
+ }
+ }
+
+ public void run() {
+ heartbeatAndCheckView();
+ }
+
+ @Override
+ public void heartbeatAndCheckView() {
+ logger.info("run: start. [ConnectorPinger of slingId="+slingId+"]");
+ synchronized(lock) {
+ if (!activated) {
+ // SLING:2895: avoid heartbeats if not activated
+ logger.debug("run: not activated yet");
+ return;
+ }
+
+ // issue a heartbeat
+ issueHeartbeat();
+
+ // check the view
+ doCheckView();
+ }
+ logger.info("run: end. [ConnectorPinger of slingId="+slingId+"]");
+ }
+
+ /** Trigger the issuance of the next heartbeat asap instead of at next heartbeat interval **/
+ public void triggerAsyncConnectorPing() {
+ forcePing = true;
+ try {
+ // then fire a job immediately
+ // use 'fireJobAt' here, instead of 'fireJob' to make sure the job can always be triggered
+ // 'fireJob' checks for a job from the same job-class to already exist
+ // 'fireJobAt' though allows to pass a name for the job - which can be made unique, thus does not conflict/already-exist
+ logger.info("triggerConnectorPing: firing job to trigger heartbeat");
+ scheduler.fireJobAt(NAME+UUID.randomUUID(), this, null, new Date(System.currentTimeMillis()-1000 /* make sure it gets triggered immediately*/));
+ } catch (Exception e) {
+ logger.info("triggerConnectorPing: Could not trigger heartbeat: " + e);
+ }
+ }
+
+ /**
+ * Issue a heartbeat.
+ * <p>
+ * This action consists of first updating the local properties,
+ * then issuing a cluster-local heartbeat (within the repository)
+ * and then a remote heartbeat (to all the topology connectors
+ * which announce this part of the topology to others)
+ */
+ protected void issueHeartbeat() {
+ if (discoveryService == null) {
+ logger.error("issueHeartbeat: discoveryService is null");
+ } else {
+ discoveryService.updateProperties();
+ }
+// issueClusterLocalHeartbeat();
+ issueConnectorPings();
+ }
+
+ /** Issue a remote heartbeat using the topology connectors **/
+ protected void issueConnectorPings() {
+ if (connectorRegistry == null) {
+ logger.error("issueConnectorPings: connectorRegistry is null");
+ return;
+ }
+ if (!startupFinished) {
+ logger.debug("issueConnectorPings: not issuing remote heartbeat yet, startup not yet finished");
+ return;
+ }
+ if (logger.isDebugEnabled()) {
+ logger.debug("issueConnectorPings: pinging outgoing topology connectors (if there is any) for "+slingId);
+ }
+ connectorRegistry.pingOutgoingConnectors(forcePing);
+ forcePing = false;
+ }
+
+ /** Check whether the established view matches the reality, ie matches the
+ * heartbeats
+ */
+ protected void doCheckView() {
+ // check the remotes first
+ if (announcementRegistry == null) {
+ logger.error("announcementRegistry is null");
+ return;
+ }
+ announcementRegistry.checkExpiredAnnouncements();
+ }
+
+ /**
+ * Bind a http service
+ */
+ protected void bindHttpService(final ServiceReference reference) {
+ final String[] endpointUrls = toStringArray(reference.getProperty(REG_PROPERTY_ENDPOINTS));
+ if ( endpointUrls != null ) {
+ synchronized ( lock ) {
+ this.endpoints.put((Long)reference.getProperty(Constants.SERVICE_ID), endpointUrls);
+ }
+ }
+ }
+
+ /**
+ * Unbind a http service
+ */
+ protected void unbindHttpService(final ServiceReference reference) {
+ synchronized ( lock ) {
+ if ( this.endpoints.remove(reference.getProperty(Constants.SERVICE_ID)) != null ) {
+ }
+ }
+ }
+
+ private String[] toStringArray(final Object propValue) {
+ if (propValue == null) {
+ // no value at all
+ return null;
+
+ } else if (propValue instanceof String) {
+ // single string
+ return new String[] { (String) propValue };
+
+ } else if (propValue instanceof String[]) {
+ // String[]
+ return (String[]) propValue;
+
+ } else if (propValue.getClass().isArray()) {
+ // other array
+ Object[] valueArray = (Object[]) propValue;
+ List<String> values = new ArrayList<String>(valueArray.length);
+ for (Object value : valueArray) {
+ if (value != null) {
+ values.add(value.toString());
+ }
+ }
+ return values.toArray(new String[values.size()]);
+
+ } else if (propValue instanceof Collection<?>) {
+ // collection
+ Collection<?> valueCollection = (Collection<?>) propValue;
+ List<String> valueList = new ArrayList<String>(valueCollection.size());
+ for (Object value : valueCollection) {
+ if (value != null) {
+ valueList.add(value.toString());
+ }
+ }
+ return valueList.toArray(new String[valueList.size()]);
+ }
+
+ return null;
+ }
+
+ protected String getEndpointsAsString() {
+ final StringBuilder sb = new StringBuilder();
+ boolean first = true;
+ for(final String[] points : endpoints.values()) {
+ for(final String point : points) {
+ if ( first ) {
+ first = false;
+ } else {
+ sb.append(",");
+ }
+ sb.append(point);
+ }
+ }
+ return sb.toString();
+
+ }
+
+}
diff --git a/src/main/java/org/apache/sling/discovery/base/commons/ClusterViewHelper.java b/src/main/java/org/apache/sling/discovery/base/commons/ClusterViewHelper.java
new file mode 100644
index 0000000..26e4f28
--- /dev/null
+++ b/src/main/java/org/apache/sling/discovery/base/commons/ClusterViewHelper.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.sling.discovery.base.commons;
+
+import java.util.Collection;
+
+import org.apache.sling.discovery.ClusterView;
+import org.apache.sling.discovery.InstanceDescription;
+
+/**
+ * Contains some static helper methods around ClusterView
+ */
+public class ClusterViewHelper {
+
+ /** checks whether the cluster view contains a particular sling id **/
+ public static boolean contains(ClusterView clusterView, String slingId) throws UndefinedClusterViewException {
+ InstanceDescription found = null;
+ for (InstanceDescription i : clusterView.getInstances()) {
+ if (i.getSlingId().equals(slingId)) {
+ if (found!=null) {
+ throw new IllegalStateException("multiple instances with slingId found: "+slingId);
+ }
+ found = i;
+ }
+ }
+ return found!=null;
+ }
+
+ /** checks whether the cluster contains any of the provided instances **/
+ public static boolean containsAny(ClusterView clusterView, Collection<InstanceDescription> listInstances)
+ throws UndefinedClusterViewException {
+ for (InstanceDescription i : listInstances) {
+ if (contains(clusterView, i.getSlingId())) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/src/main/java/org/apache/sling/discovery/base/commons/ClusterViewService.java b/src/main/java/org/apache/sling/discovery/base/commons/ClusterViewService.java
new file mode 100644
index 0000000..4552e81
--- /dev/null
+++ b/src/main/java/org/apache/sling/discovery/base/commons/ClusterViewService.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.sling.discovery.base.commons;
+
+import org.apache.sling.discovery.commons.providers.spi.LocalClusterView;
+
+/**
+ * The ClusterViewService is capable of determining the
+ * ClusterView of the local cluster (ie of the instances
+ * that are all hooked to the same underlying repository).
+ */
+public interface ClusterViewService {
+
+ /** the sling id of the local instance **/
+ String getSlingId();
+
+ /**
+ * Returns the current, local cluster view - throwing an
+ * UndefinedClusterViewException if it cannot determine
+ * a clusterView at the moment.
+ * @return the current cluster view - never returns null
+ * (it rather throws an UndefinedClusterViewException that
+ * contains more details about why exactly the clusterView
+ * is undefined at the moment)
+ * @throws UndefinedClusterViewException thrown when
+ * the ClusterView cannot be determined at the moment
+ * (also contains more details as to why exactly)
+ */
+ LocalClusterView getLocalClusterView() throws UndefinedClusterViewException;
+
+}
diff --git a/src/main/java/org/apache/sling/discovery/base/commons/DefaultTopologyView.java b/src/main/java/org/apache/sling/discovery/base/commons/DefaultTopologyView.java
new file mode 100644
index 0000000..bc97d79
--- /dev/null
+++ b/src/main/java/org/apache/sling/discovery/base/commons/DefaultTopologyView.java
@@ -0,0 +1,252 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.sling.discovery.base.commons;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.sling.discovery.ClusterView;
+import org.apache.sling.discovery.InstanceDescription;
+import org.apache.sling.discovery.InstanceFilter;
+import org.apache.sling.discovery.TopologyEvent.Type;
+import org.apache.sling.discovery.commons.providers.BaseTopologyView;
+import org.apache.sling.discovery.commons.providers.spi.LocalClusterView;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Default Implementation of the topology view interface
+ */
+public class DefaultTopologyView extends BaseTopologyView {
+
+ private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+ /** the instances that are part of this topology **/
+ private final Set<InstanceDescription> instances = new HashSet<InstanceDescription>();
+
+ private String localClusterSyncTokenId;
+
+ /** Create a new empty topology **/
+ public DefaultTopologyView() {
+ // nothing to be initialized then
+ }
+
+ /** Create a new topology filled with the given list of instances **/
+ public DefaultTopologyView(final Collection<InstanceDescription> instances) {
+ if (instances != null) {
+ this.instances.addAll(instances);
+ }
+ }
+
+ /**
+ * Compare this topology with the given one and determine how they compare
+ * @param other the other topology against which to compare
+ * @return the type describing how these two compare
+ * @see Type
+ */
+ public Type compareTopology(final DefaultTopologyView other) {
+ if (other == null) {
+ throw new IllegalArgumentException("other must not be null");
+ }
+ if (this.instances.size() != other.instances.size()) {
+ logger.debug("compareTopology: different number of instances");
+ return Type.TOPOLOGY_CHANGED;
+ }
+ boolean propertiesChanged = false;
+ for(final InstanceDescription instance : this.instances) {
+
+ final Iterator<InstanceDescription> it2 = other.instances.iterator();
+ InstanceDescription matchingInstance = null;
+ while (it2.hasNext()) {
+ final InstanceDescription otherInstance = it2.next();
+ if (instance.getSlingId().equals(otherInstance.getSlingId())) {
+ matchingInstance = otherInstance;
+ break;
+ }
+ }
+ if (matchingInstance == null) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("compareTopology: no matching instance found for {}", instance);
+ }
+ return Type.TOPOLOGY_CHANGED;
+ }
+ if (!instance.getClusterView().getId()
+ .equals(matchingInstance.getClusterView().getId())) {
+ logger.debug("compareTopology: cluster view id does not match");
+ return Type.TOPOLOGY_CHANGED;
+ }
+ if (!instance.isLeader()==matchingInstance.isLeader()) {
+ logger.debug("compareTopology: leaders differ");
+ return Type.TOPOLOGY_CHANGED;
+ }
+ if (!instance.getProperties().equals(
+ matchingInstance.getProperties())) {
+ propertiesChanged = true;
+ }
+ }
+ if (propertiesChanged) {
+ return Type.PROPERTIES_CHANGED;
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (obj == null || !(obj instanceof DefaultTopologyView)) {
+ return false;
+ }
+ DefaultTopologyView other = (DefaultTopologyView) obj;
+ if (this.isCurrent() != other.isCurrent()) {
+ return false;
+ }
+ Type diff = compareTopology(other);
+ return diff == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int code = 0;
+ for (Iterator<InstanceDescription> it = instances.iterator(); it
+ .hasNext();) {
+ InstanceDescription instance = it.next();
+ code += instance.hashCode();
+ }
+ return code;
+ }
+
+ /**
+ * @see org.apache.sling.discovery.TopologyView#getLocalInstance()
+ */
+ public InstanceDescription getLocalInstance() {
+ for (Iterator<InstanceDescription> it = instances.iterator(); it
+ .hasNext();) {
+ InstanceDescription instance = it.next();
+ if (instance.isLocal()) {
+ return instance;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * @see org.apache.sling.discovery.TopologyView#getInstances()
+ */
+ public Set<InstanceDescription> getInstances() {
+ return Collections.unmodifiableSet(instances);
+ }
+
+ /**
+ * Adds the instances of the given ClusterView to this topology
+ */
+ public void setLocalClusterView(final LocalClusterView localClusterView) {
+ if (localClusterView == null) {
+ throw new IllegalArgumentException("localClusterView must not be null");
+ }
+ final List<InstanceDescription> instances = localClusterView.getInstances();
+ addInstances(instances);
+
+ this.localClusterSyncTokenId = localClusterView.getLocalClusterSyncTokenId();
+ }
+
+ /**
+ * Adds the given instances to this topology
+ */
+ public void addInstances(final Collection<InstanceDescription> instances) {
+ if (instances == null) {
+ return;
+ }
+ outerLoop: for (Iterator<InstanceDescription> it = instances.iterator(); it
+ .hasNext();) {
+ InstanceDescription instanceDescription = it.next();
+ for (Iterator<InstanceDescription> it2 = this.instances.iterator(); it2.hasNext();) {
+ InstanceDescription existingInstance = it2.next();
+ if (existingInstance.getSlingId().equals(instanceDescription.getSlingId())) {
+ // SLING-3726:
+ // while 'normal duplicate instances' are filtered out here correctly,
+ // 'hidden duplicate instances' that are added via this instanceDescription's
+ // cluster, are not caught.
+ // there is, however, no simple fix for this. Since the reason is
+ // inconsistent state information in /var/discovery/impl - either
+ // due to stale-announcements (SLING-4139) - or by some manualy
+ // copying of data from one cluster to the next (which will also
+ // be cleaned up by SLING-4139 though)
+ // so the fix for avoiding duplicate instances is really SLING-4139
+ logger.info("addInstance: cannot add same instance twice: "
+ + instanceDescription);
+ continue outerLoop;
+ }
+ }
+ this.instances.add(instanceDescription);
+ }
+ }
+
+ /**
+ * @see org.apache.sling.discovery.TopologyView#findInstances(org.apache.sling.discovery.InstanceFilter)
+ */
+ public Set<InstanceDescription> findInstances(final InstanceFilter picker) {
+ if (picker == null) {
+ throw new IllegalArgumentException("picker must not be null");
+ }
+ Set<InstanceDescription> result = new HashSet<InstanceDescription>();
+ for (Iterator<InstanceDescription> it = instances.iterator(); it
+ .hasNext();) {
+ InstanceDescription instance = it.next();
+ if (picker.accept(instance)) {
+ result.add(instance);
+ }
+ }
+ return result;
+ }
+
+ /**
+ * @see org.apache.sling.discovery.TopologyView#getClusterViews()
+ */
+ public Set<ClusterView> getClusterViews() {
+ Set<ClusterView> result = new HashSet<ClusterView>();
+ for (Iterator<InstanceDescription> it = instances.iterator(); it
+ .hasNext();) {
+ InstanceDescription instance = it.next();
+ ClusterView cluster = instance.getClusterView();
+ if (cluster != null) {
+ result.add(cluster);
+ }
+ }
+ return new HashSet<ClusterView>(result);
+ }
+
+ @Override
+ public String toString() {
+ return "TopologyViewImpl [current=" + isCurrent() + ", num=" + instances.size() + ", instances="
+ + instances + "]";
+ }
+
+ @Override
+ public String getLocalClusterSyncTokenId() {
+ if (localClusterSyncTokenId==null) {
+ throw new IllegalStateException("no syncToken set");
+ } else {
+ return localClusterSyncTokenId;
+ }
+ }
+}
diff --git a/src/main/java/org/apache/sling/discovery/base/commons/UndefinedClusterViewException.java b/src/main/java/org/apache/sling/discovery/base/commons/UndefinedClusterViewException.java
new file mode 100644
index 0000000..9a300aa
--- /dev/null
+++ b/src/main/java/org/apache/sling/discovery/base/commons/UndefinedClusterViewException.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.sling.discovery.base.commons;
+
+/**
+ * This exception is thrown when the ClusterViewService
+ * does not have a cluster view that is valid.
+ * That can either be because it cannot access the repository
+ * (login or other repository exception) or that there is
+ * no established view yet at all (not yet voted case) -
+ * or that there is an established view but it doesn't include
+ * the local instance (isolated case)
+ */
+@SuppressWarnings("serial")
+public class UndefinedClusterViewException extends Exception {
+
+ public static enum Reason {
+ /** used when the local instance is isolated from the topology
+ * (which is noticed by an established view that does not include
+ * the local instance)
+ */
+ ISOLATED_FROM_TOPOLOGY,
+
+ /** used when there is no established view yet
+ * (happens on a fresh installation)
+ */
+ NO_ESTABLISHED_VIEW,
+
+ /** used when we couldn't reach the repository **/
+ REPOSITORY_EXCEPTION
+ }
+
+ private final Reason reason;
+
+ public UndefinedClusterViewException(Reason reason) {
+ super();
+ this.reason = reason;
+ }
+
+ public UndefinedClusterViewException(Reason reason, String msg) {
+ super(msg);
+ this.reason = reason;
+ }
+
+ public Reason getReason() {
+ return reason;
+ }
+}
diff --git a/src/main/java/org/apache/sling/discovery/base/commons/ViewChecker.java b/src/main/java/org/apache/sling/discovery/base/commons/ViewChecker.java
new file mode 100644
index 0000000..47e123e
--- /dev/null
+++ b/src/main/java/org/apache/sling/discovery/base/commons/ViewChecker.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.sling.discovery.base.commons;
+
+/**
+ * A ViewChecker is capable of issuing a heartbeat and
+ * subsequently checking the resulting, current view
+ * in the local cluster.
+ * <p>
+ * This is mostly used as hooks for testing
+ */
+public interface ViewChecker {
+
+ /**
+ * Check the view (without issuing a heartbeat)
+ */
+ public void checkView();
+
+ /**
+ * Issue a heartbeat and check the view
+ */
+ public void heartbeatAndCheckView();
+
+}
diff --git a/src/main/java/org/apache/sling/discovery/base/commons/package-info.java b/src/main/java/org/apache/sling/discovery/base/commons/package-info.java
new file mode 100644
index 0000000..7923ff4
--- /dev/null
+++ b/src/main/java/org/apache/sling/discovery/base/commons/package-info.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.
+ */
+
+/**
+ * Provides some common classes for discovery implementors that
+ * choose to extend from discovery.base
+ *
+ * @version 1.0.0
+ */
+@Version("1.0.0")
+package org.apache.sling.discovery.base.commons;
+
+import aQute.bnd.annotation.Version;
+
diff --git a/src/main/java/org/apache/sling/discovery/base/connectors/BaseConfig.java b/src/main/java/org/apache/sling/discovery/base/connectors/BaseConfig.java
new file mode 100644
index 0000000..fdc0435
--- /dev/null
+++ b/src/main/java/org/apache/sling/discovery/base/connectors/BaseConfig.java
@@ -0,0 +1,129 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.sling.discovery.base.connectors;
+
+import java.net.URL;
+
+/**
+ * Configuration for discovery.base
+ */
+public interface BaseConfig {
+
+ /**
+ * Returns the socket connect() timeout used by the topology connector, 0 disables the timeout
+ * @return the socket connect() timeout used by the topology connector, 0 disables the timeout
+ */
+ public int getSocketConnectionTimeout();
+
+ /**
+ * Returns the socket read timeout (SO_TIMEOUT) used by the topology connector, 0 disables the timeout
+ * @return the socket read timeout (SO_TIMEOUT) used by the topology connector, 0 disables the timeout
+ */
+ public int getSoTimeout();
+
+ /**
+ * Returns the URLs to which to open a topology connector - or null/empty if no topology connector
+ * is configured (default is null)
+ * @return the URLs to which to open a topology connector - or null/empty if no topology connector
+ * is configured
+ */
+ public URL[] getTopologyConnectorURLs();
+
+ /**
+ * Returns a comma separated list of hostnames and/or ip addresses which are allowed as
+ * remote hosts to open connections to the topology connector servlet
+ * @return a comma separated list of hostnames and/or ip addresses which are allowed as
+ * remote hosts to open connections to the topology connector servlet
+ */
+ public String[] getTopologyConnectorWhitelist();
+
+ /**
+ * Returns the resource path where cluster instance informations are stored.
+ * @return the resource path where cluster instance informations are stored
+ */
+ public String getClusterInstancesPath();
+
+
+ /**
+ * @return true if hmac is enabled.
+ */
+ public boolean isHmacEnabled();
+
+ /**
+ * @return the shared key
+ */
+ public String getSharedKey();
+
+ /**
+ * @return the interval of the shared key for hmac.
+ */
+ public long getKeyInterval();
+
+ /**
+ * @return true if encryption is enabled.
+ */
+ public boolean isEncryptionEnabled();
+
+ /**
+ * @return true if requests on the topology connector should be gzipped
+ * (which only works if the server accepts that.. ie discovery.impl 1.0.4+)
+ */
+ public boolean isGzipConnectorRequestsEnabled();
+
+ /**
+ * @return true if the auto-stopping of local-loop topology connectors is enabled.
+ */
+ public boolean isAutoStopLocalLoopEnabled();
+
+ /**
+ * Returns the backoff factor to be used for standby (loop) connectors
+ * @return the backoff factor to be used for standby (loop) connectors
+ */
+ public int getBackoffStandbyFactor();
+
+ /**
+ * Returns the (maximum) backoff factor to be used for stable connectors
+ * @return the (maximum) backoff factor to be used for stable connectors
+ */
+ public int getBackoffStableFactor();
+
+ /**
+ * Returns the backoff interval for standby (loop) connectors in seconds
+ * @return the backoff interval for standby (loop) connectors in seconds
+ */
+ public long getBackoffStandbyInterval();
+
+ /**
+ * Returns the interval (in seconds) in which connectors are pinged
+ * @return the interval (in seconds) in which connectors are pinged
+ */
+ public long getConnectorPingInterval();
+
+ /**
+ * Returns the timeout (in seconds) after which a connector ping is considered invalid/timed out
+ * @return the timeout (in seconds) after which a connector ping is considered invalid/timed out
+ */
+ public long getConnectorPingTimeout();
+
+ /**
+ * The minEventDelay to apply to the ViewStateManager
+ */
+ public int getMinEventDelay();
+
+}
diff --git a/src/main/java/org/apache/sling/discovery/base/connectors/announcement/Announcement.java b/src/main/java/org/apache/sling/discovery/base/connectors/announcement/Announcement.java
new file mode 100644
index 0000000..c4345ae
--- /dev/null
+++ b/src/main/java/org/apache/sling/discovery/base/connectors/announcement/Announcement.java
@@ -0,0 +1,460 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.sling.discovery.base.connectors.announcement;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.apache.sling.api.resource.ModifiableValueMap;
+import org.apache.sling.api.resource.PersistenceException;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.commons.json.JSONArray;
+import org.apache.sling.commons.json.JSONException;
+import org.apache.sling.commons.json.JSONObject;
+import org.apache.sling.discovery.ClusterView;
+import org.apache.sling.discovery.InstanceDescription;
+import org.apache.sling.discovery.commons.providers.DefaultClusterView;
+import org.apache.sling.discovery.commons.providers.DefaultInstanceDescription;
+import org.apache.sling.discovery.commons.providers.NonLocalInstanceDescription;
+
+/**
+ * An announcement is the information exchanged by the topology connector and
+ * contains all clusters and instances which both the topology connector client
+ * and servlet see (in their part before joining the two worlds).
+ * <p>
+ * An announcement is exchanged in json format and carries a timeout.
+ */
+public class Announcement {
+
+ /** the protocol version this announcement currently represents. Mismatching protocol versions are
+ * used to detect incompatible topology connectors
+ */
+ private final static int PROTOCOL_VERSION = 1;
+
+ /** the sling id of the owner of this announcement. the owner is where this announcement comes from **/
+ private final String ownerId;
+
+ /** announcement protocol version **/
+ private final int protocolVersion;
+
+ /** the local cluster view **/
+ private ClusterView localCluster;
+
+ /** the incoming instances **/
+ private List<Announcement> incomings = new LinkedList<Announcement>();
+
+ /** whether or not this annoucement was inherited (response of a connect) or incoming (the connect) **/
+ private boolean inherited = false;
+
+ /** some information about the server where this announcement came from **/
+ private String serverInfo;
+
+ /** whether or not this announcement represents a loop detected in the topology connectors **/
+ private boolean loop = false;
+
+ /** SLING-3382: Sets the backoffInterval which the connector servlets passes back to the client to use as the next heartbeatInterval **/
+ private long backoffInterval = -1;
+
+ /** SLING-3382: the resetBackoff flag is sent from client to server and indicates that the client wants to start from (backoff) scratch **/
+ private boolean resetBackoff = false;
+
+ public Announcement(final String ownerId) {
+ this(ownerId, PROTOCOL_VERSION);
+ }
+
+ public Announcement(final String ownerId, int protocolVersion) {
+ if (ownerId==null || ownerId.length()==0) {
+ throw new IllegalArgumentException("ownerId must not be null or empty");
+ }
+ this.ownerId = ownerId;
+ this.protocolVersion = protocolVersion;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder incomingList = new StringBuilder();
+ for (Iterator<Announcement> it = incomings.iterator(); it.hasNext();) {
+ Announcement anIncomingAnnouncement = it.next();
+ if (incomingList.length()!=0) {
+ incomingList.append(", ");
+ }
+ incomingList.append(anIncomingAnnouncement);
+ }
+ return "Announcement[ownerId="+getOwnerId()+
+ ", protocolVersion="+protocolVersion+
+ ", inherited="+isInherited()+
+ ", loop="+loop+
+ ", incomings="+incomingList+"]";
+ }
+
+ /** check whether this is announcement contains the valid protocol version **/
+ public boolean isCorrectVersion() {
+ return (protocolVersion==PROTOCOL_VERSION);
+ }
+
+ /** check whether this is a valid announcement, containing the minimal information **/
+ public boolean isValid() {
+ if (ownerId==null || ownerId.length()==0) {
+ return false;
+ }
+ if (loop) {
+ return true;
+ }
+ if (!isCorrectVersion()) {
+ return false;
+ }
+ if (localCluster==null) {
+ return false;
+ }
+ try{
+ List<InstanceDescription> instances = localCluster.getInstances();
+ if (instances==null || instances.size()==0) {
+ return false;
+ }
+ boolean isOwnerMemberOfLocalCluster = false;
+ for (Iterator<InstanceDescription> it = instances.iterator(); it.hasNext();) {
+ InstanceDescription instanceDescription = it.next();
+ if (instanceDescription.getSlingId().equals(ownerId)) {
+ isOwnerMemberOfLocalCluster = true;
+ }
+ }
+ if (!isOwnerMemberOfLocalCluster) {
+ return false;
+ }
+ } catch(Exception ise) {
+ return false;
+ }
+ return true;
+ }
+
+ /** set the inherited flag - if true this means this announcement is the response of a topology connect **/
+ public void setInherited(final boolean inherited) {
+ this.inherited = inherited;
+ }
+
+ /** Returns the inherited flag - if true this means that this announcement is the response of a topology connect **/
+ public boolean isInherited() {
+ return inherited;
+ }
+
+ /** Sets the loop falg - set true when this announcement should represent a loop detected in the topology connectors **/
+ public void setLoop(final boolean loop) {
+ this.loop = loop;
+ }
+
+ /** Sets the backoffInterval which the connector servlets passes back to the client to use as the next heartbeatInterval **/
+ public void setBackoffInterval(long backoffInterval) {
+ this.backoffInterval = backoffInterval;
+ }
+
+ /** Gets the backoffInterval which the connector servlets passes back to the client to use as the next heartbeatInterval **/
+ public long getBackoffInterval() {
+ return this.backoffInterval;
+ }
+
+ /** sets the resetBackoff flag **/
+ public void setResetBackoff(boolean resetBackoff) {
+ this.resetBackoff = resetBackoff;
+ }
+
+ /** gets the resetBackoff flag **/
+ public boolean getResetBackoff() {
+ return resetBackoff;
+ }
+
+ /** Returns the loop flag - set when this announcement represents a loop detected in the topology connectors **/
+ public boolean isLoop() {
+ return loop;
+ }
+
+ /** Returns the protocolVersion of this announcement **/
+ public int getProtocolVersion() {
+ return protocolVersion;
+ }
+
+ /** sets the information about the server where this announcement came from **/
+ public void setServerInfo(final String serverInfo) {
+ this.serverInfo = serverInfo;
+ }
+
+ /** the information about the server where this announcement came from **/
+ public String getServerInfo() {
+ return serverInfo;
+ }
+
+ /**
+ * Returns the slingid of the owner of this announcement.
+ * <p>
+ * The owner is the instance which initiated the topology connection
+ */
+ public String getOwnerId() {
+ return ownerId;
+ }
+
+ /** Convert this announcement into a json object **/
+ public JSONObject asJSONObject() throws JSONException {
+ return asJSONObject(false);
+ }
+
+ /** Convert this announcement into a json object **/
+ private JSONObject asJSONObject(boolean filterTimes) throws JSONException {
+ JSONObject announcement = new JSONObject();
+ announcement.put("ownerId", ownerId);
+ announcement.put("protocolVersion", protocolVersion);
+ // SLING-3389: leaving the 'created' property in the announcement
+ // for backwards compatibility!
+ if (!filterTimes) {
+ announcement.put("created", System.currentTimeMillis());
+ }
+ announcement.put("inherited", inherited);
+ if (loop) {
+ announcement.put("loop", loop);
+ }
+ if (serverInfo != null) {
+ announcement.put("serverInfo", serverInfo);
+ }
+ if (localCluster!=null) {
+ announcement.put("localClusterView", asJSON(localCluster));
+ }
+ if (!filterTimes && backoffInterval>0) {
+ announcement.put("backoffInterval", backoffInterval);
+ }
+ if (resetBackoff) {
+ announcement.put("resetBackoff", resetBackoff);
+ }
+ JSONArray incomingAnnouncements = new JSONArray();
+ for (Iterator<Announcement> it = incomings.iterator(); it.hasNext();) {
+ Announcement incoming = it.next();
+ incomingAnnouncements.put(incoming.asJSONObject(filterTimes));
+ }
+ announcement.put("topologyAnnouncements", incomingAnnouncements);
+ return announcement;
+ }
+
+ /** Create an announcement form json **/
+ public static Announcement fromJSON(final String topologyAnnouncementJSON)
+ throws JSONException {
+ JSONObject announcement = new JSONObject(topologyAnnouncementJSON);
+ final String ownerId = announcement.getString("ownerId");
+ final int protocolVersion;
+ if (!announcement.has("protocolVersion")) {
+ protocolVersion = -1;
+ } else {
+ protocolVersion = announcement.getInt("protocolVersion");
+ }
+ final Announcement result = new Announcement(ownerId, protocolVersion);
+ if (announcement.has("backoffInterval")) {
+ long backoffInterval = announcement.getLong("backoffInterval");
+ result.backoffInterval = backoffInterval;
+ }
+ if (announcement.has("resetBackoff")) {
+ boolean resetBackoff = announcement.getBoolean("resetBackoff");
+ result.resetBackoff = resetBackoff;
+ }
+ if (announcement.has("loop") && announcement.getBoolean("loop")) {
+ result.setLoop(true);
+ return result;
+ }
+ final String localClusterViewJSON = announcement
+ .getString("localClusterView");
+ final ClusterView localClusterView = asClusterView(localClusterViewJSON);
+ final JSONArray subAnnouncements = announcement
+ .getJSONArray("topologyAnnouncements");
+
+ if (announcement.has("inherited")) {
+ final Boolean inherited = announcement.getBoolean("inherited");
+ result.inherited = inherited;
+ }
+ if (announcement.has("serverInfo")) {
+ String serverInfo = announcement.getString("serverInfo");
+ result.serverInfo = serverInfo;
+ }
+ result.setLocalCluster(localClusterView);
+ for (int i = 0; i < subAnnouncements.length(); i++) {
+ String subAnnouncementJSON = subAnnouncements.getString(i);
+ result.addIncomingTopologyAnnouncement(fromJSON(subAnnouncementJSON));
+ }
+ return result;
+ }
+
+ /** create a clusterview from json **/
+ private static ClusterView asClusterView(final String localClusterViewJSON)
+ throws JSONException {
+ JSONObject obj = new JSONObject(localClusterViewJSON);
+ DefaultClusterView clusterView = new DefaultClusterView(
+ obj.getString("id"));
+ JSONArray instancesObj = obj.getJSONArray("instances");
+
+ for (int i = 0; i < instancesObj.length(); i++) {
+ JSONObject anInstance = instancesObj.getJSONObject(i);
+ clusterView.addInstanceDescription(asInstance(anInstance));
+ }
+
+ return clusterView;
+ }
+
+ /** convert a clusterview into json **/
+ private static JSONObject asJSON(final ClusterView clusterView)
+ throws JSONException {
+ JSONObject obj = new JSONObject();
+ obj.put("id", clusterView.getId());
+ JSONArray instancesObj = new JSONArray();
+ List<InstanceDescription> instances = clusterView.getInstances();
+ for (Iterator<InstanceDescription> it = instances.iterator(); it
+ .hasNext();) {
+ InstanceDescription instanceDescription = it.next();
+ instancesObj.put(asJSON(instanceDescription));
+ }
+ obj.put("instances", instancesObj);
+ return obj;
+ }
+
+ /** create an instancedescription from json **/
+ private static DefaultInstanceDescription asInstance(
+ final JSONObject anInstance) throws JSONException {
+ final boolean isLeader = anInstance.getBoolean("isLeader");
+ final String slingId = anInstance.getString("slingId");
+
+ final JSONObject propertiesObj = anInstance.getJSONObject("properties");
+ Iterator<String> it = propertiesObj.keys();
+ Map<String, String> properties = new HashMap<String, String>();
+ while (it.hasNext()) {
+ String key = it.next();
+ properties.put(key, propertiesObj.getString(key));
+ }
+
+ NonLocalInstanceDescription instance = new NonLocalInstanceDescription(
+ null, isLeader, slingId, properties);
+ return instance;
+ }
+
+ /** convert an instance description into a json object **/
+ private static JSONObject asJSON(final InstanceDescription instanceDescription)
+ throws JSONException {
+ JSONObject obj = new JSONObject();
+ obj.put("slingId", instanceDescription.getSlingId());
+ obj.put("isLeader", instanceDescription.isLeader());
+ ClusterView cluster = instanceDescription.getClusterView();
+ if (cluster != null) {
+ obj.put("cluster", cluster.getId());
+ }
+ JSONObject propertiesObj = new JSONObject();
+ Map<String, String> propertiesMap = instanceDescription.getProperties();
+ for (Iterator<Entry<String, String>> it = propertiesMap.entrySet()
+ .iterator(); it.hasNext();) {
+ Entry<String, String> entry = it.next();
+ propertiesObj.put(entry.getKey(), entry.getValue());
+ }
+ obj.put("properties", propertiesObj);
+ return obj;
+ }
+
+ /** sets the local clusterview **/
+ public void setLocalCluster(ClusterView localCluster) {
+ this.localCluster = localCluster;
+ }
+
+ /** adds an incoming announcement to this announcement **/
+ public void addIncomingTopologyAnnouncement(
+ Announcement incomingTopologyAnnouncement) {
+ incomings.add(incomingTopologyAnnouncement);
+ }
+
+ /** Convert this announcement into json **/
+ public String asJSON() throws JSONException {
+ return asJSONObject().toString();
+ }
+
+ /** the key which is unique to this announcement **/
+ public String getPrimaryKey() {
+ return ownerId;
+ }
+
+ /** Returns the list of instances that are contained in this announcement **/
+ public Collection<InstanceDescription> listInstances() {
+ Collection<InstanceDescription> instances = new LinkedList<InstanceDescription>();
+ instances.addAll(localCluster.getInstances());
+
+ for (Iterator<Announcement> it = incomings.iterator(); it.hasNext();) {
+ Announcement incomingAnnouncement = it.next();
+ instances.addAll(incomingAnnouncement.listInstances());
+ }
+ return instances;
+ }
+
+ /**
+ * Persists this announcement using the given 'announcements' resource,
+ * under which a node with the primary key is created
+ **/
+ public void persistTo(Resource announcementsResource)
+ throws PersistenceException, JSONException {
+ Resource announcementChildResource = announcementsResource.getChild(getPrimaryKey());
+
+ // SLING-2967 used to introduce 'resetting the created time' here
+ // in order to become machine-clock independent.
+ // With introduction of SLING-3389, where we dont store any
+ // announcement-heartbeat-dates anymore at all, this resetting here
+ // became unnecessary.
+
+ final String announcementJson = asJSON();
+ if (announcementChildResource==null) {
+ final ResourceResolver resourceResolver = announcementsResource.getResourceResolver();
+ Map<String, Object> properties = new HashMap<String, Object>();
+ properties.put("topologyAnnouncement", announcementJson);
+ resourceResolver.create(announcementsResource, getPrimaryKey(), properties);
+ } else {
+ final ModifiableValueMap announcementChildMap = announcementChildResource.adaptTo(ModifiableValueMap.class);
+ announcementChildMap.put("topologyAnnouncement", announcementJson);
+ }
+ }
+
+ /**
+ * Remove all announcements that match the given owner Id
+ */
+ public void removeInherited(final String ownerId) {
+ for (Iterator<Announcement> it = incomings.iterator(); it.hasNext();) {
+ Announcement anIncomingAnnouncement = it.next();
+ if (anIncomingAnnouncement.isInherited()
+ && anIncomingAnnouncement.getOwnerId().equals(ownerId)) {
+ // then filter this
+ it.remove();
+ }
+
+ }
+ }
+
+ /**
+ * Compare this Announcement with another one, ignoring the 'created'
+ * property - which gets added to the JSON object automatically due
+ * to SLING-3389 wire-backwards-compatibility - and backoffInterval
+ * introduced as part of SLING-3382
+ */
+ public boolean correspondsTo(Announcement announcement) throws JSONException {
+ final JSONObject myJson = asJSONObject(true);
+ final JSONObject otherJson = announcement.asJSONObject(true);
+ return myJson.toString().equals(otherJson.toString());
+ }
+
+}
diff --git a/src/main/java/org/apache/sling/discovery/base/connectors/announcement/AnnouncementFilter.java b/src/main/java/org/apache/sling/discovery/base/connectors/announcement/AnnouncementFilter.java
new file mode 100644
index 0000000..f1664a2
--- /dev/null
+++ b/src/main/java/org/apache/sling/discovery/base/connectors/announcement/AnnouncementFilter.java
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.sling.discovery.base.connectors.announcement;
+
+/**
+ * Filter used during announcement processing internally
+ **/
+public interface AnnouncementFilter {
+
+ /**
+ * Check if the provided announcement, which was received by the provided
+ * slingId can be accepted or not.
+ **/
+ boolean accept(String receivingSlingId, Announcement announcement);
+
+}
diff --git a/src/main/java/org/apache/sling/discovery/base/connectors/announcement/AnnouncementRegistry.java b/src/main/java/org/apache/sling/discovery/base/connectors/announcement/AnnouncementRegistry.java
new file mode 100644
index 0000000..fb8b2c1
--- /dev/null
+++ b/src/main/java/org/apache/sling/discovery/base/connectors/announcement/AnnouncementRegistry.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.sling.discovery.base.connectors.announcement;
+
+import java.util.Collection;
+
+import org.apache.sling.discovery.ClusterView;
+import org.apache.sling.discovery.InstanceDescription;
+
+/**
+ * The announcement registry keeps track of all the announcement that this
+ * instance either received by a joined topology connector or that a topology
+ * connector inherited from the counterpart (the topology connector servlet)
+ */
+public interface AnnouncementRegistry {
+
+ /**
+ * Register the given announcement - and returns the backoff interval (in seconds)
+ * for stable connectors
+ * - or -1 if the registration was not successful (likely indicating a loop)
+ * @return the backoff interval (in seconds) for stable connectors
+ * - or -1 if the registration was not successful (likely indicating a loop)
+ */
+ long registerAnnouncement(Announcement topologyAnnouncement);
+
+ /** list all announcements that were received by instances in the local cluster **/
+ Collection<Announcement> listAnnouncementsInSameCluster(ClusterView localClusterView);
+
+ /** list all announcements that were received (incoming or inherited) by this instance **/
+ Collection<Announcement> listLocalAnnouncements();
+
+ /** list all announcements that this instance received (incoming) **/
+ Collection<CachedAnnouncement> listLocalIncomingAnnouncements();
+
+ /** Check for expired announcements and remove any if applicable **/
+ void checkExpiredAnnouncements();
+
+ /** Returns the list of instances contained in all non-expired announcements of this registry **/
+ Collection<InstanceDescription> listInstances(ClusterView localClusterView);
+
+ /** Add all registered announcements to the given target announcement that are accepted by the given filter **/
+ void addAllExcept(Announcement target, ClusterView localClusterView, AnnouncementFilter filter);
+
+ /** Unregister the announcement owned by the given slingId **/
+ void unregisterAnnouncement(String ownerId);
+
+ /** Whether or not the given owner has an active (ie not expired) announcement registered **/
+ boolean hasActiveAnnouncement(String ownerId);
+
+}
diff --git a/src/main/java/org/apache/sling/discovery/base/connectors/announcement/AnnouncementRegistryImpl.java b/src/main/java/org/apache/sling/discovery/base/connectors/announcement/AnnouncementRegistryImpl.java
new file mode 100644
index 0000000..bb109a7
--- /dev/null
+++ b/src/main/java/org/apache/sling/discovery/base/connectors/announcement/AnnouncementRegistryImpl.java
@@ -0,0 +1,593 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.sling.discovery.base.connectors.announcement;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.Service;
+import org.apache.sling.api.resource.LoginException;
+import org.apache.sling.api.resource.PersistenceException;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ResourceResolverFactory;
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.commons.json.JSONException;
+import org.apache.sling.discovery.ClusterView;
+import org.apache.sling.discovery.InstanceDescription;
+import org.apache.sling.discovery.base.connectors.BaseConfig;
+import org.apache.sling.discovery.commons.providers.util.ResourceHelper;
+import org.apache.sling.settings.SlingSettingsService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Default implementation of the AnnouncementRegistry which
+ * handles JSON-backed announcements and does so by storing
+ * them in a local like /var/discovery/impl/clusterNodes/$slingId/announcement.
+ */
+@Component
+@Service(value = AnnouncementRegistry.class)
+public class AnnouncementRegistryImpl implements AnnouncementRegistry {
+
+ private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+ @Reference
+ private ResourceResolverFactory resourceResolverFactory;
+
+ @Reference
+ private SlingSettingsService settingsService;
+
+ private String slingId;
+
+ @Reference
+ private BaseConfig config;
+
+ public static AnnouncementRegistryImpl testConstructorAndActivate(ResourceResolverFactory resourceResolverFactory,
+ SlingSettingsService slingSettingsService, BaseConfig config) {
+ AnnouncementRegistryImpl registry = testConstructor(resourceResolverFactory, slingSettingsService, config);
+ registry.activate();
+ return registry;
+ }
+
+ public static AnnouncementRegistryImpl testConstructor(ResourceResolverFactory resourceResolverFactory,
+ SlingSettingsService slingSettingsService, BaseConfig config) {
+ AnnouncementRegistryImpl registry = new AnnouncementRegistryImpl();
+ registry.resourceResolverFactory = resourceResolverFactory;
+ registry.settingsService = slingSettingsService;
+ registry.config = config;
+ return registry;
+ }
+
+ @Activate
+ protected void activate() {
+ slingId = settingsService.getSlingId();
+ }
+
+ private final Map<String,CachedAnnouncement> ownAnnouncementsCache =
+ new HashMap<String,CachedAnnouncement>();
+
+ public synchronized void unregisterAnnouncement(final String ownerId) {
+ if (ownerId==null || ownerId.length()==0) {
+ throw new IllegalArgumentException("ownerId must not be null or empty");
+ }
+ // remove from the cache - even if there's an error afterwards
+ ownAnnouncementsCache.remove(ownerId);
+
+ if (resourceResolverFactory == null) {
+ logger.error("unregisterAnnouncement: resourceResolverFactory is null");
+ return;
+ }
+ ResourceResolver resourceResolver = null;
+ try {
+ resourceResolver = resourceResolverFactory
+ .getAdministrativeResourceResolver(null);
+
+ final String path = config.getClusterInstancesPath()
+ + "/"
+ + slingId
+ + "/announcements/" + ownerId;
+ final Resource announcementsResource = resourceResolver.getResource(path);
+ if (announcementsResource!=null) {
+ resourceResolver.delete(announcementsResource);
+ resourceResolver.commit();
+ }
+
+ } catch (LoginException e) {
+ logger.error(
+ "unregisterAnnouncement: could not log in administratively: "
+ + e, e);
+ throw new RuntimeException("Could not log in to repository (" + e
+ + ")", e);
+ } catch (PersistenceException e) {
+ logger.error("unregisterAnnouncement: got a PersistenceException: "
+ + e, e);
+ throw new RuntimeException(
+ "Exception while talking to repository (" + e + ")", e);
+ } finally {
+ if (resourceResolver != null) {
+ resourceResolver.close();
+ }
+ }
+ }
+
+ public synchronized Collection<Announcement> listLocalAnnouncements() {
+ return fillWithCachedAnnouncements(new LinkedList<Announcement>());
+ }
+
+ public synchronized Collection<CachedAnnouncement> listLocalIncomingAnnouncements() {
+ Collection<CachedAnnouncement> result = new LinkedList<CachedAnnouncement>(ownAnnouncementsCache.values());
+ for (Iterator<CachedAnnouncement> it = result.iterator(); it.hasNext();) {
+ CachedAnnouncement cachedAnnouncement = it.next();
+ if (cachedAnnouncement.getAnnouncement().isInherited()) {
+ it.remove();
+ continue;
+ }
+ if (cachedAnnouncement.hasExpired()) {
+ it.remove();
+ continue;
+ }
+ }
+ return result;
+ }
+
+ private final InstanceDescription getLocalInstanceDescription(final ClusterView localClusterView) {
+ for (Iterator<InstanceDescription> it = localClusterView.getInstances().iterator(); it
+ .hasNext();) {
+ InstanceDescription id = it.next();
+ if (id.isLocal()) {
+ return id;
+ }
+ }
+ return null;
+ }
+
+ public synchronized Collection<Announcement> listAnnouncementsInSameCluster(final ClusterView localClusterView) {
+ logger.debug("listAnnouncementsInSameCluster: start. localClusterView: {}", localClusterView);
+ if (localClusterView==null) {
+ throw new IllegalArgumentException("clusterView must not be null");
+ }
+ ResourceResolver resourceResolver = null;
+ final Collection<Announcement> incomingAnnouncements = new LinkedList<Announcement>();
+ final InstanceDescription localInstance = getLocalInstanceDescription(localClusterView);
+ try {
+ resourceResolver = resourceResolverFactory
+ .getAdministrativeResourceResolver(null);
+
+ Resource clusterInstancesResource = ResourceHelper
+ .getOrCreateResource(
+ resourceResolver,
+ config.getClusterInstancesPath());
+
+ Iterator<Resource> it0 = clusterInstancesResource.getChildren()
+ .iterator();
+ while (it0.hasNext()) {
+ Resource aClusterInstanceResource = it0.next();
+ final String instanceId = aClusterInstanceResource.getName();
+ logger.debug("listAnnouncementsInSameCluster: handling clusterInstance: {}", instanceId);
+ if (localInstance!=null && localInstance.getSlingId().equals(instanceId)) {
+ // this is the local instance then - which we serve from the cache only
+ logger.debug("listAnnouncementsInSameCluster: matched localInstance, filling with cache: {}", instanceId);
+ fillWithCachedAnnouncements(incomingAnnouncements);
+ continue;
+ }
+
+ //TODO: add ClusterView.contains(instanceSlingId) for convenience to next api change
+ if (!contains(localClusterView, instanceId)) {
+ logger.debug("listAnnouncementsInSameCluster: instance is not in my view, ignoring: {}", instanceId);
+ // then the instance is not in my view, hence ignore its announcements
+ // (corresponds to earlier expiry-handling)
+ continue;
+ }
+ final Resource announcementsResource = aClusterInstanceResource
+ .getChild("announcements");
+ if (announcementsResource == null) {
+ logger.debug("listAnnouncementsInSameCluster: instance has no announcements: {}", instanceId);
+ continue;
+ }
+ logger.debug("listAnnouncementsInSameCluster: instance has announcements: {}", instanceId);
+ Iterator<Resource> it = announcementsResource.getChildren()
+ .iterator();
+ Announcement topologyAnnouncement;
+ while (it.hasNext()) {
+ Resource anAnnouncement = it.next();
+ topologyAnnouncement = Announcement
+ .fromJSON(anAnnouncement
+ .adaptTo(ValueMap.class).get(
+ "topologyAnnouncement",
+ String.class));
+ logger.debug("listAnnouncementsInSameCluster: found announcement: {}", topologyAnnouncement);
+ incomingAnnouncements.add(topologyAnnouncement);
+ // SLING-3389: no longer check for expired announcements -
+ // instead make use of the fact that this instance
+ // has a clusterView and that every live instance
+ // is responsible of cleaning up expired announcements
+ // with the repository
+ }
+ }
+ // since SLING-3389 this method does only read operations, hence
+ // no commit necessary anymore - close happens in below finally block
+ } catch (LoginException e) {
+ logger.error(
+ "listAnnouncementsInSameCluster: could not log in administratively: " + e, e);
+ throw new RuntimeException("Could not log in to repository (" + e
+ + ")", e);
+ } catch (PersistenceException e) {
+ logger.error("listAnnouncementsInSameCluster: got a PersistenceException: " + e, e);
+ throw new RuntimeException(
+ "Exception while talking to repository (" + e + ")", e);
+ } catch (JSONException e) {
+ logger.error("listAnnouncementsInSameCluster: got a JSONException: " + e, e);
+ throw new RuntimeException("Exception while converting json (" + e
+ + ")", e);
+ } finally {
+ if (resourceResolver != null) {
+ resourceResolver.close();
+ }
+ }
+ if (logger.isDebugEnabled()) {
+ logger.debug("listAnnouncementsInSameCluster: result: "+incomingAnnouncements.size());
+ }
+ return incomingAnnouncements;
+ }
+
+ private final Collection<Announcement> fillWithCachedAnnouncements(
+ final Collection<Announcement> incomingAnnouncements) {
+ for (Iterator<Entry<String, CachedAnnouncement>> it = ownAnnouncementsCache.entrySet().iterator(); it
+ .hasNext();) {
+ final Entry<String, CachedAnnouncement> entry = it.next();
+ if (entry.getValue().hasExpired()) {
+ // filter this one out then
+ continue;
+ }
+ incomingAnnouncements.add(entry.getValue().getAnnouncement());
+ }
+ return incomingAnnouncements;
+ }
+
+ private final boolean contains(final ClusterView clusterView, final String instanceId) {
+ for (Iterator<InstanceDescription> it = clusterView.getInstances().iterator(); it
+ .hasNext();) {
+ InstanceDescription instance = it.next();
+ if (instance.getSlingId().equals(instanceId)) {
+ // fine, then the instance is in the view
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public synchronized boolean hasActiveAnnouncement(final String ownerId) {
+ if (ownerId==null || ownerId.length()==0) {
+ throw new IllegalArgumentException("ownerId must not be null or empty: "+ownerId);
+ }
+ final CachedAnnouncement cachedAnnouncement = ownAnnouncementsCache.get(ownerId);
+ if (cachedAnnouncement==null) {
+ return false;
+ }
+
+ return !cachedAnnouncement.hasExpired();
+ }
+
+ public synchronized long registerAnnouncement(final Announcement topologyAnnouncement) {
+ if (topologyAnnouncement==null) {
+ throw new IllegalArgumentException("topologyAnnouncement must not be null");
+ }
+ if (!topologyAnnouncement.isValid()) {
+ logger.warn("topologyAnnouncement is not valid");
+ return -1;
+ }
+ if (resourceResolverFactory == null) {
+ logger.error("registerAnnouncement: resourceResolverFactory is null");
+ return -1;
+ }
+
+ final CachedAnnouncement cachedAnnouncement =
+ ownAnnouncementsCache.get(topologyAnnouncement.getOwnerId());
+ if (cachedAnnouncement!=null) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("registerAnnouncement: got existing cached announcement for ownerId="+topologyAnnouncement.getOwnerId());
+ }
+ try{
+ if (topologyAnnouncement.correspondsTo(cachedAnnouncement.getAnnouncement())) {
+ // then nothing has changed with this announcement, so just update
+ // the heartbeat and fine is.
+ // this should actually be the normal case for a stable connector
+ logger.debug("registerAnnouncement: nothing has changed, only updating heartbeat in-memory.");
+ return cachedAnnouncement.registerPing(topologyAnnouncement, config);
+ }
+ logger.debug("registerAnnouncement: incoming announcement differs from existing one!");
+
+ } catch(JSONException e) {
+ logger.error("registerAnnouncement: got JSONException while converting incoming announcement to JSON: "+e, e);
+ }
+ // otherwise the repository and the cache require to be updated
+ // resetting the cache therefore at this point already
+ ownAnnouncementsCache.remove(topologyAnnouncement.getOwnerId());
+ } else {
+ logger.debug("registerAnnouncement: no cached announcement yet for ownerId="+topologyAnnouncement.getOwnerId());
+ }
+
+ logger.debug("registerAnnouncement: getting the list of all local announcements");
+ final Collection<Announcement> announcements = new LinkedList<Announcement>();
+ fillWithCachedAnnouncements(announcements);
+ if (logger.isDebugEnabled()) {
+ logger.debug("registerAnnouncement: list returned: "+(announcements==null ? "null" : announcements.size()));
+ }
+ for (Iterator<Announcement> it1 = announcements.iterator(); it1
+ .hasNext();) {
+ Announcement announcement = it1.next();
+ if (announcement.getOwnerId().equals(
+ topologyAnnouncement.getOwnerId())) {
+ // then this is from the same owner - skip this
+ continue;
+ }
+ // analyse to see if any of the instances in the announcement
+ // include the new owner
+ Collection<InstanceDescription> attachedInstances = announcement
+ .listInstances();
+ for (Iterator<InstanceDescription> it2 = attachedInstances
+ .iterator(); it2.hasNext();) {
+ InstanceDescription instanceDescription = it2.next();
+ if (topologyAnnouncement.getOwnerId().equals(
+ instanceDescription.getSlingId())) {
+ logger.info("registerAnnouncement: already have this instance attached: "
+ + instanceDescription.getSlingId());
+ return -1;
+ }
+ }
+ }
+
+ ResourceResolver resourceResolver = null;
+ try {
+ resourceResolver = resourceResolverFactory
+ .getAdministrativeResourceResolver(null);
+
+ final Resource announcementsResource = ResourceHelper
+ .getOrCreateResource(
+ resourceResolver,
+ config.getClusterInstancesPath()
+ + "/"
+ + slingId
+ + "/announcements");
+
+ topologyAnnouncement.persistTo(announcementsResource);
+ resourceResolver.commit();
+ ownAnnouncementsCache.put(topologyAnnouncement.getOwnerId(),
+ new CachedAnnouncement(topologyAnnouncement, config));
+ } catch (LoginException e) {
+ logger.error(
+ "registerAnnouncement: could not log in administratively: "
+ + e, e);
+ throw new RuntimeException("Could not log in to repository (" + e
+ + ")", e);
+ } catch (PersistenceException e) {
+ logger.error("registerAnnouncement: got a PersistenceException: "
+ + e, e);
+ throw new RuntimeException(
+ "Exception while talking to repository (" + e + ")", e);
+ } catch (JSONException e) {
+ logger.error("registerAnnouncement: got a JSONException: " + e, e);
+ throw new RuntimeException("Exception while converting json (" + e
+ + ")", e);
+ } finally {
+ if (resourceResolver != null) {
+ resourceResolver.close();
+ }
+ }
+ return 0;
+ }
+
+ public synchronized void addAllExcept(final Announcement target, final ClusterView clusterView,
+ final AnnouncementFilter filter) {
+ ResourceResolver resourceResolver = null;
+ try {
+ resourceResolver = resourceResolverFactory
+ .getAdministrativeResourceResolver(null);
+
+ final Resource clusterInstancesResource = ResourceHelper
+ .getOrCreateResource(
+ resourceResolver,
+ config.getClusterInstancesPath());
+
+ final Iterator<Resource> it0 = clusterInstancesResource.getChildren()
+ .iterator();
+ Resource announcementsResource;
+ while (it0.hasNext()) {
+ final Resource aClusterInstanceResource = it0.next();
+ final String instanceId = aClusterInstanceResource.getName();
+ //TODO: add ClusterView.contains(instanceSlingId) for convenience to next api change
+ if (!contains(clusterView, instanceId)) {
+ // then the instance is not in my view, hence dont propagate
+ // its announcements
+ // (corresponds to earlier expiry-handling)
+ continue;
+ }
+ announcementsResource = aClusterInstanceResource
+ .getChild("announcements");
+ if (announcementsResource == null) {
+ continue;
+ }
+ Iterator<Resource> it = announcementsResource.getChildren()
+ .iterator();
+ while (it.hasNext()) {
+ Resource anAnnouncement = it.next();
+ if (logger.isDebugEnabled()) {
+ logger.debug("addAllExcept: anAnnouncement="
+ + anAnnouncement);
+ }
+ Announcement topologyAnnouncement;
+ topologyAnnouncement = Announcement.fromJSON(anAnnouncement
+ .adaptTo(ValueMap.class).get(
+ "topologyAnnouncement", String.class));
+ if (filter != null && !filter.accept(aClusterInstanceResource.getName(), topologyAnnouncement)) {
+ continue;
+ }
+ target.addIncomingTopologyAnnouncement(topologyAnnouncement);
+ }
+ }
+ // even before SLING-3389 this method only did read operations,
+ // hence no commit was ever necessary. The close happens in the finally block
+ } catch (LoginException e) {
+ logger.error(
+ "handleEvent: could not log in administratively: " + e, e);
+ throw new RuntimeException("Could not log in to repository (" + e
+ + ")", e);
+ } catch (PersistenceException e) {
+ logger.error("handleEvent: got a PersistenceException: " + e, e);
+ throw new RuntimeException(
+ "Exception while talking to repository (" + e + ")", e);
+ } catch (JSONException e) {
+ logger.error("handleEvent: got a JSONException: " + e, e);
+ throw new RuntimeException("Exception while converting json (" + e
+ + ")", e);
+ } finally {
+ if (resourceResolver != null) {
+ resourceResolver.close();
+ }
+ }
+ }
+
+ public synchronized void checkExpiredAnnouncements() {
+ for (Iterator<Entry<String, CachedAnnouncement>> it =
+ ownAnnouncementsCache.entrySet().iterator(); it.hasNext();) {
+ final Entry<String, CachedAnnouncement> entry = it.next();
+ if (entry.getValue().hasExpired()) {
+ // then we have an expiry
+ it.remove();
+
+ final String instanceId = entry.getKey();
+ logger.info("checkExpiredAnnouncements: topology connector of "+instanceId+
+ " (to me="+slingId+
+ ", inherited="+entry.getValue().getAnnouncement().isInherited()+") has expired.");
+ deleteAnnouncementsOf(instanceId);
+ }
+ }
+ //SLING-4139 : also make sure there are no stale announcements
+ // in the repository (from a crash or any other action).
+ // The ownAnnouncementsCache is the authorative set
+ // of announcements that are registered to this
+ // instance's registry - and the repository must not
+ // contain any additional announcements
+ ResourceResolver resourceResolver = null;
+ try {
+ resourceResolver = resourceResolverFactory
+ .getAdministrativeResourceResolver(null);
+ final Resource announcementsResource = ResourceHelper
+ .getOrCreateResource(
+ resourceResolver,
+ config.getClusterInstancesPath()
+ + "/"
+ + slingId
+ + "/announcements");
+ final Iterator<Resource> it = announcementsResource.getChildren().iterator();
+ while(it.hasNext()) {
+ final Resource res = it.next();
+ final String ownerId = res.getName();
+ // ownerId is the slingId of the owner of the announcement (ie of the peer of the connector).
+ // let's check if the we have that owner's announcement in the cache
+
+ if (ownAnnouncementsCache.containsKey(ownerId)) {
+ // fine then, we'll leave this announcement untouched
+ continue;
+ }
+ // otherwise this announcement is likely from an earlier incarnation
+ // of this instance - hence stale - hence we must remove it now
+ // (SLING-4139)
+ ResourceHelper.deleteResource(resourceResolver,
+ res.getPath());
+ }
+ resourceResolver.commit();
+ resourceResolver.close();
+ resourceResolver = null;
+ } catch (LoginException e) {
+ logger.error(
+ "checkExpiredAnnouncements: could not log in administratively when checking "
+ + "for expired announcements of slingId="+slingId+": " + e, e);
+ } catch (PersistenceException e) {
+ logger.error(
+ "checkExpiredAnnouncements: got PersistenceException when checking "
+ + "for expired announcements of slingId="+slingId+": " + e, e);
+ } finally {
+ if (resourceResolver!=null) {
+ resourceResolver.revert();
+ resourceResolver.close();
+ resourceResolver = null;
+ }
+ }
+ }
+
+ private final void deleteAnnouncementsOf(final String instanceId) {
+ ResourceResolver resourceResolver = null;
+ try {
+ resourceResolver = resourceResolverFactory
+ .getAdministrativeResourceResolver(null);
+ ResourceHelper.deleteResource(resourceResolver,
+ config.getClusterInstancesPath()
+ + "/"
+ + slingId
+ + "/announcements/"
+ + instanceId);
+ resourceResolver.commit();
+ resourceResolver.close();
+ resourceResolver = null;
+ } catch (LoginException e) {
+ logger.error(
+ "deleteAnnouncementsOf: could not log in administratively when deleting "
+ + "announcements of instanceId="+instanceId+": " + e, e);
+ } catch (PersistenceException e) {
+ logger.error(
+ "deleteAnnouncementsOf: got PersistenceException when deleting "
+ + "announcements of instanceId="+instanceId+": " + e, e);
+ } finally {
+ if (resourceResolver!=null) {
+ resourceResolver.revert();
+ resourceResolver.close();
+ resourceResolver = null;
+ }
+ }
+ }
+
+ public synchronized Collection<InstanceDescription> listInstances(final ClusterView localClusterView) {
+ logger.debug("listInstances: start. localClusterView: {}", localClusterView);
+ final Collection<InstanceDescription> instances = new LinkedList<InstanceDescription>();
+
+ final Collection<Announcement> announcements = listAnnouncementsInSameCluster(localClusterView);
+ if (announcements == null) {
+ logger.debug("listInstances: no announcement found. end. instances: {}", instances);
+ return instances;
+ }
+
+ for (Iterator<Announcement> it = announcements.iterator(); it.hasNext();) {
+ final Announcement announcement = it.next();
+ logger.debug("listInstances: adding announcement: {}", announcement);
+ instances.addAll(announcement.listInstances());
+ }
+ logger.debug("listInstances: announcements added. end. instances: {}", instances);
+ return instances;
+ }
+
+}
diff --git a/src/main/java/org/apache/sling/discovery/base/connectors/announcement/CachedAnnouncement.java b/src/main/java/org/apache/sling/discovery/base/connectors/announcement/CachedAnnouncement.java
new file mode 100644
index 0000000..fc946ef
--- /dev/null
+++ b/src/main/java/org/apache/sling/discovery/base/connectors/announcement/CachedAnnouncement.java
@@ -0,0 +1,122 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.sling.discovery.base.connectors.announcement;
+
+import org.apache.sling.discovery.base.connectors.BaseConfig;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * With SLING-3389 the Announcement itself doesn't use the created
+ * (ie timeout) field anymore (it still has it currently for backwards
+ * compatibility on the wire-level) - hence that's why there's this
+ * small in-memory wrapper object which contains an Announcement and
+ * carries a lastHeartbeat property.
+ */
+public class CachedAnnouncement {
+
+ private final static Logger logger = LoggerFactory.getLogger(CachedAnnouncement.class);
+
+ private long lastPing = System.currentTimeMillis();
+
+ private final Announcement announcement;
+
+ private long firstPing = System.currentTimeMillis();
+
+ private long backoffIntervalSeconds = -1;
+
+ private final BaseConfig config;
+
+ CachedAnnouncement(final Announcement announcement, final BaseConfig config) {
+ this.announcement = announcement;
+ this.config = config;
+ }
+
+ private long getConfiguredConnectorTimeout() {
+ return config.getConnectorPingTimeout();
+ }
+
+ private long getConfiguredConnectorInterval() {
+ return config.getConnectorPingInterval();
+ }
+
+ public final boolean hasExpired() {
+ final long now = System.currentTimeMillis();
+ final long diff = now-lastPing;
+ if (diff<1000*getEffectiveHeartbeatTimeout()) {
+ return false;
+ } else {
+ return true;
+ }
+ }
+
+ public final long getLastPing() {
+ return lastPing;
+ }
+
+ /** Returns the second until the next heartbeat is expected, otherwise the timeout will hit **/
+ public final long getSecondsUntilTimeout() {
+ final long now = System.currentTimeMillis();
+ final long diff = now-lastPing;
+ final long left = 1000*getEffectiveHeartbeatTimeout() - diff;
+ return left/1000;
+ }
+
+
+ private final long getEffectiveHeartbeatTimeout() {
+ final long configuredGoodwill = getConfiguredConnectorTimeout() - getConfiguredConnectorInterval();
+ return Math.max(getConfiguredConnectorTimeout(), backoffIntervalSeconds + configuredGoodwill);
+ }
+
+ /** Registers a heartbeat event, and returns the new resulting backoff interval -
+ * or 0 if no backoff is applicable yet.
+ * @param incomingAnnouncement
+ * @return the new resulting backoff interval -
+ * or 0 if no backoff is applicable yet.
+ */
+ final long registerPing(Announcement incomingAnnouncement, BaseConfig config) {
+ lastPing = System.currentTimeMillis();
+ if (incomingAnnouncement.isInherited()) {
+ // then we are the client, we inherited this announcement from the server
+ // hence we have no power to do any backoff instructions towards the server
+ // (since the server decides about backoff-ing). hence returning 0 here
+ // but taking note of what the server instructed us in terms of backoff
+ backoffIntervalSeconds = incomingAnnouncement.getBackoffInterval();
+ logger.debug("registerPing: inherited announcement - hence returning 0");
+ return 0;
+ }
+ if (incomingAnnouncement.getResetBackoff()) {
+ // on resetBackoff we reset the firstHeartbeat and start
+ // from 0 again
+ firstPing = lastPing;
+ logger.debug("registerPing: got a resetBackoff - hence returning 0");
+ return 0;
+ }
+ final long stableSince = lastPing - firstPing;
+ final long numStableTimeouts = stableSince / (1000 * config.getConnectorPingTimeout());
+ final long backoffFactor = Math.min(numStableTimeouts, config.getBackoffStableFactor());
+ backoffIntervalSeconds = backoffFactor * config.getConnectorPingInterval();
+ return backoffIntervalSeconds;
+ }
+
+ public final Announcement getAnnouncement() {
+ return announcement;
+ }
+
+}
diff --git a/src/main/java/org/apache/sling/discovery/base/connectors/announcement/package-info.java b/src/main/java/org/apache/sling/discovery/base/connectors/announcement/package-info.java
new file mode 100644
index 0000000..42eab68
--- /dev/null
+++ b/src/main/java/org/apache/sling/discovery/base/connectors/announcement/package-info.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.
+ */
+
+/**
+ * Provides topology announcement implementations for discovery
+ * implementors that choose to extend from discovery.base
+ *
+ * @version 1.0.0
+ */
+@Version("1.0.0")
+package org.apache.sling.discovery.base.connectors.announcement;
+
+import aQute.bnd.annotation.Version;
+
diff --git a/src/main/java/org/apache/sling/discovery/base/connectors/package-info.java b/src/main/java/org/apache/sling/discovery/base/connectors/package-info.java
new file mode 100644
index 0000000..2fa7bf8
--- /dev/null
+++ b/src/main/java/org/apache/sling/discovery/base/connectors/package-info.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.
+ */
+
+/**
+ * Provides topology connector related classes for discovery
+ * implementors that choose to extend from discovery.base
+ *
+ * @version 1.0.0
+ */
+@Version("1.0.0")
+package org.apache.sling.discovery.base.connectors;
+
+import aQute.bnd.annotation.Version;
+
diff --git a/src/main/java/org/apache/sling/discovery/base/connectors/ping/ConnectorRegistry.java b/src/main/java/org/apache/sling/discovery/base/connectors/ping/ConnectorRegistry.java
new file mode 100644
index 0000000..ba81211
--- /dev/null
+++ b/src/main/java/org/apache/sling/discovery/base/connectors/ping/ConnectorRegistry.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.sling.discovery.base.connectors.ping;
+
+import java.net.URL;
+import java.util.Collection;
+
+import org.apache.sling.discovery.base.commons.ClusterViewService;
+
+/**
+ * Registry for topology connector clients
+ */
+public interface ConnectorRegistry {
+
+ /** Register an outgoing topology connector using the provided endpoint url **/
+ TopologyConnectorClientInformation registerOutgoingConnector(
+ ClusterViewService clusterViewService, URL topologyConnectorEndpoint);
+
+ /** Lists all outgoing topology connectors **/
+ Collection<TopologyConnectorClientInformation> listOutgoingConnectors();
+
+ /** ping all outgoing topology connectors **/
+ void pingOutgoingConnectors(boolean force);
+
+ /** Unregister an outgoing topology connector identified by the given (connector) id **/
+ boolean unregisterOutgoingConnector(String id);
+
+}
diff --git a/src/main/java/org/apache/sling/discovery/base/connectors/ping/ConnectorRegistryImpl.java b/src/main/java/org/apache/sling/discovery/base/connectors/ping/ConnectorRegistryImpl.java
new file mode 100644
index 0000000..f46a6e4
--- /dev/null
+++ b/src/main/java/org/apache/sling/discovery/base/connectors/ping/ConnectorRegistryImpl.java
@@ -0,0 +1,157 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.sling.discovery.base.connectors.ping;
+
+import java.net.InetAddress;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Deactivate;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.Service;
+import org.apache.sling.discovery.base.commons.ClusterViewService;
+import org.apache.sling.discovery.base.connectors.BaseConfig;
+import org.apache.sling.discovery.base.connectors.announcement.AnnouncementRegistry;
+import org.osgi.service.component.ComponentContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Default implementation of the ConnectorRegistry which
+ * keeps a list of outgoing connectors and is capable of
+ * pinging them.
+ */
+@Component
+@Service(value = ConnectorRegistry.class)
+public class ConnectorRegistryImpl implements ConnectorRegistry {
+
+ private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+ /** A map of id-> topology connector clients currently registered/activate **/
+ private final Map<String, TopologyConnectorClient> outgoingClientsMap = new HashMap<String, TopologyConnectorClient>();
+
+ @Reference
+ private AnnouncementRegistry announcementRegistry;
+
+ @Reference
+ private BaseConfig config;
+
+ /** the local port is added to the announcement as the serverInfo object **/
+ private String port = "";
+
+ public static ConnectorRegistry testConstructor(AnnouncementRegistry announcementRegistry,
+ BaseConfig config) {
+ ConnectorRegistryImpl registry = new ConnectorRegistryImpl();
+ registry.announcementRegistry = announcementRegistry;
+ registry.config = config;
+ // Note that port is not set - but that is only for information purpose
+ // and not useful for testing
+ return registry;
+ }
+
+ @Activate
+ protected void activate(final ComponentContext cc) {
+ port = cc.getBundleContext().getProperty("org.osgi.service.http.port");
+ }
+
+ @Deactivate
+ protected void deactivate() {
+ synchronized (outgoingClientsMap) {
+ for (Iterator<TopologyConnectorClient> it = outgoingClientsMap.values().iterator(); it.hasNext();) {
+ final TopologyConnectorClient client = it.next();
+ client.disconnect();
+ it.remove();
+ }
+ }
+ }
+
+ public TopologyConnectorClientInformation registerOutgoingConnector(
+ final ClusterViewService clusterViewService, final URL connectorUrl) {
+ if (announcementRegistry == null) {
+ logger.error("registerOutgoingConnection: announcementRegistry is null");
+ return null;
+ }
+ TopologyConnectorClient client;
+ synchronized (outgoingClientsMap) {
+ for (Iterator<Entry<String, TopologyConnectorClient>> it = outgoingClientsMap
+ .entrySet().iterator(); it.hasNext();) {
+ Entry<String, TopologyConnectorClient> entry = it.next();
+ if (entry.getValue().getConnectorUrl().toExternalForm().equals(connectorUrl.toExternalForm())) {
+ it.remove();
+ logger.info("registerOutgoingConnection: re-registering connector: "+connectorUrl);
+ }
+ }
+ String serverInfo;
+ try {
+ serverInfo = InetAddress.getLocalHost().getCanonicalHostName()
+ + ":" + port;
+ } catch (Exception e) {
+ serverInfo = "localhost:" + port;
+ }
+ client = new TopologyConnectorClient(clusterViewService,
+ announcementRegistry, config, connectorUrl,
+ serverInfo);
+ outgoingClientsMap.put(client.getId(), client);
+ }
+ client.ping(false);
+ return client;
+ }
+
+ public Collection<TopologyConnectorClientInformation> listOutgoingConnectors() {
+ final List<TopologyConnectorClientInformation> result = new ArrayList<TopologyConnectorClientInformation>();
+ synchronized (outgoingClientsMap) {
+ result.addAll(outgoingClientsMap.values());
+ }
+ return result;
+ }
+
+ public boolean unregisterOutgoingConnector(final String id) {
+ if (id == null || id.length() == 0) {
+ throw new IllegalArgumentException("id must not be null");
+ }
+ synchronized (outgoingClientsMap) {
+ TopologyConnectorClient client = outgoingClientsMap.remove(id);
+ if (client != null) {
+ client.disconnect();
+ }
+ return client != null;
+ }
+ }
+
+ public void pingOutgoingConnectors(boolean force) {
+ List<TopologyConnectorClient> outgoingTemplatesClone;
+ synchronized (outgoingClientsMap) {
+ outgoingTemplatesClone = new ArrayList<TopologyConnectorClient>(
+ outgoingClientsMap.values());
+ }
+ for (Iterator<TopologyConnectorClient> it = outgoingTemplatesClone
+ .iterator(); it.hasNext();) {
+ it.next().ping(force);
+ }
+ }
+
+}
diff --git a/src/main/java/org/apache/sling/discovery/base/connectors/ping/TopologyConnectorClient.java b/src/main/java/org/apache/sling/discovery/base/connectors/ping/TopologyConnectorClient.java
new file mode 100644
index 0000000..5686ec5
--- /dev/null
+++ b/src/main/java/org/apache/sling/discovery/base/connectors/ping/TopologyConnectorClient.java
@@ -0,0 +1,496 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.sling.discovery.base.connectors.ping;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.net.URL;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.UUID;
+import java.util.zip.GZIPOutputStream;
+
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.http.Header;
+import org.apache.http.auth.AuthScope;
+import org.apache.http.auth.Credentials;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpDelete;
+import org.apache.http.client.methods.HttpPut;
+import org.apache.http.client.protocol.HttpClientContext;
+import org.apache.http.config.SocketConfig;
+import org.apache.http.entity.ByteArrayEntity;
+import org.apache.http.entity.ContentType;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.sling.commons.json.JSONException;
+import org.apache.sling.discovery.ClusterView;
+import org.apache.sling.discovery.InstanceDescription;
+import org.apache.sling.discovery.base.commons.ClusterViewService;
+import org.apache.sling.discovery.base.commons.UndefinedClusterViewException;
+import org.apache.sling.discovery.base.connectors.BaseConfig;
+import org.apache.sling.discovery.base.connectors.announcement.Announcement;
+import org.apache.sling.discovery.base.connectors.announcement.AnnouncementFilter;
+import org.apache.sling.discovery.base.connectors.announcement.AnnouncementRegistry;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A topology connector client is used for sending (pinging) a remote topology
+ * connector servlet and exchanging announcements with it
+ */
+public class TopologyConnectorClient implements
+ TopologyConnectorClientInformation {
+
+ private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+ /** the endpoint url **/
+ private final URL connectorUrl;
+
+ /** the cluster view service **/
+ private final ClusterViewService clusterViewService;
+
+ /** the config service to user **/
+ private final BaseConfig config;
+
+ /** the id of this connection **/
+ private final UUID id;
+
+ /** the announcement registry **/
+ private final AnnouncementRegistry announcementRegistry;
+
+ /** the last inherited announcement **/
+ private Announcement lastInheritedAnnouncement;
+
+ /** the time when the last announcement was inherited - for webconsole use only **/
+ private long lastPingedAt;
+
+ /** the information about this server **/
+ private final String serverInfo;
+
+ /** the status code of the last post **/
+ private int lastStatusCode = -1;
+
+ /** SLING-3316: whether or not this connector was auto-stopped **/
+ private boolean autoStopped = false;
+
+ /** more details about connection failures **/
+ private String statusDetails = null;
+
+ /** SLING-2882: whether or not to suppress ping warnings **/
+ private boolean suppressPingWarnings_ = false;
+
+ private TopologyRequestValidator requestValidator;
+
+ /** value of Content-Encoding of the last request **/
+ private String lastRequestEncoding;
+
+ /** value of Content-Encoding of the last repsonse **/
+ private String lastResponseEncoding;
+
+ /** SLING-3382: unix-time at which point the backoff-period ends and pings can be sent again **/
+ private long backoffPeriodEnd = -1;
+
+ TopologyConnectorClient(final ClusterViewService clusterViewService,
+ final AnnouncementRegistry announcementRegistry, final BaseConfig config,
+ final URL connectorUrl, final String serverInfo) {
+ if (clusterViewService == null) {
+ throw new IllegalArgumentException(
+ "clusterViewService must not be null");
+ }
+ if (announcementRegistry == null) {
+ throw new IllegalArgumentException(
+ "announcementRegistry must not be null");
+ }
+ if (config == null) {
+ throw new IllegalArgumentException("config must not be null");
+ }
+ if (connectorUrl == null) {
+ throw new IllegalArgumentException("connectorUrl must not be null");
+ }
+ this.requestValidator = new TopologyRequestValidator(config);
+ this.clusterViewService = clusterViewService;
+ this.announcementRegistry = announcementRegistry;
+ this.config = config;
+ this.connectorUrl = connectorUrl;
+ this.serverInfo = serverInfo;
+ this.id = UUID.randomUUID();
+ }
+
+ /** ping the server and pass the announcements between the two **/
+ void ping(final boolean force) {
+ if (autoStopped) {
+ // then we suppress any further pings!
+ logger.debug("ping: autoStopped=true, hence suppressing any further pings.");
+ return;
+ }
+ if (force) {
+ backoffPeriodEnd = -1;
+ } else if (backoffPeriodEnd>0) {
+ if (System.currentTimeMillis()<backoffPeriodEnd) {
+ logger.debug("ping: not issueing a heartbeat due to backoff instruction from peer.");
+ return;
+ } else {
+ logger.debug("ping: backoff period ended, issuing another ping now.");
+ }
+ }
+ final String uri = connectorUrl.toString()+"."+clusterViewService.getSlingId()+".json";
+ if (logger.isDebugEnabled()) {
+ logger.debug("ping: connectorUrl=" + connectorUrl + ", complete uri=" + uri);
+ }
+ final HttpClientContext clientContext = HttpClientContext.create();
+ final CloseableHttpClient httpClient = createHttpClient();
+ final HttpPut putRequest = new HttpPut(uri);
+
+ // setting the connection timeout (idle connection, configured in seconds)
+ putRequest.setConfig(RequestConfig.
+ custom().
+ setConnectTimeout(1000*config.getSocketConnectionTimeout()).
+ build());
+
+ Announcement resultingAnnouncement = null;
+ try {
+ String userInfo = connectorUrl.getUserInfo();
+ if (userInfo != null) {
+ Credentials c = new UsernamePasswordCredentials(userInfo);
+ clientContext.getCredentialsProvider().setCredentials(
+ new AuthScope(putRequest.getURI().getHost(), putRequest
+ .getURI().getPort()), c);
+ }
+
+ Announcement topologyAnnouncement = new Announcement(
+ clusterViewService.getSlingId());
+ topologyAnnouncement.setServerInfo(serverInfo);
+ final ClusterView clusterView;
+ try {
+ clusterView = clusterViewService
+ .getLocalClusterView();
+ } catch (UndefinedClusterViewException e) {
+ // SLING-5030 : then we cannot ping
+ logger.warn("ping: no clusterView available at the moment, cannot ping others now: "+e);
+ return;
+ }
+ topologyAnnouncement.setLocalCluster(clusterView);
+ if (force) {
+ logger.debug("ping: sending a resetBackoff");
+ topologyAnnouncement.setResetBackoff(true);
+ }
+ announcementRegistry.addAllExcept(topologyAnnouncement, clusterView, new AnnouncementFilter() {
+
+ public boolean accept(final String receivingSlingId, final Announcement announcement) {
+ // filter out announcements that are of old cluster instances
+ // which I dont really have in my cluster view at the moment
+ final Iterator<InstanceDescription> it =
+ clusterView.getInstances().iterator();
+ while(it.hasNext()) {
+ final InstanceDescription instance = it.next();
+ if (instance.getSlingId().equals(receivingSlingId)) {
+ // then I have the receiving instance in my cluster view
+ // all fine then
+ return true;
+ }
+ }
+ // looks like I dont have the receiving instance in my cluster view
+ // then I should also not propagate that announcement anywhere
+ return false;
+ }
+ });
+ final String p = requestValidator.encodeMessage(topologyAnnouncement.asJSON());
+
+ if (logger.isDebugEnabled()) {
+ logger.debug("ping: topologyAnnouncement json is: " + p);
+ }
+ requestValidator.trustMessage(putRequest, p);
+ if (config.isGzipConnectorRequestsEnabled()) {
+ // tell the server that the content is gzipped:
+ putRequest.addHeader("Content-Encoding", "gzip");
+ // and gzip the body:
+ final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ final GZIPOutputStream gzipOut = new GZIPOutputStream(baos);
+ gzipOut.write(p.getBytes("UTF-8"));
+ gzipOut.close();
+ final byte[] gzippedEncodedJson = baos.toByteArray();
+ putRequest.setEntity(new ByteArrayEntity(gzippedEncodedJson, ContentType.APPLICATION_JSON));
+ lastRequestEncoding = "gzip";
+ } else {
+ // otherwise plaintext:
+ final StringEntity plaintext = new StringEntity(p, "UTF-8");
+ plaintext.setContentType(ContentType.APPLICATION_JSON.getMimeType());
+ putRequest.setEntity(plaintext);
+ lastRequestEncoding = "plaintext";
+ }
+ // independent of request-gzipping, we do accept the response to be gzipped,
+ // so indicate this to the server:
+ putRequest.addHeader("Accept-Encoding", "gzip");
+ final CloseableHttpResponse response = httpClient.execute(putRequest, clientContext);
+ if (logger.isDebugEnabled()) {
+ logger.debug("ping: done. code=" + response.getStatusLine().getStatusCode() + " - "
+ + response.getStatusLine().getReasonPhrase());
+ }
+ lastStatusCode = response.getStatusLine().getStatusCode();
+ lastResponseEncoding = null;
+ if (response.getStatusLine().getStatusCode()==HttpServletResponse.SC_OK) {
+ final Header contentEncoding = response.getFirstHeader("Content-Encoding");
+ if (contentEncoding!=null && contentEncoding.getValue()!=null &&
+ contentEncoding.getValue().contains("gzip")) {
+ lastResponseEncoding = "gzip";
+ } else {
+ lastResponseEncoding = "plaintext";
+ }
+ final String responseBody = requestValidator.decodeMessage(putRequest.getURI().getPath(), response); // limiting to 16MB, should be way enough
+ if (logger.isDebugEnabled()) {
+ logger.debug("ping: response body=" + responseBody);
+ }
+ if (responseBody!=null && responseBody.length()>0) {
+ Announcement inheritedAnnouncement = Announcement
+ .fromJSON(responseBody);
+ final long backoffInterval = inheritedAnnouncement.getBackoffInterval();
+ if (backoffInterval>0) {
+ // then reset the backoffPeriodEnd:
+
+ /* minus 1 sec to avoid slipping the interval by a few millis */
+ this.backoffPeriodEnd = System.currentTimeMillis() + (1000 * backoffInterval) - 1000;
+ logger.debug("ping: servlet instructed to backoff: backoffInterval="+backoffInterval+", resulting in period end of "+new Date(backoffPeriodEnd));
+ } else {
+ logger.debug("ping: servlet did not instruct any backoff-ing at this stage");
+ this.backoffPeriodEnd = -1;
+ }
+ if (inheritedAnnouncement.isLoop()) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("ping: connector response indicated a loop detected. not registering this announcement from "+
+ inheritedAnnouncement.getOwnerId());
+ }
+ if (inheritedAnnouncement.getOwnerId().equals(clusterViewService.getSlingId())) {
+ // SLING-3316 : local-loop detected. Check config to see if we should stop this connector
+
+ if (config.isAutoStopLocalLoopEnabled()) {
+ inheritedAnnouncement = null; // results in connected -> false and representsloop -> true
+ autoStopped = true; // results in isAutoStopped -> true
+ }
+ }
+ } else {
+ inheritedAnnouncement.setInherited(true);
+ if (announcementRegistry
+ .registerAnnouncement(inheritedAnnouncement)==-1) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("ping: connector response is from an instance which I already see in my topology"
+ + inheritedAnnouncement);
+ }
+ statusDetails = "receiving side is seeing me via another path (connector or cluster) already (loop)";
+ return;
+ }
+ }
+ resultingAnnouncement = inheritedAnnouncement;
+ statusDetails = null;
+ } else {
+ statusDetails = "no response body received";
+ }
+ } else {
+ statusDetails = "got HTTP Status-Code: "+lastStatusCode;
+ }
+ // SLING-2882 : reset suppressPingWarnings_ flag in success case
+ suppressPingWarnings_ = false;
+ } catch (IOException e) {
+ // SLING-2882 : set/check the suppressPingWarnings_ flag
+ if (suppressPingWarnings_) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("ping: got IOException: " + e + ", uri=" + uri);
+ }
+ } else {
+ suppressPingWarnings_ = true;
+ logger.warn("ping: got IOException [suppressing further warns]: " + e + ", uri=" + uri);
+ }
+ statusDetails = e.toString();
+ } catch (JSONException e) {
+ logger.warn("ping: got JSONException: " + e);
+ statusDetails = e.toString();
+ } catch (RuntimeException re) {
+ logger.warn("ping: got RuntimeException: " + re, re);
+ statusDetails = re.toString();
+ } finally {
+ putRequest.releaseConnection();
+ lastInheritedAnnouncement = resultingAnnouncement;
+ lastPingedAt = System.currentTimeMillis();
+ try {
+ httpClient.close();
+ } catch (IOException e) {
+ logger.error("disconnect: could not close httpClient: "+e, e);
+ }
+ }
+ }
+
+ private CloseableHttpClient createHttpClient() {
+ final HttpClientBuilder builder = HttpClientBuilder.create();
+ // setting the SoTimeout (which is configured in seconds)
+ builder.setDefaultSocketConfig(SocketConfig.
+ custom().
+ setSoTimeout(1000*config.getSoTimeout()).
+ build());
+ builder.setRetryHandler(new DefaultHttpRequestRetryHandler(0, false));
+
+ return builder.build();
+ }
+
+ public int getStatusCode() {
+ return lastStatusCode;
+ }
+
+ public URL getConnectorUrl() {
+ return connectorUrl;
+ }
+
+ public boolean representsLoop() {
+ if (autoStopped) {
+ return true;
+ }
+ if (lastInheritedAnnouncement == null) {
+ return false;
+ } else {
+ return lastInheritedAnnouncement.isLoop();
+ }
+ }
+
+ public boolean isConnected() {
+ if (autoStopped) {
+ return false;
+ }
+ if (lastInheritedAnnouncement == null) {
+ return false;
+ } else {
+ return announcementRegistry.hasActiveAnnouncement(lastInheritedAnnouncement.getOwnerId());
+ }
+ }
+
+ public String getStatusDetails() {
+ if (autoStopped) {
+ return "auto-stopped";
+ }
+ if (lastInheritedAnnouncement == null) {
+ return statusDetails;
+ } else {
+ if (announcementRegistry.hasActiveAnnouncement(lastInheritedAnnouncement.getOwnerId())) {
+ // still active - so no status details
+ return null;
+ } else {
+ return "received announcement has expired (it was last renewed "+new Date(lastPingedAt)+") - consider increasing heartbeat timeout";
+ }
+ }
+ }
+
+ public long getLastPingSent() {
+ return lastPingedAt;
+ }
+
+ public int getNextPingDue() {
+ final long absDue;
+ if (backoffPeriodEnd>0) {
+ absDue = backoffPeriodEnd;
+ } else {
+ absDue = lastPingedAt + 1000*config.getConnectorPingInterval();
+ }
+ final int relDue = (int) ((absDue - System.currentTimeMillis()) / 1000);
+ if (relDue<0) {
+ return -1;
+ } else {
+ return relDue;
+ }
+ }
+
+ public boolean isAutoStopped() {
+ return autoStopped;
+ }
+
+ public String getLastRequestEncoding() {
+ return lastRequestEncoding==null ? "" : lastRequestEncoding;
+ }
+
+ public String getLastResponseEncoding() {
+ return lastResponseEncoding==null ? "" : lastResponseEncoding;
+ }
+
+ public String getRemoteSlingId() {
+ if (lastInheritedAnnouncement == null) {
+ return null;
+ } else {
+ return lastInheritedAnnouncement.getOwnerId();
+ }
+ }
+
+ public String getId() {
+ return id.toString();
+ }
+
+ /** Disconnect this connector **/
+ public void disconnect() {
+ final String uri = connectorUrl.toString()+"."+clusterViewService.getSlingId()+".json";
+ if (logger.isDebugEnabled()) {
+ logger.debug("disconnect: connectorUrl=" + connectorUrl + ", complete uri="+uri);
+ }
+
+ if (lastInheritedAnnouncement != null) {
+ announcementRegistry
+ .unregisterAnnouncement(lastInheritedAnnouncement
+ .getOwnerId());
+ }
+
+ final HttpClientContext clientContext = HttpClientContext.create();
+ final CloseableHttpClient httpClient = createHttpClient();
+ final HttpDelete deleteRequest = new HttpDelete(uri);
+ // setting the connection timeout (idle connection, configured in seconds)
+ deleteRequest.setConfig(RequestConfig.
+ custom().
+ setConnectTimeout(1000*config.getSocketConnectionTimeout()).
+ build());
+
+ try {
+ String userInfo = connectorUrl.getUserInfo();
+ if (userInfo != null) {
+ Credentials c = new UsernamePasswordCredentials(userInfo);
+ clientContext.getCredentialsProvider().setCredentials(
+ new AuthScope(deleteRequest.getURI().getHost(), deleteRequest
+ .getURI().getPort()), c);
+ }
+
+ requestValidator.trustMessage(deleteRequest, null);
+ final CloseableHttpResponse response = httpClient.execute(deleteRequest, clientContext);
+ if (logger.isDebugEnabled()) {
+ logger.debug("disconnect: done. code=" + response.getStatusLine().getStatusCode()
+ + " - " + response.getStatusLine().getReasonPhrase());
+ }
+ // ignoring the actual statuscode though as there's little we can
+ // do about it after this point
+ } catch (IOException e) {
+ logger.warn("disconnect: got IOException: " + e);
+ } catch (RuntimeException re) {
+ logger.error("disconnect: got RuntimeException: " + re, re);
+ } finally {
+ deleteRequest.releaseConnection();
+ try {
+ httpClient.close();
+ } catch (IOException e) {
+ logger.error("disconnect: could not close httpClient: "+e, e);
+ }
+ }
+ }
+}
diff --git a/src/main/java/org/apache/sling/discovery/base/connectors/ping/TopologyConnectorClientInformation.java b/src/main/java/org/apache/sling/discovery/base/connectors/ping/TopologyConnectorClientInformation.java
new file mode 100644
index 0000000..e410c73
--- /dev/null
+++ b/src/main/java/org/apache/sling/discovery/base/connectors/ping/TopologyConnectorClientInformation.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.sling.discovery.base.connectors.ping;
+
+import java.net.URL;
+
+/**
+ * provides information about a topology connector client
+ */
+public interface TopologyConnectorClientInformation {
+
+ /** the endpoint url where this connector is connecting to **/
+ URL getConnectorUrl();
+
+ /** return the http status code of the last post to the servlet, -1 if no post was ever done **/
+ int getStatusCode();
+
+ /** SLING-3316 : whether or not this connector was auto-stopped **/
+ boolean isAutoStopped();
+
+ /** whether or not this connector was able to successfully connect **/
+ boolean isConnected();
+
+ /** provides more details about connection failures **/
+ String getStatusDetails();
+
+ /** whether or not the counterpart of this connector has detected a loop in the topology connectors **/
+ boolean representsLoop();
+
+ /** the sling id of the remote end **/
+ String getRemoteSlingId();
+
+ /** the unique id of this connector **/
+ String getId();
+
+ /** the Content-Encoding of the last request **/
+ String getLastRequestEncoding();
+
+ /** the Content-Encoding of the last response **/
+ String getLastResponseEncoding();
+
+ /** the unix-millis when the last heartbeat was sent **/
+ long getLastPingSent();
+
+ /** the seconds until the next heartbeat is due **/
+ int getNextPingDue();
+}
diff --git a/src/main/java/org/apache/sling/discovery/base/connectors/ping/TopologyConnectorServlet.java b/src/main/java/org/apache/sling/discovery/base/connectors/ping/TopologyConnectorServlet.java
new file mode 100644
index 0000000..4211cf3
--- /dev/null
+++ b/src/main/java/org/apache/sling/discovery/base/connectors/ping/TopologyConnectorServlet.java
@@ -0,0 +1,360 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.sling.discovery.base.connectors.ping;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+import java.util.StringTokenizer;
+import java.util.zip.GZIPOutputStream;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Deactivate;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.Service;
+import org.apache.sling.commons.json.JSONException;
+import org.apache.sling.discovery.ClusterView;
+import org.apache.sling.discovery.base.commons.ClusterViewHelper;
+import org.apache.sling.discovery.base.commons.ClusterViewService;
+import org.apache.sling.discovery.base.commons.UndefinedClusterViewException;
+import org.apache.sling.discovery.base.connectors.BaseConfig;
+import org.apache.sling.discovery.base.connectors.announcement.Announcement;
+import org.apache.sling.discovery.base.connectors.announcement.AnnouncementFilter;
+import org.apache.sling.discovery.base.connectors.announcement.AnnouncementRegistry;
+import org.apache.sling.discovery.base.connectors.ping.wl.SubnetWhitelistEntry;
+import org.apache.sling.discovery.base.connectors.ping.wl.WhitelistEntry;
+import org.apache.sling.discovery.base.connectors.ping.wl.WildcardWhitelistEntry;
+import org.osgi.service.component.ComponentContext;
+import org.osgi.service.http.HttpService;
+import org.osgi.service.http.NamespaceException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Servlet which receives topology announcements at
+ * /libs/sling/topology/connector*
+ * without authorization (authorization is handled either via
+ * hmac-signature with a shared key or via a flexible whitelist)
+ */
+@SuppressWarnings("serial")
+@Component(immediate = true)
+@Service(value=TopologyConnectorServlet.class)
+public class TopologyConnectorServlet extends HttpServlet {
+
+ /**
+ * prefix under which the topology connector servlet is registered -
+ * the URL will consist of this prefix + "connector.slingId.json"
+ */
+ private static final String TOPOLOGY_CONNECTOR_PREFIX = "/libs/sling/topology";
+
+ private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+ @Reference
+ private AnnouncementRegistry announcementRegistry;
+
+ @Reference
+ private ClusterViewService clusterViewService;
+
+ @Reference
+ private HttpService httpService;
+
+ @Reference
+ private BaseConfig config;
+
+ /**
+ * This list contains WhitelistEntry (ips/hostnames, cidr, wildcards),
+ * each filtering some hostname/addresses that are allowed to connect to this servlet.
+ **/
+ private final List<WhitelistEntry> whitelist = new ArrayList<WhitelistEntry>();
+
+ /** Set of plaintext whitelist entries - for faster lookups **/
+ private final Set<String> plaintextWhitelist = new HashSet<String>();
+
+ private TopologyRequestValidator requestValidator;
+
+ @Activate
+ protected void activate(final ComponentContext context) {
+ whitelist.clear();
+ if (!config.isHmacEnabled()) {
+ String[] whitelistConfig = config.getTopologyConnectorWhitelist();
+ initWhitelist(whitelistConfig);
+ }
+ requestValidator = new TopologyRequestValidator(config);
+
+ try {
+ httpService.registerServlet(TopologyConnectorServlet.TOPOLOGY_CONNECTOR_PREFIX,
+ this, null, null);
+ logger.info("activate: connector servlet registered at "+
+ TopologyConnectorServlet.TOPOLOGY_CONNECTOR_PREFIX);
+ } catch (ServletException e) {
+ logger.error("activate: ServletException while registering topology connector servlet: "+e, e);
+ } catch (NamespaceException e) {
+ logger.error("activate: NamespaceException while registering topology connector servlet: "+e, e);
+ }
+ }
+
+ @Deactivate
+ protected void deactivate() {
+ httpService.unregister(TOPOLOGY_CONNECTOR_PREFIX);
+ }
+
+ void initWhitelist(String[] whitelistConfig) {
+ if (whitelistConfig==null) {
+ return;
+ }
+ for (int i = 0; i < whitelistConfig.length; i++) {
+ String aWhitelistEntry = whitelistConfig[i];
+
+ WhitelistEntry whitelistEntry = null;
+ if (aWhitelistEntry.contains(".") && aWhitelistEntry.contains("/")) {
+ // then this is a CIDR notation
+ try{
+ whitelistEntry = new SubnetWhitelistEntry(aWhitelistEntry);
+ } catch(Exception e) {
+ logger.error("activate: wrongly formatted CIDR subnet definition. Expected eg '1.2.3.4/24'. ignoring: "+aWhitelistEntry);
+ continue;
+ }
+ } else if (aWhitelistEntry.contains(".") && aWhitelistEntry.contains(" ")) {
+ // then this is a IP/subnet-mask notation
+ try{
+ final StringTokenizer st = new StringTokenizer(aWhitelistEntry, " ");
+ final String ip = st.nextToken();
+ if (st.hasMoreTokens()) {
+ final String mask = st.nextToken();
+ if (st.hasMoreTokens()) {
+ logger.error("activate: wrongly formatted ip subnet definition. Expected '10.1.2.3 255.0.0.0'. Ignoring: "+aWhitelistEntry);
+ continue;
+ }
+ whitelistEntry = new SubnetWhitelistEntry(ip, mask);
+ }
+ } catch(Exception e) {
+ logger.error("activate: wrongly formatted ip subnet definition. Expected '10.1.2.3 255.0.0.0'. Ignoring: "+aWhitelistEntry);
+ continue;
+ }
+ }
+ if (whitelistEntry==null) {
+ if (aWhitelistEntry.contains("*") || aWhitelistEntry.contains("?")) {
+ whitelistEntry = new WildcardWhitelistEntry(aWhitelistEntry);
+ } else {
+ plaintextWhitelist.add(aWhitelistEntry);
+ }
+ }
+ logger.info("activate: adding whitelist entry: " + aWhitelistEntry);
+ if (whitelistEntry!=null) {
+ whitelist.add(whitelistEntry);
+ }
+ }
+ }
+
+ @Override
+ protected void doDelete(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+
+ if (!isWhitelisted(request)) {
+ // in theory it would be 403==forbidden, but that would reveal that
+ // a resource would exist there in the first place
+ response.sendError(HttpServletResponse.SC_NOT_FOUND);
+ return;
+ }
+
+ final String[] pathInfo = request.getPathInfo().split("\\.");
+ final String extension = pathInfo.length==3 ? pathInfo[2] : "";
+ if (!"json".equals(extension)) {
+ response.sendError(HttpServletResponse.SC_NOT_FOUND);
+ return;
+ }
+ final String selector = pathInfo.length==3 ? pathInfo[1] : "";
+
+ announcementRegistry.unregisterAnnouncement(selector);
+ }
+
+ @Override
+ protected void doPut(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+
+ if (!isWhitelisted(request)) {
+ // in theory it would be 403==forbidden, but that would reveal that
+ // a resource would exist there in the first place
+ response.sendError(HttpServletResponse.SC_NOT_FOUND);
+ return;
+ }
+
+ final String[] pathInfo = request.getPathInfo().split("\\.");
+ final String extension = pathInfo.length==3 ? pathInfo[2] : "";
+ if (!"json".equals(extension)) {
+ response.sendError(HttpServletResponse.SC_NOT_FOUND);
+ return;
+ }
+
+ final String selector = pathInfo.length==3 ? pathInfo[1] : "";
+
+ String topologyAnnouncementJSON = requestValidator.decodeMessage(request);
+ if (logger.isDebugEnabled()) {
+ logger.debug("doPost: incoming topology announcement is: "
+ + topologyAnnouncementJSON);
+ }
+ final Announcement incomingTopologyAnnouncement;
+ try {
+ incomingTopologyAnnouncement = Announcement
+ .fromJSON(topologyAnnouncementJSON);
+
+ if (!incomingTopologyAnnouncement.getOwnerId().equals(selector)) {
+ response.sendError(HttpServletResponse.SC_BAD_REQUEST);
+ return;
+ }
+
+ String slingId = clusterViewService.getSlingId();
+ if (slingId==null) {
+ response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+ logger.info("doPut: no slingId available. Service not ready as expected at the moment.");
+ return;
+ }
+ incomingTopologyAnnouncement.removeInherited(slingId);
+
+ final Announcement replyAnnouncement = new Announcement(
+ slingId);
+
+ long backoffInterval = -1;
+ ClusterView clusterView = clusterViewService.getLocalClusterView();
+ if (!incomingTopologyAnnouncement.isCorrectVersion()) {
+ logger.warn("doPost: rejecting an announcement from an incompatible connector protocol version: "
+ + incomingTopologyAnnouncement);
+ response.sendError(HttpServletResponse.SC_BAD_REQUEST);
+ return;
+ } else if (ClusterViewHelper.contains(clusterView, incomingTopologyAnnouncement
+ .getOwnerId())) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("doPost: rejecting an announcement from an instance that is part of my cluster: "
+ + incomingTopologyAnnouncement);
+ }
+ // marking as 'loop'
+ replyAnnouncement.setLoop(true);
+ backoffInterval = config.getBackoffStandbyInterval();
+ } else if (ClusterViewHelper.containsAny(clusterView, incomingTopologyAnnouncement
+ .listInstances())) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("doPost: rejecting an announcement as it contains instance(s) that is/are part of my cluster: "
+ + incomingTopologyAnnouncement);
+ }
+ // marking as 'loop'
+ replyAnnouncement.setLoop(true);
+ backoffInterval = config.getBackoffStandbyInterval();
+ } else {
+ backoffInterval = announcementRegistry
+ .registerAnnouncement(incomingTopologyAnnouncement);
+ if (logger.isDebugEnabled()) {
+ logger.debug("doPost: backoffInterval after registration: "+backoffInterval);
+ }
+ if (backoffInterval==-1) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("doPost: rejecting an announcement from an instance that I already see in my topology: "
+ + incomingTopologyAnnouncement);
+ }
+ // marking as 'loop'
+ replyAnnouncement.setLoop(true);
+ backoffInterval = config.getBackoffStandbyInterval();
+ } else {
+ // normal, successful case: replying with the part of the topology which this instance sees
+ replyAnnouncement.setLocalCluster(clusterView);
+ announcementRegistry.addAllExcept(replyAnnouncement, clusterView,
+ new AnnouncementFilter() {
+
+ public boolean accept(final String receivingSlingId, Announcement announcement) {
+ if (announcement.getPrimaryKey().equals(
+ incomingTopologyAnnouncement
+ .getPrimaryKey())) {
+ return false;
+ }
+ return true;
+ }
+ });
+ }
+ }
+ if (backoffInterval>0) {
+ replyAnnouncement.setBackoffInterval(backoffInterval);
+ if (logger.isDebugEnabled()) {
+ logger.debug("doPost: backoffInterval for client set to "+replyAnnouncement.getBackoffInterval());
+ }
+ }
+ final String p = requestValidator.encodeMessage(replyAnnouncement.asJSON());
+ requestValidator.trustMessage(response, request, p);
+ // gzip the response if the client accepts this
+ final String acceptEncodingHeader = request.getHeader("Accept-Encoding");
+ if (acceptEncodingHeader!=null && acceptEncodingHeader.contains("gzip")) {
+ // tell the client that the content is gzipped:
+ response.setHeader("Content-Encoding", "gzip");
+
+ // then gzip the body
+ final GZIPOutputStream gzipOut = new GZIPOutputStream(response.getOutputStream());
+ gzipOut.write(p.getBytes("UTF-8"));
+ gzipOut.close();
+ } else {
+ // otherwise plaintext
+ final PrintWriter pw = response.getWriter();
+ pw.print(p);
+ pw.flush();
+ }
+ } catch (JSONException e) {
+ logger.error("doPost: Got a JSONException: " + e, e);
+ response.sendError(500);
+ } catch (UndefinedClusterViewException e) {
+ logger.warn("doPost: no clusterView available at the moment - cannot handle connectors now: "+e);
+ response.sendError(503); // "please retry, but atm I can't help since I'm isolated"
+ }
+
+ }
+
+ /** Checks if the provided request's remote server is whitelisted **/
+ boolean isWhitelisted(final HttpServletRequest request) {
+ if (config.isHmacEnabled()) {
+ final boolean isTrusted = requestValidator.isTrusted(request);
+ if (!isTrusted) {
+ logger.info("isWhitelisted: rejecting distrusted " + request.getRemoteAddr()
+ + ", " + request.getRemoteHost());
+ }
+ return isTrusted;
+ }
+
+ if (plaintextWhitelist.contains(request.getRemoteHost()) ||
+ plaintextWhitelist.contains(request.getRemoteAddr())) {
+ return true;
+ }
+
+ for (Iterator<WhitelistEntry> it = whitelist.iterator(); it.hasNext();) {
+ WhitelistEntry whitelistEntry = it.next();
+ if (whitelistEntry.accepts(request)) {
+ return true;
+ }
+ }
+ logger.info("isWhitelisted: rejecting " + request.getRemoteAddr()
+ + ", " + request.getRemoteHost());
+ return false;
+ }
+
+}
diff --git a/src/main/java/org/apache/sling/discovery/base/connectors/ping/TopologyRequestValidator.java b/src/main/java/org/apache/sling/discovery/base/connectors/ping/TopologyRequestValidator.java
new file mode 100644
index 0000000..fee273c
--- /dev/null
+++ b/src/main/java/org/apache/sling/discovery/base/connectors/ping/TopologyRequestValidator.java
@@ -0,0 +1,585 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF 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.apache.sling.discovery.base.connectors.ping;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.security.AlgorithmParameters;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.Key;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.InvalidParameterSpecException;
+import java.security.spec.KeySpec;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.zip.GZIPInputStream;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.Mac;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.PBEKeySpec;
+import javax.crypto.spec.SecretKeySpec;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.io.IOUtils;
+import org.apache.http.Header;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.sling.commons.json.JSONArray;
+import org.apache.sling.commons.json.JSONException;
+import org.apache.sling.commons.json.JSONObject;
+import org.apache.sling.discovery.base.connectors.BaseConfig;
+
+/**
+ * Request Validator helper.
+ */
+public class TopologyRequestValidator {
+
+ public static final String SIG_HEADER = "X-SlingTopologyTrust";
+
+ public static final String HASH_HEADER = "X-SlingTopologyHash";
+
+ /**
+ * Maximum number of keys to keep in memory.
+ */
+ private static final int MAXKEYS = 5;
+
+ /**
+ * Minimum number of keys to keep in memory.
+ */
+ private static final int MINKEYS = 3;
+
+ /**
+ * true if trust information should be in request headers.
+ */
+ private boolean trustEnabled;
+
+ /**
+ * true if encryption of the message payload should be encrypted.
+ */
+ private boolean encryptionEnabled;
+
+ /**
+ * map of hmac keys, keyed by key number.
+ */
+ private Map<Integer, Key> keys = new ConcurrentHashMap<Integer, Key>();
+
+ /**
+ * The shared key.
+ */
+ private String sharedKey;
+
+ /**
+ * TTL of each shared key generation.
+ */
+ private long interval;
+
+ /**
+ * If true, everything is deactivated.
+ */
+ private boolean deactivated;
+
+ private SecureRandom random = new SecureRandom();
+
+ /**
+ * Create a TopologyRequestValidator.
+ *
+ * @param config the configuation object
+ */
+ public TopologyRequestValidator(BaseConfig config) {
+ trustEnabled = false;
+ encryptionEnabled = false;
+ if (config.isHmacEnabled()) {
+ trustEnabled = true;
+ sharedKey = config.getSharedKey();
+ interval = config.getKeyInterval();
+ encryptionEnabled = config.isEncryptionEnabled();
+ }
+ deactivated = false;
+ }
+
+ /**
+ * Encodes a request returning the encoded body
+ *
+ * @param body
+ * @return the encoded body.
+ * @throws IOException
+ */
+ public String encodeMessage(String body) throws IOException {
+ checkActive();
+ if (encryptionEnabled) {
+ try {
+ JSONObject json = new JSONObject();
+ json.put("payload", new JSONArray(encrypt(body)));
+ return json.toString();
+ } catch (InvalidKeyException e) {
+ e.printStackTrace();
+ throw new IOException("Unable to Encrypt Message " + e.getMessage());
+ } catch (IllegalBlockSizeException e) {
+ throw new IOException("Unable to Encrypt Message " + e.getMessage());
+ } catch (BadPaddingException e) {
+ throw new IOException("Unable to Encrypt Message " + e.getMessage());
+ } catch (UnsupportedEncodingException e) {
+ throw new IOException("Unable to Encrypt Message " + e.getMessage());
+ } catch (NoSuchAlgorithmException e) {
+ throw new IOException("Unable to Encrypt Message " + e.getMessage());
+ } catch (NoSuchPaddingException e) {
+ throw new IOException("Unable to Encrypt Message " + e.getMessage());
+ } catch (JSONException e) {
+ throw new IOException("Unable to Encrypt Message " + e.getMessage());
+ } catch (InvalidKeySpecException e) {
+ throw new IOException("Unable to Encrypt Message " + e.getMessage());
+ } catch (InvalidParameterSpecException e) {
+ throw new IOException("Unable to Encrypt Message " + e.getMessage());
+ }
+
+ }
+ return body;
+ }
+
+ /**
+ * Decode a message sent from the client.
+ *
+ * @param request the request object for the message.
+ * @return the message in clear text.
+ * @throws IOException if there is a problem decoding the message or the
+ * message is invalid.
+ */
+ public String decodeMessage(HttpServletRequest request) throws IOException {
+ checkActive();
+ return decodeMessage("request:", request.getRequestURI(), getRequestBody(request),
+ request.getHeader(HASH_HEADER));
+ }
+
+ /**
+ * Decode a response from the server.
+ *
+ * @param response the response.
+ * @return the message in clear text.
+ * @throws IOException if there was a problem decoding the message.
+ */
+ public String decodeMessage(String uri, HttpResponse response) throws IOException {
+ checkActive();
+ return decodeMessage("response:", uri, getResponseBody(response),
+ getResponseHeader(response, HASH_HEADER));
+ }
+
+ /**
+ * Decode a message
+ *
+ * @param prefix the prefix to indicate if the message is a request or
+ * response message.
+ * @param url the url associated with the message.
+ * @param body the body of the message.
+ * @param requestHash a hash of the message.
+ * @return the message in clear text
+ * @throws IOException if the message can't be decrypted.
+ */
+ private String decodeMessage(String prefix, String url, String body, String requestHash)
+ throws IOException {
+ if (trustEnabled) {
+ String bodyHash = hash(prefix + url + ":" + body);
+ if (bodyHash.equals(requestHash)) {
+ if (encryptionEnabled) {
+ try {
+ JSONObject json = new JSONObject(body);
+ if (json.has("payload")) {
+ return decrypt(json.getJSONArray("payload"));
+ }
+ } catch (JSONException e) {
+ throw new IOException("Encrypted Message is in the correct json format");
+ } catch (InvalidKeyException e) {
+ throw new IOException("Encrypted Message is in the correct json format");
+ } catch (IllegalBlockSizeException e) {
+ throw new IOException("Encrypted Message is in the correct json format");
+ } catch (BadPaddingException e) {
+ throw new IOException("Encrypted Message is in the correct json format");
+ } catch (NoSuchAlgorithmException e) {
+ throw new IOException("Encrypted Message is in the correct json format");
+ } catch (NoSuchPaddingException e) {
+ throw new IOException("Encrypted Message is in the correct json format");
+ } catch (InvalidAlgorithmParameterException e) {
+ throw new IOException("Encrypted Message is in the correct json format");
+ } catch (InvalidKeySpecException e) {
+ throw new IOException("Encrypted Message is in the correct json format");
+ }
+
+ }
+ }
+ throw new IOException("Message is not valid, hash does not match message");
+ }
+ return body;
+ }
+
+ /**
+ * Is the request from the client trusted, based on the signature headers.
+ *
+ * @param request the request.
+ * @return true if trusted, or true if this component is disabled.
+ */
+ public boolean isTrusted(HttpServletRequest request) {
+ checkActive();
+ if (trustEnabled) {
+ return checkTrustHeader(request.getHeader(HASH_HEADER),
+ request.getHeader(SIG_HEADER));
+ }
+ return false;
+ }
+
+ /**
+ * Is the response from the server to be trusted by the client.
+ *
+ * @param response the response
+ * @return true if trusted, or true if this component is disabled.
+ */
+ public boolean isTrusted(HttpResponse response) {
+ checkActive();
+ if (trustEnabled) {
+ return checkTrustHeader(getResponseHeader(response, HASH_HEADER),
+ getResponseHeader(response, SIG_HEADER));
+ }
+ return false;
+ }
+
+ /**
+ * Trust a message on the client before sending, only if trust is enabled.
+ *
+ * @param method the method which will have headers set after the call.
+ * @param body the body.
+ */
+ public void trustMessage(HttpUriRequest method, String body) {
+ checkActive();
+ if (trustEnabled) {
+ String bodyHash = hash("request:" + method.getURI().getPath() + ":" + body);
+ method.setHeader(HASH_HEADER, bodyHash);
+ method.setHeader(SIG_HEADER, createTrustHeader(bodyHash));
+ }
+ }
+
+ /**
+ * Trust a response message sent from the server to the client.
+ *
+ * @param response the response.
+ * @param request the request,
+ * @param body body of the response.
+ */
+ public void trustMessage(HttpServletResponse response, HttpServletRequest request, String body) {
+ checkActive();
+ if (trustEnabled) {
+ String bodyHash = hash("response:" + request.getRequestURI() + ":" + body);
+ response.setHeader(HASH_HEADER, bodyHash);
+ response.setHeader(SIG_HEADER, createTrustHeader(bodyHash));
+ }
+ }
+
+ /**
+ * @param body
+ * @return a hash of body base64 encoded.
+ */
+ private String hash(String toHash) {
+ try {
+ MessageDigest m = MessageDigest.getInstance("SHA-256");
+ return new String(Base64.encodeBase64(m.digest(toHash.getBytes("UTF-8"))), "UTF-8");
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException(e.getMessage(), e);
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException(e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Generate a signature of the bodyHash and encode it so that it contains
+ * the key number used to generate the signature.
+ *
+ * @param bodyHash a hash
+ * @return the signature.
+ */
+ private String createTrustHeader(String bodyHash) {
+ try {
+ int keyNo = getCurrentKey();
+ return keyNo + "/" + hmac(keyNo, bodyHash);
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException(e.getMessage(), e);
+ } catch (InvalidKeyException e) {
+ throw new RuntimeException(e.getMessage(), e);
+ } catch (IllegalStateException e) {
+ throw new RuntimeException(e.getMessage(), e);
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException(e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Check that the signature is a signature of the body hash.
+ *
+ * @param bodyHash the body hash.
+ * @param signature the signature.
+ * @return true if the signature can be trusted.
+ */
+ private boolean checkTrustHeader(String bodyHash, String signature) {
+ try {
+ if (bodyHash == null || signature == null ) {
+ return false;
+ }
+ String[] parts = signature.split("/", 2);
+ int keyNo = Integer.parseInt(parts[0]);
+ return hmac(keyNo, bodyHash).equals(parts[1]);
+ } catch (ArrayIndexOutOfBoundsException e) {
+ return false;
+ } catch (IllegalArgumentException e) {
+ return false;
+ } catch (InvalidKeyException e) {
+ throw new RuntimeException(e.getMessage(), e);
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException(e.getMessage(), e);
+ } catch (IllegalStateException e) {
+ throw new RuntimeException(e.getMessage(), e);
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException(e.getMessage(), e);
+ } catch (Exception e) {
+ throw new RuntimeException(e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Get a Mac instance for the key number.
+ *
+ * @param keyNo the key number.
+ * @return the mac instance.
+ * @throws NoSuchAlgorithmException
+ * @throws InvalidKeyException
+ * @throws UnsupportedEncodingException
+ */
+ private Mac getMac(int keyNo) throws NoSuchAlgorithmException, InvalidKeyException,
+ UnsupportedEncodingException {
+ Mac m = Mac.getInstance("HmacSHA256");
+ m.init(getKey(keyNo));
+ return m;
+ }
+
+ /**
+ * Perform a HMAC on the body using the key specified.
+ *
+ * @param keyNo the key number.
+ * @param bodyHash a hash of the body.
+ * @return the hmac signature.
+ * @throws InvalidKeyException
+ * @throws UnsupportedEncodingException
+ * @throws IllegalStateException
+ * @throws NoSuchAlgorithmException
+ */
+ private String hmac(int keyNo, String bodyHash) throws InvalidKeyException,
+ UnsupportedEncodingException, IllegalStateException, NoSuchAlgorithmException {
+ return new String(Base64.encodeBase64(getMac(keyNo).doFinal(bodyHash.getBytes("UTF-8"))),
+ "UTF-8");
+ }
+
+ /**
+ * Decrypt the body.
+ *
+ * @param jsonArray the encrypted payload
+ * @return the decrypted payload.
+ * @throws IllegalBlockSizeException
+ * @throws BadPaddingException
+ * @throws UnsupportedEncodingException
+ * @throws InvalidKeyException
+ * @throws NoSuchAlgorithmException
+ * @throws NoSuchPaddingException
+ * @throws InvalidKeySpecException
+ * @throws InvalidAlgorithmParameterException
+ * @throws JSONException
+ */
+ private String decrypt(JSONArray jsonArray) throws IllegalBlockSizeException,
+ BadPaddingException, UnsupportedEncodingException, InvalidKeyException,
+ NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException, InvalidKeySpecException, JSONException {
+ Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
+ cipher.init(Cipher.DECRYPT_MODE, getCiperKey(Base64.decodeBase64(jsonArray.getString(0).getBytes("UTF-8"))), new IvParameterSpec(Base64.decodeBase64(jsonArray.getString(1).getBytes("UTF-8"))));
+ return new String(cipher.doFinal(Base64.decodeBase64(jsonArray.getString(2).getBytes("UTF-8"))));
+ }
+
+ /**
+ * Encrypt a payload with the numbed key/
+ *
+ * @param payload the payload.
+ * @param keyNo the key number.
+ * @return an encrypted version.
+ * @throws IllegalBlockSizeException
+ * @throws BadPaddingException
+ * @throws UnsupportedEncodingException
+ * @throws InvalidKeyException
+ * @throws NoSuchAlgorithmException
+ * @throws NoSuchPaddingException
+ * @throws InvalidKeySpecException
+ * @throws InvalidParameterSpecException
+ */
+ private List<String> encrypt(String payload) throws IllegalBlockSizeException,
+ BadPaddingException, UnsupportedEncodingException, InvalidKeyException,
+ NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeySpecException, InvalidParameterSpecException {
+ Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
+ byte[] salt = new byte[9];
+ random.nextBytes(salt);
+ cipher.init(Cipher.ENCRYPT_MODE, getCiperKey(salt));
+ AlgorithmParameters params = cipher.getParameters();
+ List<String> encrypted = new ArrayList<String>();
+ encrypted.add(new String(Base64.encodeBase64(salt)));
+ encrypted.add(new String(Base64.encodeBase64(params.getParameterSpec(IvParameterSpec.class).getIV())));
+ encrypted.add(new String(Base64.encodeBase64(cipher.doFinal(payload.getBytes("UTF-8")))));
+ return encrypted;
+ }
+
+ /**
+ * @param salt number of the key.
+ * @return the CupherKey.
+ * @throws UnsupportedEncodingException
+ * @throws NoSuchAlgorithmException
+ * @throws InvalidKeySpecException
+ */
+ private Key getCiperKey(byte[] salt) throws UnsupportedEncodingException, NoSuchAlgorithmException, InvalidKeySpecException {
+ SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
+ // hashing the password 65K times takes 151ms, hashing 256 times takes 2ms.
+ // Since the salt has 2^^72 values, 256 times is probably good enough.
+ KeySpec spec = new PBEKeySpec(sharedKey.toCharArray(), salt, 256, 128);
+ SecretKey tmp = factory.generateSecret(spec);
+ SecretKey key = new SecretKeySpec(tmp.getEncoded(), "AES");
+ return key;
+ }
+
+ /**
+ * @param keyNo number of the key.
+ * @return the HMac key.
+ * @throws UnsupportedEncodingException
+ */
+ private Key getKey(int keyNo) throws UnsupportedEncodingException {
+ if(Math.abs(keyNo - getCurrentKey()) > 1 ) {
+ throw new IllegalArgumentException("Key has expired");
+ }
+ if (keys.containsKey(keyNo)) {
+ return keys.get(keyNo);
+ }
+ trimKeys();
+ SecretKeySpec key = new SecretKeySpec(hash(sharedKey + keyNo).getBytes("UTF-8"),
+ "HmacSHA256");
+ keys.put(keyNo, key);
+ return key;
+ }
+
+ private int getCurrentKey() {
+ return (int) (System.currentTimeMillis() / interval);
+ }
+
+ /**
+ * dump olf keys.
+ */
+ private void trimKeys() {
+ if (keys.size() > MAXKEYS) {
+ List<Integer> keysKeys = new ArrayList<Integer>(keys.keySet());
+ Collections.sort(keysKeys);
+ for (Integer k : keysKeys) {
+ if (keys.size() < MINKEYS) {
+ break;
+ }
+ keys.remove(k);
+ }
+ }
+
+ }
+
+ /**
+ * Get the value of a response header.
+ *
+ * @param response the response
+ * @param name the name of the response header.
+ * @return the value of the response header, null if none.
+ */
+ private String getResponseHeader(HttpResponse response, String name) {
+ Header h = response.getFirstHeader(name);
+ if (h == null) {
+ return null;
+ }
+ return h.getValue();
+ }
+
+ /**
+ * Get the request body.
+ *
+ * @param request the request.
+ * @return the body as a string.
+ * @throws IOException
+ */
+ private String getRequestBody(HttpServletRequest request) throws IOException {
+ final String contentEncoding = request.getHeader("Content-Encoding");
+ if (contentEncoding!=null && contentEncoding.contains("gzip")) {
+ // then treat the request body as gzip:
+ final GZIPInputStream gzipIn = new GZIPInputStream(request.getInputStream());
+ final String gunzippedEncodedJson = IOUtils.toString(gzipIn);
+ gzipIn.close();
+ return gunzippedEncodedJson;
+ } else {
+ // otherwise assume plain-text:
+ return IOUtils.toString(request.getReader());
+ }
+ }
+
+ /**
+ * @param response the response
+ * @return the body of the response from the server.
+ * @throws IOException
+ */
+ private String getResponseBody(HttpResponse response) throws IOException {
+ final Header contentEncoding = response.getFirstHeader("Content-Encoding");
+ if (contentEncoding!=null && contentEncoding.getValue()!=null &&
+ contentEncoding.getValue().contains("gzip")) {
+ // then the server sent gzip - treat it so:
+ final GZIPInputStream gzipIn = new GZIPInputStream(response.getEntity().getContent());
+ final String gunzippedEncodedJson = IOUtils.toString(gzipIn);
+ gzipIn.close();
+ return gunzippedEncodedJson;
+ } else {
+ // otherwise the server sent plaintext:
+ return IOUtils.toString(response.getEntity().getContent(), "UTF-8");
+ }
+ }
+
+ /**
+ * throw an exception if not active.
+ */
+ private void checkActive() {
+ if (deactivated) {
+ throw new IllegalStateException(this.getClass().getName() + " is not active");
+ }
+ if ((trustEnabled || encryptionEnabled) && sharedKey == null) {
+ throw new IllegalStateException(this.getClass().getName()
+ + " Shared Key must be set if encryption or signing is enabled.");
+ }
+ }
+
+}
diff --git a/src/main/java/org/apache/sling/discovery/base/connectors/ping/package-info.java b/src/main/java/org/apache/sling/discovery/base/connectors/ping/package-info.java
new file mode 100644
index 0000000..c1aa7e0
--- /dev/null
+++ b/src/main/java/org/apache/sling/discovery/base/connectors/ping/package-info.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.
+ */
+
+/**
+ * Provides topology connector implementations for discovery
+ * implementors that choose to extend from discovery.base
+ *
+ * @version 1.0.0
+ */
+@Version("1.0.0")
+package org.apache.sling.discovery.base.connectors.ping;
+
+import aQute.bnd.annotation.Version;
+
diff --git a/src/main/java/org/apache/sling/discovery/base/connectors/ping/wl/SubnetWhitelistEntry.java b/src/main/java/org/apache/sling/discovery/base/connectors/ping/wl/SubnetWhitelistEntry.java
new file mode 100644
index 0000000..7dae377
--- /dev/null
+++ b/src/main/java/org/apache/sling/discovery/base/connectors/ping/wl/SubnetWhitelistEntry.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.sling.discovery.base.connectors.ping.wl;
+
+import javax.servlet.ServletRequest;
+
+import org.apache.commons.net.util.SubnetUtils;
+import org.apache.commons.net.util.SubnetUtils.SubnetInfo;
+
+/**
+ * Implementation of a WhitelistEntry which accepts
+ * cidr and ip mask notations.
+ */
+public class SubnetWhitelistEntry implements WhitelistEntry {
+
+ private final SubnetInfo subnetInfo;
+
+ public SubnetWhitelistEntry(String cidrNotation) {
+ subnetInfo = new SubnetUtils(cidrNotation).getInfo();
+ }
+
+ public SubnetWhitelistEntry(String ip, String subnetMask) {
+ subnetInfo = new SubnetUtils(ip, subnetMask).getInfo();
+ }
+
+ public boolean accepts(ServletRequest request) {
+ final String remoteAddr = request.getRemoteAddr();
+ return subnetInfo.isInRange(remoteAddr);
+ }
+
+}
diff --git a/src/main/java/org/apache/sling/discovery/base/connectors/ping/wl/WhitelistEntry.java b/src/main/java/org/apache/sling/discovery/base/connectors/ping/wl/WhitelistEntry.java
new file mode 100644
index 0000000..af18554
--- /dev/null
+++ b/src/main/java/org/apache/sling/discovery/base/connectors/ping/wl/WhitelistEntry.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.sling.discovery.base.connectors.ping.wl;
+
+import javax.servlet.ServletRequest;
+
+/**
+ * A WhitelistEntry is capable of accepting certain requests
+ * depending on a configuration.
+ */
+public interface WhitelistEntry {
+
+ /**
+ * @param request the incoming request which should be accepted or rejected
+ * @return true if the request is accepted by this WhitelistEntry
+ */
+ public boolean accepts(ServletRequest request);
+
+}
diff --git a/src/main/java/org/apache/sling/discovery/base/connectors/ping/wl/WildcardHelper.java b/src/main/java/org/apache/sling/discovery/base/connectors/ping/wl/WildcardHelper.java
new file mode 100644
index 0000000..a402860
--- /dev/null
+++ b/src/main/java/org/apache/sling/discovery/base/connectors/ping/wl/WildcardHelper.java
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.sling.discovery.base.connectors.ping.wl;
+
+import java.util.regex.Pattern;
+
+/** Helper class for wildcards **/
+public class WildcardHelper {
+
+ /** converts a string containing wildcards (* and ?) into a valid regex **/
+ public static String wildcardAsRegex(String patternWithWildcards) {
+ if (patternWithWildcards==null) {
+ throw new IllegalArgumentException("patternWithWildcards must not be null");
+ }
+ return "\\Q"+patternWithWildcards.replace("?", "\\E.\\Q").replace("*", "\\E.*\\Q")+"\\E";
+ }
+
+ /**
+ * Compare a given string (comparee) against a pattern that contains wildcards
+ * and return true if it matches.
+ * @param comparee the string which should be tested against a pattern containing wildcards
+ * @param patternWithWildcards the pattern containing wildcards (* and ?)
+ * @return true if the comparee string matches against the pattern containing wildcards
+ */
+ public static boolean matchesWildcard(String comparee, String patternWithWildcards) {
+ if (comparee==null) {
+ throw new IllegalArgumentException("comparee must not be null");
+ }
+ if (patternWithWildcards==null) {
+ throw new IllegalArgumentException("patternWithEWildcards must not be null");
+ }
+ final String regex = wildcardAsRegex(patternWithWildcards);
+ return Pattern.matches(regex, comparee);
+ }
+
+}
diff --git a/src/main/java/org/apache/sling/discovery/base/connectors/ping/wl/WildcardWhitelistEntry.java b/src/main/java/org/apache/sling/discovery/base/connectors/ping/wl/WildcardWhitelistEntry.java
new file mode 100644
index 0000000..396d8a4
--- /dev/null
+++ b/src/main/java/org/apache/sling/discovery/base/connectors/ping/wl/WildcardWhitelistEntry.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.sling.discovery.base.connectors.ping.wl;
+
+import javax.servlet.ServletRequest;
+
+/**
+ * Implementation of a WhitelistEntry which can accept
+ * wildcards (* and ?) in both IP and hostname
+ */
+public class WildcardWhitelistEntry implements WhitelistEntry {
+
+ private final String hostOrAddressWithWildcard;
+
+ public WildcardWhitelistEntry(String hostOrAddressWithWildcard) {
+ this.hostOrAddressWithWildcard = hostOrAddressWithWildcard;
+ }
+
+ public boolean accepts(ServletRequest request) {
+ if (WildcardHelper.matchesWildcard(request.getRemoteAddr(), hostOrAddressWithWildcard)) {
+ return true;
+ }
+ if (WildcardHelper.matchesWildcard(request.getRemoteHost(), hostOrAddressWithWildcard)) {
+ return true;
+ }
+ return false;
+ }
+
+}
diff --git a/src/main/java/org/apache/sling/discovery/base/connectors/ping/wl/package-info.java b/src/main/java/org/apache/sling/discovery/base/connectors/ping/wl/package-info.java
new file mode 100644
index 0000000..091715c
--- /dev/null
+++ b/src/main/java/org/apache/sling/discovery/base/connectors/ping/wl/package-info.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.
+ */
+
+/**
+ * Provides whitelist-related classes for discovery
+ * implementors that choose to extend from discovery.base
+ *
+ * @version 1.0.0
+ */
+@Version("1.0.0")
+package org.apache.sling.discovery.base.connectors.ping.wl;
+
+import aQute.bnd.annotation.Version;
+
diff --git a/src/test/java/org/apache/sling/discovery/base/commons/DefaultTopologyViewTest.java b/src/test/java/org/apache/sling/discovery/base/commons/DefaultTopologyViewTest.java
new file mode 100644
index 0000000..27446f3
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/commons/DefaultTopologyViewTest.java
@@ -0,0 +1,209 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.sling.discovery.base.commons;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.Iterator;
+import java.util.Set;
+import java.util.UUID;
+
+import org.apache.sling.discovery.InstanceDescription;
+import org.apache.sling.discovery.InstanceFilter;
+import org.apache.sling.discovery.TopologyEvent.Type;
+import org.apache.sling.discovery.base.its.setup.TopologyHelper;
+import org.apache.sling.discovery.commons.providers.DefaultClusterView;
+import org.apache.sling.discovery.commons.providers.DefaultInstanceDescription;
+import org.junit.Test;
+
+import junitx.util.PrivateAccessor;
+
+public class DefaultTopologyViewTest {
+
+ @Test
+ public void testForcedLeaderChangeCompare() throws Exception {
+ // create view 1 with first instance the leader
+ final String slingId1 = UUID.randomUUID().toString();
+ final DefaultTopologyView view1 = TopologyHelper.createTopologyView(UUID
+ .randomUUID().toString(), slingId1);
+ final DefaultInstanceDescription id2 = TopologyHelper.addInstanceDescription(view1, TopologyHelper
+ .createInstanceDescription(view1.getClusterViews().iterator()
+ .next()));
+ final String slingId2 = id2.getSlingId();
+ final DefaultInstanceDescription id3 = TopologyHelper.addInstanceDescription(view1, TopologyHelper
+ .createInstanceDescription(view1.getClusterViews().iterator()
+ .next()));
+ final String slingId3 = id3.getSlingId();
+
+ // now create view 2 with exactly the same instances as above, but the second instance the leader
+ DefaultTopologyView view2 = TopologyHelper.cloneTopologyView(view1, slingId2);
+ // make sure we've chosen a new leader:
+ assertNotEquals(view1.getClusterViews().iterator().next().getLeader().getSlingId(),
+ view2.getClusterViews().iterator().next().getLeader().getSlingId());
+ // and now test the compare method which should catch the leader change
+ assertTrue(view1.compareTopology(view2)==Type.TOPOLOGY_CHANGED);
+
+ // same thing now with view3 which takes slingId3 as the leader
+ DefaultTopologyView view3 = TopologyHelper.cloneTopologyView(view1, slingId3);
+ // make sure we've chosen a new leader:
+ assertNotEquals(view1.getClusterViews().iterator().next().getLeader().getSlingId(),
+ view3.getClusterViews().iterator().next().getLeader().getSlingId());
+ // and now test the compare method which should catch the leader change
+ assertTrue(view1.compareTopology(view3)==Type.TOPOLOGY_CHANGED);
+ }
+
+ @Test
+ public void testCompare() throws Exception {
+
+ DefaultTopologyView newView = TopologyHelper.createTopologyView(UUID
+ .randomUUID().toString(), UUID.randomUUID().toString());
+
+ try {
+ newView.compareTopology(null);
+ fail("Should complain about null");
+ } catch (Exception e) {
+ // ok
+ }
+
+ DefaultTopologyView oldView = TopologyHelper
+ .cloneTopologyView(newView);
+ assertNull(newView.compareTopology(oldView));
+
+ DefaultInstanceDescription id = TopologyHelper
+ .createInstanceDescription(newView.getClusterViews().iterator()
+ .next());
+ TopologyHelper.addInstanceDescription(newView, id);
+ assertEquals(Type.TOPOLOGY_CHANGED, newView.compareTopology(oldView));
+
+ assertEquals(2, newView.getInstances().size());
+ // addInstanceDescription now no longer throws an exception if you add
+ // the same
+ // instance twice. this provides greater stability
+ TopologyHelper.addInstanceDescription(newView, id);
+ assertEquals(2, newView.getInstances().size());
+ // try{
+ // TopologyTestHelper.addInstanceDescription(newView, id);
+ // fail("should not be able to add twice");
+ // } catch(Exception e) {
+ // // ok
+ // }
+
+ oldView = TopologyHelper.cloneTopologyView(newView);
+ assertNull(newView.compareTopology(oldView));
+
+ DefaultInstanceDescription instance = (DefaultInstanceDescription) newView.getInstances().iterator().next();
+ instance.setProperty("a", "b");
+ assertEquals(Type.PROPERTIES_CHANGED, newView.compareTopology(oldView));
+ oldView = TopologyHelper.cloneTopologyView(newView);
+ assertNull(newView.compareTopology(oldView));
+
+ instance.setProperty("a", "B");
+ assertEquals(Type.PROPERTIES_CHANGED, newView.compareTopology(oldView));
+ oldView = TopologyHelper.cloneTopologyView(newView);
+ assertNull(newView.compareTopology(oldView));
+
+ instance.setProperty("a", "B");
+ assertNull(newView.compareTopology(oldView));
+
+ // now change the properties of the first instance but modify the second instance' cluster
+ Iterator<InstanceDescription> it = newView.getInstances().iterator();
+ DefaultInstanceDescription firstInstance = (DefaultInstanceDescription) it.next();
+ assertNotNull(firstInstance);
+ DefaultInstanceDescription secondInstance = (DefaultInstanceDescription) it.next();
+ assertNotNull(secondInstance);
+ firstInstance.setProperty("c", "d");
+ DefaultClusterView cluster = new DefaultClusterView(UUID.randomUUID().toString());
+ PrivateAccessor.setField(secondInstance, "clusterView", null);
+ cluster.addInstanceDescription(secondInstance);
+ assertEquals(Type.TOPOLOGY_CHANGED, newView.compareTopology(oldView));
+ }
+
+ @Test
+ public void testFind() throws Exception {
+ DefaultTopologyView newView = TopologyHelper.createTopologyView(UUID
+ .randomUUID().toString(), UUID.randomUUID().toString());
+ TopologyHelper.createAndAddInstanceDescription(newView, newView
+ .getClusterViews().iterator().next());
+
+ try {
+ newView.findInstances(null);
+ fail("should complain");
+ } catch (IllegalArgumentException iae) {
+ // ok
+ }
+
+ final DefaultInstanceDescription id = TopologyHelper
+ .createAndAddInstanceDescription(newView, newView
+ .getClusterViews().iterator().next());
+ TopologyHelper.createAndAddInstanceDescription(newView, newView
+ .getClusterViews().iterator().next());
+ assertEquals(4, newView.findInstances(new InstanceFilter() {
+
+ public boolean accept(InstanceDescription instance) {
+ return true;
+ }
+ }).size());
+ assertEquals(1, newView.findInstances(new InstanceFilter() {
+
+ public boolean accept(InstanceDescription instance) {
+ return instance.getSlingId().equals(id.getSlingId());
+ }
+ }).size());
+ assertEquals(1, newView.findInstances(new InstanceFilter() {
+
+ public boolean accept(InstanceDescription instance) {
+ return instance.isLeader();
+ }
+ }).size());
+ assertEquals(1, newView.findInstances(new InstanceFilter() {
+ boolean first = true;
+
+ public boolean accept(InstanceDescription instance) {
+ if (!first) {
+ return false;
+ }
+ first = false;
+ return true;
+ }
+ }).size());
+ }
+
+ @Test
+ public void testGetInstances() throws Exception {
+ DefaultTopologyView newView = TopologyHelper.createTopologyView(UUID
+ .randomUUID().toString(), UUID.randomUUID().toString());
+
+ Set<InstanceDescription> instances = newView.getInstances();
+ assertNotNull(instances);
+
+ try {
+ instances.remove(instances.iterator().next());
+ fail("list should not be modifiable");
+ } catch (Exception e) {
+ // ok
+ }
+
+ }
+
+}
diff --git a/src/test/java/org/apache/sling/discovery/base/commons/DummyDiscoveryService.java b/src/test/java/org/apache/sling/discovery/base/commons/DummyDiscoveryService.java
new file mode 100644
index 0000000..ddde4fc
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/commons/DummyDiscoveryService.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.sling.discovery.base.commons;
+
+import org.apache.sling.api.resource.ResourceResolverFactory;
+import org.apache.sling.commons.scheduler.Scheduler;
+import org.apache.sling.discovery.base.connectors.BaseConfig;
+import org.apache.sling.discovery.base.connectors.announcement.AnnouncementRegistry;
+import org.apache.sling.discovery.base.connectors.ping.ConnectorRegistry;
+
+public class DummyDiscoveryService extends BaseDiscoveryService {
+
+ private final ClusterViewService clusterViewService;
+ private final AnnouncementRegistry topologyRegistry;
+
+ public DummyDiscoveryService(String slingId,
+ ClusterViewService clusterViewService,
+ AnnouncementRegistry topologyRegistry,
+ ResourceResolverFactory resourceResolverFactory, BaseConfig config,
+ ConnectorRegistry connectorRegistry, Scheduler scheduler) {
+ this.clusterViewService = clusterViewService;
+ this.topologyRegistry = topologyRegistry;
+ }
+
+ @Override
+ public void updateProperties() {
+ throw new IllegalStateException("updateProperties not yet impl");
+ }
+
+ @Override
+ protected ClusterViewService getClusterViewService() {
+ return clusterViewService;
+ }
+
+ @Override
+ protected AnnouncementRegistry getAnnouncementRegistry() {
+ return topologyRegistry;
+ }
+
+ @Override
+ protected void handleIsolatedFromTopology() {
+ throw new IllegalStateException("handleIsolatedFromTopology not yet impl");
+ }
+
+ @Override
+ public void handlePotentialTopologyChange() {
+ throw new IllegalStateException("handlePotentialTopologyChange not yet impl");
+ }
+
+ @Override
+ public void handleTopologyChanging() {
+ throw new IllegalStateException("handleTopologyChanging not yet impl");
+ }
+
+}
diff --git a/src/test/java/org/apache/sling/discovery/base/connectors/DummyVirtualInstanceBuilder.java b/src/test/java/org/apache/sling/discovery/base/connectors/DummyVirtualInstanceBuilder.java
new file mode 100644
index 0000000..0761613
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/connectors/DummyVirtualInstanceBuilder.java
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.sling.discovery.base.connectors;
+
+import org.apache.sling.discovery.base.commons.BaseDiscoveryService;
+import org.apache.sling.discovery.base.commons.ClusterViewService;
+import org.apache.sling.discovery.base.commons.DummyDiscoveryService;
+import org.apache.sling.discovery.base.commons.ViewChecker;
+import org.apache.sling.discovery.base.its.setup.ModifiableTestBaseConfig;
+import org.apache.sling.discovery.base.its.setup.VirtualInstance;
+import org.apache.sling.discovery.base.its.setup.VirtualInstanceBuilder;
+import org.apache.sling.discovery.base.its.setup.mock.DummyViewChecker;
+import org.apache.sling.discovery.base.its.setup.mock.MockFactory;
+import org.apache.sling.discovery.base.its.setup.mock.SimpleClusterViewService;
+import org.apache.sling.discovery.base.its.setup.mock.SimpleConnectorConfig;
+
+public class DummyVirtualInstanceBuilder extends VirtualInstanceBuilder {
+
+ private ModifiableTestBaseConfig connectorConfig;
+
+ public DummyVirtualInstanceBuilder() {
+ }
+
+ @Override
+ public VirtualInstanceBuilder createNewRepository() throws Exception {
+ this.factory = MockFactory.mockResourceResolverFactory();
+ return this;
+ }
+
+ @Override
+ public VirtualInstanceBuilder setPath(String string) {
+ // nothing to do now
+ return this;
+ }
+
+ @Override
+ public Object[] getAdditionalServices(VirtualInstance instance) throws Exception {
+ return null;
+ }
+
+ protected ClusterViewService createClusterViewService() {
+ return new SimpleClusterViewService(getSlingId());
+ }
+
+ protected ViewChecker createViewChecker() throws Exception {
+ return DummyViewChecker.testConstructor(getSlingSettingsService(), getResourceResolverFactory(), getConnectorRegistry(), getAnnouncementRegistry(), getScheduler(), getConnectorConfig());
+ }
+
+ protected BaseDiscoveryService createDiscoveryService() throws Exception {
+ return new DummyDiscoveryService(getSlingId(), getClusterViewService(), getAnnouncementRegistry(), getResourceResolverFactory(), getConnectorConfig(), getConnectorRegistry(), getScheduler());
+ }
+
+ @Override
+ public ModifiableTestBaseConfig getConnectorConfig() {
+ if (connectorConfig==null) {
+ connectorConfig = createConnectorConfig();
+ }
+ return connectorConfig;
+ }
+
+ private ModifiableTestBaseConfig createConnectorConfig() {
+ return new SimpleConnectorConfig();
+ }
+
+ @Override
+ protected void resetRepo() {
+ // does nothing
+ }
+}
diff --git a/src/test/java/org/apache/sling/discovery/base/connectors/LargeTopologyWithHubTest.java b/src/test/java/org/apache/sling/discovery/base/connectors/LargeTopologyWithHubTest.java
new file mode 100644
index 0000000..c98c30f
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/connectors/LargeTopologyWithHubTest.java
@@ -0,0 +1,123 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.sling.discovery.base.connectors;
+
+import static org.junit.Assert.assertNotNull;
+
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.apache.sling.commons.testing.junit.Retry;
+import org.apache.sling.commons.testing.junit.RetryRule;
+import org.apache.sling.discovery.ClusterView;
+import org.apache.sling.discovery.TopologyView;
+import org.apache.sling.discovery.base.its.setup.TopologyHelper;
+import org.apache.sling.discovery.base.its.setup.VirtualConnector;
+import org.apache.sling.discovery.base.its.setup.VirtualInstance;
+import org.apache.sling.discovery.base.its.setup.VirtualInstanceBuilder;
+import org.apache.sling.testing.tools.sling.TimeoutsProvider;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class LargeTopologyWithHubTest {
+
+ private static final Logger logger = LoggerFactory.getLogger(LargeTopologyWithHubTest.class);
+
+ private static List<VirtualInstance> instances;
+ private static VirtualInstance hub;
+ private static List<String> slingIds;
+ private static final int TEST_SIZE = 50;
+
+ @Rule
+ public final RetryRule retryRule = new RetryRule();
+
+ private VirtualInstanceBuilder newBuilder() {
+ return new DummyVirtualInstanceBuilder();
+ }
+
+ @Before
+ public void setup() throws Throwable {
+ instances = new LinkedList<VirtualInstance>();
+ final int defaultHeartbeatTimeout = 3600; // 1 hour should be enough, really
+ final int heartbeatTimeout = TimeoutsProvider.getInstance().getTimeout(defaultHeartbeatTimeout);
+ VirtualInstanceBuilder hubBuilder = newBuilder()
+ .newRepository("/var/discovery/impl/", true)
+ .setDebugName("hub")
+ .setConnectorPingInterval(5)
+ .setConnectorPingTimeout(heartbeatTimeout);
+ hub = hubBuilder.build();
+ instances.add(hub);
+ hub.getConfig().setViewCheckTimeout(heartbeatTimeout);
+// hub.installVotingOnHeartbeatHandler();
+ hub.heartbeatsAndCheckView();
+ hub.heartbeatsAndCheckView();
+ assertNotNull(hub.getClusterViewService().getLocalClusterView());
+ hub.startViewChecker(1);
+ hub.dumpRepo();
+
+ slingIds = new LinkedList<String>();
+ slingIds.add(hub.getSlingId());
+ logger.info("setUp: using heartbeatTimeout of "+heartbeatTimeout+"sec "
+ + "(default: "+defaultHeartbeatTimeout+")");
+ for(int i=0; i<TEST_SIZE; i++) {
+ logger.info("setUp: creating instance"+i);
+ VirtualInstanceBuilder builder2 = newBuilder()
+ .newRepository("/var/discovery/impl/", false)
+ .setDebugName("instance"+i)
+ .setConnectorPingInterval(5)
+ .setConnectorPingTimeout(heartbeatTimeout);
+ VirtualInstance instance = builder2.build();
+ instances.add(instance);
+ instance.getConfig().setViewCheckTimeout(heartbeatTimeout);
+// instance.installVotingOnHeartbeatHandler();
+ instance.heartbeatsAndCheckView();
+ instance.heartbeatsAndCheckView();
+ ClusterView clusterView = instance.getClusterViewService().getLocalClusterView();
+ assertNotNull(clusterView);
+ new VirtualConnector(instance, hub);
+ slingIds.add(instance.getSlingId());
+ }
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ for (Iterator<VirtualInstance> it = instances.iterator(); it.hasNext();) {
+ final VirtualInstance instance = it.next();
+ instance.stop();
+ }
+ }
+
+ @Test
+ @Retry(timeoutMsec=30000, intervalMsec=500)
+ public void testLargeTopologyWithHub() throws Exception {
+ hub.dumpRepo();
+ final TopologyView tv = hub.getDiscoveryService().getTopology();
+ assertNotNull(tv);
+ logger.info(
+ "testLargeTopologyWithHub: checking if all connectors are registered, TopologyView has {} Instances",
+ tv.getInstances().size());
+ TopologyHelper.assertTopologyConsistsOf(tv, slingIds.toArray(new String[slingIds.size()]));
+ logger.info("testLargeTopologyWithHub: test passed");
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/discovery/base/connectors/announcement/AnnouncementRegistryImplTest.java b/src/test/java/org/apache/sling/discovery/base/connectors/announcement/AnnouncementRegistryImplTest.java
new file mode 100644
index 0000000..6a38bea
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/connectors/announcement/AnnouncementRegistryImplTest.java
@@ -0,0 +1,391 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.sling.discovery.base.connectors.announcement;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.List;
+import java.util.UUID;
+
+import javax.jcr.Session;
+
+import org.apache.sling.api.resource.ResourceResolverFactory;
+import org.apache.sling.commons.testing.jcr.RepositoryProvider;
+import org.apache.sling.discovery.ClusterView;
+import org.apache.sling.discovery.InstanceDescription;
+import org.apache.sling.discovery.base.connectors.BaseConfig;
+import org.apache.sling.discovery.base.its.setup.TopologyHelper;
+import org.apache.sling.discovery.base.its.setup.VirtualInstanceHelper;
+import org.apache.sling.discovery.base.its.setup.mock.MockFactory;
+import org.apache.sling.discovery.base.its.setup.mock.SimpleConnectorConfig;
+import org.apache.sling.discovery.commons.providers.DefaultClusterView;
+import org.apache.sling.discovery.commons.providers.DefaultInstanceDescription;
+import org.apache.sling.discovery.commons.providers.spi.impl.DummySlingSettingsService;
+import org.junit.Before;
+import org.junit.Test;
+
+public class AnnouncementRegistryImplTest {
+
+ private AnnouncementRegistryImpl registry;
+ private String slingId;
+ private ResourceResolverFactory resourceResolverFactory;
+ private BaseConfig config;
+
+ @Before
+ public void setup() throws Exception {
+ resourceResolverFactory = MockFactory
+ .mockResourceResolverFactory();
+ config = new SimpleConnectorConfig() {
+ public long getConnectorPingTimeout() {
+ // 10s for tests that also run on apache jenkins
+ return 10;
+ };
+ };
+ slingId = UUID.randomUUID().toString();
+ Session l = RepositoryProvider.instance().getRepository()
+ .loginAdministrative(null);
+ try {
+ l.removeItem("/var");
+ l.save();
+ l.logout();
+ } catch (Exception e) {
+ l.refresh(false);
+ l.logout();
+ }
+ registry = AnnouncementRegistryImpl.testConstructorAndActivate(
+ resourceResolverFactory, new DummySlingSettingsService(slingId), config);
+ }
+
+ @Test
+ public void testRegisterUnregister() throws Exception {
+ try{
+ registry.registerAnnouncement(null);
+ fail("should complain");
+ } catch(IllegalArgumentException iae) {
+ // ok
+ }
+ try{
+ registry.unregisterAnnouncement(null);
+ fail("should complain");
+ } catch(IllegalArgumentException iae) {
+ // ok
+ }
+ try{
+ registry.unregisterAnnouncement("");
+ fail("should complain");
+ } catch(IllegalArgumentException iae) {
+ // ok
+ }
+
+ try{
+ new Announcement(null);
+ fail("should complain");
+ } catch(IllegalArgumentException iae) {
+ // ok
+ }
+ try{
+ new Announcement("");
+ fail("should complain");
+ } catch(IllegalArgumentException iae) {
+ // ok
+ }
+
+ Announcement ann = new Announcement(slingId);
+ assertFalse(ann.isValid());
+ assertFalse(registry.registerAnnouncement(ann)!=-1);
+
+ DefaultClusterView localCluster = new DefaultClusterView(UUID.randomUUID().toString());
+ ann.setLocalCluster(localCluster);
+ assertFalse(ann.isValid());
+ assertFalse(registry.registerAnnouncement(ann)!=-1);
+
+ try{
+ registry.listInstances(localCluster);
+ fail("doing getInstances() on an empty cluster should throw an illegalstateexception");
+ } catch(IllegalStateException ise) {
+ // ok
+ }
+
+ DefaultInstanceDescription instance = TopologyHelper.createInstanceDescription(ann.getOwnerId(), true, localCluster);
+ assertEquals(instance.getSlingId(), ann.getOwnerId());
+ assertTrue(ann.isValid());
+ assertTrue(registry.registerAnnouncement(ann)!=-1);
+
+ assertEquals(1, registry.listInstances(localCluster).size());
+
+ registry.checkExpiredAnnouncements();
+ assertEquals(1, registry.listInstances(localCluster).size());
+
+ registry.unregisterAnnouncement(ann.getOwnerId());
+ assertEquals(0, registry.listInstances(localCluster).size());
+ assertTrue(ann.isValid());
+ assertTrue(registry.registerAnnouncement(ann)!=-1);
+ assertEquals(1, registry.listInstances(localCluster).size());
+
+ Thread.sleep(10500);
+ assertEquals(0, registry.listInstances(localCluster).size());
+
+ }
+
+ @Test
+ public void testLists() throws Exception {
+ try{
+ registry.listAnnouncementsInSameCluster(null);
+ fail("should complain");
+ } catch(IllegalArgumentException iae) {
+ // ok
+ }
+ try{
+ registry.listAnnouncementsInSameCluster(null);
+ fail("should complain");
+ } catch(IllegalArgumentException iae) {
+ // ok
+ }
+ assertEquals(0, registry.listLocalAnnouncements().size());
+ assertEquals(0, registry.listLocalIncomingAnnouncements().size());
+ DefaultClusterView localCluster = new DefaultClusterView(UUID.randomUUID().toString());
+ DefaultInstanceDescription instance = TopologyHelper.createInstanceDescription(slingId, true, localCluster);
+ assertEquals(0, registry.listAnnouncementsInSameCluster(localCluster).size());
+ assertEquals(0, registry.listLocalAnnouncements().size());
+ assertEquals(0, registry.listLocalIncomingAnnouncements().size());
+
+ Announcement ann = new Announcement(slingId);
+ ann.setLocalCluster(localCluster);
+ ann.setInherited(true);
+ registry.registerAnnouncement(ann);
+ assertEquals(1, registry.listAnnouncementsInSameCluster(localCluster).size());
+ assertEquals(1, registry.listLocalAnnouncements().size());
+ assertEquals(0, registry.listLocalIncomingAnnouncements().size());
+ ann.setInherited(true);
+ assertEquals(0, registry.listLocalIncomingAnnouncements().size());
+ assertTrue(registry.hasActiveAnnouncement(slingId));
+ assertFalse(registry.hasActiveAnnouncement(UUID.randomUUID().toString()));
+ registry.unregisterAnnouncement(slingId);
+ assertEquals(0, registry.listAnnouncementsInSameCluster(localCluster).size());
+ assertEquals(0, registry.listLocalAnnouncements().size());
+ assertEquals(0, registry.listLocalIncomingAnnouncements().size());
+ assertFalse(registry.hasActiveAnnouncement(slingId));
+ assertFalse(registry.hasActiveAnnouncement(UUID.randomUUID().toString()));
+ ann.setInherited(false);
+ registry.registerAnnouncement(ann);
+ assertEquals(1, registry.listAnnouncementsInSameCluster(localCluster).size());
+ assertEquals(1, registry.listLocalAnnouncements().size());
+ assertEquals(1, registry.listLocalIncomingAnnouncements().size());
+ assertTrue(registry.hasActiveAnnouncement(slingId));
+ assertFalse(registry.hasActiveAnnouncement(UUID.randomUUID().toString()));
+ registry.unregisterAnnouncement(slingId);
+ assertEquals(0, registry.listAnnouncementsInSameCluster(localCluster).size());
+ assertEquals(0, registry.listLocalAnnouncements().size());
+ assertEquals(0, registry.listLocalIncomingAnnouncements().size());
+ assertFalse(registry.hasActiveAnnouncement(slingId));
+ assertFalse(registry.hasActiveAnnouncement(UUID.randomUUID().toString()));
+
+ assertEquals(1, ann.listInstances().size());
+ registry.addAllExcept(ann, localCluster, new AnnouncementFilter() {
+
+ public boolean accept(String receivingSlingId, Announcement announcement) {
+ assertNotNull(receivingSlingId);
+ assertNotNull(announcement);
+ return true;
+ }
+ });
+ assertEquals(1, ann.listInstances().size());
+ registry.registerAnnouncement(createAnnouncement(createCluster(3), 1, false));
+ assertEquals(1, registry.listAnnouncementsInSameCluster(localCluster).size());
+ assertEquals(3, registry.listInstances(localCluster).size());
+ registry.addAllExcept(ann, localCluster, new AnnouncementFilter() {
+
+ public boolean accept(String receivingSlingId, Announcement announcement) {
+ assertNotNull(receivingSlingId);
+ assertNotNull(announcement);
+ return true;
+ }
+ });
+ assertEquals(4, ann.listInstances().size());
+ registry.registerAnnouncement(ann);
+ assertEquals(2, registry.listAnnouncementsInSameCluster(localCluster).size());
+ }
+
+ private ClusterView createCluster(int numInstances) {
+ DefaultClusterView localCluster = new DefaultClusterView(UUID.randomUUID().toString());
+ for (int i = 0; i < numInstances; i++) {
+ DefaultInstanceDescription instance = TopologyHelper.createInstanceDescription(UUID.randomUUID().toString(), (i==0 ? true : false), localCluster);
+ }
+ return localCluster;
+ }
+
+ private ClusterView createCluster(String... instanceIds) {
+ DefaultClusterView localCluster = new DefaultClusterView(UUID.randomUUID().toString());
+ for (int i = 0; i < instanceIds.length; i++) {
+ DefaultInstanceDescription instance = TopologyHelper.createInstanceDescription(instanceIds[i], (i==0 ? true : false), localCluster);
+ }
+ return localCluster;
+ }
+
+ private Announcement createAnnouncement(ClusterView remoteCluster, int ownerIndex, boolean inherited) {
+ List<InstanceDescription> instances = remoteCluster.getInstances();
+ Announcement ann = new Announcement(instances.get(ownerIndex).getSlingId());
+ ann.setInherited(inherited);
+ ann.setLocalCluster(remoteCluster);
+ return ann;
+ }
+
+ @Test
+ public void testExpiry() throws InterruptedException, NoSuchFieldException {
+ ClusterView cluster1 = createCluster(4);
+ ClusterView cluster2 = createCluster(3);
+ ClusterView cluster3 = createCluster(5);
+
+ ClusterView myCluster = createCluster(slingId);
+
+ Announcement ann1 = createAnnouncement(cluster1, 0, true);
+ Announcement ann2 = createAnnouncement(cluster2, 1, true);
+ Announcement ann3 = createAnnouncement(cluster3, 1, false);
+
+ assertTrue(registry.registerAnnouncement(ann1)!=-1);
+ assertTrue(registry.registerAnnouncement(ann2)!=-1);
+ assertTrue(registry.registerAnnouncement(ann3)!=-1);
+ assertTrue(registry.hasActiveAnnouncement(cluster1.getInstances().get(0).getSlingId()));
+ assertTrue(registry.hasActiveAnnouncement(cluster2.getInstances().get(1).getSlingId()));
+ assertTrue(registry.hasActiveAnnouncement(cluster3.getInstances().get(1).getSlingId()));
+ assertEquals(3, registry.listAnnouncementsInSameCluster(myCluster).size());
+ assertEquals(3, registry.listLocalAnnouncements().size());
+ assertEquals(1, registry.listLocalIncomingAnnouncements().size());
+
+
+ {
+ Announcement testAnn = createAnnouncement(myCluster, 0, false);
+ assertEquals(1, testAnn.listInstances().size());
+ registry.addAllExcept(testAnn, myCluster, null);
+ assertEquals(13, testAnn.listInstances().size());
+ }
+
+
+ Thread.sleep(10500);
+ {
+ Announcement testAnn = createAnnouncement(myCluster, 0, false);
+ assertEquals(1, testAnn.listInstances().size());
+ registry.addAllExcept(testAnn, myCluster, null);
+ assertEquals(13, testAnn.listInstances().size());
+ }
+ assertTrue(registry.registerAnnouncement(ann3)!=-1);
+ {
+ Announcement testAnn = createAnnouncement(myCluster, 0, false);
+ assertEquals(1, testAnn.listInstances().size());
+ registry.addAllExcept(testAnn, myCluster, null);
+ assertEquals(13, testAnn.listInstances().size());
+ }
+
+ registry.checkExpiredAnnouncements();
+
+ assertEquals(1, registry.listAnnouncementsInSameCluster(myCluster).size());
+ assertEquals(1, registry.listLocalAnnouncements().size());
+ assertEquals(1, registry.listLocalIncomingAnnouncements().size());
+ assertFalse(registry.hasActiveAnnouncement(cluster1.getInstances().get(0).getSlingId()));
+ assertFalse(registry.hasActiveAnnouncement(cluster2.getInstances().get(1).getSlingId()));
+ assertTrue(registry.hasActiveAnnouncement(cluster3.getInstances().get(1).getSlingId()));
+ {
+ Announcement testAnn = createAnnouncement(myCluster, 0, false);
+ assertEquals(1, testAnn.listInstances().size());
+ registry.addAllExcept(testAnn, myCluster, null);
+ assertEquals(6, testAnn.listInstances().size());
+ }
+
+ }
+
+ @Test
+ public void testCluster() throws Exception {
+ ClusterView cluster1 = createCluster(2);
+ ClusterView cluster2 = createCluster(4);
+ ClusterView cluster3 = createCluster(7);
+
+ Announcement ann1 = createAnnouncement(cluster1, 1, true);
+ Announcement ann2 = createAnnouncement(cluster2, 2, true);
+ Announcement ann3 = createAnnouncement(cluster3, 3, false);
+
+ final String instance1 = UUID.randomUUID().toString();
+ final String instance2 = UUID.randomUUID().toString();
+ final String instance3 = UUID.randomUUID().toString();
+ ClusterView myCluster = createCluster(instance1, instance2, instance3);
+
+ AnnouncementRegistryImpl registry1 = AnnouncementRegistryImpl.testConstructorAndActivate(
+ resourceResolverFactory, new DummySlingSettingsService(instance1), config);
+ AnnouncementRegistryImpl registry2 = AnnouncementRegistryImpl.testConstructorAndActivate(
+ resourceResolverFactory, new DummySlingSettingsService(instance2), config);
+ AnnouncementRegistryImpl registry3 = AnnouncementRegistryImpl.testConstructorAndActivate(
+ resourceResolverFactory, new DummySlingSettingsService(instance3), config);
+
+ assertTrue(registry1.registerAnnouncement(ann1)!=-1);
+ assertTrue(registry2.registerAnnouncement(ann2)!=-1);
+ assertTrue(registry3.registerAnnouncement(ann3)!=-1);
+
+ assertTrue(registry1.hasActiveAnnouncement(cluster1.getInstances().get(1).getSlingId()));
+ assertTrue(registry2.hasActiveAnnouncement(cluster2.getInstances().get(2).getSlingId()));
+ assertTrue(registry3.hasActiveAnnouncement(cluster3.getInstances().get(3).getSlingId()));
+
+ assertEquals(3, registry1.listAnnouncementsInSameCluster(myCluster).size());
+ assertEquals(1, registry1.listLocalAnnouncements().size());
+ assertEquals(0, registry1.listLocalIncomingAnnouncements().size());
+ assertAnnouncements(registry1, myCluster, 4, 16);
+
+ assertEquals(3, registry2.listAnnouncementsInSameCluster(myCluster).size());
+ assertEquals(1, registry2.listLocalAnnouncements().size());
+ assertEquals(0, registry2.listLocalIncomingAnnouncements().size());
+ assertAnnouncements(registry2, myCluster, 4, 16);
+
+ assertEquals(3, registry3.listAnnouncementsInSameCluster(myCluster).size());
+ assertEquals(1, registry3.listLocalAnnouncements().size());
+ assertEquals(1, registry3.listLocalIncomingAnnouncements().size());
+ assertAnnouncements(registry3, myCluster, 4, 16);
+
+ myCluster = createCluster(instance1, instance2);
+
+ VirtualInstanceHelper.dumpRepo(resourceResolverFactory);
+
+ assertEquals(2, registry1.listAnnouncementsInSameCluster(myCluster).size());
+ assertEquals(1, registry1.listLocalAnnouncements().size());
+ assertEquals(0, registry1.listLocalIncomingAnnouncements().size());
+ assertAnnouncements(registry1, myCluster, 3, 8);
+
+ assertEquals(2, registry2.listAnnouncementsInSameCluster(myCluster).size());
+ assertEquals(1, registry2.listLocalAnnouncements().size());
+ assertEquals(0, registry2.listLocalIncomingAnnouncements().size());
+ assertAnnouncements(registry2, myCluster, 3, 8);
+
+ Thread.sleep(10500);
+ assertAnnouncements(registry1, myCluster, 3, 8);
+ assertAnnouncements(registry2, myCluster, 3, 8);
+ registry1.checkExpiredAnnouncements();
+ registry2.checkExpiredAnnouncements();
+ assertAnnouncements(registry1, myCluster, 1, 2);
+ assertAnnouncements(registry2, myCluster, 1, 2);
+ }
+
+ private void assertAnnouncements(AnnouncementRegistryImpl registry,
+ ClusterView myCluster, int expectedNumAnnouncements, int expectedNumInstances) {
+ Announcement ann = createAnnouncement(myCluster, 0, false);
+ registry.addAllExcept(ann, myCluster, null);
+ assertEquals(expectedNumInstances, ann.listInstances().size());
+ }
+
+}
diff --git a/src/test/java/org/apache/sling/discovery/base/connectors/ping/ConnectorRegistryImplTest.java b/src/test/java/org/apache/sling/discovery/base/connectors/ping/ConnectorRegistryImplTest.java
new file mode 100644
index 0000000..9972e85
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/connectors/ping/ConnectorRegistryImplTest.java
@@ -0,0 +1,118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.sling.discovery.base.connectors.ping;
+
+import static org.junit.Assert.fail;
+
+import java.net.URL;
+import java.util.UUID;
+
+import org.apache.sling.discovery.base.commons.ClusterViewService;
+import org.apache.sling.discovery.base.connectors.BaseConfig;
+import org.apache.sling.discovery.base.connectors.DummyVirtualInstanceBuilder;
+import org.apache.sling.discovery.base.connectors.announcement.AnnouncementRegistryImpl;
+import org.apache.sling.discovery.base.its.setup.VirtualInstance;
+import org.apache.sling.discovery.base.its.setup.VirtualInstanceBuilder;
+import org.apache.sling.discovery.base.its.setup.mock.MockFactory;
+import org.apache.sling.discovery.base.its.setup.mock.SimpleConnectorConfig;
+import org.apache.sling.discovery.commons.providers.spi.impl.DummySlingSettingsService;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class ConnectorRegistryImplTest {
+
+ private VirtualInstance i;
+
+ public VirtualInstanceBuilder newBuilder() {
+ return new DummyVirtualInstanceBuilder();
+ }
+
+ @Before
+ public void setup() throws Exception {
+ VirtualInstanceBuilder builder = newBuilder()
+ .newRepository("/var/discovery/impl/", true)
+ .setDebugName("i")
+ .setConnectorPingInterval(20)
+ .setConnectorPingTimeout(20);
+ i = builder.build();
+ }
+
+ @After
+ public void teardown() throws Exception {
+ if (i!=null) {
+ try {
+ i.stopViewChecker();
+ } catch (Throwable e) {
+ e.printStackTrace();
+ i.stop();
+ throw new RuntimeException(e);
+ }
+ i.stop();
+ }
+ }
+
+ @Test
+ public void testRegisterUnregister() throws Exception {
+ BaseConfig config = new SimpleConnectorConfig() {
+ @Override
+ public long getConnectorPingTimeout() {
+ return 20000;
+ }
+ };
+ AnnouncementRegistryImpl announcementRegistry = AnnouncementRegistryImpl.testConstructorAndActivate(
+ MockFactory.mockResourceResolverFactory(), new DummySlingSettingsService(UUID.randomUUID().toString()), config);
+
+ ConnectorRegistry c = ConnectorRegistryImpl.testConstructor(
+ announcementRegistry, config);
+
+ final URL url = new URL("http://localhost:1234/connector");
+ final ClusterViewService cvs = i.getClusterViewService();
+ try {
+ c.registerOutgoingConnector(null, url);
+ fail("should have complained");
+ } catch (IllegalArgumentException e) {
+ // ok
+ }
+ try {
+ c.registerOutgoingConnector(cvs, null);
+ fail("should have complained");
+ } catch (IllegalArgumentException e) {
+ // ok
+ }
+ TopologyConnectorClientInformation client = c
+ .registerOutgoingConnector(cvs, url);
+ try {
+ // should not be able to register same url twice
+ client = c.registerOutgoingConnector(cvs, url);
+ // ok - no longer complains - SLING-3446
+ } catch (IllegalStateException e) {
+ fail("should no longer be thrown"); // SLING-3446
+ }
+
+ try {
+ c.unregisterOutgoingConnector(null);
+ fail("should have complained");
+ } catch (IllegalArgumentException e) {
+ // ok
+ }
+
+ c.unregisterOutgoingConnector(client.getId());
+ }
+}
diff --git a/src/test/java/org/apache/sling/discovery/base/connectors/ping/TopologyConnectorServletTest.java b/src/test/java/org/apache/sling/discovery/base/connectors/ping/TopologyConnectorServletTest.java
new file mode 100644
index 0000000..8bf4c5a
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/connectors/ping/TopologyConnectorServletTest.java
@@ -0,0 +1,115 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.sling.discovery.base.connectors.ping;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.sling.discovery.base.connectors.BaseConfig;
+import org.junit.Before;
+import org.junit.Test;
+
+import junitx.util.PrivateAccessor;
+
+public class TopologyConnectorServletTest {
+
+ private TopologyConnectorServlet servlet;
+
+ private HttpServletRequest getRequest(String host, String addr) {
+ HttpServletRequest result = mock(HttpServletRequest.class);
+ when(result.getRemoteAddr()).thenReturn(addr);
+ when(result.getRemoteHost()).thenReturn(host);
+ return result;
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ servlet = new TopologyConnectorServlet();
+ BaseConfig config = mock(BaseConfig.class);
+ PrivateAccessor.setField(servlet, "config", config);
+ }
+
+ @Test
+ public void testNull() throws Exception {
+ servlet.initWhitelist(null); // should work fine
+ servlet.initWhitelist(new String[0]); // should also work fine
+ }
+
+ @Test
+ public void testPlaintextWhitelist_enabled() throws Exception {
+ servlet.initWhitelist(new String[] {"foo", "bar"});
+ assertTrue(servlet.isWhitelisted(getRequest("foo", "x")));
+ assertTrue(servlet.isWhitelisted(getRequest("bar", "x")));
+ assertTrue(servlet.isWhitelisted(getRequest("y", "foo")));
+ assertTrue(servlet.isWhitelisted(getRequest("y", "bar")));
+ }
+
+ @Test
+ public void testPlaintextWhitelist_disabled() throws Exception {
+ servlet.initWhitelist(new String[] {});
+ assertFalse(servlet.isWhitelisted(getRequest("foo", "x")));
+ assertFalse(servlet.isWhitelisted(getRequest("bar", "x")));
+ assertFalse(servlet.isWhitelisted(getRequest("y", "foo")));
+ assertFalse(servlet.isWhitelisted(getRequest("y", "bar")));
+ }
+
+ @Test
+ public void testWildcardWhitelist() throws Exception {
+ servlet.initWhitelist(new String[] {"foo*", "b?r", "test"});
+ assertTrue(servlet.isWhitelisted(getRequest("foo", "x")));
+ assertTrue(servlet.isWhitelisted(getRequest("fooo", "x")));
+ assertTrue(servlet.isWhitelisted(getRequest("foooo", "x")));
+ assertTrue(servlet.isWhitelisted(getRequest("x", "foo")));
+ assertTrue(servlet.isWhitelisted(getRequest("x", "fooo")));
+ assertTrue(servlet.isWhitelisted(getRequest("x", "foooo")));
+ assertTrue(servlet.isWhitelisted(getRequest("bur", "x")));
+ assertTrue(servlet.isWhitelisted(getRequest("x", "bur")));
+ assertTrue(servlet.isWhitelisted(getRequest("x", "test")));
+ assertFalse(servlet.isWhitelisted(getRequest("fo", "x")));
+ assertFalse(servlet.isWhitelisted(getRequest("x", "testy")));
+ }
+
+ @Test
+ public void testSubnetMaskWhitelist() throws Exception {
+ servlet.initWhitelist(new String[] {"1.2.3.4/24", "2.3.4.1/30", "3.4.5.6/31"});
+
+ assertTrue(servlet.isWhitelisted(getRequest("foo", "1.2.3.4")));
+ assertFalse(servlet.isWhitelisted(getRequest("1.2.3.4", "1.2.4.3")));
+ assertTrue(servlet.isWhitelisted(getRequest("foo", "1.2.3.1")));
+ assertTrue(servlet.isWhitelisted(getRequest("foo", "1.2.3.254")));
+ assertFalse(servlet.isWhitelisted(getRequest("foo", "1.2.4.5")));
+
+ assertTrue(servlet.isWhitelisted(getRequest("foo", "2.3.4.1")));
+ assertTrue(servlet.isWhitelisted(getRequest("foo", "2.3.4.2")));
+ assertFalse(servlet.isWhitelisted(getRequest("foo", "2.3.4.3")));
+ assertFalse(servlet.isWhitelisted(getRequest("foo", "2.3.4.4")));
+
+ assertFalse(servlet.isWhitelisted(getRequest("foo", "3.4.5.1")));
+ assertFalse(servlet.isWhitelisted(getRequest("foo", "3.4.5.2")));
+ assertFalse(servlet.isWhitelisted(getRequest("foo", "3.4.5.3")));
+ assertFalse(servlet.isWhitelisted(getRequest("foo", "3.4.5.4")));
+ assertFalse(servlet.isWhitelisted(getRequest("foo", "3.4.5.5")));
+ assertFalse(servlet.isWhitelisted(getRequest("foo", "3.4.5.6")));
+ assertFalse(servlet.isWhitelisted(getRequest("foo", "3.4.5.7")));
+ }
+}
diff --git a/src/test/java/org/apache/sling/discovery/base/connectors/ping/TopologyRequestValidatorTest.java b/src/test/java/org/apache/sling/discovery/base/connectors/ping/TopologyRequestValidatorTest.java
new file mode 100644
index 0000000..accac53
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/connectors/ping/TopologyRequestValidatorTest.java
@@ -0,0 +1,180 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF 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.apache.sling.discovery.base.connectors.ping;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.StringReader;
+import java.lang.reflect.Field;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.methods.HttpPut;
+import org.apache.http.message.BasicHeader;
+import org.apache.sling.discovery.base.connectors.BaseConfig;
+import org.apache.sling.discovery.base.its.setup.mock.SimpleConnectorConfig;
+import org.hamcrest.Description;
+import org.jmock.Expectations;
+import org.jmock.Mockery;
+import org.jmock.api.Action;
+import org.jmock.api.Invocation;
+import org.jmock.integration.junit4.JUnit4Mockery;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+public class TopologyRequestValidatorTest {
+
+ private TopologyRequestValidator topologyRequestValidator;
+ private Mockery context = new JUnit4Mockery();
+
+
+ @Before
+ public void before() throws SecurityException, IllegalArgumentException, NoSuchFieldException, IllegalAccessException {
+ BaseConfig config= new SimpleConnectorConfig();
+ setPrivate(config, "sharedKey", "testKey");
+ setPrivate(config, "hmacEnabled", true);
+ setPrivate(config, "encryptionEnabled", true);
+ setPrivate(config, "keyInterval", 3600*100*4);
+ topologyRequestValidator = new TopologyRequestValidator(config);
+ }
+
+ private void setPrivate(Object o, String field, Object value) throws SecurityException, NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
+ Field f = o.getClass().getDeclaredField(field);
+ if ( !f.isAccessible()) {
+ f.setAccessible(true);
+ }
+ f.set(o, value);
+ }
+
+ @Test
+ public void testTrustRequest() throws IOException {
+ final HttpPut method = new HttpPut("/TestUri");
+ String clearMessage = "TestMessage";
+ final String message = topologyRequestValidator.encodeMessage(clearMessage);
+ Assert.assertNotNull(message);
+ Assert.assertNotEquals(message, clearMessage);
+ topologyRequestValidator.trustMessage(method, message);
+
+ Assert.assertNotNull(method.getFirstHeader(TopologyRequestValidator.HASH_HEADER));
+ Assert.assertNotNull(method.getFirstHeader(TopologyRequestValidator.HASH_HEADER).getValue());
+ Assert.assertTrue(method.getFirstHeader(TopologyRequestValidator.HASH_HEADER).getValue().length() > 0);
+ Assert.assertNotNull(method.getFirstHeader(TopologyRequestValidator.SIG_HEADER));
+ Assert.assertNotNull(method.getFirstHeader(TopologyRequestValidator.SIG_HEADER).getValue());
+ Assert.assertTrue(method.getFirstHeader(TopologyRequestValidator.SIG_HEADER).getValue().length() > 0);
+ final HttpServletRequest request = context.mock(HttpServletRequest.class);
+ context.checking(new Expectations() {
+ {
+ allowing(request).getHeader(with(TopologyRequestValidator.HASH_HEADER));
+ will(returnValue(method.getFirstHeader(TopologyRequestValidator.HASH_HEADER).getValue()));
+
+ allowing(request).getHeader(with(TopologyRequestValidator.SIG_HEADER));
+ will(returnValue(method.getFirstHeader(TopologyRequestValidator.SIG_HEADER).getValue()));
+
+ allowing(request).getHeader(with("Content-Encoding"));
+ will(returnValue(""));
+
+ allowing(request).getRequestURI();
+ will(returnValue(method.getURI().getPath()));
+
+ allowing(request).getReader();
+ will(returnValue(new BufferedReader(new StringReader(message))));
+ }
+ });
+
+ Assert.assertTrue(topologyRequestValidator.isTrusted(request));
+ Assert.assertEquals(clearMessage, topologyRequestValidator.decodeMessage(request));
+ }
+
+
+
+ @Test
+ public void testTrustResponse() throws IOException {
+ final HttpServletRequest request = context.mock(HttpServletRequest.class);
+ context.checking(new Expectations() {
+ {
+ allowing(request).getRequestURI();
+ will(returnValue("/Test/Uri2"));
+ }
+ });
+
+ final HttpServletResponse response = context.mock(HttpServletResponse.class);
+ final Map<Object, Object> headers = new HashMap<Object, Object>();
+ context.checking(new Expectations() {
+ {
+ allowing(response).setHeader(with(any(String.class)), with(any(String.class)));
+ will(new Action(){
+
+ public void describeTo(Description desc) {
+ desc.appendText("Setting header ");
+ }
+
+ public Object invoke(Invocation invocation) throws Throwable {
+ headers.put(invocation.getParameter(0), invocation.getParameter(1));
+ return null;
+ }
+
+ });
+ }
+ });
+
+ String clearMessage = "TestMessage2";
+ final String message = topologyRequestValidator.encodeMessage(clearMessage);
+ topologyRequestValidator.trustMessage(response, request, message);
+
+ final HttpEntity responseEntity = context.mock(HttpEntity.class);
+ context.checking(new Expectations() {
+ {
+ allowing(responseEntity).getContent();
+ will(returnValue(new ByteArrayInputStream(message.getBytes())));
+ }
+ });
+
+ final HttpResponse resp = context.mock(HttpResponse.class);
+ context.checking(new Expectations(){
+ {
+ allowing(resp).getFirstHeader(with(any(String.class)));
+ will(new Action() {
+ public void describeTo(Description desc) {
+ desc.appendText("Getting (first) header ");
+ }
+
+ public Object invoke(Invocation invocation) throws Throwable {
+ return new BasicHeader((String)invocation.getParameter(0), (String)headers.get(invocation.getParameter(0)));
+ }
+
+ });
+
+ allowing(resp).getEntity();
+ will(returnValue(responseEntity));
+ }
+ });
+ topologyRequestValidator.isTrusted(resp);
+ topologyRequestValidator.decodeMessage("/Test/Uri2", resp);
+
+ }
+
+
+
+}
diff --git a/src/test/java/org/apache/sling/discovery/base/connectors/ping/wl/WildcardHelperTest.java b/src/test/java/org/apache/sling/discovery/base/connectors/ping/wl/WildcardHelperTest.java
new file mode 100644
index 0000000..6931b14
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/connectors/ping/wl/WildcardHelperTest.java
@@ -0,0 +1,117 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.sling.discovery.base.connectors.ping.wl;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.UUID;
+
+import org.apache.commons.net.util.SubnetUtils;
+import org.junit.Test;
+
+public class WildcardHelperTest {
+
+ @Test
+ public void testNullValues() {
+
+ SubnetUtils s = new SubnetUtils("1.2.3.4/10");
+ s = new SubnetUtils("1.2.3.4", "255.255.0.0");
+
+ try{
+ WildcardHelper.wildcardAsRegex(null);
+ fail("should complain");
+ } catch(IllegalArgumentException iae) {
+ // ok
+ }
+ try{
+ WildcardHelper.matchesWildcard(null, "foo");
+ fail("should complain");
+ } catch(IllegalArgumentException iae) {
+ // ok
+ }
+ try{
+ WildcardHelper.matchesWildcard("foo", null);
+ fail("should complain");
+ } catch(IllegalArgumentException iae) {
+ // ok
+ }
+ }
+
+ @Test
+ public void testEmptyValue() {
+ final String expected = "\\Q\\E";
+ assertEquals(expected, WildcardHelper.wildcardAsRegex(""));
+ }
+
+ @Test
+ public void testWithoutWildcards() {
+ for(int i=0; i<1000; i++) {
+ String randomString = UUID.randomUUID().toString();
+ assertTrue(WildcardHelper.matchesWildcard(randomString, randomString));
+ }
+ }
+
+ @Test
+ public void testWildcards() {
+ assertTrue(WildcardHelper.matchesWildcard("", "*"));
+ assertTrue(WildcardHelper.matchesWildcard("fooo", "*"));
+ assertTrue(WildcardHelper.matchesWildcard("fooo", "**"));
+ assertTrue(WildcardHelper.matchesWildcard("fooo", "**?"));
+ assertTrue(WildcardHelper.matchesWildcard("fooo", "????"));
+ assertFalse(WildcardHelper.matchesWildcard("fooo", "???"));
+ assertFalse(WildcardHelper.matchesWildcard("fooo", "?????"));
+
+ for(int i=0; i<100; i++) {
+ String randomString = UUID.randomUUID().toString();
+ assertTrue(WildcardHelper.matchesWildcard(randomString, "*"));
+ assertTrue(WildcardHelper.matchesWildcard(randomString, randomString.substring(0, randomString.length()-1)+"?"));
+ assertTrue(WildcardHelper.matchesWildcard(randomString, randomString.substring(0, randomString.length()-1)+"*"));
+ assertTrue(WildcardHelper.matchesWildcard(randomString, randomString.substring(0, randomString.length()-2)+"??"));
+ assertTrue(WildcardHelper.matchesWildcard(randomString, randomString.substring(0, randomString.length()-2)+"*"));
+ assertTrue(WildcardHelper.matchesWildcard(randomString, "?" + randomString.substring(1, randomString.length())));
+ assertTrue(WildcardHelper.matchesWildcard(randomString, "*" + randomString.substring(1, randomString.length())));
+ assertTrue(WildcardHelper.matchesWildcard(randomString, "??" + randomString.substring(2, randomString.length())));
+ assertTrue(WildcardHelper.matchesWildcard(randomString, "*" + randomString.substring(2, randomString.length())));
+ }
+
+ assertTrue(WildcardHelper.matchesWildcard("fooo", "f*"));
+ assertTrue(WildcardHelper.matchesWildcard("fooo", "fo*"));
+ assertTrue(WildcardHelper.matchesWildcard("fooo", "foo*"));
+ assertTrue(WildcardHelper.matchesWildcard("fooo", "fooo*"));
+ assertFalse(WildcardHelper.matchesWildcard("fooo", "fooo?"));
+ assertFalse(WildcardHelper.matchesWildcard("fooo", "foooo*"));
+
+ assertTrue(WildcardHelper.matchesWildcard("fooo", "*"));
+ assertFalse(WildcardHelper.matchesWildcard("fooo", "?"));
+ assertFalse(WildcardHelper.matchesWildcard("fooo", "f?"));
+
+ assertTrue(WildcardHelper.matchesWildcard("fo", "f?"));
+
+ assertTrue(WildcardHelper.matchesWildcard("foooba", "f*b?"));
+ assertFalse(WildcardHelper.matchesWildcard("fooobar", "f*b?"));
+ assertFalse(WildcardHelper.matchesWildcard("foooba", "f*b?r"));
+ assertTrue(WildcardHelper.matchesWildcard("fooobar", "f*b?r"));
+ assertTrue(WildcardHelper.matchesWildcard("foooba", "f???b?"));
+ assertFalse(WildcardHelper.matchesWildcard("foooba", "f??b?"));
+ assertFalse(WildcardHelper.matchesWildcard("foooba", "f??b?"));
+ }
+}
diff --git a/src/test/java/org/apache/sling/discovery/base/its/AbstractClusterLoadTest.java b/src/test/java/org/apache/sling/discovery/base/its/AbstractClusterLoadTest.java
new file mode 100644
index 0000000..1e934fd
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/its/AbstractClusterLoadTest.java
@@ -0,0 +1,287 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF 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.apache.sling.discovery.base.its;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Random;
+
+import org.apache.sling.discovery.base.commons.UndefinedClusterViewException;
+import org.apache.sling.discovery.base.its.setup.VirtualInstance;
+import org.apache.sling.discovery.base.its.setup.VirtualInstanceBuilder;
+import org.apache.sling.discovery.base.its.setup.WithholdingAppender;
+import org.apache.sling.testing.tools.retry.RetryLoop;
+import org.junit.After;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Migrated from org.apache.sling.discovery.impl.cluster.ClusterLoadTest
+ */
+public abstract class AbstractClusterLoadTest {
+
+ // wait up to 120 sec - in 1sec wait-intervals
+ private static final int INSTANCE_VIEW_TIMEOUT_SECONDS = 120;
+ private static final int INSTANCE_VIEW_POLL_INTERVAL_MILLIS = 500;
+
+ private final Random random = new Random();
+
+ private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+ List<VirtualInstance> instances = new LinkedList<VirtualInstance>();
+
+ @After
+ public void tearDown() throws Exception {
+ if (instances==null || instances.size()==0) {
+ return;
+ }
+ for (Iterator<VirtualInstance> it = instances.iterator(); it.hasNext();) {
+ VirtualInstance i = it.next();
+ i.stop();
+ it.remove();
+ }
+ }
+
+ public abstract VirtualInstanceBuilder newBuilder();
+
+
+ @Test
+ public void testFramework() throws Exception {
+ logger.info("testFramework: building 1st instance..");
+ VirtualInstanceBuilder builder = newBuilder()
+ .newRepository("/var/discovery/impl/ClusterLoadTest/testFramework/", true)
+ .setDebugName("firstInstance")
+ .setConnectorPingTimeout(3)
+ .setConnectorPingInterval(20)
+ .setMinEventDelay(0);
+ VirtualInstance firstInstance = builder.build();
+ instances.add(firstInstance);
+ Thread.sleep(2000);
+ // without any heartbeat action, the discovery service reports its local instance
+ // in so called 'isolated' mode - lets test for that
+ try{
+ firstInstance.getClusterViewService().getLocalClusterView();
+ fail("should complain");
+ } catch(UndefinedClusterViewException e) {
+ // SLING-5030:
+ }
+ firstInstance.startViewChecker(1);
+ Thread.sleep(4000);
+ // after a heartbeat and letting it settle, the discovery service must have
+ // established a view - test for that
+ firstInstance.dumpRepo();
+ firstInstance.assertEstablishedView();
+
+ VirtualInstanceBuilder builder2 = newBuilder()
+ .useRepositoryOf(builder)
+ .setDebugName("secondInstance")
+ .setConnectorPingTimeout(3)
+ .setConnectorPingInterval(20)
+ .setMinEventDelay(0);
+ firstInstance.dumpRepo();
+ logger.info("testFramework: building 2nd instance..");
+ VirtualInstance secondInstance = builder2.build();
+ instances.add(secondInstance);
+ secondInstance.startViewChecker(1);
+ Thread.sleep(4000);
+ firstInstance.dumpRepo();
+ assertEquals(firstInstance.getClusterViewService().getLocalClusterView().getInstances().size(), 2);
+ assertEquals(secondInstance.getClusterViewService().getLocalClusterView().getInstances().size(), 2);
+ }
+
+ @Test
+ public void testTwoInstances() throws Throwable {
+ doTest(2, 5);
+ }
+
+ @Test
+ public void testThreeInstances() throws Throwable {
+ doTest(3, 6);
+ }
+
+ @Test
+ public void testFourInstances() throws Throwable {
+ doTest(4, 7);
+ }
+
+ @Test
+ public void testFiveInstances() throws Throwable {
+ doTest(5, 8);
+ }
+
+ @Test
+ public void testSixInstances() throws Throwable {
+ doTest(6, 9);
+ }
+
+ @Test
+ public void testSevenInstances() throws Throwable {
+ doTest(7, 10);
+ }
+
+ private void doTest(final int size, final int loopCnt) throws Throwable {
+ WithholdingAppender withholdingAppender = null;
+ boolean failure = true;
+ try{
+ logger.info("doTest("+size+","+loopCnt+"): muting log output...");
+ withholdingAppender = WithholdingAppender.install();
+ doDoTest(size, loopCnt);
+ failure = false;
+ } finally {
+ if (withholdingAppender!=null) {
+ if (failure) {
+ logger.info("doTest("+size+","+loopCnt+"): writing muted log output due to failure...");
+ }
+ withholdingAppender.release(failure);
+ if (!failure) {
+ logger.info("doTest("+size+","+loopCnt+"): not writing muted log output due to success...");
+ }
+ }
+ logger.info("doTest("+size+","+loopCnt+"): unmuted log output.");
+ }
+ }
+
+ private void doDoTest(final int size, final int loopCnt) throws Throwable {
+ if (size<2) {
+ fail("can only test 2 or more instances");
+ }
+ VirtualInstanceBuilder builder = newBuilder()
+ .newRepository("/var/discovery/impl/ClusterLoadTest/doTest-"+size+"-"+loopCnt+"/", true)
+ .setDebugName("firstInstance")
+ .setConnectorPingTimeout(3)
+ .setConnectorPingInterval(20)
+ .setMinEventDelay(0);
+ VirtualInstance firstInstance = builder.build();
+ firstInstance.startViewChecker(1);
+ instances.add(firstInstance);
+ for(int i=1; i<size; i++) {
+ VirtualInstanceBuilder builder2 = newBuilder()
+ .useRepositoryOf(builder)
+ .setDebugName("subsequentInstance-"+i)
+ .setConnectorPingTimeout(3)
+ .setMinEventDelay(0)
+ .setConnectorPingInterval(20);
+ VirtualInstance subsequentInstance = builder2.build();
+ instances.add(subsequentInstance);
+ subsequentInstance.startViewChecker(1);
+ }
+
+ for(int i=0; i<loopCnt; i++) {
+ logger.info("=====================");
+ logger.info(" START of LOOP "+i);
+ logger.info("=====================");
+
+ // count how many instances had heartbeats running in the first place
+ int aliveCnt = 0;
+ for (Iterator<VirtualInstance> it = instances.iterator(); it.hasNext();) {
+ VirtualInstance instance = it.next();
+ if (instance.isViewCheckerRunning()) {
+ aliveCnt++;
+ }
+ }
+ logger.info("=====================");
+ logger.info(" original aliveCnt "+aliveCnt);
+ logger.info("=====================");
+ if (aliveCnt==0) {
+ // if no one is sending heartbeats, all instances go back to isolated mode
+ aliveCnt=1;
+ }
+
+ final int aliveCntFinal = aliveCnt;
+
+ for (Iterator<VirtualInstance> it = instances.iterator(); it.hasNext();) {
+ VirtualInstance instance = it.next();
+ try {
+ instance.dumpRepo();
+ } catch (Exception e) {
+ logger.error("Failed dumping repo for instance " + instance.getSlingId(), e);
+ }
+ }
+
+ // then verify that each instance sees that many instances
+ for (Iterator<VirtualInstance> it = instances.iterator(); it.hasNext();) {
+ final VirtualInstance instance = it.next();
+ if (!instance.isViewCheckerRunning()) {
+ // if the heartbeat is not running, this instance is considered dead
+ // hence we're not doing any assert here (as the count is only
+ // valid if heartbeat/checkView is running and that would void the test)
+ } else {
+ new RetryLoop(new ConditionImplementation(instance, aliveCntFinal), INSTANCE_VIEW_TIMEOUT_SECONDS,
+ INSTANCE_VIEW_POLL_INTERVAL_MILLIS);
+ }
+ }
+
+ // start/stop heartbeats accordingly
+ logger.info("Starting/Stopping heartbeats with count="+instances.size());
+ for (Iterator<VirtualInstance> it = instances.iterator(); it.hasNext();) {
+ VirtualInstance instance = it.next();
+ if (random.nextBoolean()) {
+ logger.info("Starting heartbeats with "+instance.slingId);
+ instance.startViewChecker(1);
+ logger.info("Started heartbeats with "+instance.slingId);
+ } else {
+ logger.info("Stopping heartbeats with "+instance.slingId);
+ instance.stopViewChecker();
+ logger.info("Stopped heartbeats with "+instance.slingId);
+ }
+ }
+
+ }
+ }
+
+ class ConditionImplementation implements RetryLoop.Condition {
+
+ private final int expectedAliveCount;
+ private final VirtualInstance instance;
+
+ private ConditionImplementation(VirtualInstance instance, int expectedAliveCount) {
+ this.expectedAliveCount = expectedAliveCount;
+ this.instance = instance;
+ }
+
+ public boolean isTrue() throws Exception {
+ boolean result = false;
+ int actualAliveCount = -1;
+ try{
+ actualAliveCount = instance.getClusterViewService().getLocalClusterView().getInstances().size();
+ result = expectedAliveCount == actualAliveCount;
+ } catch(UndefinedClusterViewException e) {
+ logger.info("no view at the moment: "+e);
+ return false;
+ } catch(Exception e) {
+ logger.error("isTrue: got exception: "+e, e);
+ throw e;
+ }
+ if (!result) {
+ logger.info("isTrue: expected="+expectedAliveCount+", actual="+actualAliveCount+", result="+result);
+ }
+ return result;
+ }
+
+ public String getDescription() {
+ return "Waiting for instance with " + instance.getSlingId() + " to see " + expectedAliveCount
+ + " instances";
+ }
+ }
+
+}
diff --git a/src/test/java/org/apache/sling/discovery/base/its/AbstractClusterTest.java b/src/test/java/org/apache/sling/discovery/base/its/AbstractClusterTest.java
new file mode 100644
index 0000000..08ead70
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/its/AbstractClusterTest.java
@@ -0,0 +1,1540 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.sling.discovery.base.its;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.Semaphore;
+
+import org.apache.log4j.Level;
+import org.apache.log4j.LogManager;
+import org.apache.sling.discovery.ClusterView;
+import org.apache.sling.discovery.InstanceDescription;
+import org.apache.sling.discovery.TopologyEvent;
+import org.apache.sling.discovery.TopologyEvent.Type;
+import org.apache.sling.discovery.TopologyEventListener;
+import org.apache.sling.discovery.TopologyView;
+import org.apache.sling.discovery.base.commons.ClusterViewHelper;
+import org.apache.sling.discovery.base.commons.ClusterViewService;
+import org.apache.sling.discovery.base.commons.UndefinedClusterViewException;
+import org.apache.sling.discovery.base.connectors.announcement.Announcement;
+import org.apache.sling.discovery.base.connectors.announcement.AnnouncementFilter;
+import org.apache.sling.discovery.base.connectors.announcement.AnnouncementRegistry;
+import org.apache.sling.discovery.base.its.setup.VirtualInstance;
+import org.apache.sling.discovery.base.its.setup.VirtualInstanceBuilder;
+import org.apache.sling.discovery.base.its.setup.mock.AcceptsMultiple;
+import org.apache.sling.discovery.base.its.setup.mock.AssertingTopologyEventListener;
+import org.apache.sling.discovery.base.its.setup.mock.PropertyProviderImpl;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public abstract class AbstractClusterTest {
+
+ private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+ private class SimpleClusterView {
+
+ private VirtualInstance[] instances;
+
+ SimpleClusterView(VirtualInstance... instances) {
+ this.instances = instances;
+ }
+
+ @Override
+ public String toString() {
+ String instanceSlingIds = "";
+ for(int i=0; i<instances.length; i++) {
+ instanceSlingIds = instanceSlingIds + instances[i].slingId + ",";
+ }
+ return "an expected cluster with "+instances.length+" instances: "+instanceSlingIds;
+ }
+ }
+
+ VirtualInstance instance1;
+ VirtualInstance instance2;
+ VirtualInstance instance3;
+
+ private String property1Value;
+
+ protected String property2Value;
+
+ private String property1Name;
+
+ private String property2Name;
+ VirtualInstance instance4;
+ VirtualInstance instance5;
+ VirtualInstance instance1Restarted;
+ private Level logLevel;
+
+ protected abstract VirtualInstanceBuilder newBuilder();
+
+ @Before
+ public void setup() throws Exception {
+ final org.apache.log4j.Logger discoveryLogger = LogManager.getRootLogger().getLogger("org.apache.sling.discovery");
+ logLevel = discoveryLogger.getLevel();
+ discoveryLogger.setLevel(Level.TRACE);
+ logger.debug("here we are");
+ instance1 = newBuilder().setDebugName("firstInstance").newRepository("/var/discovery/impl/", true).build();
+ instance2 = newBuilder().setDebugName("secondInstance").useRepositoryOf(instance1).build();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ if (instance5 != null) {
+ instance5.stop();
+ }
+ if (instance4 != null) {
+ instance4.stop();
+ }
+ if (instance3 != null) {
+ instance3.stop();
+ }
+ if (instance3 != null) {
+ instance3.stop();
+ }
+ if (instance2 != null) {
+ instance2.stop();
+ }
+ if (instance1 != null) {
+ instance1.stop();
+ }
+ if (instance1Restarted != null) {
+ instance1Restarted.stop();
+ }
+ instance1Restarted = null;
+ instance1 = null;
+ instance2 = null;
+ instance3 = null;
+ instance4 = null;
+ instance5 = null;
+ final org.apache.log4j.Logger discoveryLogger = LogManager.getRootLogger().getLogger("org.apache.sling.discovery");
+ discoveryLogger.setLevel(logLevel);
+ }
+
+ /** test leader behaviour with ascending slingIds, SLING-3253 **/
+ @Test
+ public void testLeaderAsc() throws Throwable {
+ logger.info("testLeaderAsc: start");
+ doTestLeader("000", "111");
+ logger.info("testLeaderAsc: end");
+ }
+
+ /** test leader behaviour with descending slingIds, SLING-3253 **/
+ @Test
+ public void testLeaderDesc() throws Throwable {
+ logger.info("testLeaderDesc: start");
+ doTestLeader("111", "000");
+ logger.info("testLeaderDesc: end");
+ }
+
+ private void doTestLeader(String slingId1, String slingId2) throws Throwable {
+ logger.info("doTestLeader("+slingId1+","+slingId2+"): start");
+ // stop 1 and 2 and create them with a lower heartbeat timeout
+ instance2.stopViewChecker();
+ instance1.stopViewChecker();
+ instance2.stop();
+ instance1.stop();
+ instance1 = newBuilder().setDebugName("firstInstance")
+ .newRepository("/var/discovery/impl/", true)
+ .setConnectorPingTimeout(30)
+ .setMinEventDelay(1)
+ .setSlingId(slingId1).build();
+ // sleep so that the two dont have the same startup time, and thus leaderElectionId is lower for instance1
+ logger.info("doTestLeader: 1st sleep 200ms");
+ Thread.sleep(200);
+ instance2 = newBuilder().setDebugName("secondInstance")
+ .useRepositoryOf(instance1)
+ .setConnectorPingTimeout(30)
+ .setMinEventDelay(1)
+ .setSlingId(slingId2).build();
+ assertNotNull(instance1);
+ assertNotNull(instance2);
+
+ // the two instances are still isolated - hence they throw an exception
+ try{
+ instance1.getClusterViewService().getLocalClusterView();
+ fail("should complain");
+ } catch(UndefinedClusterViewException e) {
+ // ok
+ }
+ try{
+ instance2.getClusterViewService().getLocalClusterView();
+ fail("should complain");
+ } catch(UndefinedClusterViewException e) {
+ // ok
+ }
+
+ // let the sync/voting happen
+ for(int m=0; m<4; m++) {
+ instance1.heartbeatsAndCheckView();
+ instance2.heartbeatsAndCheckView();
+ logger.info("doTestLeader: sleep 500ms");
+ Thread.sleep(500);
+ }
+ instance1.heartbeatsAndCheckView();
+ instance2.heartbeatsAndCheckView();
+
+ // now they must be in the same cluster, so in a cluster of size 1
+ assertEquals(2, instance1.getClusterViewService().getLocalClusterView().getInstances().size());
+ assertEquals(2, instance2.getClusterViewService().getLocalClusterView().getInstances().size());
+
+ // the first instance should be the leader - since it was started first
+ assertTrue(instance1.getLocalInstanceDescription().isLeader());
+ assertFalse(instance2.getLocalInstanceDescription().isLeader());
+ logger.info("doTestLeader("+slingId1+","+slingId2+"): end");
+ }
+
+ /**
+ * Tests stale announcement reported in SLING-4139:
+ * An instance which crashes but had announcements, never cleans up those announcements.
+ * Thus on a restart, those announcements are still there, even if the connector
+ * would no longer be in use (or point somewhere else etc).
+ * That has various effects, one of them tested in this method: peers in the same cluster,
+ * after the crashed/stopped instance restarts, will assume those stale announcements
+ * as being correct and include them in the topology - hence reporting stale instances
+ * (which can be old instances or even duplicates).
+ */
+ @Test
+ public void testStaleAnnouncementsVisibleToClusterPeers4139() throws Throwable {
+ logger.info("testStaleAnnouncementsVisibleToClusterPeers4139: start");
+ final String instance1SlingId = prepare4139();
+
+ // remove topology connector from instance3 to instance1
+ // -> corresponds to stop pinging
+ // (nothing to assert additionally here)
+
+ // start instance 1
+ instance1Restarted = newBuilder().setDebugName("firstInstance")
+ .useRepositoryOf(instance2)
+ .setConnectorPingTimeout(Integer.MAX_VALUE /* no timeout */)
+ .setMinEventDelay(1)
+ .setSlingId(instance1SlingId).build();
+ runHeartbeatOnceWith(instance1Restarted, instance2, instance3);
+ Thread.sleep(500);
+ runHeartbeatOnceWith(instance1Restarted, instance2, instance3);
+ Thread.sleep(500);
+ runHeartbeatOnceWith(instance1Restarted, instance2, instance3);
+
+ // facts: connector 3->1 does not exist actively anymore,
+ // instance 1+2 should build a cluster,
+ // instance 3 should be isolated
+ logger.info("instance1Restarted.dump: "+instance1Restarted.slingId);
+ instance1Restarted.dumpRepo();
+
+ logger.info("instance2.dump: "+instance2.slingId);
+ instance2.dumpRepo();
+
+ logger.info("instance3.dump: "+instance3.slingId);
+ instance3.dumpRepo();
+
+ assertTopology(instance1Restarted, new SimpleClusterView(instance1Restarted, instance2));
+ assertTopology(instance3, new SimpleClusterView(instance3));
+ assertTopology(instance2, new SimpleClusterView(instance1Restarted, instance2));
+ instance1Restarted.stop();
+ logger.info("testStaleAnnouncementsVisibleToClusterPeers4139: end");
+ }
+
+ /**
+ * Tests a situation where a connector was done to instance1, which eventually
+ * crashed, then the connector is done to instance2. Meanwhile instance1
+ * got restarted and this test assures that the instance3 is not reported
+ * twice in the topology. Did not happen before 4139, but should never afterwards neither
+ */
+ @Test
+ public void testDuplicateInstanceIn2Clusters4139() throws Throwable {
+ logger.info("testDuplicateInstanceIn2Clusters4139: start");
+ final String instance1SlingId = prepare4139();
+
+ // remove topology connector from instance3 to instance1
+ // -> corresponds to stop pinging
+ // (nothing to assert additionally here)
+ // instead, now start a connector from instance3 to instance2
+ pingConnector(instance3, instance2);
+
+ // start instance 1
+ instance1Restarted = newBuilder().setDebugName("firstInstance")
+ .useRepositoryOf(instance2)
+ .setConnectorPingTimeout(Integer.MAX_VALUE /* no timeout */)
+ .setMinEventDelay(1)
+ .setSlingId(instance1SlingId).build();
+ runHeartbeatOnceWith(instance1Restarted, instance2, instance3);
+ pingConnector(instance3, instance2);
+ runHeartbeatOnceWith(instance1Restarted, instance2, instance3);
+ pingConnector(instance3, instance2);
+ logger.info("iteration 0");
+ logger.info("instance1Restarted.slingId: "+instance1Restarted.slingId);
+ logger.info("instance2.slingId: "+instance2.slingId);
+ logger.info("instance3.slingId: "+instance3.slingId);
+ instance1Restarted.dumpRepo();
+ assertSameTopology(new SimpleClusterView(instance1Restarted, instance2), new SimpleClusterView(instance3));
+
+ Thread.sleep(500);
+ runHeartbeatOnceWith(instance1Restarted, instance2, instance3);
+ pingConnector(instance3, instance2);
+ runHeartbeatOnceWith(instance1Restarted, instance2, instance3);
+ pingConnector(instance3, instance2);
+ logger.info("iteration 1");
+ logger.info("instance1Restarted.slingId: "+instance1Restarted.slingId);
+ logger.info("instance2.slingId: "+instance2.slingId);
+ logger.info("instance3.slingId: "+instance3.slingId);
+ instance1Restarted.dumpRepo();
+ assertSameTopology(new SimpleClusterView(instance1Restarted, instance2), new SimpleClusterView(instance3));
+ instance1Restarted.stop();
+
+ logger.info("testDuplicateInstanceIn2Clusters4139: end");
+ }
+
+/* ok, this test should do the following:
+ * cluster A with instance 1 and instance 2
+ * cluster B with instance 3 and instance 4
+ * cluster C with instance 5
+
+ * initially, instance3 is pinging instance1, and instance 5 is pinging instance1 as well (MAC hub)
+ * that should result in instance3 and 5 to inherit the rest from instance1
+ * then simulate load balancer switching from instance1 to instance2 - hence pings go to instance2
+ *
+ */
+ @Test
+ public void testConnectorSwitching4139() throws Throwable {
+ final int MIN_EVENT_DELAY = 1;
+
+ tearDown(); // reset any setup that was done - we start with a different setup than the default one
+ final org.apache.log4j.Logger discoveryLogger = LogManager.getRootLogger().getLogger("org.apache.sling.discovery");
+ logLevel = discoveryLogger.getLevel();
+ discoveryLogger.setLevel(Level.DEBUG);
+
+ instance1 = newBuilder().setDebugName("instance1")
+ .newRepository("/var/discovery/clusterA/", true)
+ .setConnectorPingTimeout(10 /* sec */)
+ .setConnectorPingInterval(999)
+ .setMinEventDelay(MIN_EVENT_DELAY).build();
+ instance2 = newBuilder().setDebugName("instance2")
+ .useRepositoryOf(instance1)
+ .setConnectorPingTimeout(10 /* sec */)
+ .setConnectorPingInterval(999)
+ .setMinEventDelay(MIN_EVENT_DELAY).build();
+ // now launch the remote instance
+ instance3 = newBuilder().setDebugName("instance3")
+ .newRepository("/var/discovery/clusterB/", false)
+ .setConnectorPingTimeout(10 /* sec */)
+ .setConnectorPingInterval(999)
+ .setMinEventDelay(MIN_EVENT_DELAY).build();
+ instance4 = newBuilder().setDebugName("instance4")
+ .useRepositoryOf(instance3)
+ .setConnectorPingTimeout(10 /* sec */)
+ .setConnectorPingInterval(999)
+ .setMinEventDelay(MIN_EVENT_DELAY).build();
+ instance5 = newBuilder().setDebugName("instance5")
+ .newRepository("/var/discovery/clusterC/", false)
+ .setConnectorPingTimeout(10 /* sec */)
+ .setConnectorPingInterval(999)
+ .setMinEventDelay(MIN_EVENT_DELAY).build();
+
+ // join the instances to form a cluster by sending out heartbeats
+ runHeartbeatOnceWith(instance1, instance2, instance3, instance4, instance5);
+ Thread.sleep(500);
+ runHeartbeatOnceWith(instance1, instance2, instance3, instance4, instance5);
+ Thread.sleep(500);
+ runHeartbeatOnceWith(instance1, instance2, instance3, instance4, instance5);
+ Thread.sleep(500);
+
+ assertSameTopology(new SimpleClusterView(instance1, instance2));
+ assertSameTopology(new SimpleClusterView(instance3, instance4));
+ assertSameTopology(new SimpleClusterView(instance5));
+
+ // create a topology connector from instance3 to instance1
+ // -> corresponds to starting to ping
+ runHeartbeatOnceWith(instance1, instance2, instance3, instance4, instance5);
+ pingConnector(instance3, instance1);
+ pingConnector(instance5, instance1);
+ Thread.sleep(500);
+ runHeartbeatOnceWith(instance1, instance2, instance3, instance4, instance5);
+ pingConnector(instance3, instance1);
+ pingConnector(instance5, instance1);
+ Thread.sleep(500);
+
+ // make asserts on the topology
+ logger.info("testConnectorSwitching4139: instance1.slingId="+instance1.slingId);
+ logger.info("testConnectorSwitching4139: instance2.slingId="+instance2.slingId);
+ logger.info("testConnectorSwitching4139: instance3.slingId="+instance3.slingId);
+ logger.info("testConnectorSwitching4139: instance4.slingId="+instance4.slingId);
+ logger.info("testConnectorSwitching4139: instance5.slingId="+instance5.slingId);
+ instance1.dumpRepo();
+
+ assertSameTopology(new SimpleClusterView(instance1, instance2),
+ new SimpleClusterView(instance3, instance4),
+ new SimpleClusterView(instance5));
+
+ // simulate a crash of instance1, resulting in load-balancer to switch the pings
+ boolean success = false;
+ for(int i=0; i<25; i++) {
+ // loop for max 25 times, min 20 times
+ runHeartbeatOnceWith(instance2, instance3, instance4, instance5);
+ final boolean ping1 = pingConnector(instance3, instance2);
+ final boolean ping2 = pingConnector(instance5, instance2);
+ if (ping1 && ping2) {
+ // both pings were fine - hence break
+ success = true;
+ logger.info("testConnectorSwitching4139: successfully switched all pings to instance2 after "+i+" rounds.");
+ if (i<20) {
+ logger.info("testConnectorSwitching4139: min loop cnt not yet reached: i="+i);
+ Thread.sleep(1000); // 20x1000ms = 20sec max - (vs 10sec timeout) - should be enough for timing out
+ continue;
+ }
+ break;
+ }
+ logger.info("testConnectorSwitching4139: looping cos ping1="+ping1+", ping2="+ping2);
+ Thread.sleep(1000); // 25x1000ms = 25sec max - (vs 10sec timeout)
+
+ }
+ assertTrue(success);
+ // one final heartbeat
+ runHeartbeatOnceWith(instance2, instance3, instance4, instance5);
+ assertTrue(pingConnector(instance3, instance2));
+ assertTrue(pingConnector(instance5, instance2));
+
+ instance2.dumpRepo();
+
+ assertSameTopology(new SimpleClusterView(instance2),
+ new SimpleClusterView(instance3, instance4),
+ new SimpleClusterView(instance5));
+
+ // restart instance1, crash instance4
+ instance4.stopViewChecker();
+ instance1Restarted = newBuilder().setDebugName("instance1")
+ .useRepositoryOf(instance2)
+ .setConnectorPingTimeout(Integer.MAX_VALUE /* no timeout */)
+ .setMinEventDelay(1)
+ .setSlingId(instance1.getSlingId()).build();
+ runHeartbeatOnceWith(instance1Restarted, instance2, instance3, instance5);
+ assertTrue(pingConnector(instance3, instance2));
+ assertTrue(pingConnector(instance5, instance2));
+ success = false;
+ for(int i=0; i<25; i++) {
+ runHeartbeatOnceWith(instance1Restarted, instance2, instance3, instance5);
+ instance1.getViewChecker().checkView();
+ assertTrue(pingConnector(instance3, instance2));
+ assertTrue(pingConnector(instance5, instance2));
+ final TopologyView topology = instance3.getDiscoveryService().getTopology();
+ InstanceDescription i3 = null;
+ for (Iterator<InstanceDescription> it = topology.getInstances().iterator(); it.hasNext();) {
+ final InstanceDescription id = it.next();
+ if (id.getSlingId().equals(instance3.slingId)) {
+ i3 = id;
+ break;
+ }
+ }
+ assertNotNull(i3);
+ assertEquals(instance3.slingId, i3.getSlingId());
+ final ClusterView i3Cluster = i3.getClusterView();
+ final int i3ClusterSize = i3Cluster.getInstances().size();
+ if (i3ClusterSize==1) {
+ if (i<20) {
+ logger.info("testConnectorSwitching4139: [2] min loop cnt not yet reached: i="+i);
+ Thread.sleep(500); // 20x500ms = 10sec max - (vs 5sec timeout) - should be enough for timing out
+ continue;
+ }
+ success = true;
+ break;
+ }
+ logger.info("testConnectorSwitching4139: i3ClusterSize: "+i3ClusterSize);
+ Thread.sleep(500);
+ }
+
+ logger.info("testConnectorSwitching4139: instance1Restarted.slingId="+instance1Restarted.slingId);
+ logger.info("testConnectorSwitching4139: instance2.slingId="+instance2.slingId);
+ logger.info("testConnectorSwitching4139: instance3.slingId="+instance3.slingId);
+ logger.info("testConnectorSwitching4139: instance4.slingId="+instance4.slingId);
+ logger.info("testConnectorSwitching4139: instance5.slingId="+instance5.slingId);
+ instance1Restarted.dumpRepo();
+ assertTrue(success);
+
+ assertSameTopology(new SimpleClusterView(instance1Restarted, instance2),
+ new SimpleClusterView(instance3),
+ new SimpleClusterView(instance5));
+ instance1Restarted.stop();
+
+ }
+
+ @Test
+ public void testDuplicateInstance3726() throws Throwable {
+ logger.info("testDuplicateInstance3726: start");
+ final int MIN_EVENT_DELAY = 1;
+
+ tearDown(); // reset any setup that was done - we start with a different setup than the default one
+ final org.apache.log4j.Logger discoveryLogger = LogManager.getRootLogger().getLogger("org.apache.sling.discovery");
+ logLevel = discoveryLogger.getLevel();
+ discoveryLogger.setLevel(Level.DEBUG);
+
+ instance1 = newBuilder().setDebugName("instance1")
+ .newRepository("/var/discovery/clusterA/", true)
+ .setConnectorPingTimeout(15 /* sec */)
+ .setMinEventDelay(MIN_EVENT_DELAY).build();
+ instance2 = newBuilder().setDebugName("instance2")
+ .useRepositoryOf(instance1)
+ .setConnectorPingTimeout(15 /* sec */)
+ .setMinEventDelay(MIN_EVENT_DELAY).build();
+ // now launch the remote instance
+ instance3 = newBuilder().setDebugName("instance3")
+ .newRepository("/var/discovery/clusterB/", false)
+ .setConnectorPingTimeout(15 /* sec */)
+ .setMinEventDelay(MIN_EVENT_DELAY).build();
+ instance5 = newBuilder().setDebugName("instance5")
+ .newRepository("/var/discovery/clusterC/", false)
+ .setConnectorPingTimeout(15 /* sec */)
+ .setMinEventDelay(MIN_EVENT_DELAY).build();
+
+ // join the instances to form a cluster by sending out heartbeats
+ runHeartbeatOnceWith(instance1, instance2, instance3, instance5);
+ Thread.sleep(500);
+ runHeartbeatOnceWith(instance1, instance2, instance3, instance5);
+ Thread.sleep(500);
+ runHeartbeatOnceWith(instance1, instance2, instance3, instance5);
+ Thread.sleep(500);
+
+ assertSameTopology(new SimpleClusterView(instance1, instance2));
+ assertSameTopology(new SimpleClusterView(instance3));
+ assertSameTopology(new SimpleClusterView(instance5));
+
+ // create a topology connector from instance3 to instance1
+ // -> corresponds to starting to ping
+ pingConnector(instance3, instance1);
+ pingConnector(instance5, instance1);
+ pingConnector(instance3, instance1);
+ pingConnector(instance5, instance1);
+
+ // make asserts on the topology
+ logger.info("testDuplicateInstance3726: instance1.slingId="+instance1.slingId);
+ logger.info("testDuplicateInstance3726: instance2.slingId="+instance2.slingId);
+ logger.info("testDuplicateInstance3726: instance3.slingId="+instance3.slingId);
+ logger.info("testDuplicateInstance3726: instance5.slingId="+instance5.slingId);
+ instance1.dumpRepo();
+
+ assertSameTopology(new SimpleClusterView(instance1, instance2),
+ new SimpleClusterView(instance3/*, instance4*/),
+ new SimpleClusterView(instance5));
+
+ // simulate a crash of instance1, resulting in load-balancer to switch the pings
+ instance1.stopViewChecker();
+ boolean success = false;
+ for(int i=0; i<25; i++) {
+ // loop for max 25 times, min 20 times
+ runHeartbeatOnceWith(instance2, instance3, /*instance4, */instance5);
+ final boolean ping1 = pingConnector(instance3, instance2);
+ final boolean ping2 = pingConnector(instance5, instance2);
+ if (ping1 && ping2) {
+ // both pings were fine - hence break
+ success = true;
+ logger.info("testDuplicateInstance3726: successfully switched all pings to instance2 after "+i+" rounds.");
+ if (i<20) {
+ logger.info("testDuplicateInstance3726: min loop cnt not yet reached: i="+i);
+ Thread.sleep(1000); // 20x1000ms = 20sec max - (vs 15sec timeout) - should be enough for timing out
+ continue;
+ }
+ break;
+ }
+ logger.info("testDuplicateInstance3726: looping");
+ Thread.sleep(1000); // 25x1000ms = 25sec max - (vs 15sec timeout)
+
+ }
+ assertTrue(success);
+ // one final heartbeat
+ runHeartbeatOnceWith(instance2, instance3, instance5);
+ assertTrue(pingConnector(instance3, instance2));
+ assertTrue(pingConnector(instance5, instance2));
+
+ instance2.dumpRepo();
+
+ assertSameTopology(new SimpleClusterView(instance2),
+ new SimpleClusterView(instance3),
+ new SimpleClusterView(instance5));
+
+ // restart instance1, start instance4
+ instance1Restarted = newBuilder().setDebugName("instance1")
+ .useRepositoryOf(instance2)
+ .setConnectorPingTimeout(Integer.MAX_VALUE /* no timeout */)
+ .setMinEventDelay(1)
+ .setSlingId(instance1.getSlingId()).build();
+ instance4 = newBuilder().setDebugName("instance4")
+ .useRepositoryOf(instance3)
+ .setConnectorPingTimeout(30 /* sec */)
+ .setMinEventDelay(MIN_EVENT_DELAY).build();
+ for(int i=0; i<3; i++) {
+ runHeartbeatOnceWith(instance1Restarted, instance2, instance3, instance4, instance5);
+ assertTrue(pingConnector(instance3, instance2));
+ assertTrue(pingConnector(instance5, instance2));
+ }
+
+ instance1Restarted.dumpRepo();
+ logger.info("testDuplicateInstance3726: instance1Restarted.slingId="+instance1Restarted.slingId);
+ logger.info("testDuplicateInstance3726: instance2.slingId="+instance2.slingId);
+ logger.info("testDuplicateInstance3726: instance3.slingId="+instance3.slingId);
+ logger.info("testDuplicateInstance3726: instance4.slingId="+instance4.slingId);
+ logger.info("testDuplicateInstance3726: instance5.slingId="+instance5.slingId);
+ assertTrue(success);
+
+ assertSameTopology(new SimpleClusterView(instance1Restarted, instance2),
+ new SimpleClusterView(instance3, instance4),
+ new SimpleClusterView(instance5));
+ instance1Restarted.stop();
+ logger.info("testDuplicateInstance3726: end");
+ }
+
+ private void assertSameTopology(SimpleClusterView... clusters) throws UndefinedClusterViewException {
+ if (clusters==null) {
+ return;
+ }
+ for(int i=0; i<clusters.length; i++) { // go through all clusters
+ final SimpleClusterView aCluster = clusters[i];
+ assertSameClusterIds(aCluster.instances);
+ for(int j=0; j<aCluster.instances.length; j++) { // and all instances therein
+ final VirtualInstance anInstance = aCluster.instances[j];
+ assertTopology(anInstance, clusters); // an verify that they all see the same
+ for(int k=0; k<clusters.length; k++) {
+ final SimpleClusterView otherCluster = clusters[k];
+ if (aCluster==otherCluster) {
+ continue; // then ignore this one
+ }
+ for(int m=0; m<otherCluster.instances.length; m++) {
+ assertNotSameClusterIds(anInstance, otherCluster.instances[m]);
+ }
+ }
+ }
+ }
+ }
+
+ private void runHeartbeatOnceWith(VirtualInstance... instances) {
+ if (instances==null) {
+ return;
+ }
+ for(int i=0; i<instances.length; i++) {
+ instances[i].heartbeatsAndCheckView();
+ }
+ }
+
+ /**
+ * Tests a situation where a connector was done to instance1, which eventually
+ * crashed, then the connector is done to instance4 (which is in a separate, 3rd cluster).
+ * Meanwhile instance1 got restarted and this test assures that the instance3 is not reported
+ * twice in the topology. This used to happen prior to SLING-4139
+ */
+ @Test
+ public void testStaleInstanceIn3Clusters4139() throws Throwable {
+ logger.info("testStaleInstanceIn3Clusters4139: start");
+ final String instance1SlingId = prepare4139();
+
+ // remove topology connector from instance3 to instance1
+ // -> corresponds to stop pinging
+ // (nothing to assert additionally here)
+
+ // start instance4 in a separate cluster
+ instance4 = newBuilder().setDebugName("remoteInstance4")
+ .newRepository("/var/discovery/implremote4/", false)
+ .setConnectorPingTimeout(Integer.MAX_VALUE /* no timeout */)
+ .setMinEventDelay(1).build();
+ try{
+ instance4.getClusterViewService().getLocalClusterView();
+ fail("should complain");
+ } catch(UndefinedClusterViewException e) {
+ // ok
+ }
+
+ // instead, now start a connector from instance3 to instance2
+ instance4.heartbeatsAndCheckView();
+ instance4.heartbeatsAndCheckView();
+ pingConnector(instance3, instance4);
+
+ // start instance 1
+ instance1Restarted = newBuilder().setDebugName("firstInstance")
+ .useRepositoryOf(instance2)
+ .setConnectorPingTimeout(Integer.MAX_VALUE /* no timeout */)
+ .setMinEventDelay(1)
+ .setSlingId(instance1SlingId).build();
+ runHeartbeatOnceWith(instance1Restarted, instance2, instance3, instance4);
+ pingConnector(instance3, instance4);
+ runHeartbeatOnceWith(instance1Restarted, instance2, instance3, instance4);
+ pingConnector(instance3, instance4);
+ logger.info("iteration 0");
+ logger.info("instance1Restarted.slingId: "+instance1Restarted.slingId);
+ logger.info("instance2.slingId: "+instance2.slingId);
+ logger.info("instance3.slingId: "+instance3.slingId);
+ logger.info("instance4.slingId: "+instance4.slingId);
+ instance1Restarted.dumpRepo();
+ assertSameTopology(
+ new SimpleClusterView(instance3),
+ new SimpleClusterView(instance4));
+ assertSameTopology(new SimpleClusterView(instance1Restarted, instance2));
+
+ Thread.sleep(100);
+ runHeartbeatOnceWith(instance1Restarted, instance2, instance3, instance4);
+ pingConnector(instance3, instance4);
+ runHeartbeatOnceWith(instance1Restarted, instance2, instance3, instance4);
+ pingConnector(instance3, instance4);
+ logger.info("iteration 1");
+ logger.info("instance1Restarted.slingId: "+instance1Restarted.slingId);
+ logger.info("instance2.slingId: "+instance2.slingId);
+ logger.info("instance3.slingId: "+instance3.slingId);
+ logger.info("instance4.slingId: "+instance4.slingId);
+ instance1Restarted.dumpRepo();
+ assertSameTopology(new SimpleClusterView(instance1Restarted, instance2));
+ assertSameTopology(
+ new SimpleClusterView(instance3),
+ new SimpleClusterView(instance4));
+
+ Thread.sleep(100);
+ runHeartbeatOnceWith(instance1Restarted, instance2, instance3, instance4);
+ pingConnector(instance3, instance4);
+
+ // now the situation should be as follows:
+ logger.info("iteration 2");
+ logger.info("instance1Restarted.slingId: "+instance1Restarted.slingId);
+ logger.info("instance2.slingId: "+instance2.slingId);
+ logger.info("instance3.slingId: "+instance3.slingId);
+ logger.info("instance4.slingId: "+instance4.slingId);
+ instance1Restarted.dumpRepo();
+ assertSameTopology(new SimpleClusterView(instance1Restarted, instance2));
+ assertSameTopology(
+ new SimpleClusterView(instance3),
+ new SimpleClusterView(instance4));
+ instance1Restarted.stop();
+
+ logger.info("testStaleInstanceIn3Clusters4139: end");
+ }
+
+ /**
+ * Preparation steps for SLING-4139 tests:
+ * Creates two clusters: A: with instance1 and 2, B with instance 3
+ * instance 3 creates a connector to instance 1
+ * then instance 1 is killed (crashes)
+ * @return the slingId of the original (crashed) instance1
+ */
+ private String prepare4139() throws Throwable, Exception,
+ InterruptedException {
+ tearDown(); // stop anything running..
+ instance1 = newBuilder().setDebugName("firstInstance")
+ .newRepository("/var/discovery/impl/", true)
+ .setConnectorPingTimeout(Integer.MAX_VALUE /* no timeout */)
+ .setMinEventDelay(1).build();
+ instance2 = newBuilder().setDebugName("secondInstance")
+ .useRepositoryOf(instance1)
+ .setConnectorPingTimeout(Integer.MAX_VALUE /* no timeout */)
+ .setMinEventDelay(1).build();
+ // join the two instances to form a cluster by sending out heartbeats
+ runHeartbeatOnceWith(instance1, instance2);
+ Thread.sleep(100);
+ runHeartbeatOnceWith(instance1, instance2);
+ Thread.sleep(100);
+ runHeartbeatOnceWith(instance1, instance2);
+ assertSameClusterIds(instance1, instance2);
+
+ // now launch the remote instance
+ instance3 = newBuilder().setDebugName("remoteInstance")
+ .newRepository("/var/discovery/implremote/", false)
+ .setConnectorPingTimeout(Integer.MAX_VALUE /* no timeout */)
+ .setMinEventDelay(1).build();
+ assertSameClusterIds(instance1, instance2);
+ try{
+ instance3.getClusterViewService().getLocalClusterView();
+ fail("should complain");
+ } catch(UndefinedClusterViewException ue) {
+ // ok
+ }
+ assertEquals(0, instance1.getAnnouncementRegistry().listLocalAnnouncements().size());
+ assertEquals(0, instance1.getAnnouncementRegistry().listLocalIncomingAnnouncements().size());
+ assertEquals(0, instance2.getAnnouncementRegistry().listLocalAnnouncements().size());
+ assertEquals(0, instance2.getAnnouncementRegistry().listLocalIncomingAnnouncements().size());
+ assertEquals(0, instance3.getAnnouncementRegistry().listLocalAnnouncements().size());
+ assertEquals(0, instance3.getAnnouncementRegistry().listLocalIncomingAnnouncements().size());
+
+ // create a topology connector from instance3 to instance1
+ // -> corresponds to starting to ping
+ instance3.heartbeatsAndCheckView();
+ instance3.heartbeatsAndCheckView();
+ pingConnector(instance3, instance1);
+ // make asserts on the topology
+ instance1.dumpRepo();
+ assertSameTopology(new SimpleClusterView(instance1, instance2), new SimpleClusterView(instance3));
+
+ // kill instance 1
+ logger.info("instance1.slingId="+instance1.slingId);
+ logger.info("instance2.slingId="+instance2.slingId);
+ logger.info("instance3.slingId="+instance3.slingId);
+ final String instance1SlingId = instance1.slingId;
+ instance1.stopViewChecker(); // and have instance3 no longer pinging instance1
+ instance1.stop(); // otherwise it will have itself still registered with the observation manager and fiddle with future events..
+ instance1 = null; // set to null to early fail if anyone still assumes (original) instance1 is up form now on
+ instance2.getConfig().setViewCheckTimeout(1); // set instance2's heartbeatTimeout to 1 sec to time out instance1 quickly!
+ instance3.getConfig().setViewCheckTimeout(1); // set instance3's heartbeatTimeout to 1 sec to time out instance1 quickly!
+ Thread.sleep(500);
+ runHeartbeatOnceWith(instance2, instance3);
+ Thread.sleep(500);
+ runHeartbeatOnceWith(instance2, instance3);
+ Thread.sleep(500);
+ runHeartbeatOnceWith(instance2, instance3);
+ // instance 2 should now be alone - in fact, 3 should be alone as well
+ instance2.dumpRepo();
+ assertTopology(instance2, new SimpleClusterView(instance2));
+ assertTopology(instance3, new SimpleClusterView(instance3));
+ instance2.getConfig().setViewCheckTimeout(Integer.MAX_VALUE /* no timeout */); // set instance2's heartbeatTimeout back to Integer.MAX_VALUE /* no timeout */
+ instance3.getConfig().setViewCheckTimeout(Integer.MAX_VALUE /* no timeout */); // set instance3's heartbeatTimeout back to Integer.MAX_VALUE /* no timeout */
+ return instance1SlingId;
+ }
+
+ private void assertNotSameClusterIds(VirtualInstance... instances) throws UndefinedClusterViewException {
+ if (instances==null) {
+ fail("must not pass empty set of instances here");
+ }
+ if (instances.length<=1) {
+ fail("must not pass 0 or 1 instance only");
+ }
+ final String clusterId1 = instances[0].getClusterViewService()
+ .getLocalClusterView().getId();
+ for(int i=1; i<instances.length; i++) {
+ final String otherClusterId = instances[i].getClusterViewService()
+ .getLocalClusterView().getId();
+ // cluster ids must NOT be the same
+ assertNotEquals(clusterId1, otherClusterId);
+ }
+ if (instances.length>2) {
+ final VirtualInstance[] subset = new VirtualInstance[instances.length-1];
+ System.arraycopy(instances, 0, subset, 1, instances.length-1);
+ assertNotSameClusterIds(subset);
+ }
+ }
+
+ private void assertSameClusterIds(VirtualInstance... instances) throws UndefinedClusterViewException {
+ if (instances==null) {
+ // then there is nothing to compare
+ return;
+ }
+ if (instances.length==1) {
+ // then there is nothing to compare
+ return;
+ }
+ final String clusterId1 = instances[0].getClusterViewService()
+ .getLocalClusterView().getId();
+ for(int i=1; i<instances.length; i++) {
+ final String otherClusterId = instances[i].getClusterViewService()
+ .getLocalClusterView().getId();
+ // cluster ids must be the same
+ if (!clusterId1.equals(otherClusterId)) {
+ logger.error("assertSameClusterIds: instances[0]: "+instances[0]);
+ logger.error("assertSameClusterIds: instances["+i+"]: "+instances[i]);
+ fail("mismatch in clusterIds: expected to equal: clusterId1="+clusterId1+", otherClusterId="+otherClusterId);
+ }
+ }
+ }
+
+ private void assertTopology(VirtualInstance instance, SimpleClusterView... assertedClusterViews) {
+ final TopologyView topology = instance.getDiscoveryService().getTopology();
+ logger.info("assertTopology: instance "+instance.slingId+" sees topology: "+topology+", expected: "+assertedClusterViews);
+ assertNotNull(topology);
+ if (assertedClusterViews.length!=topology.getClusterViews().size()) {
+ dumpFailureDetails(topology, assertedClusterViews);
+ fail("instance "+instance.slingId+ " expected "+assertedClusterViews.length+", got: "+topology.getClusterViews().size());
+ }
+ final Set<ClusterView> actualClusters = new HashSet<ClusterView>(topology.getClusterViews());
+ for(int i=0; i<assertedClusterViews.length; i++) {
+ final SimpleClusterView assertedClusterView = assertedClusterViews[i];
+ boolean foundMatch = false;
+ for (Iterator<ClusterView> it = actualClusters.iterator(); it
+ .hasNext();) {
+ final ClusterView actualClusterView = it.next();
+ if (matches(assertedClusterView, actualClusterView)) {
+ it.remove();
+ foundMatch = true;
+ break;
+ }
+ }
+ if (!foundMatch) {
+ dumpFailureDetails(topology, assertedClusterViews);
+ fail("instance "+instance.slingId+ " could not find a match in the topology with instance="+instance.slingId+" and clusterViews="+assertedClusterViews.length);
+ }
+ }
+ assertEquals("not all asserted clusterviews are in the actual view with instance="+instance+" and clusterViews="+assertedClusterViews, actualClusters.size(), 0);
+ }
+
+ private void dumpFailureDetails(TopologyView topology, SimpleClusterView... assertedClusterViews) {
+ logger.error("assertTopology: expected: "+assertedClusterViews.length);
+ for(int j=0; j<assertedClusterViews.length; j++) {
+ logger.error("assertTopology: ["+j+"]: "+assertedClusterViews[j].toString());
+ }
+ final Set<ClusterView> clusterViews = topology.getClusterViews();
+ final Set<InstanceDescription> instances = topology.getInstances();
+ logger.error("assertTopology: actual: "+clusterViews.size()+" clusters with a total of "+instances.size()+" instances");
+ for (Iterator<ClusterView> it = clusterViews.iterator(); it.hasNext();) {
+ final ClusterView aCluster = it.next();
+ logger.error("assertTopology: a cluster: "+aCluster.getId());
+ for (Iterator<InstanceDescription> it2 = aCluster.getInstances().iterator(); it2.hasNext();) {
+ final InstanceDescription id = it2.next();
+ logger.error("assertTopology: - an instance "+id.getSlingId());
+ }
+ }
+ logger.error("assertTopology: list of all instances: "+instances.size());
+ for (Iterator<InstanceDescription> it = instances.iterator(); it.hasNext();) {
+ final InstanceDescription id = it.next();
+ logger.error("assertTopology: - an instance: "+id.getSlingId());
+ }
+ }
+
+ private boolean matches(SimpleClusterView assertedClusterView,
+ ClusterView actualClusterView) {
+ assertNotNull(assertedClusterView);
+ assertNotNull(actualClusterView);
+ if (assertedClusterView.instances.length!=actualClusterView.getInstances().size()) {
+ return false;
+ }
+ final Set<InstanceDescription> actualInstances = new HashSet<InstanceDescription>(actualClusterView.getInstances());
+ outerLoop:for(int i=0; i<assertedClusterView.instances.length; i++) {
+ final VirtualInstance assertedInstance = assertedClusterView.instances[i];
+ for (Iterator<InstanceDescription> it = actualInstances.iterator(); it
+ .hasNext();) {
+ final InstanceDescription anActualInstance = it.next();
+ if (assertedInstance.slingId.equals(anActualInstance.getSlingId())) {
+ continue outerLoop;
+ }
+ }
+ return false;
+ }
+ return true;
+ }
+
+ private boolean pingConnector(final VirtualInstance from, final VirtualInstance to) throws UndefinedClusterViewException {
+ final Announcement fromAnnouncement = createFromAnnouncement(from);
+ Announcement replyAnnouncement = null;
+ try{
+ replyAnnouncement = ping(to, fromAnnouncement);
+ } catch(AssertionError e) {
+ logger.warn("pingConnector: ping failed, assertionError: "+e);
+ return false;
+ } catch (UndefinedClusterViewException e) {
+ logger.warn("pingConnector: ping failed, currently the cluster view is undefined: "+e);
+ return false;
+ }
+ registerReplyAnnouncement(from, replyAnnouncement);
+ return true;
+ }
+
+ private void registerReplyAnnouncement(VirtualInstance from,
+ Announcement inheritedAnnouncement) {
+ final AnnouncementRegistry announcementRegistry = from.getAnnouncementRegistry();
+ if (inheritedAnnouncement.isLoop()) {
+ fail("loop detected");
+ // we dont currently support loops here in the junit tests
+ return;
+ } else {
+ inheritedAnnouncement.setInherited(true);
+ if (announcementRegistry
+ .registerAnnouncement(inheritedAnnouncement)==-1) {
+ logger.info("ping: connector response is from an instance which I already see in my topology"
+ + inheritedAnnouncement);
+ return;
+ }
+ }
+// resultingAnnouncement = inheritedAnnouncement;
+// statusDetails = null;
+ }
+
+ private Announcement ping(VirtualInstance to, final Announcement incomingTopologyAnnouncement)
+ throws UndefinedClusterViewException {
+ final String slingId = to.slingId;
+ final ClusterViewService clusterViewService = to.getClusterViewService();
+ final AnnouncementRegistry announcementRegistry = to.getAnnouncementRegistry();
+
+ incomingTopologyAnnouncement.removeInherited(slingId);
+
+ final Announcement replyAnnouncement = new Announcement(
+ slingId);
+
+ long backoffInterval = -1;
+ final ClusterView clusterView = clusterViewService.getLocalClusterView();
+ if (!incomingTopologyAnnouncement.isCorrectVersion()) {
+ fail("incorrect version");
+ return null; // never reached
+ } else if (ClusterViewHelper.contains(clusterView, incomingTopologyAnnouncement
+ .getOwnerId())) {
+ fail("loop=true");
+ return null; // never reached
+ } else if (ClusterViewHelper.containsAny(clusterView, incomingTopologyAnnouncement
+ .listInstances())) {
+ fail("incoming announcement contains instances that are part of my cluster");
+ return null; // never reached
+ } else {
+ backoffInterval = announcementRegistry
+ .registerAnnouncement(incomingTopologyAnnouncement);
+ if (backoffInterval==-1) {
+ fail("rejecting an announcement from an instance that I already see in my topology: ");
+ return null; // never reached
+ } else {
+ // normal, successful case: replying with the part of the topology which this instance sees
+ replyAnnouncement.setLocalCluster(clusterView);
+ announcementRegistry.addAllExcept(replyAnnouncement, clusterView,
+ new AnnouncementFilter() {
+
+ public boolean accept(final String receivingSlingId, Announcement announcement) {
+ if (announcement.getPrimaryKey().equals(
+ incomingTopologyAnnouncement
+ .getPrimaryKey())) {
+ return false;
+ }
+ return true;
+ }
+ });
+ return replyAnnouncement;
+ }
+ }
+ }
+
+ private Announcement createFromAnnouncement(final VirtualInstance from) throws UndefinedClusterViewException {
+ // TODO: refactor TopologyConnectorClient to avoid duplicating code from there (ping())
+ Announcement topologyAnnouncement = new Announcement(from.slingId);
+ topologyAnnouncement.setServerInfo(from.slingId);
+ final ClusterView clusterView = from.getClusterViewService().getLocalClusterView();
+ topologyAnnouncement.setLocalCluster(clusterView);
+ from.getAnnouncementRegistry().addAllExcept(topologyAnnouncement, clusterView, new AnnouncementFilter() {
+
+ public boolean accept(final String receivingSlingId, final Announcement announcement) {
+ // filter out announcements that are of old cluster instances
+ // which I dont really have in my cluster view at the moment
+ final Iterator<InstanceDescription> it =
+ clusterView.getInstances().iterator();
+ while(it.hasNext()) {
+ final InstanceDescription instance = it.next();
+ if (instance.getSlingId().equals(receivingSlingId)) {
+ // then I have the receiving instance in my cluster view
+ // all fine then
+ return true;
+ }
+ }
+ // looks like I dont have the receiving instance in my cluster view
+ // then I should also not propagate that announcement anywhere
+ return false;
+ }
+ });
+ return topologyAnnouncement;
+ }
+
+ @Test
+ public void testStableClusterId() throws Throwable {
+ logger.info("testStableClusterId: start");
+ // stop 1 and 2 and create them with a lower heartbeat timeout
+ instance2.stopViewChecker();
+ instance1.stopViewChecker();
+ instance2.stop();
+ instance1.stop();
+ // SLING-4302 : first set the heartbeatTimeout to 100 sec - large enough to work on all CI instances
+ instance1 = newBuilder().setDebugName("firstInstance")
+ .newRepository("/var/discovery/impl/", true)
+ .setConnectorPingTimeout(100)
+ .setMinEventDelay(1).build();
+ instance2 = newBuilder().setDebugName("secondInstance")
+ .useRepositoryOf(instance1)
+ .setConnectorPingTimeout(100)
+ .setMinEventDelay(1).build();
+ assertNotNull(instance1);
+ assertNotNull(instance2);
+
+ try{
+ instance1.getClusterViewService().getLocalClusterView();
+ fail("should complain");
+ } catch(UndefinedClusterViewException e) {
+ // ok
+ }
+ try{
+ instance2.getClusterViewService().getLocalClusterView();
+ fail("should complain");
+ } catch(UndefinedClusterViewException e) {
+ // ok
+ }
+
+ // let the sync/voting happen
+ instance1.heartbeatsAndCheckView();
+ instance2.heartbeatsAndCheckView();
+ Thread.sleep(500);
+ instance1.heartbeatsAndCheckView();
+ instance2.heartbeatsAndCheckView();
+ Thread.sleep(500);
+ instance1.heartbeatsAndCheckView();
+ instance2.heartbeatsAndCheckView();
+
+ String newClusterId1 = instance1.getClusterViewService()
+ .getLocalClusterView().getId();
+ String newClusterId2 = instance2.getClusterViewService()
+ .getLocalClusterView().getId();
+ // both cluster ids must be the same
+ assertEquals(newClusterId1, newClusterId1);
+
+ instance1.dumpRepo();
+ assertEquals(2, instance1.getClusterViewService().getLocalClusterView().getInstances().size());
+ assertEquals(2, instance2.getClusterViewService().getLocalClusterView().getInstances().size());
+
+ // let instance2 'die' by now longer doing heartbeats
+ // SLING-4302 : then set the heartbeatTimeouts back to 1 sec to have them properly time out with the sleeps applied below
+ instance2.getConfig().setViewCheckTimeout(1);
+ instance1.getConfig().setViewCheckTimeout(1);
+ instance2.stopViewChecker(); // would actually not be necessary as it was never started.. this test only runs heartbeats manually
+ instance1.heartbeatsAndCheckView();
+ Thread.sleep(500);
+ instance1.heartbeatsAndCheckView();
+ Thread.sleep(500);
+ instance1.heartbeatsAndCheckView();
+ Thread.sleep(500);
+ instance1.heartbeatsAndCheckView();
+ Thread.sleep(500);
+ instance1.heartbeatsAndCheckView();
+ Thread.sleep(500);
+ instance1.heartbeatsAndCheckView();
+ // the cluster should now have size 1
+ assertEquals(1, instance1.getClusterViewService().getLocalClusterView().getInstances().size());
+ // the instance 2 should be in isolated mode as it is no longer in the established view
+ // hence null
+ try{
+ instance2.getViewChecker().checkView();
+ instance2.getClusterViewService().getLocalClusterView();
+ fail("should complain");
+ } catch(UndefinedClusterViewException e) {
+ // ok
+ }
+
+ // but the cluster id must have remained stable
+ instance1.dumpRepo();
+ String actualClusterId = instance1.getClusterViewService()
+ .getLocalClusterView().getId();
+ logger.info("expected cluster id: "+newClusterId1);
+ logger.info("actual cluster id: "+actualClusterId);
+ assertEquals(newClusterId1, actualClusterId);
+ logger.info("testStableClusterId: end");
+ }
+
+ @Test
+ public void testClusterView() throws Exception {
+ logger.info("testClusterView: start");
+ assertNotNull(instance1);
+ assertNotNull(instance2);
+ assertNull(instance3);
+ instance3 = newBuilder().setDebugName("thirdInstance")
+ .useRepositoryOf(instance1)
+ .build();
+ assertNotNull(instance3);
+
+ assertEquals(instance1.getSlingId(), instance1.getClusterViewService()
+ .getSlingId());
+ assertEquals(instance2.getSlingId(), instance2.getClusterViewService()
+ .getSlingId());
+ assertEquals(instance3.getSlingId(), instance3.getClusterViewService()
+ .getSlingId());
+
+ try{
+ instance1.getClusterViewService().getLocalClusterView();
+ fail("should complain");
+ } catch(UndefinedClusterViewException e) {
+ // ok
+ }
+ try{
+ instance2.getClusterViewService().getLocalClusterView();
+ fail("should complain");
+ } catch(UndefinedClusterViewException e) {
+ // ok
+ }
+ try{
+ instance3.getClusterViewService().getLocalClusterView();
+ fail("should complain");
+ } catch(UndefinedClusterViewException e) {
+ // ok
+ }
+
+ instance1.dumpRepo();
+
+ instance1.heartbeatsAndCheckView();
+ instance2.heartbeatsAndCheckView();
+ instance3.heartbeatsAndCheckView();
+
+ instance1.dumpRepo();
+ logger.info("testClusterView: 1st 2s sleep");
+ Thread.sleep(2000);
+
+ instance1.heartbeatsAndCheckView();
+ instance2.heartbeatsAndCheckView();
+ instance3.heartbeatsAndCheckView();
+ logger.info("testClusterView: 2nd 2s sleep");
+ Thread.sleep(2000);
+
+ instance1.dumpRepo();
+ String clusterId1 = instance1.getClusterViewService().getLocalClusterView()
+ .getId();
+ logger.info("clusterId1=" + clusterId1);
+ String clusterId2 = instance2.getClusterViewService().getLocalClusterView()
+ .getId();
+ logger.info("clusterId2=" + clusterId2);
+ String clusterId3 = instance3.getClusterViewService().getLocalClusterView()
+ .getId();
+ logger.info("clusterId3=" + clusterId3);
+ assertEquals(clusterId1, clusterId2);
+ assertEquals(clusterId1, clusterId3);
+
+ assertEquals(3, instance1.getClusterViewService().getLocalClusterView()
+ .getInstances().size());
+ assertEquals(3, instance2.getClusterViewService().getLocalClusterView()
+ .getInstances().size());
+ assertEquals(3, instance3.getClusterViewService().getLocalClusterView()
+ .getInstances().size());
+ logger.info("testClusterView: end");
+ }
+
+ @Test
+ public void testAdditionalInstance() throws Throwable {
+ logger.info("testAdditionalInstance: start");
+ assertNotNull(instance1);
+ assertNotNull(instance2);
+
+ assertEquals(instance1.getSlingId(), instance1.getClusterViewService()
+ .getSlingId());
+ assertEquals(instance2.getSlingId(), instance2.getClusterViewService()
+ .getSlingId());
+
+ try{
+ instance1.getClusterViewService().getLocalClusterView();
+ fail("should complain");
+ } catch(UndefinedClusterViewException e) {
+ // ok
+ }
+ try{
+ instance2.getClusterViewService().getLocalClusterView();
+ fail("should complain");
+ } catch(UndefinedClusterViewException e) {
+ // ok
+ }
+
+ instance1.heartbeatsAndCheckView();
+ instance2.heartbeatsAndCheckView();
+
+ instance1.dumpRepo();
+ logger.info("testAdditionalInstance: 1st 2s sleep");
+ Thread.sleep(2000);
+
+ instance1.heartbeatsAndCheckView();
+ instance2.heartbeatsAndCheckView();
+ logger.info("testAdditionalInstance: 2nd 2s sleep");
+ Thread.sleep(2000);
+
+ instance1.dumpRepo();
+ String clusterId1 = instance1.getClusterViewService().getLocalClusterView()
+ .getId();
+ logger.info("clusterId1=" + clusterId1);
+ String clusterId2 = instance2.getClusterViewService().getLocalClusterView()
+ .getId();
+ logger.info("clusterId2=" + clusterId2);
+ assertEquals(clusterId1, clusterId2);
+
+ assertEquals(2, instance1.getClusterViewService().getLocalClusterView()
+ .getInstances().size());
+ assertEquals(2, instance2.getClusterViewService().getLocalClusterView()
+ .getInstances().size());
+
+ AssertingTopologyEventListener assertingTopologyEventListener = new AssertingTopologyEventListener();
+ assertingTopologyEventListener.addExpected(Type.TOPOLOGY_INIT);
+ assertEquals(1, assertingTopologyEventListener.getRemainingExpectedCount());
+ instance1.bindTopologyEventListener(assertingTopologyEventListener);
+ Thread.sleep(500); // SLING-4755: async event sending requires some minimal wait time nowadays
+ assertEquals(0, assertingTopologyEventListener.getRemainingExpectedCount());
+
+ // startup instance 3
+ AcceptsMultiple acceptsMultiple = new AcceptsMultiple(
+ Type.TOPOLOGY_CHANGING, Type.TOPOLOGY_CHANGED);
+ assertingTopologyEventListener.addExpected(acceptsMultiple);
+ assertingTopologyEventListener.addExpected(acceptsMultiple);
+ instance3 = newBuilder().setDebugName("thirdInstance")
+ .useRepositoryOf(instance1)
+ .build();
+ instance1.heartbeatsAndCheckView();
+ instance2.heartbeatsAndCheckView();
+ instance3.heartbeatsAndCheckView();
+ logger.info("testAdditionalInstance: 3rd 2s sleep");
+ Thread.sleep(2000);
+ instance1.heartbeatsAndCheckView();
+ instance2.heartbeatsAndCheckView();
+ instance3.heartbeatsAndCheckView();
+ logger.info("testAdditionalInstance: 4th 2s sleep");
+ Thread.sleep(2000);
+ assertEquals(1, acceptsMultiple.getEventCnt(Type.TOPOLOGY_CHANGING));
+ assertEquals(1, acceptsMultiple.getEventCnt(Type.TOPOLOGY_CHANGED));
+ logger.info("testAdditionalInstance: end");
+ }
+
+ @Test
+ public void testPropertyProviders() throws Throwable {
+ logger.info("testPropertyProviders: start");
+ instance1.heartbeatsAndCheckView();
+ instance2.heartbeatsAndCheckView();
+ assertNull(instance3);
+ instance3 = newBuilder().setDebugName("thirdInstance")
+ .useRepositoryOf(instance1)
+ .build();
+ instance3.heartbeatsAndCheckView();
+ logger.info("testPropertyProviders: 1st 2s sleep");
+ Thread.sleep(2000);
+ instance1.heartbeatsAndCheckView();
+ instance2.heartbeatsAndCheckView();
+ instance3.heartbeatsAndCheckView();
+ logger.info("testPropertyProviders: 2nd 2s sleep");
+ Thread.sleep(2000);
+ instance1.heartbeatsAndCheckView();
+ instance2.heartbeatsAndCheckView();
+ instance3.heartbeatsAndCheckView();
+ logger.info("testPropertyProviders: 3rd 2s sleep");
+ Thread.sleep(2000);
+
+ property1Value = UUID.randomUUID().toString();
+ property1Name = UUID.randomUUID().toString();
+ PropertyProviderImpl pp1 = new PropertyProviderImpl();
+ pp1.setProperty(property1Name, property1Value);
+ instance1.bindPropertyProvider(pp1, property1Name);
+
+ property2Value = UUID.randomUUID().toString();
+ property2Name = UUID.randomUUID().toString();
+ PropertyProviderImpl pp2 = new PropertyProviderImpl();
+ pp2.setProperty(property2Name, property2Value);
+ instance2.bindPropertyProvider(pp2, property2Name);
+
+ assertPropertyValues();
+
+ property1Value = UUID.randomUUID().toString();
+ pp1.setProperty(property1Name, property1Value);
+ instance1.heartbeatsAndCheckView();
+ instance2.heartbeatsAndCheckView();
+
+ assertPropertyValues();
+ assertNull(instance1.getClusterViewService().getLocalClusterView()
+ .getInstances().get(0)
+ .getProperty(UUID.randomUUID().toString()));
+ assertNull(instance2.getClusterViewService().getLocalClusterView()
+ .getInstances().get(0)
+ .getProperty(UUID.randomUUID().toString()));
+ logger.info("testPropertyProviders: end");
+ }
+
+ private void assertPropertyValues() throws UndefinedClusterViewException {
+ assertPropertyValues(instance1.getSlingId(), property1Name,
+ property1Value);
+ assertPropertyValues(instance2.getSlingId(), property2Name,
+ property2Value);
+ }
+
+ private void assertPropertyValues(String slingId, String name, String value) throws UndefinedClusterViewException {
+ assertEquals(value, getInstance(instance1, slingId).getProperty(name));
+ assertEquals(value, getInstance(instance2, slingId).getProperty(name));
+ }
+
+ private InstanceDescription getInstance(VirtualInstance instance, String slingId) throws UndefinedClusterViewException {
+ Iterator<InstanceDescription> it = instance.getClusterViewService()
+ .getLocalClusterView().getInstances().iterator();
+ while (it.hasNext()) {
+ InstanceDescription id = it.next();
+ if (id.getSlingId().equals(slingId)) {
+ return id;
+ }
+ }
+ throw new IllegalStateException("instance not found: instance="
+ + instance + ", slingId=" + slingId);
+ }
+
+ class LongRunningListener implements TopologyEventListener {
+
+ String failMsg = null;
+
+ boolean initReceived = false;
+ int noninitReceived;
+
+ private Semaphore changedSemaphore = new Semaphore(0);
+
+ public void assertNoFail() {
+ if (failMsg!=null) {
+ fail(failMsg);
+ }
+ }
+
+ public Semaphore getChangedSemaphore() {
+ return changedSemaphore;
+ }
+
+ public void handleTopologyEvent(TopologyEvent event) {
+ if (failMsg!=null) {
+ failMsg += "/ Already failed, got another event; "+event;
+ return;
+ }
+ if (!initReceived) {
+ if (event.getType()!=Type.TOPOLOGY_INIT) {
+ failMsg = "Expected TOPOLOGY_INIT first, got: "+event.getType();
+ return;
+ }
+ initReceived = true;
+ return;
+ }
+ if (event.getType()==Type.TOPOLOGY_CHANGED) {
+ try {
+ changedSemaphore.acquire();
+ } catch (InterruptedException e) {
+ throw new Error("don't interrupt me pls: "+e);
+ }
+ }
+ noninitReceived++;
+ }
+ }
+
+ /**
+ * Test plan:
+ * * have a discoveryservice with two listeners registered
+ * * one of them (the 'first' one) is long running
+ * * during one of the topology changes, when the first
+ * one is hit, deactivate the discovery service
+ * * that deactivation used to block (SLING-4755) due
+ * to synchronized(lock) which was blocked by the
+ * long running listener. With having asynchronous
+ * event sending this should no longer be the case
+ * * also, once asserted that deactivation finished,
+ * and that the first listener is still busy, make
+ * sure that once the first listener finishes, that
+ * the second listener still gets the event
+ * @throws Throwable
+ */
+ @Test
+ public void testLongRunningListener() throws Throwable {
+ // let the instance1 become alone, instance2 is idle
+ instance1.getConfig().setViewCheckTimeout(2);
+ instance2.getConfig().setViewCheckTimeout(2);
+ logger.info("testLongRunningListener : letting instance2 remain silent from now on");
+ instance1.heartbeatsAndCheckView();
+ Thread.sleep(1500);
+ instance1.heartbeatsAndCheckView();
+ Thread.sleep(1500);
+ instance1.heartbeatsAndCheckView();
+ Thread.sleep(1500);
+ instance1.heartbeatsAndCheckView();
+ logger.info("testLongRunningListener : instance 2 should now be considered dead");
+// instance1.dumpRepo();
+
+ LongRunningListener longRunningListener1 = new LongRunningListener();
+ AssertingTopologyEventListener fastListener2 = new AssertingTopologyEventListener();
+ fastListener2.addExpected(Type.TOPOLOGY_INIT);
+ longRunningListener1.assertNoFail();
+ assertEquals(1, fastListener2.getRemainingExpectedCount());
+ logger.info("testLongRunningListener : binding longRunningListener1 ...");
+ instance1.bindTopologyEventListener(longRunningListener1);
+ logger.info("testLongRunningListener : binding fastListener2 ...");
+ instance1.bindTopologyEventListener(fastListener2);
+ logger.info("testLongRunningListener : waiting a bit for longRunningListener1 to receive the TOPOLOGY_INIT event");
+ Thread.sleep(2500); // SLING-4755: async event sending requires some minimal wait time nowadays
+ assertEquals(0, fastListener2.getRemainingExpectedCount());
+ assertTrue(longRunningListener1.initReceived);
+
+ // after INIT, now do an actual change where listener1 will do a long-running handling
+ fastListener2.addExpected(Type.TOPOLOGY_CHANGING);
+ fastListener2.addExpected(Type.TOPOLOGY_CHANGED);
+ instance1.getConfig().setViewCheckTimeout(10);
+ instance2.getConfig().setViewCheckTimeout(10);
+ instance1.heartbeatsAndCheckView();
+ instance2.heartbeatsAndCheckView();
+ Thread.sleep(500);
+ instance1.heartbeatsAndCheckView();
+ instance2.heartbeatsAndCheckView();
+ Thread.sleep(500);
+ instance1.heartbeatsAndCheckView();
+ instance2.heartbeatsAndCheckView();
+ Thread.sleep(500);
+
+ instance1.dumpRepo();
+ longRunningListener1.assertNoFail();
+ // nothing unexpected should arrive at listener2:
+ assertEquals(0, fastListener2.getUnexpectedCount());
+ // however, listener2 should only get one (CHANGING) event, cos the CHANGED event is still blocked
+ assertEquals(1, fastListener2.getRemainingExpectedCount());
+ // and also listener2 should only get CHANGING, the CHANGED is blocked via changedSemaphore
+ assertEquals(1, longRunningListener1.noninitReceived);
+ assertTrue(longRunningListener1.getChangedSemaphore().hasQueuedThreads());
+ Thread.sleep(2000);
+ // even after a 2sec sleep things should be unchanged:
+ assertEquals(0, fastListener2.getUnexpectedCount());
+ assertEquals(1, fastListener2.getRemainingExpectedCount());
+ assertEquals(1, longRunningListener1.noninitReceived);
+ assertTrue(longRunningListener1.getChangedSemaphore().hasQueuedThreads());
+
+ // now let's simulate SLING-4755: deactivation while longRunningListener1 does long processing
+ // - which is simulated by waiting on changedSemaphore.
+ final List<Exception> asyncException = new LinkedList<Exception>();
+ Thread th = new Thread(new Runnable() {
+
+ public void run() {
+ try {
+ instance1.stop();
+ } catch (Exception e) {
+ synchronized(asyncException) {
+ asyncException.add(e);
+ }
+ }
+ }
+
+ });
+ th.start();
+ logger.info("Waiting max 4 sec...");
+ th.join(4000);
+ logger.info("Done waiting max 4 sec...");
+ if (th.isAlive()) {
+ logger.warn("Thread still alive: "+th.isAlive());
+ // release before issuing fail as otherwise test will block forever
+ longRunningListener1.getChangedSemaphore().release();
+ fail("Thread was still alive");
+ }
+ logger.info("Thread was no longer alive: "+th.isAlive());
+ synchronized(asyncException) {
+ logger.info("Async exceptions: "+asyncException.size());
+ if (asyncException.size()!=0) {
+ // release before issuing fail as otherwise test will block forever
+ longRunningListener1.getChangedSemaphore().release();
+ fail("async exceptions: "+asyncException.size()+", first: "+asyncException.get(0));
+ }
+ }
+
+ // now the test consists of
+ // a) the fact that we reached this place without unlocking the changedSemaphore
+ // b) when we now unlock the changedSemaphore the remaining events should flush through
+ longRunningListener1.getChangedSemaphore().release();
+ Thread.sleep(500);// shouldn't take long and then things should have flushed:
+ assertEquals(0, fastListener2.getUnexpectedCount());
+ assertEquals(0, fastListener2.getRemainingExpectedCount());
+ assertEquals(2, longRunningListener1.noninitReceived);
+ assertFalse(longRunningListener1.getChangedSemaphore().hasQueuedThreads());
+ }
+
+
+}
diff --git a/src/test/java/org/apache/sling/discovery/base/its/AbstractSingleInstanceTest.java b/src/test/java/org/apache/sling/discovery/base/its/AbstractSingleInstanceTest.java
new file mode 100644
index 0000000..52a60b8
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/its/AbstractSingleInstanceTest.java
@@ -0,0 +1,293 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.sling.discovery.base.its;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import org.apache.log4j.Level;
+import org.apache.log4j.LogManager;
+import org.apache.sling.discovery.ClusterView;
+import org.apache.sling.discovery.InstanceDescription;
+import org.apache.sling.discovery.TopologyEvent;
+import org.apache.sling.discovery.TopologyEvent.Type;
+import org.apache.sling.discovery.base.commons.UndefinedClusterViewException;
+import org.apache.sling.discovery.base.its.setup.VirtualInstance;
+import org.apache.sling.discovery.base.its.setup.VirtualInstanceBuilder;
+import org.apache.sling.discovery.base.its.setup.mock.AssertingTopologyEventListener;
+import org.apache.sling.discovery.base.its.setup.mock.PropertyProviderImpl;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public abstract class AbstractSingleInstanceTest {
+
+ private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+ VirtualInstance instance;
+
+ String propertyValue;
+
+ private Level logLevel;
+
+ protected abstract VirtualInstanceBuilder newBuilder();
+
+ @Before
+ public void setup() throws Exception {
+ final org.apache.log4j.Logger discoveryLogger = LogManager.getRootLogger().getLogger("org.apache.sling.discovery");
+ logLevel = discoveryLogger.getLevel();
+ discoveryLogger.setLevel(Level.DEBUG);
+ logger.info("setup: creating new standalone instance");
+ instance = newBuilder().setDebugName("standaloneInstance")
+ .newRepository("/var/discovery/impl/", true)
+ .setConnectorPingTimeout(20)
+ .setConnectorPingInterval(999)/*long enough heartbeat interval to prevent them to disturb the explicit heartbeats during the test*/
+ .setMinEventDelay(3).build();
+ logger.info("setup: creating new standalone instance done.");
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ final org.apache.log4j.Logger discoveryLogger = LogManager.getRootLogger().getLogger("org.apache.sling.discovery");
+ discoveryLogger.setLevel(logLevel);
+ logger.info("tearDown: stopping standalone instance");
+ if (instance!=null) {
+ instance.stop();
+ instance = null;
+ }
+ logger.info("tearDown: stopping standalone instance done");
+ }
+
+ @Test
+ public void testGetters() throws UndefinedClusterViewException, InterruptedException {
+ logger.info("testGetters: start");
+ assertNotNull(instance);
+ logger.info("sling id=" + instance.getSlingId());
+ try{
+ instance.getClusterViewService().getLocalClusterView();
+ fail("should complain"); // SLING-5030
+ } catch(UndefinedClusterViewException e) {
+ // ok
+ }
+
+ instance.heartbeatsAndCheckView();
+ // wait 100ms for the vote to happen
+ Thread.sleep(100);
+
+ assertNotNull(instance.getClusterViewService().getLocalClusterView());
+ ClusterView cv = instance.getClusterViewService().getLocalClusterView();
+ logger.info("cluster view: id=" + cv.getId());
+ assertNotNull(cv.getId());
+ assertNotSame(cv.getId(), "");
+
+ List<InstanceDescription> instances = cv.getInstances();
+ assertNotNull(instances);
+ assertTrue(instances.size() == 1);
+
+ InstanceDescription myInstance = instances.get(0);
+ assertNotNull(myInstance);
+ assertTrue(myInstance.getClusterView() == cv);
+ logger.info("instance id: " + myInstance.getSlingId());
+ assertEquals(instance.getSlingId(), myInstance.getSlingId());
+
+ Map<String, String> properties = myInstance.getProperties();
+ assertNotNull(properties);
+
+ assertNull(myInstance.getProperty("foo"));
+
+ assertTrue(myInstance.isLeader());
+
+ assertTrue(myInstance.isLocal());
+ logger.info("testGetters: end");
+ }
+
+ @Test
+ public void testPropertyProviders() throws Throwable {
+ logger.info("testPropertyProviders: start");
+ final String propertyName = UUID.randomUUID().toString();
+ propertyValue = UUID.randomUUID().toString();
+ PropertyProviderImpl pp = new PropertyProviderImpl();
+ pp.setProperty(propertyName, propertyValue);
+ instance.bindPropertyProvider(pp, propertyName);
+
+ instance.heartbeatsAndCheckView();
+ // wait 100ms for the vote to happen
+ Thread.sleep(100);
+ assertEquals(propertyValue,
+ instance.getClusterViewService().getLocalClusterView()
+ .getInstances().get(0).getProperty(propertyName));
+
+ propertyValue = UUID.randomUUID().toString();
+ pp.setProperty(propertyName, propertyValue);
+ instance.heartbeatsAndCheckView();
+
+ assertEquals(propertyValue,
+ instance.getClusterViewService().getLocalClusterView()
+ .getInstances().get(0).getProperty(propertyName));
+ assertNull(instance.getClusterViewService().getLocalClusterView()
+ .getInstances().get(0)
+ .getProperty(UUID.randomUUID().toString()));
+ logger.info("testPropertyProviders: end");
+ }
+
+ @Test
+ public void testInvalidProperties() throws Throwable {
+ logger.info("testInvalidProperties: start");
+
+ instance.heartbeatsAndCheckView();
+ instance.heartbeatsAndCheckView();
+
+ final String propertyValue = UUID.randomUUID().toString();
+ doTestProperty(UUID.randomUUID().toString(), propertyValue, propertyValue);
+
+ doTestProperty("", propertyValue, null);
+ doTestProperty("-", propertyValue, propertyValue);
+ doTestProperty("_", propertyValue, propertyValue);
+ doTestProperty("jcr:" + UUID.randomUUID().toString(), propertyValue, null);
+ doTestProperty("var/" + UUID.randomUUID().toString(), propertyValue, null);
+ doTestProperty(UUID.randomUUID().toString() + "@test", propertyValue, null);
+ doTestProperty(UUID.randomUUID().toString() + "!test", propertyValue, null);
+ logger.info("testInvalidProperties: end");
+ }
+
+ private void doTestProperty(final String propertyName,
+ final String propertyValue,
+ final String expectedPropertyValue) throws Throwable {
+ PropertyProviderImpl pp = new PropertyProviderImpl();
+ pp.setProperty(propertyName, propertyValue);
+ instance.bindPropertyProvider(pp, propertyName);
+ assertEquals(expectedPropertyValue,
+ instance.getClusterViewService().getLocalClusterView()
+ .getInstances().get(0).getProperty(propertyName));
+ }
+
+ @Test
+ public void testTopologyEventListeners() throws Throwable {
+ logger.info("testTopologyEventListeners: start");
+ instance.heartbeatsAndCheckView();
+ logger.info("testTopologyEventListeners: 1st sleep 2s");
+ Thread.sleep(2000);
+ instance.heartbeatsAndCheckView();
+ logger.info("testTopologyEventListeners: 2nd sleep 2s");
+ Thread.sleep(2000);
+
+ AssertingTopologyEventListener assertingTopologyEventListener = new AssertingTopologyEventListener();
+ assertingTopologyEventListener.addExpected(Type.TOPOLOGY_INIT);
+ logger.info("testTopologyEventListeners: binding the event listener");
+ instance.bindTopologyEventListener(assertingTopologyEventListener);
+ Thread.sleep(500); // SLING-4755: async event sending requires some minimal wait time nowadays
+ assertEquals(0, assertingTopologyEventListener.getRemainingExpectedCount());
+
+ final String propertyName = UUID.randomUUID().toString();
+ propertyValue = UUID.randomUUID().toString();
+ PropertyProviderImpl pp = new PropertyProviderImpl();
+ pp.setProperty(propertyName, propertyValue);
+
+ assertingTopologyEventListener.addExpected(Type.PROPERTIES_CHANGED);
+
+ assertEquals(1, assertingTopologyEventListener.getRemainingExpectedCount());
+ assertEquals(0, pp.getGetCnt());
+ instance.bindPropertyProvider(pp, propertyName);
+ logger.info("testTopologyEventListeners: 3rd sleep 1.5s");
+ Thread.sleep(1500);
+ logger.info("testTopologyEventListeners: dumping due to failure: ");
+ assertingTopologyEventListener.dump();
+ assertEquals(0, assertingTopologyEventListener.getRemainingExpectedCount());
+ // we can only assume that the getProperty was called at least once - it
+ // could be called multiple times though..
+ assertTrue(pp.getGetCnt() > 0);
+
+ assertingTopologyEventListener.addExpected(Type.PROPERTIES_CHANGED);
+
+ assertEquals(1, assertingTopologyEventListener.getRemainingExpectedCount());
+ pp.setGetCnt(0);
+ propertyValue = UUID.randomUUID().toString();
+ pp.setProperty(propertyName, propertyValue);
+ assertEquals(0, pp.getGetCnt());
+ instance.heartbeatsAndCheckView();
+ logger.info("testTopologyEventListeners: 4th sleep 2s");
+ Thread.sleep(2000);
+ assertEquals(0, assertingTopologyEventListener.getRemainingExpectedCount());
+ assertEquals(1, pp.getGetCnt());
+
+ // a heartbeat repeat should not result in another call though
+ instance.heartbeatsAndCheckView();
+ logger.info("testTopologyEventListeners: 5th sleep 2s");
+ Thread.sleep(2000);
+ assertEquals(0, assertingTopologyEventListener.getRemainingExpectedCount());
+ assertEquals(2, pp.getGetCnt());
+ logger.info("testTopologyEventListeners: done");
+ }
+
+ @Test
+ public void testBootstrap() throws Throwable {
+ logger.info("testBootstrap: start");
+ try{
+ instance.getClusterViewService().getLocalClusterView();
+ fail("should complain");
+ } catch(UndefinedClusterViewException e) {
+ // SLING-5030 : isolated mode is gone, replaced with exception
+ // ok
+ }
+
+ // SLING-3750 : with delaying the init event, we now should NOT get any events
+ // before we let the view establish (which happens via heartbeats below)
+ AssertingTopologyEventListener ada = new AssertingTopologyEventListener();
+ instance.bindTopologyEventListener(ada);
+ assertEquals(0, ada.getEvents().size());
+ assertEquals(0, ada.getUnexpectedCount());
+
+ try{
+ instance.getClusterViewService().getLocalClusterView();
+ fail("should complain");
+ } catch(UndefinedClusterViewException e) {
+ // ok
+ }
+
+ ada.addExpected(Type.TOPOLOGY_INIT);
+ instance.heartbeatsAndCheckView();
+ Thread.sleep(1000);
+ instance.heartbeatsAndCheckView();
+ Thread.sleep(1000);
+ instance.dumpRepo();
+ ada.dump();
+ assertEquals(0, ada.getUnexpectedCount());
+ assertEquals(1, ada.getEvents().size());
+ TopologyEvent initEvent = ada.getEvents().remove(0);
+ assertNotNull(initEvent);
+ assertNotNull(initEvent.getNewView());
+ assertNotNull(initEvent.getNewView().getClusterViews());
+
+ // after the view was established though, we expect it to be a normal
+ // EstablishedInstanceDescription
+ instance.assertEstablishedView();
+ logger.info("testBootstrap: end");
+ }
+
+}
diff --git a/src/test/java/org/apache/sling/discovery/base/its/AbstractTopologyEventTest.java b/src/test/java/org/apache/sling/discovery/base/its/AbstractTopologyEventTest.java
new file mode 100644
index 0000000..d8c7792
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/its/AbstractTopologyEventTest.java
@@ -0,0 +1,252 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.sling.discovery.base.its;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import org.apache.log4j.Level;
+import org.apache.log4j.LogManager;
+import org.apache.sling.discovery.TopologyEvent;
+import org.apache.sling.discovery.TopologyEvent.Type;
+import org.apache.sling.discovery.TopologyView;
+import org.apache.sling.discovery.base.its.setup.VirtualInstance;
+import org.apache.sling.discovery.base.its.setup.VirtualInstanceBuilder;
+import org.apache.sling.discovery.base.its.setup.mock.AssertingTopologyEventListener;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Test class covering correct sending of TopologyEvents
+ * in various scenarios (which are not covered in other tests already).
+ */
+public abstract class AbstractTopologyEventTest {
+
+ private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+ private VirtualInstance instance1;
+ private VirtualInstance instance2;
+
+ private Level logLevel;
+
+ @Before
+ public void setup() throws Exception {
+ final org.apache.log4j.Logger discoveryLogger = LogManager.getRootLogger().getLogger("org.apache.sling.discovery");
+ logLevel = discoveryLogger.getLevel();
+ discoveryLogger.setLevel(Level.DEBUG);
+ }
+
+ @After
+ public void tearDown() throws Throwable {
+ if (instance1!=null) {
+ instance1.stopViewChecker();
+ instance1.stop();
+ instance1 = null;
+ }
+ if (instance2!=null) {
+ instance2.stopViewChecker();
+ instance2.stop();
+ instance2 = null;
+ }
+ final org.apache.log4j.Logger discoveryLogger = LogManager.getRootLogger().getLogger("org.apache.sling.discovery");
+ discoveryLogger.setLevel(logLevel);
+ }
+
+ public abstract VirtualInstanceBuilder newBuilder();
+
+ /**
+ * Tests the fact that the INIT event is delayed until voting has succeeded
+ * (which is the default with SLIGN-5030 and SLING-4959
+ * @throws Throwable
+ */
+ @Test
+ public void testDelayedInitEvent() throws Throwable {
+ logger.info("testDelayedInitEvent: start");
+ instance1 = newBuilder().setDebugName("firstInstanceA")
+ .newRepository("/var/discovery/impl/", true)
+ .setConnectorPingTimeout(3 /* heartbeat-timeout */)
+ .setMinEventDelay(3 /*min event delay*/).build();
+ AssertingTopologyEventListener l1 = new AssertingTopologyEventListener("instance1.l1");
+ instance1.bindTopologyEventListener(l1);
+ logger.info("testDelayedInitEvent: instance1 created, no events expected yet. slingId="+instance1.slingId);
+
+ // should not have received any events yet
+ assertEquals(0, l1.getEvents().size());
+ assertEquals(0, l1.getUnexpectedCount());
+
+ // one heartbeat doesn't make a day yet - and is 2sec too early for the init
+ instance1.heartbeatsAndCheckView();
+ Thread.sleep(1200);
+ logger.info("testDelayedInitEvent: even after 500ms no events expected, as it needs more than 1 heartbeat");
+ // should not have received any events yet
+ assertEquals(0, l1.getEvents().size());
+ assertEquals(0, l1.getUnexpectedCount());
+
+ // but two are a good start
+ l1.addExpected(Type.TOPOLOGY_INIT);
+ instance1.heartbeatsAndCheckView();
+ Thread.sleep(1200);
+ instance1.heartbeatsAndCheckView();
+ Thread.sleep(1200);
+ logger.info("testDelayedInitEvent: 2nd/3rd heartbeat sent - now expecting a TOPOLOGY_INIT");
+ instance1.dumpRepo();
+ assertEquals(1, l1.getEvents().size()); // one event
+ assertEquals(0, l1.getRemainingExpectedCount()); // the expected one
+ assertEquals(0, l1.getUnexpectedCount());
+
+ logger.info("testDelayedInitEvent: creating instance2");
+ instance2 = newBuilder().setDebugName("secondInstanceB")
+ .useRepositoryOf(instance1)
+ .setConnectorPingTimeout(20)
+ .setMinEventDelay(3).build();
+ logger.info("testDelayedInitEvent: instance2 created with slingId="+instance2.slingId);
+ AssertingTopologyEventListener l2 = new AssertingTopologyEventListener("instance2.l2");
+ instance2.bindTopologyEventListener(l2);
+ logger.info("testDelayedInitEvent: listener instance2.l2 added - it should not get any events though");
+ AssertingTopologyEventListener l1Two = new AssertingTopologyEventListener("instance1.l1Two");
+ l1Two.addExpected(Type.TOPOLOGY_INIT);
+ logger.info("testDelayedInitEvent: listener instance1.l1Two added - it expects an INIT now");
+ instance1.bindTopologyEventListener(l1Two);
+
+ Thread.sleep(500); // SLING-4755: async event sending requires some minimal wait time nowadays
+
+ // just because instance2 is started doesn't kick off any events yet
+ // since instance2 didn't send heartbeats yet
+ assertEquals(1, l1.getEvents().size()); // one event
+ assertEquals(0, l1.getRemainingExpectedCount()); // the expected one
+ assertEquals(0, l1.getUnexpectedCount());
+ assertEquals(0, l2.getEvents().size());
+ assertEquals(0, l2.getUnexpectedCount());
+ assertEquals(1, l1Two.getEvents().size());
+ assertEquals(0, l1Two.getRemainingExpectedCount()); // the expected one
+ assertEquals(0, l1Two.getUnexpectedCount());
+
+
+ // the second & third heartbeat though triggers the voting etc
+ logger.info("testDelayedInitEvent: two more heartbeats should trigger events");
+ l1.addExpected(Type.TOPOLOGY_CHANGING);
+ l1Two.addExpected(Type.TOPOLOGY_CHANGING);
+ Thread.sleep(500);
+ l2.addExpected(Type.TOPOLOGY_INIT);
+ instance1.heartbeatsAndCheckView();
+ instance2.heartbeatsAndCheckView();
+ Thread.sleep(500);
+ instance1.heartbeatsAndCheckView();
+ instance2.heartbeatsAndCheckView();
+ Thread.sleep(500);
+ instance1.heartbeatsAndCheckView();
+ instance2.heartbeatsAndCheckView();
+ logger.info("testDelayedInitEvent: instance1: "+instance1.slingId);
+ logger.info("testDelayedInitEvent: instance2: "+instance2.slingId);
+ instance1.dumpRepo();
+ assertEquals(0, l1.getUnexpectedCount());
+ assertEquals(2, l1.getEvents().size());
+ assertEquals(0, l2.getUnexpectedCount());
+ assertEquals(1, l2.getEvents().size());
+ assertEquals(0, l1Two.getUnexpectedCount());
+ assertEquals(2, l1Two.getEvents().size());
+
+ // wait until CHANGED is sent - which is 3 sec after CHANGING
+ l1.addExpected(Type.TOPOLOGY_CHANGED);
+ l1Two.addExpected(Type.TOPOLOGY_CHANGED);
+ Thread.sleep(4000);
+ assertEquals(0, l1.getUnexpectedCount());
+ assertEquals(3, l1.getEvents().size()); // one event
+ assertEquals(0, l2.getUnexpectedCount());
+ assertEquals(1, l2.getEvents().size());
+ assertEquals(0, l1Two.getUnexpectedCount());
+ assertEquals(3, l1Two.getEvents().size());
+ logger.info("testDelayedInitEvent: end");
+ }
+
+ @Test
+ public void testGetDuringDelay() throws Throwable {
+ instance1 = newBuilder().setDebugName("firstInstanceA")
+ .newRepository("/var/discovery/impl/", true)
+ .setConnectorPingTimeout(20 /* heartbeat-timeout */)
+ .setMinEventDelay(6 /* min event delay */).build();
+ AssertingTopologyEventListener l1 = new AssertingTopologyEventListener("instance1.l1");
+ l1.addExpected(TopologyEvent.Type.TOPOLOGY_INIT);
+ instance1.bindTopologyEventListener(l1);
+
+ TopologyView earlyTopo = instance1.getDiscoveryService().getTopology();
+ assertNotNull(earlyTopo);
+ assertFalse(earlyTopo.isCurrent());
+ assertEquals(1, earlyTopo.getInstances().size());
+
+ for(int i=0; i<4; i++) {
+ instance1.heartbeatsAndCheckView();
+ Thread.sleep(125);
+ }
+ TopologyView secondTopo = instance1.getDiscoveryService().getTopology();
+ assertEquals(1, secondTopo.getInstances().size());
+ assertEquals(instance1.getSlingId(), secondTopo.getInstances().iterator().next().getSlingId());
+ assertTrue(secondTopo.isCurrent());
+ instance1.dumpRepo();
+
+ assertEarlyAndFirstClusterViewIdMatches(earlyTopo, secondTopo);
+
+ Thread.sleep(500);
+ // should have gotten the INIT, hence 0 remaining expected events
+ assertEquals(0, l1.getRemainingExpectedCount());
+ assertEquals(0, l1.getUnexpectedCount());
+
+ l1.addExpected(TopologyEvent.Type.TOPOLOGY_CHANGING);
+ instance2 = newBuilder().setDebugName("secondInstanceB")
+ .useRepositoryOf(instance1)
+ .setConnectorPingTimeout(20)
+ .setMinEventDelay(1).build();
+ AssertingTopologyEventListener l2 = new AssertingTopologyEventListener("instance2.l1");
+ l2.addExpected(TopologyEvent.Type.TOPOLOGY_INIT);
+ instance2.bindTopologyEventListener(l2);
+
+ for(int i=0; i<4; i++) {
+ instance2.heartbeatsAndCheckView();
+ instance1.heartbeatsAndCheckView();
+ Thread.sleep(750);
+ }
+
+ assertEquals(0, l1.getUnexpectedCount());
+ TopologyView topo2 = instance2.getDiscoveryService().getTopology();
+ assertTrue(topo2.isCurrent());
+ assertEquals(2, topo2.getInstances().size());
+ TopologyView topo1 = instance1.getDiscoveryService().getTopology();
+ assertTrue(topo1.isCurrent());
+ assertEquals(2, topo1.getInstances().size());
+
+ l1.addExpected(TopologyEvent.Type.TOPOLOGY_CHANGED);
+ Thread.sleep(5000);
+ assertEquals(0, l1.getRemainingExpectedCount());
+ assertEquals(0, l1.getUnexpectedCount());
+ assertEquals(0, l2.getRemainingExpectedCount());
+ assertEquals(0, l2.getUnexpectedCount());
+ assertTrue(instance2.getDiscoveryService().getTopology().isCurrent());
+ assertEquals(2, instance2.getDiscoveryService().getTopology().getInstances().size());
+ assertTrue(instance1.getDiscoveryService().getTopology().isCurrent());
+ assertEquals(2, instance1.getDiscoveryService().getTopology().getInstances().size());
+ }
+
+ public abstract void assertEarlyAndFirstClusterViewIdMatches(TopologyView earlyTopo, TopologyView secondTopo);
+
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/discovery/base/its/TopologyTest.java b/src/test/java/org/apache/sling/discovery/base/its/TopologyTest.java
new file mode 100644
index 0000000..2af8402
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/its/TopologyTest.java
@@ -0,0 +1,150 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.sling.discovery.base.its;
+
+import static org.junit.Assert.assertEquals;
+
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.sling.discovery.InstanceDescription;
+import org.apache.sling.discovery.base.connectors.DummyVirtualInstanceBuilder;
+import org.apache.sling.discovery.base.connectors.announcement.Announcement;
+import org.apache.sling.discovery.base.its.setup.TopologyHelper;
+import org.apache.sling.discovery.base.its.setup.VirtualConnector;
+import org.apache.sling.discovery.base.its.setup.VirtualInstance;
+import org.apache.sling.discovery.base.its.setup.VirtualInstanceBuilder;
+import org.junit.After;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class TopologyTest {
+
+ private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+ private final List<VirtualInstance> instances = new LinkedList<VirtualInstance>();
+
+ private VirtualInstanceBuilder newBuilder() {
+ return new DummyVirtualInstanceBuilder();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ for (Iterator<VirtualInstance> it = instances.iterator(); it.hasNext();) {
+ final VirtualInstance instance = it.next();
+ instance.stop();
+ }
+ }
+
+ @Test
+ public void testTwoNodes() throws Throwable {
+ VirtualInstanceBuilder builder1 = newBuilder()
+ .newRepository("/var/discovery/impl/", true)
+ .setDebugName("instance1")
+ .setConnectorPingInterval(20)
+ .setConnectorPingTimeout(200);
+ VirtualInstance instance1 = builder1.build();
+ instances.add(instance1);
+ VirtualInstanceBuilder builder2 = newBuilder()
+ .useRepositoryOf(builder1)
+ .setDebugName("instance2")
+ .setConnectorPingInterval(20)
+ .setConnectorPingTimeout(200);
+ VirtualInstance instance2 = builder2.build();
+ instances.add(instance2);
+ instance1.getConfig().setViewCheckTimeout(8);
+ instance1.getConfig().setViewCheckInterval(1);
+ instance2.getConfig().setViewCheckTimeout(2);
+ instance2.getConfig().setViewCheckInterval(1);
+
+ for(int i=0; i<5; i++) {
+ instance1.heartbeatsAndCheckView();
+ instance2.heartbeatsAndCheckView();
+ Thread.sleep(500);
+ }
+
+ Set<InstanceDescription> instances1 = instance1.getDiscoveryService().getTopology().getInstances();
+ Set<InstanceDescription> instances2 = instance2.getDiscoveryService().getTopology().getInstances();
+
+ assertEquals(1, instances1.size());
+ assertEquals(1, instances2.size());
+ assertEquals(instance1.getSlingId(), instances1.iterator().next().getSlingId());
+ assertEquals(instance2.getSlingId(), instances2.iterator().next().getSlingId());
+
+ new VirtualConnector(instance1, instance2);
+
+ // check instance 1's announcements
+ Collection<Announcement> instance1LocalAnnouncements =
+ instance1.getAnnouncementRegistry().listLocalAnnouncements();
+ assertEquals(1, instance1LocalAnnouncements.size());
+ Announcement instance1LocalAnnouncement = instance1LocalAnnouncements.iterator().next();
+ assertEquals(instance2.getSlingId(), instance1LocalAnnouncement.getOwnerId());
+ assertEquals(true, instance1LocalAnnouncement.isInherited());
+
+ // check instance 2's announcements
+ Collection<Announcement> instance2LocalAnnouncements =
+ instance2.getAnnouncementRegistry().listLocalAnnouncements();
+ assertEquals(1, instance2LocalAnnouncements.size());
+ Announcement instance2LocalAnnouncement = instance2LocalAnnouncements.iterator().next();
+ assertEquals(instance1.getSlingId(), instance2LocalAnnouncement.getOwnerId());
+ assertEquals(false, instance2LocalAnnouncement.isInherited());
+
+ // check topology
+ TopologyHelper.assertTopologyConsistsOf(instance1.getDiscoveryService().getTopology(), instance1.getSlingId(), instance2.getSlingId());
+ TopologyHelper.assertTopologyConsistsOf(instance2.getDiscoveryService().getTopology(), instance1.getSlingId(), instance2.getSlingId());
+
+ instance1LocalAnnouncements =
+ instance1.getAnnouncementRegistry().listLocalAnnouncements();
+ assertEquals(1, instance1LocalAnnouncements.size());
+ instance2LocalAnnouncements =
+ instance2.getAnnouncementRegistry().listLocalAnnouncements();
+ assertEquals(1, instance2LocalAnnouncements.size());
+
+ Thread.sleep(2200); // sleep of 2.2sec ensures instance2's heartbeat timeout (which is 2sec) hits
+
+ instance1LocalAnnouncements =
+ instance1.getAnnouncementRegistry().listLocalAnnouncements();
+ assertEquals(1, instance1LocalAnnouncements.size());
+ instance2LocalAnnouncements =
+ instance2.getAnnouncementRegistry().listLocalAnnouncements();
+ assertEquals(0, instance2LocalAnnouncements.size());
+
+ logger.info("testTwoNodes: instance1: "+instance1.getSlingId());
+ instance1.dumpRepo();
+ logger.info("testTwoNodes: instance2: "+instance2.getSlingId());
+ instance2.dumpRepo();
+ TopologyHelper.assertTopologyConsistsOf(instance1.getDiscoveryService().getTopology(), instance1.getSlingId(), instance2.getSlingId());
+ TopologyHelper.assertTopologyConsistsOf(instance2.getDiscoveryService().getTopology(), instance2.getSlingId());
+
+ Thread.sleep(6000); // another sleep 6s (2.2+6 = 8.2sec) ensures instance1's heartbeat timeout (which is 8sec) hits as well
+ instance1LocalAnnouncements =
+ instance1.getAnnouncementRegistry().listLocalAnnouncements();
+ assertEquals(0, instance1LocalAnnouncements.size());
+ instance2LocalAnnouncements =
+ instance2.getAnnouncementRegistry().listLocalAnnouncements();
+ assertEquals(0, instance2LocalAnnouncements.size());
+
+ TopologyHelper.assertTopologyConsistsOf(instance1.getDiscoveryService().getTopology(), instance1.getSlingId());
+ TopologyHelper.assertTopologyConsistsOf(instance2.getDiscoveryService().getTopology(), instance2.getSlingId());
+ }
+}
diff --git a/src/test/java/org/apache/sling/discovery/base/its/setup/ModifiableTestBaseConfig.java b/src/test/java/org/apache/sling/discovery/base/its/setup/ModifiableTestBaseConfig.java
new file mode 100644
index 0000000..56766dc
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/its/setup/ModifiableTestBaseConfig.java
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.sling.discovery.base.its.setup;
+
+import org.apache.sling.discovery.base.connectors.BaseConfig;
+
+/**
+ * test extension of the BaseConfig that allows setting some
+ * parameters in test classes
+ */
+public interface ModifiableTestBaseConfig extends BaseConfig {
+
+ void addTopologyConnectorWhitelistEntry(String string);
+
+ void setMinEventDelay(int minEventDelay);
+
+ void setViewCheckTimeout(int viewCheckTimeout);
+
+ void setViewCheckInterval(int viewCheckInterval);
+
+}
diff --git a/src/test/java/org/apache/sling/discovery/base/its/setup/OSGiMock.java b/src/test/java/org/apache/sling/discovery/base/its/setup/OSGiMock.java
new file mode 100644
index 0000000..537600a
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/its/setup/OSGiMock.java
@@ -0,0 +1,124 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.sling.discovery.base.its.setup;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.apache.sling.discovery.base.its.setup.mock.MockFactory;
+import org.osgi.framework.BundleContext;
+import org.osgi.service.component.ComponentContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class OSGiMock {
+
+ private static final Logger logger = LoggerFactory.getLogger(OSGiMock.class);
+
+ private final List<Object> services = new LinkedList<Object>();
+
+ public void addService(Object service) {
+ if (service==null) {
+ throw new IllegalArgumentException("service must not be null");
+ }
+ services.add(service);
+ }
+
+ public void activateAll() throws Exception {
+ for (@SuppressWarnings("rawtypes")
+ Iterator it = services.iterator(); it.hasNext();) {
+ Object aService = it.next();
+
+ activate(aService);
+ }
+ }
+
+ public static void activate(Object aService) throws IllegalAccessException,
+ InvocationTargetException {
+ Class<?> clazz = aService.getClass();
+ while (clazz != null) {
+ Method[] methods = clazz.getDeclaredMethods();
+ for (int i = 0; i < methods.length; i++) {
+ Method method = methods[i];
+ if (method.getName().equals("activate")) {
+ method.setAccessible(true);
+ if ( method.getParameterTypes().length == 0 ) {
+ logger.info("activate: activating "+aService+"...");
+ method.invoke(aService, null);
+ logger.info("activate: activating "+aService+" done.");
+ } else if (method.getParameterTypes().length==1 && (method.getParameterTypes()[0]==ComponentContext.class)){
+ logger.info("activate: activating "+aService+"...");
+ method.invoke(aService, MockFactory.mockComponentContext());
+ logger.info("activate: activating "+aService+" done.");
+ } else if (method.getParameterTypes().length==1 && (method.getParameterTypes()[0]==BundleContext.class)){
+ logger.info("activate: activating "+aService+"...");
+ method.invoke(aService, MockFactory.mockBundleContext());
+ logger.info("activate: activating "+aService+" done.");
+ } else {
+ throw new IllegalStateException("unsupported activate variant: "+method);
+ }
+ return;
+ }
+ }
+ clazz = clazz.getSuperclass();
+ }
+ }
+
+ public void deactivateAll() throws Exception {
+ for (@SuppressWarnings("rawtypes")
+ Iterator it = services.iterator(); it.hasNext();) {
+ Object aService = it.next();
+
+ deactivate(aService);
+ }
+ }
+
+ public static void deactivate(Object aService) throws IllegalAccessException,
+ InvocationTargetException {
+ Class<?> clazz = aService.getClass();
+ while (clazz != null) {
+ Method[] methods = clazz.getDeclaredMethods();
+ for (int i = 0; i < methods.length; i++) {
+ Method method = methods[i];
+ if (method.getName().equals("deactivate")) {
+ method.setAccessible(true);
+ if ( method.getParameterTypes().length == 0 ) {
+ method.invoke(aService, null);
+ } else {
+ method.invoke(aService, MockFactory.mockComponentContext());
+ }
+ return;
+ }
+ }
+ clazz = clazz.getSuperclass();
+ }
+ }
+
+ public void addServices(Object[] additionalServices) {
+ if (additionalServices==null) {
+ return;
+ }
+ for (Object additionalService : additionalServices) {
+ addService(additionalService);
+ }
+ }
+}
diff --git a/src/test/java/org/apache/sling/discovery/base/its/setup/TopologyHelper.java b/src/test/java/org/apache/sling/discovery/base/its/setup/TopologyHelper.java
new file mode 100644
index 0000000..c7d9995
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/its/setup/TopologyHelper.java
@@ -0,0 +1,145 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.sling.discovery.base.its.setup;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.Set;
+import java.util.UUID;
+
+import org.apache.sling.discovery.ClusterView;
+import org.apache.sling.discovery.InstanceDescription;
+import org.apache.sling.discovery.TopologyView;
+import org.apache.sling.discovery.base.commons.DefaultTopologyView;
+import org.apache.sling.discovery.commons.providers.DefaultClusterView;
+import org.apache.sling.discovery.commons.providers.DefaultInstanceDescription;
+
+import junitx.util.PrivateAccessor;
+
+public class TopologyHelper {
+
+ public static DefaultInstanceDescription createInstanceDescription(
+ ClusterView clusterView) {
+ return createInstanceDescription(UUID.randomUUID().toString(), false, clusterView);
+ }
+
+ public static DefaultInstanceDescription createInstanceDescription(
+ String instanceId, boolean isLocal, ClusterView clusterView) {
+ if (!(clusterView instanceof DefaultClusterView)) {
+ throw new IllegalArgumentException(
+ "Must pass a clusterView of type "
+ + DefaultClusterView.class);
+ }
+ DefaultInstanceDescription i = new DefaultInstanceDescription(
+ (DefaultClusterView) clusterView, false, isLocal, instanceId, new HashMap<String, String>());
+ return i;
+ }
+
+ public static DefaultTopologyView createTopologyView(String clusterViewId,
+ String slingId) {
+ DefaultTopologyView t = new DefaultTopologyView();
+ DefaultClusterView c = new DefaultClusterView(clusterViewId);
+ DefaultInstanceDescription i = new DefaultInstanceDescription(
+ c, true, false, slingId, new HashMap<String, String>());
+ Collection<InstanceDescription> instances = new LinkedList<InstanceDescription>();
+ instances.add(i);
+ t.addInstances(instances);
+ return t;
+ }
+
+ public static DefaultTopologyView cloneTopologyView(DefaultTopologyView original) {
+ DefaultTopologyView t = new DefaultTopologyView();
+ Iterator<ClusterView> it = original.getClusterViews().iterator();
+ while (it.hasNext()) {
+ DefaultClusterView c = (DefaultClusterView) it.next();
+ t.addInstances(clone(c).getInstances());
+ }
+ return t;
+ }
+
+ public static DefaultClusterView clone(DefaultClusterView original) {
+ DefaultClusterView c = new DefaultClusterView(original.getId());
+ Iterator<InstanceDescription> it = original.getInstances().iterator();
+ while (it.hasNext()) {
+ DefaultInstanceDescription id = (DefaultInstanceDescription) it
+ .next();
+ c.addInstanceDescription(cloneWOClusterView(id));
+ }
+ return c;
+ }
+
+ public static DefaultInstanceDescription cloneWOClusterView(
+ DefaultInstanceDescription original) {
+ DefaultInstanceDescription id = new DefaultInstanceDescription(
+ null, original.isLeader(), original.isLocal(),
+ original.getSlingId(), new HashMap<String, String>(
+ original.getProperties()));
+ return id;
+ }
+
+ public static DefaultInstanceDescription createAndAddInstanceDescription(
+ DefaultTopologyView newView, ClusterView clusterView) {
+ DefaultInstanceDescription i = createInstanceDescription(clusterView);
+ return addInstanceDescription(newView, i);
+ }
+
+ public static DefaultInstanceDescription addInstanceDescription(
+ DefaultTopologyView newView, DefaultInstanceDescription i) {
+ Collection<InstanceDescription> instances = new LinkedList<InstanceDescription>();
+ instances.add(i);
+ newView.addInstances(instances);
+ return i;
+ }
+
+ public static DefaultTopologyView cloneTopologyView(DefaultTopologyView view,
+ String newLeader) throws NoSuchFieldException {
+ final DefaultTopologyView clone = cloneTopologyView(view);
+ final DefaultClusterView cluster = (DefaultClusterView) clone.getClusterViews().iterator().next();
+ for (Iterator it = cluster.getInstances().iterator(); it.hasNext();) {
+ DefaultInstanceDescription id = (DefaultInstanceDescription) it.next();
+ PrivateAccessor.setField(id, "isLeader", id.getSlingId().equals(newLeader));
+ }
+ return clone;
+ }
+
+ public static void assertTopologyConsistsOf(TopologyView topology, String... slingIds) {
+ assertNotNull(topology);
+ assertEquals(slingIds.length, topology.getInstances().size());
+ for(int i=0; i<slingIds.length; i++) {
+ final String aSlingId = slingIds[i];
+ final Set<?> instances = topology.getInstances();
+ boolean found = false;
+ for (Iterator<?> it = instances.iterator(); it.hasNext();) {
+ InstanceDescription anInstance = (InstanceDescription) it.next();
+ if (anInstance.getSlingId().equals(aSlingId)) {
+ found = true;
+ break;
+ }
+ }
+ assertTrue(found);
+ }
+ }
+
+}
diff --git a/src/test/java/org/apache/sling/discovery/base/its/setup/VirtualConnector.java b/src/test/java/org/apache/sling/discovery/base/its/setup/VirtualConnector.java
new file mode 100644
index 0000000..e6de0ef
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/its/setup/VirtualConnector.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.sling.discovery.base.its.setup;
+
+import org.apache.sling.discovery.base.connectors.ping.TopologyConnectorClientInformation;
+
+public class VirtualConnector {
+ @SuppressWarnings("unused")
+ private final VirtualInstance from;
+ @SuppressWarnings("unused")
+ private final VirtualInstance to;
+ private final int jettyPort;
+ @SuppressWarnings("unused")
+ private final TopologyConnectorClientInformation connectorInfo;
+
+ public VirtualConnector(VirtualInstance from, VirtualInstance to) throws Throwable {
+ this.from = from;
+ this.to = to;
+ to.startJetty();
+ this.jettyPort = to.getJettyPort();
+ this.connectorInfo = from.connectTo("http://localhost:"+jettyPort+"/system/console/topology/connector");
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/discovery/base/its/setup/VirtualInstance.java b/src/test/java/org/apache/sling/discovery/base/its/setup/VirtualInstance.java
new file mode 100644
index 0000000..a21becb
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/its/setup/VirtualInstance.java
@@ -0,0 +1,374 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.sling.discovery.base.its.setup;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.lang.reflect.InvocationTargetException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Date;
+import java.util.Dictionary;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+import javax.jcr.Session;
+import javax.servlet.Servlet;
+
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ResourceResolverFactory;
+import org.apache.sling.commons.testing.jcr.RepositoryProvider;
+import org.apache.sling.discovery.InstanceDescription;
+import org.apache.sling.discovery.PropertyProvider;
+import org.apache.sling.discovery.TopologyEventListener;
+import org.apache.sling.discovery.base.commons.BaseDiscoveryService;
+import org.apache.sling.discovery.base.commons.ClusterViewService;
+import org.apache.sling.discovery.base.commons.ViewChecker;
+import org.apache.sling.discovery.base.commons.UndefinedClusterViewException;
+import org.apache.sling.discovery.base.connectors.announcement.AnnouncementRegistry;
+import org.apache.sling.discovery.base.connectors.ping.ConnectorRegistry;
+import org.apache.sling.discovery.base.connectors.ping.TopologyConnectorClientInformation;
+import org.apache.sling.discovery.base.connectors.ping.TopologyConnectorServlet;
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.nio.SelectChannelConnector;
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.servlet.ServletHolder;
+import org.jmock.Expectations;
+import org.jmock.Mockery;
+import org.jmock.integration.junit4.JUnit4Mockery;
+import org.osgi.framework.Constants;
+import org.osgi.service.component.ComponentContext;
+import org.osgi.service.http.HttpContext;
+import org.osgi.service.http.HttpService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import junitx.util.PrivateAccessor;
+
+public class VirtualInstance {
+
+ protected final static Logger logger = LoggerFactory.getLogger(VirtualInstance.class);
+
+ public final String slingId;
+
+ ClusterViewService clusterViewService;
+
+ private final ResourceResolverFactory resourceResolverFactory;
+
+ private final OSGiMock osgiMock;
+
+ private final BaseDiscoveryService discoveryService;
+
+ private final AnnouncementRegistry announcementRegistry;
+
+ private final ConnectorRegistry connectorRegistry;
+
+ protected final String debugName;
+
+ private ResourceResolver resourceResolver;
+
+ private int serviceId = 999;
+
+ private ViewCheckerRunner viewCheckerRunner = null;
+
+ private ServletContextHandler servletContext;
+
+ private Server jettyServer;
+
+ private ModifiableTestBaseConfig config;
+
+ private ViewChecker viewChecker;
+
+ private final VirtualInstanceBuilder builder;
+
+ private class ViewCheckerRunner implements Runnable {
+
+ private final int intervalInSeconds;
+
+ private boolean stopped_ = false;
+
+ public ViewCheckerRunner(int intervalInSeconds) {
+ this.intervalInSeconds = intervalInSeconds;
+ }
+
+ public synchronized void stop() {
+ logger.info("Stopping Instance ["+slingId+"]");
+ stopped_ = true;
+ }
+
+ public void run() {
+ while(true) {
+ synchronized(this) {
+ if (stopped_) {
+ logger.info("Instance ["+slingId+"] stopps.");
+ return;
+ }
+ }
+ try{
+ heartbeatsAndCheckView();
+ } catch(Exception e) {
+ logger.error("run: ping connector for slingId="+slingId+" threw exception: "+e, e);
+ }
+ try {
+ Thread.sleep(intervalInSeconds*1000);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ return;
+ }
+ }
+ }
+
+ }
+
+ public VirtualInstance(VirtualInstanceBuilder builder) throws Exception {
+ this.builder = builder;
+ this.slingId = builder.getSlingId();
+ this.debugName = builder.getDebugName();
+ logger.info("<init>: starting slingId="+slingId+", debugName="+debugName);
+
+ osgiMock = new OSGiMock();
+
+ this.resourceResolverFactory = builder.getResourceResolverFactory();
+
+ config = builder.getConnectorConfig();
+ config.addTopologyConnectorWhitelistEntry("127.0.0.1");
+ config.setMinEventDelay(builder.getMinEventDelay());
+
+ clusterViewService = builder.getClusterViewService();
+ announcementRegistry = builder.getAnnouncementRegistry();
+ connectorRegistry = builder.getConnectorRegistry();
+ viewChecker = builder.getViewChecker();
+ discoveryService = builder.getDiscoverService();
+
+ osgiMock.addService(clusterViewService);
+ osgiMock.addService(announcementRegistry);
+ osgiMock.addService(connectorRegistry);
+ osgiMock.addService(viewChecker);
+ osgiMock.addService(discoveryService);
+ osgiMock.addServices(builder.getAdditionalServices(this));
+
+ resourceResolver = resourceResolverFactory
+ .getAdministrativeResourceResolver(null);
+
+ if (builder.isResetRepo()) {
+ //SLING-4587 : do resetRepo before creating the observationListener
+ // otherwise it will get tons of events from the deletion of /var
+ // which the previous test could have left over.
+ // Doing it before addEventListener should prevent that.
+ builder.resetRepo();
+ }
+
+ osgiMock.activateAll();
+ }
+
+ @Override
+ public String toString() {
+ return "a [Test]Instance[slingId="+slingId+", debugName="+debugName+"]";
+ }
+
+ public void bindPropertyProvider(PropertyProvider propertyProvider,
+ String... propertyNames) throws Throwable {
+ Map<String, Object> props = new HashMap<String, Object>();
+ props.put(Constants.SERVICE_ID, (long) serviceId++);
+ props.put(PropertyProvider.PROPERTY_PROPERTIES, propertyNames);
+
+ PrivateAccessor.invoke(discoveryService, "bindPropertyProvider",
+ new Class[] { PropertyProvider.class, Map.class },
+ new Object[] { propertyProvider, props });
+ }
+
+ public String getSlingId() {
+ return slingId;
+ }
+
+ public ClusterViewService getClusterViewService() {
+ return clusterViewService;
+ }
+
+ public BaseDiscoveryService getDiscoveryService() {
+ return discoveryService;
+ }
+
+ public AnnouncementRegistry getAnnouncementRegistry() {
+ return announcementRegistry;
+ }
+
+ public synchronized void startJetty() throws Throwable {
+ if (jettyServer!=null) {
+ return;
+ }
+ servletContext = new ServletContextHandler(ServletContextHandler.NO_SECURITY);
+ servletContext.setContextPath("/");
+
+ TopologyConnectorServlet servlet = new TopologyConnectorServlet();
+ PrivateAccessor.setField(servlet, "config", config);
+ PrivateAccessor.setField(servlet, "clusterViewService", clusterViewService);
+ PrivateAccessor.setField(servlet, "announcementRegistry", announcementRegistry);
+
+ Mockery context = new JUnit4Mockery();
+ final HttpService httpService = context.mock(HttpService.class);
+ context.checking(new Expectations() {
+ {
+ allowing(httpService).registerServlet(with(any(String.class)),
+ with(any(Servlet.class)),
+ with(any(Dictionary.class)),
+ with(any(HttpContext.class)));
+ }
+ });
+ PrivateAccessor.setField(servlet, "httpService", httpService);
+ ComponentContext cc = null;
+ PrivateAccessor.invoke(servlet, "activate", new Class[] {ComponentContext.class}, new Object[] {cc});
+
+ ServletHolder holder =
+ new ServletHolder(servlet);
+
+ servletContext.addServlet(holder, "/system/console/topology/*");
+
+ jettyServer = new Server();
+ jettyServer.setHandler(servletContext);
+ Connector connector=new SelectChannelConnector();
+ jettyServer.setConnectors(new Connector[]{connector});
+ jettyServer.start();
+ }
+
+ public synchronized int getJettyPort() {
+ if (jettyServer==null) {
+ throw new IllegalStateException("jettyServer not started");
+ }
+ final Connector[] connectors = jettyServer.getConnectors();
+ return connectors[0].getLocalPort();
+ }
+
+ public TopologyConnectorClientInformation connectTo(String url) throws MalformedURLException {
+ return connectorRegistry.registerOutgoingConnector(clusterViewService, new URL(url));
+ }
+
+ public InstanceDescription getLocalInstanceDescription() throws UndefinedClusterViewException {
+ final Iterator<InstanceDescription> it = getClusterViewService().getLocalClusterView().getInstances().iterator();
+ while(it.hasNext()) {
+ final InstanceDescription id = it.next();
+ if (slingId.equals(id.getSlingId())) {
+ return id;
+ }
+ }
+ fail("no local instanceDescription found");
+ // never called:
+ return null;
+ }
+
+ public void heartbeatsAndCheckView() {
+ logger.info("Instance ["+slingId+"] issues a pulse now "+new Date());
+ viewChecker.heartbeatAndCheckView();
+ }
+
+ public void startViewChecker(int intervalInSeconds) throws IllegalAccessException, InvocationTargetException {
+ logger.info("startViewChecker: intervalInSeconds="+intervalInSeconds);
+ if (viewCheckerRunner!=null) {
+ logger.info("startViewChecker: stopping first...");
+ viewCheckerRunner.stop();
+ logger.info("startViewChecker: stopped.");
+ }
+ logger.info("startViewChecker: activating...");
+ try{
+ OSGiMock.activate(viewChecker);
+ } catch(Error er) {
+ er.printStackTrace(System.out);
+ throw er;
+ } catch(RuntimeException re) {
+ re.printStackTrace(System.out);
+ }
+ logger.info("startViewChecker: initializing...");
+ viewCheckerRunner = new ViewCheckerRunner(intervalInSeconds);
+ Thread th = new Thread(viewCheckerRunner, "Test-ViewCheckerRunner ["+debugName+"]");
+ th.setDaemon(true);
+ logger.info("startViewChecker: starting thread...");
+ th.start();
+ logger.info("startViewChecker: done.");
+ }
+
+ public boolean isViewCheckerRunning() {
+ return (viewCheckerRunner!=null);
+ }
+
+ public void stopViewChecker() throws Throwable {
+ if (viewCheckerRunner!=null) {
+ viewCheckerRunner.stop();
+ viewCheckerRunner = null;
+ }
+ try{
+ OSGiMock.deactivate(viewChecker);
+ } catch(Error er) {
+ er.printStackTrace(System.out);
+ throw er;
+ } catch(RuntimeException re) {
+ re.printStackTrace(System.out);
+ throw re;
+ }
+ }
+
+ public void dumpRepo() throws Exception {
+ VirtualInstanceHelper.dumpRepo(resourceResolverFactory);
+ }
+
+ public ResourceResolverFactory getResourceResolverFactory() {
+ return resourceResolverFactory;
+ }
+
+ public void stop() throws Exception {
+ logger.info("stop: stopping slingId="+slingId+", debugName="+debugName);
+ try {
+ stopViewChecker();
+ } catch (Throwable e) {
+ throw new Exception("Caught Throwable in stopConnectorPinger: "+e, e);
+ }
+
+ if (resourceResolver != null) {
+ resourceResolver.close();
+ }
+ osgiMock.deactivateAll();
+ logger.info("stop: stopped slingId="+slingId+", debugName="+debugName);
+ }
+
+ public void bindTopologyEventListener(TopologyEventListener eventListener)
+ throws Throwable {
+ PrivateAccessor.invoke(discoveryService, "bindTopologyEventListener",
+ new Class[] { TopologyEventListener.class },
+ new Object[] { eventListener });
+ }
+
+ public ModifiableTestBaseConfig getConfig() {
+ return config;
+ }
+
+ public ViewChecker getViewChecker() {
+ return viewChecker;
+ }
+
+ public void assertEstablishedView() {
+ assertTrue(getDiscoveryService().getTopology().isCurrent());
+ }
+
+ public VirtualInstanceBuilder getBuilder() {
+ return builder;
+ }
+
+}
diff --git a/src/test/java/org/apache/sling/discovery/base/its/setup/VirtualInstanceBuilder.java b/src/test/java/org/apache/sling/discovery/base/its/setup/VirtualInstanceBuilder.java
new file mode 100644
index 0000000..feaaf97
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/its/setup/VirtualInstanceBuilder.java
@@ -0,0 +1,238 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.sling.discovery.base.its.setup;
+
+import java.util.UUID;
+
+import org.apache.sling.api.resource.ResourceResolverFactory;
+import org.apache.sling.commons.scheduler.Scheduler;
+import org.apache.sling.commons.scheduler.impl.QuartzScheduler;
+import org.apache.sling.commons.threads.ThreadPoolManager;
+import org.apache.sling.commons.threads.impl.DefaultThreadPoolManager;
+import org.apache.sling.discovery.base.commons.BaseDiscoveryService;
+import org.apache.sling.discovery.base.commons.ClusterViewService;
+import org.apache.sling.discovery.base.commons.ViewChecker;
+import org.apache.sling.discovery.base.connectors.announcement.AnnouncementRegistry;
+import org.apache.sling.discovery.base.connectors.announcement.AnnouncementRegistryImpl;
+import org.apache.sling.discovery.base.connectors.ping.ConnectorRegistry;
+import org.apache.sling.discovery.base.connectors.ping.ConnectorRegistryImpl;
+import org.apache.sling.discovery.base.its.setup.mock.FailingScheduler;
+import org.apache.sling.discovery.commons.providers.spi.impl.DummySlingSettingsService;
+import org.apache.sling.settings.SlingSettingsService;
+
+import junitx.util.PrivateAccessor;
+
+public abstract class VirtualInstanceBuilder {
+
+ private static Scheduler singletonScheduler = null;
+
+ public static Scheduler getSingletonScheduler() throws Exception {
+ if (singletonScheduler!=null) {
+ return singletonScheduler;
+ }
+ final Scheduler newscheduler = new QuartzScheduler();
+ final ThreadPoolManager tpm = new DefaultThreadPoolManager(null, null);
+ try {
+ PrivateAccessor.invoke(newscheduler, "bindThreadPoolManager",
+ new Class[] { ThreadPoolManager.class },
+ new Object[] { tpm });
+ } catch (Throwable e1) {
+ org.junit.Assert.fail(e1.toString());
+ }
+ OSGiMock.activate(newscheduler);
+ singletonScheduler = newscheduler;
+ return singletonScheduler;
+ }
+
+ private String debugName;
+ protected ResourceResolverFactory factory;
+ private boolean resetRepo;
+ private String slingId = UUID.randomUUID().toString();
+ private ClusterViewService clusterViewService;
+ protected ViewChecker viewChecker;
+ private AnnouncementRegistry announcementRegistry;
+ private ConnectorRegistry connectorRegistry;
+ private Scheduler scheduler;
+ private BaseDiscoveryService discoveryService;
+ private SlingSettingsService slingSettingsService;
+ protected boolean ownRepository;
+ private int minEventDelay = 1;
+ protected VirtualInstanceBuilder hookedToBuilder;
+
+ public VirtualInstanceBuilder() {
+ }
+
+ public VirtualInstanceBuilder newRepository(String path, boolean resetRepo) throws Exception {
+ createNewRepository();
+ ownRepository = true;
+ this.resetRepo = resetRepo;
+ setPath(path);
+ return this;
+ }
+
+ public abstract VirtualInstanceBuilder createNewRepository() throws Exception;
+
+ public VirtualInstanceBuilder useRepositoryOf(VirtualInstance other) throws Exception {
+ return useRepositoryOf(other.getBuilder());
+ }
+
+ public VirtualInstanceBuilder useRepositoryOf(VirtualInstanceBuilder other) throws Exception {
+ factory = other.factory;
+ hookedToBuilder = other;
+ ownRepository = false;
+ return this;
+ }
+
+ public VirtualInstanceBuilder setConnectorPingTimeout(int connectorPingTimeout) {
+ getConnectorConfig().setViewCheckTimeout(connectorPingTimeout);
+ return this;
+ }
+
+ public VirtualInstanceBuilder setConnectorPingInterval(int connectorPingInterval) {
+ getConnectorConfig().setViewCheckInterval(connectorPingInterval);
+ return this;
+ }
+
+ public boolean isResetRepo() {
+ return resetRepo;
+ }
+
+ public String getSlingId() {
+ return slingId;
+ }
+
+ public String getDebugName() {
+ return debugName;
+ }
+
+ public ResourceResolverFactory getResourceResolverFactory() {
+ return factory;
+ }
+
+ public ClusterViewService getClusterViewService() {
+ if (clusterViewService==null) {
+ clusterViewService = createClusterViewService();
+ }
+ return clusterViewService;
+ }
+
+ protected abstract ClusterViewService createClusterViewService();
+
+ public ViewChecker getViewChecker() throws Exception {
+ if (viewChecker==null) {
+ viewChecker = createViewChecker();
+ }
+ return viewChecker;
+ }
+
+ public AnnouncementRegistry getAnnouncementRegistry() {
+ if (announcementRegistry==null) {
+ announcementRegistry = createAnnouncementRegistry();
+ }
+ return announcementRegistry;
+ }
+
+ protected AnnouncementRegistry createAnnouncementRegistry() {
+ return AnnouncementRegistryImpl.testConstructor(
+ getResourceResolverFactory(), getSlingSettingsService(), getConnectorConfig());
+ }
+
+ public ConnectorRegistry getConnectorRegistry() {
+ if (connectorRegistry==null) {
+ connectorRegistry = createConnectorRegistry();
+ }
+ return connectorRegistry;
+ }
+
+ protected ConnectorRegistry createConnectorRegistry() {
+ return ConnectorRegistryImpl.testConstructor(announcementRegistry, getConnectorConfig());
+ }
+
+ protected abstract ViewChecker createViewChecker() throws Exception;
+
+ protected abstract VirtualInstanceBuilder setPath(String string);
+
+ public VirtualInstanceBuilder setDebugName(String debugName) {
+ this.debugName = debugName;
+ return this;
+ }
+
+ public abstract ModifiableTestBaseConfig getConnectorConfig();
+
+ public void setScheduler(Scheduler singletonScheduler) {
+ this.scheduler = singletonScheduler;
+ }
+
+ public Scheduler getScheduler() throws Exception {
+ if (scheduler == null) {
+ scheduler = getSingletonScheduler();
+ }
+ return scheduler;
+ }
+
+ public BaseDiscoveryService getDiscoverService() throws Exception {
+ if (discoveryService==null) {
+ discoveryService = createDiscoveryService();
+ }
+ return discoveryService;
+ }
+
+ protected abstract BaseDiscoveryService createDiscoveryService() throws Exception;
+
+ protected SlingSettingsService getSlingSettingsService() {
+ if (slingSettingsService==null) {
+ slingSettingsService = createSlingSettingsService();
+ }
+ return slingSettingsService;
+ }
+
+ protected SlingSettingsService createSlingSettingsService() {
+ return new DummySlingSettingsService(getSlingId());
+ }
+
+ public abstract Object[] getAdditionalServices(VirtualInstance instance) throws Exception;
+
+ public VirtualInstanceBuilder setMinEventDelay(int minEventDelay) {
+ this.minEventDelay = minEventDelay;
+ return this;
+ }
+
+ public int getMinEventDelay() {
+ return minEventDelay;
+ }
+
+ public VirtualInstance build() throws Exception {
+ return new VirtualInstance(this);
+ }
+
+ public VirtualInstanceBuilder setSlingId(String slingId) {
+ this.slingId = slingId;
+ return this;
+ }
+
+ public VirtualInstanceBuilder withFailingScheduler(boolean useFailingScheduler) {
+ if (useFailingScheduler) {
+ this.scheduler = new FailingScheduler();
+ }
+ return this;
+ }
+
+ protected abstract void resetRepo() throws Exception;
+
+}
diff --git a/src/test/java/org/apache/sling/discovery/base/its/setup/VirtualInstanceHelper.java b/src/test/java/org/apache/sling/discovery/base/its/setup/VirtualInstanceHelper.java
new file mode 100644
index 0000000..ffbaff4
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/its/setup/VirtualInstanceHelper.java
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.sling.discovery.base.its.setup;
+
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.Property;
+import javax.jcr.PropertyIterator;
+import javax.jcr.PropertyType;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+import org.apache.sling.api.resource.ResourceResolverFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class VirtualInstanceHelper {
+
+ private final static Logger logger = LoggerFactory.getLogger(VirtualInstanceHelper.class);
+
+ public static void dumpRepo(ResourceResolverFactory resourceResolverFactory) throws Exception {
+ Session session = resourceResolverFactory
+ .getAdministrativeResourceResolver(null).adaptTo(Session.class);
+ logger.info("dumpRepo: ====== START =====");
+ logger.info("dumpRepo: repo = " + session.getRepository());
+
+ dump(session.getRootNode());
+
+ // session.logout();
+ logger.info("dumpRepo: ====== END =====");
+
+ session.logout();
+ }
+
+ public static void dump(Node node) throws RepositoryException {
+ if (node.getPath().equals("/jcr:system")
+ || node.getPath().equals("/rep:policy")) {
+ // ignore that one
+ return;
+ }
+
+ PropertyIterator pi = node.getProperties();
+ StringBuilder sb = new StringBuilder();
+ while (pi.hasNext()) {
+ Property p = pi.nextProperty();
+ sb.append(" ");
+ sb.append(p.getName());
+ sb.append("=");
+ if (p.getType() == PropertyType.BOOLEAN) {
+ sb.append(p.getBoolean());
+ } else if (p.getType() == PropertyType.STRING) {
+ sb.append(p.getString());
+ } else if (p.getType() == PropertyType.DATE) {
+ sb.append(p.getDate().getTime());
+ } else {
+ sb.append("<unknown type=" + p.getType() + "/>");
+ }
+ }
+
+ StringBuffer depth = new StringBuffer();
+ for(int i=0; i<node.getDepth(); i++) {
+ depth.append(" ");
+ }
+ logger.info(depth + "/" + node.getName() + " -- " + sb);
+ NodeIterator it = node.getNodes();
+ while (it.hasNext()) {
+ Node child = it.nextNode();
+ dump(child);
+ }
+ }
+
+}
diff --git a/src/test/java/org/apache/sling/discovery/base/its/setup/VirtualRepository.java b/src/test/java/org/apache/sling/discovery/base/its/setup/VirtualRepository.java
new file mode 100644
index 0000000..958f90b
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/its/setup/VirtualRepository.java
@@ -0,0 +1,27 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.sling.discovery.base.its.setup;
+
+import org.apache.sling.api.resource.ResourceResolverFactory;
+
+public interface VirtualRepository {
+
+ ResourceResolverFactory getResourceResolverFactory();
+
+}
diff --git a/src/test/java/org/apache/sling/discovery/base/its/setup/WithholdingAppender.java b/src/test/java/org/apache/sling/discovery/base/its/setup/WithholdingAppender.java
new file mode 100644
index 0000000..a294be6
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/its/setup/WithholdingAppender.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF 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.apache.sling.discovery.base.its.setup;
+
+import java.io.BufferedWriter;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.net.URL;
+
+import org.apache.log4j.Layout;
+import org.apache.log4j.LogManager;
+import org.apache.log4j.PatternLayout;
+import org.apache.log4j.PropertyConfigurator;
+import org.apache.log4j.WriterAppender;
+
+public class WithholdingAppender extends WriterAppender {
+
+ private final ByteArrayOutputStream baos;
+ private final Writer writer;
+
+ /**
+ * Install the WithholdingAppender, essentially muting all logging
+ * and withholding it until release() is called
+ * @return the WithholdingAppender that can be used to get the
+ * withheld log output
+ */
+ public static WithholdingAppender install() {
+ LogManager.getRootLogger().removeAllAppenders();
+ final WithholdingAppender withholdingAppender = new WithholdingAppender(
+ new PatternLayout("%d{dd.MM.yyyy HH:mm:ss} *%-5p* [%t] %c{1}: %m\n"));
+ LogManager.getRootLogger().addAppender(withholdingAppender);
+ return withholdingAppender;
+ }
+
+ /**
+ * Release this WithholdingAppender and optionally dump what was
+ * withheld (eg in case of an exception)
+ * @param dumpToSysout
+ */
+ public void release(boolean dumpToSysout) {
+ LogManager.resetConfiguration();
+ URL log4jPropertiesFile = getClass().getResource("/log4j.properties");
+ PropertyConfigurator.configure(log4jPropertiesFile);
+ if (dumpToSysout) {
+ String withheldLogoutput = getBuffer();
+ System.out.println(withheldLogoutput);
+ }
+ }
+
+ public WithholdingAppender(Layout layout) {
+ this.layout = layout;
+ this.baos = new ByteArrayOutputStream();
+ this.writer = new BufferedWriter(new OutputStreamWriter(baos));
+ this.setWriter(writer);
+ }
+
+ public String getBuffer() {
+ try{
+ writer.flush();
+ } catch(IOException e) {
+ // ignore
+ }
+ return baos.toString();
+ }
+}
diff --git a/src/test/java/org/apache/sling/discovery/base/its/setup/mock/AcceptsMultiple.java b/src/test/java/org/apache/sling/discovery/base/its/setup/mock/AcceptsMultiple.java
new file mode 100644
index 0000000..0562555
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/its/setup/mock/AcceptsMultiple.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.sling.discovery.base.its.setup.mock;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.sling.discovery.TopologyEvent;
+import org.apache.sling.discovery.TopologyEvent.Type;
+
+public class AcceptsMultiple implements TopologyEventAsserter {
+
+ private final Type[] acceptedTypes;
+
+ private final Map<Type, Integer> counts = new HashMap<Type, Integer>();
+
+ public AcceptsMultiple(Type... acceptedTypes) {
+ this.acceptedTypes = acceptedTypes;
+ }
+
+ public synchronized void assertOk(TopologyEvent event) {
+ for (int i = 0; i < acceptedTypes.length; i++) {
+ Type aType = acceptedTypes[i];
+ if (aType == event.getType()) {
+ // perfect
+ Integer c = counts.remove(aType);
+ if (c == null) {
+ counts.put(aType, new Integer(1));
+ } else {
+ counts.put(aType, new Integer(c + 1));
+ }
+ return;
+ }
+ }
+
+ throw new IllegalStateException("Got an Event which I did not expect: "
+ + event.getType());
+ }
+
+ public synchronized int getEventCnt(Type type) {
+ Integer i = counts.get(type);
+ if (i!=null) {
+ return i;
+ } else {
+ return 0;
+ }
+ }
+
+}
diff --git a/src/test/java/org/apache/sling/discovery/base/its/setup/mock/AcceptsParticularTopologyEvent.java b/src/test/java/org/apache/sling/discovery/base/its/setup/mock/AcceptsParticularTopologyEvent.java
new file mode 100644
index 0000000..1d3cbda
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/its/setup/mock/AcceptsParticularTopologyEvent.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.sling.discovery.base.its.setup.mock;
+
+import org.apache.sling.discovery.TopologyEvent;
+import org.apache.sling.discovery.TopologyEvent.Type;
+
+public class AcceptsParticularTopologyEvent implements TopologyEventAsserter {
+
+ private final Type particularType;
+
+ private int eventCnt = 0;
+
+ /**
+ * @param singleInstanceTest
+ */
+ public AcceptsParticularTopologyEvent(Type particularType) {
+ this.particularType = particularType;
+ }
+
+ public void assertOk(TopologyEvent event) {
+ if (event.getType() == particularType) {
+ // fine
+ eventCnt++;
+ } else {
+ throw new IllegalStateException("expected " + particularType
+ + ", got " + event.getType());
+ }
+ }
+
+ public int getEventCnt() {
+ return eventCnt;
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/discovery/base/its/setup/mock/AssertingTopologyEventListener.java b/src/test/java/org/apache/sling/discovery/base/its/setup/mock/AssertingTopologyEventListener.java
new file mode 100644
index 0000000..d5c912f
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/its/setup/mock/AssertingTopologyEventListener.java
@@ -0,0 +1,160 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.sling.discovery.base.its.setup.mock;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.apache.sling.discovery.TopologyEvent;
+import org.apache.sling.discovery.TopologyEvent.Type;
+import org.apache.sling.discovery.TopologyEventListener;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class AssertingTopologyEventListener implements TopologyEventListener {
+ private final Logger logger = LoggerFactory.getLogger(this.getClass());
+ private final List<TopologyEventAsserter> expectedEvents = new LinkedList<TopologyEventAsserter>();
+
+ private String debugInfo = null;
+
+ private String errorMsg = null;
+
+ public AssertingTopologyEventListener() {
+ }
+
+ public AssertingTopologyEventListener(String debugInfo) {
+ this.debugInfo = debugInfo;
+ }
+
+ @Override
+ public String toString() {
+ return super.toString()+"-[debugInfo="+debugInfo+"]";
+ }
+
+ private List<TopologyEvent> events_ = new LinkedList<TopologyEvent>();
+
+ private List<TopologyEvent> unexpectedEvents_ = new LinkedList<TopologyEvent>();
+
+ public void handleTopologyEvent(TopologyEvent event) {
+ final String logPrefix = "handleTopologyEvent["+(debugInfo!=null ? debugInfo : "this="+this) +"] ";
+ logger.info(logPrefix + "got event=" + event);
+ TopologyEventAsserter asserter = null;
+ synchronized (expectedEvents) {
+ if (expectedEvents.size() == 0) {
+ unexpectedEvents_.add(event);
+ throw new IllegalStateException(
+ "no expected events anymore. But got: " + event);
+ }
+ asserter = expectedEvents.remove(0);
+ }
+ if (asserter == null) {
+ throw new IllegalStateException("this should not occur");
+ }
+ try{
+ asserter.assertOk(event);
+ logger.info(logPrefix + "event matched expectations (" + event+")");
+ } catch(RuntimeException re) {
+ synchronized(expectedEvents) {
+ unexpectedEvents_.add(event);
+ }
+ throw re;
+ } catch(Error er) {
+ synchronized(expectedEvents) {
+ unexpectedEvents_.add(event);
+ }
+ throw er;
+ }
+ try{
+ switch(event.getType()) {
+ case PROPERTIES_CHANGED: {
+ assertNotNull(event.getOldView());
+ assertNotNull(event.getNewView());
+ assertTrue(event.getNewView().isCurrent());
+ assertFalse(event.getOldView().isCurrent());
+ break;
+ }
+ case TOPOLOGY_CHANGED: {
+ assertNotNull(event.getOldView());
+ assertNotNull(event.getNewView());
+ assertTrue(event.getNewView().isCurrent());
+ assertFalse(event.getOldView().isCurrent());
+ break;
+ }
+ case TOPOLOGY_CHANGING: {
+ assertNotNull(event.getOldView());
+ assertNull(event.getNewView());
+ assertFalse(event.getOldView().isCurrent());
+ break;
+ }
+ case TOPOLOGY_INIT: {
+ assertNull(event.getOldView());
+ assertNotNull(event.getNewView());
+ // cannot make any assertions on event.getNewView().isCurrent()
+ // as that can be true or false
+ break;
+ }
+ }
+ } catch(RuntimeException re) {
+ logger.error("RuntimeException: "+re, re);
+ throw re;
+ } catch(AssertionError e) {
+ logger.error("AssertionError: "+e, e);
+ throw e;
+ }
+ events_.add(event);
+ }
+
+ public List<TopologyEvent> getEvents() {
+ return events_;
+ }
+
+ public void addExpected(Type expectedType) {
+ addExpected(new AcceptsParticularTopologyEvent(expectedType));
+ }
+
+ public void addExpected(TopologyEventAsserter topologyEventAsserter) {
+ expectedEvents.add(topologyEventAsserter);
+ }
+
+ public int getRemainingExpectedCount() {
+ return expectedEvents.size();
+ }
+
+ public int getUnexpectedCount() {
+ return unexpectedEvents_.size();
+ }
+
+ public void dump() {
+ StringBuffer ue = new StringBuffer();
+ if (unexpectedEvents_.size()>0) {
+ for (Iterator<TopologyEvent> it = unexpectedEvents_.iterator(); it.hasNext();) {
+ TopologyEvent topologyEvent = it.next();
+ ue.append(topologyEvent+", ");
+ }
+ unexpectedEvents_.iterator();
+ }
+ logger.info("dump: got "+events_.size()+" events, "+unexpectedEvents_.size()+" (details: "+ue+") thereof unexpected. My list of expected events contains "+expectedEvents.size());
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/discovery/base/its/setup/mock/DummyViewChecker.java b/src/test/java/org/apache/sling/discovery/base/its/setup/mock/DummyViewChecker.java
new file mode 100644
index 0000000..e66ec09
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/its/setup/mock/DummyViewChecker.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.sling.discovery.base.its.setup.mock;
+
+import org.apache.sling.api.resource.ResourceResolverFactory;
+import org.apache.sling.commons.scheduler.Scheduler;
+import org.apache.sling.discovery.base.commons.BaseViewChecker;
+import org.apache.sling.discovery.base.connectors.BaseConfig;
+import org.apache.sling.discovery.base.connectors.announcement.AnnouncementRegistry;
+import org.apache.sling.discovery.base.connectors.ping.ConnectorRegistry;
+import org.apache.sling.settings.SlingSettingsService;
+
+public class DummyViewChecker extends BaseViewChecker {
+
+ public static DummyViewChecker testConstructor(
+ SlingSettingsService slingSettingsService,
+ ResourceResolverFactory resourceResolverFactory,
+ ConnectorRegistry connectorRegistry,
+ AnnouncementRegistry announcementRegistry,
+ Scheduler scheduler,
+ BaseConfig connectorConfig) {
+ DummyViewChecker pinger = new DummyViewChecker();
+ pinger.slingSettingsService = slingSettingsService;
+ pinger.resourceResolverFactory = resourceResolverFactory;
+ pinger.connectorRegistry = connectorRegistry;
+ pinger.announcementRegistry = announcementRegistry;
+ pinger.scheduler = scheduler;
+ pinger.connectorConfig = connectorConfig;
+ return pinger;
+ }
+
+}
diff --git a/src/test/java/org/apache/sling/discovery/base/its/setup/mock/FailingScheduler.java b/src/test/java/org/apache/sling/discovery/base/its/setup/mock/FailingScheduler.java
new file mode 100644
index 0000000..726c713
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/its/setup/mock/FailingScheduler.java
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.sling.discovery.base.its.setup.mock;
+
+import java.io.Serializable;
+import java.util.Date;
+import java.util.Map;
+import java.util.NoSuchElementException;
+
+import org.apache.sling.commons.scheduler.Scheduler;
+
+public class FailingScheduler implements Scheduler {
+
+ @Override
+ public void removeJob(String name) throws NoSuchElementException {
+ // nothing to do here
+ }
+
+ @Override
+ public boolean fireJobAt(String name, Object job, Map<String, Serializable> config, Date date, int times, long period) {
+ return false;
+ }
+
+ @Override
+ public void fireJobAt(String name, Object job, Map<String, Serializable> config, Date date) throws Exception {
+ throw new Exception("cos you are really worth it");
+ }
+
+ @Override
+ public boolean fireJob(Object job, Map<String, Serializable> config, int times, long period) {
+ return false;
+ }
+
+ @Override
+ public void fireJob(Object job, Map<String, Serializable> config) throws Exception {
+ throw new Exception("cos you are really worth it");
+ }
+
+ @Override
+ public void addPeriodicJob(String name, Object job, Map<String, Serializable> config, long period, boolean canRunConcurrently,
+ boolean startImmediate) throws Exception {
+ throw new Exception("cos you are really worth it");
+ }
+
+ @Override
+ public void addPeriodicJob(String name, Object job, Map<String, Serializable> config, long period, boolean canRunConcurrently)
+ throws Exception {
+ throw new Exception("cos you are really worth it");
+ }
+
+ @Override
+ public void addJob(String name, Object job, Map<String, Serializable> config, String schedulingExpression,
+ boolean canRunConcurrently) throws Exception {
+ throw new Exception("cos you are really worth it");
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/discovery/base/its/setup/mock/MockFactory.java b/src/test/java/org/apache/sling/discovery/base/its/setup/mock/MockFactory.java
new file mode 100644
index 0000000..f4e2404
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/its/setup/mock/MockFactory.java
@@ -0,0 +1,126 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.sling.discovery.base.its.setup.mock;
+
+import java.util.Dictionary;
+import java.util.Properties;
+
+import org.apache.sling.api.resource.ResourceResolverFactory;
+import org.apache.sling.jcr.api.SlingRepository;
+import org.apache.sling.settings.SlingSettingsService;
+import org.hamcrest.Description;
+import org.jmock.Expectations;
+import org.jmock.Mockery;
+import org.jmock.api.Action;
+import org.jmock.api.Invocation;
+import org.jmock.integration.junit4.JUnit4Mockery;
+import org.jmock.lib.action.ReturnValueAction;
+import org.jmock.lib.action.VoidAction;
+import org.osgi.framework.BundleContext;
+import org.osgi.service.component.ComponentContext;
+
+public class MockFactory {
+
+ public final Mockery context = new JUnit4Mockery();
+
+ public static ResourceResolverFactory mockResourceResolverFactory()
+ throws Exception {
+ return mockResourceResolverFactory(null);
+ }
+
+ public static ResourceResolverFactory mockResourceResolverFactory(final SlingRepository repositoryOrNull)
+ throws Exception {
+ Mockery context = new JUnit4Mockery();
+
+ final ResourceResolverFactory resourceResolverFactory = context
+ .mock(ResourceResolverFactory.class);
+ // final ResourceResolver resourceResolver = new MockResourceResolver();
+ // final ResourceResolver resourceResolver = new
+ // MockedResourceResolver();
+
+ context.checking(new Expectations() {
+ {
+ allowing(resourceResolverFactory)
+ .getAdministrativeResourceResolver(null);
+ will(new Action() {
+
+ public Object invoke(Invocation invocation)
+ throws Throwable {
+ return new MockedResourceResolver(repositoryOrNull);
+ }
+
+ public void describeTo(Description arg0) {
+ arg0.appendText("whateva - im going to create a new mockedresourceresolver");
+ }
+ });
+ }
+ });
+ return resourceResolverFactory;
+ }
+
+ public static SlingSettingsService mockSlingSettingsService(
+ final String slingId) {
+ Mockery context = new JUnit4Mockery();
+
+ final SlingSettingsService settingsService = context
+ .mock(SlingSettingsService.class);
+ context.checking(new Expectations() {
+ {
+ allowing(settingsService).getSlingId();
+ will(returnValue(slingId));
+
+ allowing(settingsService).getSlingHomePath();
+ will(returnValue("/n/a"));
+ }
+ });
+ return settingsService;
+ }
+
+ public static ComponentContext mockComponentContext() {
+ Mockery context = new JUnit4Mockery();
+ final BundleContext bc = context.mock(BundleContext.class);
+ context.checking(new Expectations() {
+ {
+ allowing(bc).registerService(with(any(String.class)),
+ with(any(Object.class)), with(any(Dictionary.class)));
+ will(VoidAction.INSTANCE);
+
+ allowing(bc).getProperty(with(any(String.class)));
+ will(new ReturnValueAction("foo"));
+ }
+ });
+
+ final ComponentContext cc = context.mock(ComponentContext.class);
+ context.checking(new Expectations() {
+ {
+ allowing(cc).getProperties();
+ will(returnValue(new Properties()));
+
+ allowing(cc).getBundleContext();
+ will(returnValue(bc));
+ }
+ });
+
+ return cc;
+ }
+
+ public static BundleContext mockBundleContext() {
+ return mockComponentContext().getBundleContext();
+ }
+}
diff --git a/src/test/java/org/apache/sling/discovery/base/its/setup/mock/MockedResource.java b/src/test/java/org/apache/sling/discovery/base/its/setup/mock/MockedResource.java
new file mode 100644
index 0000000..040842b
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/its/setup/mock/MockedResource.java
@@ -0,0 +1,297 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.sling.discovery.base.its.setup.mock;
+
+import java.util.Calendar;
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import javax.jcr.Node;
+import javax.jcr.PathNotFoundException;
+import javax.jcr.Property;
+import javax.jcr.PropertyIterator;
+import javax.jcr.PropertyType;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+import org.apache.sling.api.resource.ModifiableValueMap;
+import org.apache.sling.api.resource.SyntheticResource;
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.api.wrappers.ValueMapDecorator;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class MockedResource extends SyntheticResource {
+
+ private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+ private final MockedResourceResolver mockedResourceResolver;
+ private Session session;
+
+ public MockedResource(MockedResourceResolver resourceResolver, String path,
+ String resourceType) {
+ super(resourceResolver, path, resourceType);
+ mockedResourceResolver = resourceResolver;
+
+ resourceResolver.register(this);
+ }
+
+ private Session getSession() {
+ synchronized (this) {
+ if (session == null) {
+ try {
+ session = mockedResourceResolver.getSession();
+ } catch (RepositoryException e) {
+ throw new RuntimeException("RepositoryException: " + e, e);
+ }
+ }
+ return session;
+ }
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+// close();
+ super.finalize();
+ }
+
+ public void close() {
+ synchronized (this) {
+ if (session != null) {
+ if (session.isLive()) {
+ session.logout();
+ }
+ session = null;
+ }
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public <AdapterType> AdapterType adaptTo(Class<AdapterType> type) {
+ if (type.equals(Node.class)) {
+ try {
+ return (AdapterType) getSession().getNode(getPath());
+ } catch (Exception e) {
+ logger.error("Exception occurred: "+e, e);
+ throw new RuntimeException("Exception occurred: " + e, e);
+ }
+ } else if (type.equals(ValueMap.class)) {
+ try {
+ Session session = getSession();
+ Node node = session.getNode(getPath());
+ HashMap<String, Object> map = new HashMap<String, Object>();
+
+ PropertyIterator properties = node.getProperties();
+ while (properties.hasNext()) {
+ Property p = properties.nextProperty();
+ if (p.getType() == PropertyType.BOOLEAN) {
+ map.put(p.getName(), p.getBoolean());
+ } else if (p.getType() == PropertyType.STRING) {
+ map.put(p.getName(), p.getString());
+ } else if (p.getType() == PropertyType.DATE) {
+ map.put(p.getName(), p.getDate().getTime());
+ } else if (p.getType() == PropertyType.NAME) {
+ map.put(p.getName(), p.getName());
+ } else if (p.getType() == PropertyType.LONG) {
+ map.put(p.getName(), p.getLong());
+ } else {
+ throw new RuntimeException(
+ "Unsupported property type: " + p.getType());
+ }
+ }
+ ValueMap valueMap = new ValueMapDecorator(map);
+ return (AdapterType) valueMap;
+ } catch (Exception e) {
+ e.printStackTrace();
+ return null;
+ }
+ } else if (type.equals(ModifiableValueMap.class)) {
+ return (AdapterType) new ModifiableValueMap() {
+
+ public Collection<Object> values() {
+ throw new UnsupportedOperationException();
+ }
+
+ public int size() {
+ throw new UnsupportedOperationException();
+ }
+
+ public Object remove(Object arg0) {
+ Session session = getSession();
+ try{
+ final Node node = session.getNode(getPath());
+ final Property p = node.getProperty(String.valueOf(arg0));
+ if (p!=null) {
+ p.remove();
+ }
+ // this is not according to the spec - but OK for tests since
+ // the return value is never used
+ return null;
+ } catch(PathNotFoundException pnfe) {
+ // perfectly fine
+ return null;
+ } catch(RepositoryException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public void putAll(Map<? extends String, ? extends Object> arg0) {
+ throw new UnsupportedOperationException();
+ }
+
+ public Object put(String arg0, Object arg1) {
+ Session session = getSession();
+ try{
+ final Node node = session.getNode(getPath());
+ Object result = null;
+ if (node.hasProperty(arg0)) {
+ final Property previous = node.getProperty(arg0);
+ if (previous==null) {
+ // null
+ } else if (previous.getType() == PropertyType.STRING) {
+ result = previous.getString();
+ } else if (previous.getType() == PropertyType.DATE) {
+ result = previous.getDate();
+ } else if (previous.getType() == PropertyType.BOOLEAN) {
+ result = previous.getBoolean();
+ } else if (previous.getType() == PropertyType.LONG) {
+ result = previous.getLong();
+ } else {
+ throw new UnsupportedOperationException();
+ }
+ }
+ if (arg1 instanceof String) {
+ node.setProperty(arg0, (String)arg1);
+ } else if (arg1 instanceof Calendar) {
+ node.setProperty(arg0, (Calendar)arg1);
+ } else if (arg1 instanceof Boolean) {
+ node.setProperty(arg0, (Boolean)arg1);
+ } else if (arg1 instanceof Date) {
+ final Calendar c = Calendar.getInstance();
+ c.setTime((Date)arg1);
+ node.setProperty(arg0, c);
+ } else if (arg1 instanceof Long) {
+ node.setProperty(arg0, (Long)arg1);
+ } else {
+ throw new UnsupportedOperationException();
+ }
+ return result;
+ } catch(RepositoryException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public Set<String> keySet() {
+ Session session = getSession();
+ try {
+ final Node node = session.getNode(getPath());
+ final PropertyIterator pi = node.getProperties();
+ final Set<String> result = new HashSet<String>();
+ while(pi.hasNext()) {
+ final Property p = pi.nextProperty();
+ result.add(p.getName());
+ }
+ return result;
+ } catch (RepositoryException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public boolean isEmpty() {
+ throw new UnsupportedOperationException();
+ }
+
+ public Object get(Object arg0) {
+ Session session = getSession();
+ try{
+ final Node node = session.getNode(getPath());
+ final String key = String.valueOf(arg0);
+ if (node.hasProperty(key)) {
+ return node.getProperty(key);
+ } else {
+ return null;
+ }
+ } catch(RepositoryException re) {
+ throw new RuntimeException(re);
+ }
+ }
+
+ public Set<Entry<String, Object>> entrySet() {
+ throw new UnsupportedOperationException();
+ }
+
+ public boolean containsValue(Object arg0) {
+ throw new UnsupportedOperationException();
+ }
+
+ public boolean containsKey(Object arg0) {
+ Session session = getSession();
+ try{
+ final Node node = session.getNode(getPath());
+ return node.hasProperty(String.valueOf(arg0));
+ } catch(RepositoryException re) {
+ throw new RuntimeException(re);
+ }
+ }
+
+ public void clear() {
+ throw new UnsupportedOperationException();
+ }
+
+ public <T> T get(String name, T defaultValue) {
+ throw new UnsupportedOperationException();
+ }
+
+ public <T> T get(String name, Class<T> type) {
+ Session session = getSession();
+ try{
+ final Node node = session.getNode(getPath());
+ if (node==null) {
+ return null;
+ }
+ if (!node.hasProperty(name)) {
+ return null;
+ }
+ Property p = node.getProperty(name);
+ if (p==null) {
+ return null;
+ }
+ if (type.equals(Calendar.class)) {
+ return (T) p.getDate();
+ } else if (type.equals(String.class)) {
+ return (T) p.getString();
+ } else {
+ throw new UnsupportedOperationException();
+ }
+ } catch(RepositoryException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ };
+ } else {
+ return super.adaptTo(type);
+ }
+ }
+
+}
diff --git a/src/test/java/org/apache/sling/discovery/base/its/setup/mock/MockedResourceResolver.java b/src/test/java/org/apache/sling/discovery/base/its/setup/mock/MockedResourceResolver.java
new file mode 100644
index 0000000..9d9fd13
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/its/setup/mock/MockedResourceResolver.java
@@ -0,0 +1,335 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.sling.discovery.base.its.setup.mock;
+
+import java.io.IOException;
+import java.util.Calendar;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import javax.jcr.Credentials;
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.PathNotFoundException;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.SimpleCredentials;
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.sling.api.resource.LoginException;
+import org.apache.sling.api.resource.PersistenceException;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.commons.testing.jcr.RepositoryProvider;
+import org.apache.sling.commons.testing.jcr.RepositoryUtil;
+import org.apache.sling.jcr.api.SlingRepository;
+import org.slf4j.LoggerFactory;
+
+public class MockedResourceResolver implements ResourceResolver {
+
+ private final SlingRepository repository;
+
+ private Session session;
+
+ private List<MockedResource> resources = new LinkedList<MockedResource>();
+
+ public MockedResourceResolver() throws RepositoryException {
+ this(null);
+ }
+
+ public MockedResourceResolver(SlingRepository repositoryOrNull) throws RepositoryException {
+ if (repositoryOrNull==null) {
+ this.repository = RepositoryProvider.instance().getRepository();
+ Session adminSession = null;
+ try {
+ adminSession = this.repository.loginAdministrative(null);
+ RepositoryUtil.registerSlingNodeTypes(adminSession);
+ } catch ( final IOException ioe ) {
+ throw new RepositoryException(ioe);
+ } finally {
+ if ( adminSession != null ) {
+ adminSession.logout();
+ }
+ }
+ } else {
+ this.repository = repositoryOrNull;
+ }
+ }
+
+ public Session getSession() throws RepositoryException {
+ synchronized (this) {
+ if (session != null) {
+ return session;
+ }
+ session = createSession();
+ return session;
+ }
+ }
+
+ private Repository getRepository() {
+ return repository;
+ }
+
+ private Session createSession() throws RepositoryException {
+ final Credentials credentials = new SimpleCredentials("admin",
+ "admin".toCharArray());
+ return repository.login(credentials, "default");
+ }
+
+
+ @SuppressWarnings("unchecked")
+ public <AdapterType> AdapterType adaptTo(Class<AdapterType> type) {
+ if (type.equals(Session.class)) {
+ try {
+ return (AdapterType) getSession();
+ } catch (RepositoryException e) {
+ throw new RuntimeException("RepositoryException: " + e, e);
+ }
+ } else if (type.equals(Repository.class)) {
+ return (AdapterType) getRepository();
+ }
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ public Resource resolve(HttpServletRequest request, String absPath) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ public Resource resolve(String absPath) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Deprecated
+ public Resource resolve(HttpServletRequest request) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ public String map(String resourcePath) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ public String map(HttpServletRequest request, String resourcePath) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ public Resource getResource(String path) {
+ Session session;
+ try {
+ session = getSession();
+ session.getNode(path);
+ } catch (PathNotFoundException e) {
+ return null;
+ } catch (RepositoryException e) {
+ throw new RuntimeException("RepositoryException: " + e, e);
+ }
+ return new MockedResource(this, path, "nt:unstructured");
+ }
+
+ public Resource getResource(Resource base, String path) {
+ if (base.getPath().equals("/")) {
+ return getResource("/" + path);
+ } else {
+ return getResource(base.getPath() + "/" + path);
+ }
+ }
+
+ public String[] getSearchPath() {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ public Iterator<Resource> listChildren(Resource parent) {
+ try {
+ Node node = parent.adaptTo(Node.class);
+ final NodeIterator nodes = node.getNodes();
+ return new Iterator<Resource>() {
+
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+
+ public Resource next() {
+ Node next = nodes.nextNode();
+ try {
+ return new MockedResource(MockedResourceResolver.this,
+ next.getPath(), "nt:unstructured");
+ } catch (RepositoryException e) {
+ throw new RuntimeException("RepositoryException: " + e,
+ e);
+ }
+ }
+
+ public boolean hasNext() {
+ return nodes.hasNext();
+ }
+ };
+ } catch (RepositoryException e) {
+ throw new RuntimeException("RepositoryException: " + e, e);
+ }
+ }
+
+ public Iterable<Resource> getChildren(Resource parent) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ public Iterator<Resource> findResources(String query, String language) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ public Iterator<Map<String, Object>> queryResources(String query,
+ String language) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ public ResourceResolver clone(Map<String, Object> authenticationInfo)
+ throws LoginException {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ public boolean isLive() {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ public void close() {
+ Iterator<MockedResource> it = resources.iterator();
+ while (it.hasNext()) {
+ MockedResource r = it.next();
+ r.close();
+ }
+ if (session != null) {
+ if (session.isLive()) {
+ session.logout();
+ }
+ session = null;
+ }
+ }
+
+ public void register(MockedResource mockedResource) {
+ resources.add(mockedResource);
+ }
+
+ public String getUserID() {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ public Iterator<String> getAttributeNames() {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ public Object getAttribute(String name) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ public void delete(Resource resource) throws PersistenceException {
+ if (resources.contains(resource)) {
+ resources.remove(resource);
+ Node node = resource.adaptTo(Node.class);
+ try {
+ node.remove();
+ } catch (RepositoryException e) {
+ throw new PersistenceException("RepositoryException: "+e, e);
+ }
+ } else {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+ }
+
+ public Resource create(Resource parent, String name,
+ Map<String, Object> properties) throws PersistenceException {
+ final Node parentNode = parent.adaptTo(Node.class);
+ boolean success = false;
+ try {
+ final Node child;
+ if (properties!=null && properties.containsKey("jcr:primaryType")) {
+ child = parentNode.addNode(name, (String) properties.get("jcr:primaryType"));
+ } else {
+ child = parentNode.addNode(name);
+ }
+ if (properties!=null) {
+ final Iterator<Entry<String, Object>> it = properties.entrySet().iterator();
+ while(it.hasNext()) {
+ final Entry<String, Object> entry = it.next();
+ if (entry.getKey().equals("jcr:primaryType")) {
+ continue;
+ }
+ if (entry.getValue() instanceof String) {
+ child.setProperty(entry.getKey(), (String)entry.getValue());
+ } else if (entry.getValue() instanceof Boolean) {
+ child.setProperty(entry.getKey(), (Boolean)entry.getValue());
+ } else if (entry.getValue() instanceof Calendar) {
+ child.setProperty(entry.getKey(), (Calendar)entry.getValue());
+ } else {
+ throw new UnsupportedOperationException("Not implemented (entry.getValue(): "+entry.getValue()+")");
+ }
+ }
+ }
+ Resource result = getResource(parent, name);
+ success = true;
+ return result;
+ } catch (RepositoryException e) {
+ throw new RuntimeException(e);
+ } finally {
+ LoggerFactory.getLogger(this.getClass()).info("create: creating of "+name+" under "+parent+" was successful="+success);
+ }
+ }
+
+ public void revert() {
+ try {
+ this.session.refresh(false);
+ } catch (final RepositoryException re) {
+ throw new RuntimeException("Unable to commit changes.", re);
+ }
+ }
+
+ public void commit() throws PersistenceException {
+ try {
+ this.session.save();
+ } catch (final RepositoryException re) {
+ throw new PersistenceException("Unable to commit changes.", re);
+ }
+ }
+
+ public boolean hasChanges() {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ public String getParentResourceType(Resource resource) {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ public String getParentResourceType(String resourceType) {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ public boolean isResourceType(Resource resource, String resourceType) {
+ // TODO Auto-generated method stub
+ return false;
+ }
+
+ public void refresh() {
+ // TODO Auto-generated method stub
+
+ }
+
+}
diff --git a/src/test/java/org/apache/sling/discovery/base/its/setup/mock/PropertyProviderImpl.java b/src/test/java/org/apache/sling/discovery/base/its/setup/mock/PropertyProviderImpl.java
new file mode 100644
index 0000000..3f1f0c9
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/its/setup/mock/PropertyProviderImpl.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.sling.discovery.base.its.setup.mock;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.sling.discovery.PropertyProvider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class PropertyProviderImpl implements PropertyProvider {
+
+ private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+ private final Map<String, String> properties = new HashMap<String, String>();
+
+ private int getCnt = 0;
+
+ public PropertyProviderImpl() {
+ // nothing so far
+ }
+
+ public String getProperty(String name) {
+ getCnt++;
+ logger.warn("getProperty: name="+name+", new getCnt="+getCnt, new Exception("getProperty-stacktrace"));
+ return properties.get(name);
+ }
+
+ public void setProperty(String name, String value) {
+ properties.put(name, value);
+ }
+
+ public void setGetCnt(int getCnt) {
+ this.getCnt = getCnt;
+ }
+
+ public int getGetCnt() {
+ return getCnt;
+ }
+
+}
diff --git a/src/test/java/org/apache/sling/discovery/base/its/setup/mock/SimpleClusterViewService.java b/src/test/java/org/apache/sling/discovery/base/its/setup/mock/SimpleClusterViewService.java
new file mode 100644
index 0000000..e397d2e
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/its/setup/mock/SimpleClusterViewService.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.sling.discovery.base.its.setup.mock;
+
+import java.util.HashMap;
+import java.util.UUID;
+
+import org.apache.sling.discovery.base.commons.ClusterViewService;
+import org.apache.sling.discovery.base.commons.UndefinedClusterViewException;
+import org.apache.sling.discovery.commons.providers.DefaultInstanceDescription;
+import org.apache.sling.discovery.commons.providers.spi.LocalClusterView;
+
+public class SimpleClusterViewService implements ClusterViewService {
+
+ private LocalClusterView clusterView;
+
+ private final String slingId;
+
+ public SimpleClusterViewService(String slingId) {
+ this.slingId = slingId;
+ LocalClusterView clusterView = new LocalClusterView(UUID.randomUUID().toString(), null);
+ new DefaultInstanceDescription(clusterView, true, true, slingId, new HashMap<String, String>());
+ this.clusterView = clusterView;
+ }
+
+ @Override
+ public String getSlingId() {
+ return slingId;
+ }
+
+ @Override
+ public LocalClusterView getLocalClusterView() throws UndefinedClusterViewException {
+ if (clusterView==null) {
+ throw new IllegalStateException("no clusterView set");
+ }
+ return clusterView;
+ }
+
+ public void setClusterView(LocalClusterView clusterView) {
+ this.clusterView = clusterView;
+ }
+
+}
diff --git a/src/test/java/org/apache/sling/discovery/base/its/setup/mock/SimpleConnectorConfig.java b/src/test/java/org/apache/sling/discovery/base/its/setup/mock/SimpleConnectorConfig.java
new file mode 100644
index 0000000..5bffff4
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/its/setup/mock/SimpleConnectorConfig.java
@@ -0,0 +1,215 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.sling.discovery.base.its.setup.mock;
+
+import java.net.URL;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.apache.sling.discovery.base.its.setup.ModifiableTestBaseConfig;
+
+public class SimpleConnectorConfig implements ModifiableTestBaseConfig {
+
+ private int connectionTimeout;
+ private int soTimeout;
+ private URL[] topologyConnectorURLs;
+ private List<String> topologyConnectorWhitelist;
+ private String clusterInstancesPath = "/var/discovery/impl/clusterInstances";
+ private boolean hmacEnabled;
+ private String sharedKey;
+ private long keyInterval;
+ private boolean encryptionEnabled;
+ private boolean gzipConnectorRequestsEnabled;
+ private boolean autoStopLocalLoopEnabled;
+ private int backoffStandbyFactor;
+ private int backoffStableFactor;
+ private long backoffStandbyInterval;
+ private long announcementInterval = 20;
+ private long announcementTimeout = 20;
+ private int minEventDelay;
+
+ @Override
+ public int getSocketConnectionTimeout() {
+ return connectionTimeout;
+ }
+
+ public void setConnectionTimeout(int connectionTimeout) {
+ this.connectionTimeout = connectionTimeout;
+ }
+
+ @Override
+ public int getSoTimeout() {
+ return soTimeout;
+ }
+
+ public void setSoTimeout(int soTimeout) {
+ this.soTimeout = soTimeout;
+ }
+
+ @Override
+ public URL[] getTopologyConnectorURLs() {
+ return topologyConnectorURLs;
+ }
+
+ public void setTopologyConnectorURLs(URL[] topologyConnectorURLs) {
+ this.topologyConnectorURLs = topologyConnectorURLs;
+ }
+
+ @Override
+ public String[] getTopologyConnectorWhitelist() {
+ if (topologyConnectorWhitelist==null) {
+ return null;
+ }
+ return topologyConnectorWhitelist.toArray(new String[topologyConnectorWhitelist.size()]);
+ }
+
+ public void addTopologyConnectorWhitelistEntry(String whitelistEntry) {
+ if (topologyConnectorWhitelist==null) {
+ topologyConnectorWhitelist = new LinkedList<String>();
+ }
+ topologyConnectorWhitelist.add(whitelistEntry);
+ }
+
+ @Override
+ public String getClusterInstancesPath() {
+ return clusterInstancesPath;
+ }
+
+ public void setClusterInstancesPath(String clusterInstancesPath) {
+ this.clusterInstancesPath = clusterInstancesPath;
+ }
+
+ @Override
+ public boolean isHmacEnabled() {
+ return hmacEnabled;
+ }
+
+ public void setHmacEnabled(boolean hmacEnabled) {
+ this.hmacEnabled = hmacEnabled;
+ }
+
+ @Override
+ public String getSharedKey() {
+ return sharedKey;
+ }
+
+ public void setSharedKey(String sharedKey) {
+ this.sharedKey = sharedKey;
+ }
+
+ @Override
+ public long getKeyInterval() {
+ return keyInterval;
+ }
+
+ public void setKeyInterval(int keyInterval) {
+ this.keyInterval = keyInterval;
+ }
+
+ @Override
+ public boolean isEncryptionEnabled() {
+ return encryptionEnabled;
+ }
+
+ public void setEncryptionEnabled(boolean encryptionEnabled) {
+ this.encryptionEnabled = encryptionEnabled;
+ }
+
+ @Override
+ public boolean isGzipConnectorRequestsEnabled() {
+ return gzipConnectorRequestsEnabled;
+ }
+
+ public void setGzipConnectorRequestsEnabled(boolean gzipConnectorRequestsEnabled) {
+ this.gzipConnectorRequestsEnabled = gzipConnectorRequestsEnabled;
+ }
+
+ @Override
+ public boolean isAutoStopLocalLoopEnabled() {
+ return autoStopLocalLoopEnabled;
+ }
+
+ public void setAutoStopLocalLoopEnabled(boolean autoStopLocalLoopEnabled) {
+ this.autoStopLocalLoopEnabled = autoStopLocalLoopEnabled;
+ }
+
+ @Override
+ public int getBackoffStandbyFactor() {
+ return backoffStandbyFactor;
+ }
+
+ public void setBackoffStandbyFactor(int backoffStandbyFactor) {
+ this.backoffStandbyFactor = backoffStandbyFactor;
+ }
+
+ @Override
+ public int getBackoffStableFactor() {
+ return backoffStableFactor;
+ }
+
+ public void setBackoffStableFactor(int backoffStableFactor) {
+ this.backoffStableFactor = backoffStableFactor;
+ }
+
+ @Override
+ public long getBackoffStandbyInterval() {
+ return backoffStandbyInterval;
+ }
+
+ public void setBackoffStandbyInterval(long backoffStandbyInterval) {
+ this.backoffStandbyInterval = backoffStandbyInterval;
+ }
+
+ @Override
+ public long getConnectorPingInterval() {
+ return announcementInterval;
+ }
+
+ public void setAnnouncementInterval(long announcementInterval) {
+ this.announcementInterval = announcementInterval;
+ }
+
+ @Override
+ public long getConnectorPingTimeout() {
+ return announcementTimeout;
+ }
+
+ public void setAnnouncementTimeout(long announcementTimeout) {
+ this.announcementTimeout = announcementTimeout;
+ }
+
+ @Override
+ public int getMinEventDelay() {
+ return minEventDelay;
+ }
+
+ public void setMinEventDelay(int minEventDelay) {
+ this.minEventDelay = minEventDelay;
+ }
+
+ @Override
+ public void setViewCheckTimeout(int viewCheckTimeout) {
+ announcementTimeout = viewCheckTimeout;
+ }
+
+ @Override
+ public void setViewCheckInterval(int viewCheckInterval) {
+ announcementInterval = viewCheckInterval;
+ }
+}
diff --git a/src/test/java/org/apache/sling/discovery/base/its/setup/mock/TopologyEventAsserter.java b/src/test/java/org/apache/sling/discovery/base/its/setup/mock/TopologyEventAsserter.java
new file mode 100644
index 0000000..15a1409
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/its/setup/mock/TopologyEventAsserter.java
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.sling.discovery.base.its.setup.mock;
+
+import org.apache.sling.discovery.TopologyEvent;
+
+public interface TopologyEventAsserter {
+ public void assertOk(TopologyEvent event);
+}
\ No newline at end of file
diff --git a/src/test/resources/log4j.properties b/src/test/resources/log4j.properties
new file mode 100644
index 0000000..7db291c
--- /dev/null
+++ b/src/test/resources/log4j.properties
@@ -0,0 +1,26 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF 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.
+
+log4j.rootLogger=INFO, stdout
+
+log4j.logger.org.apache.jackrabbit.core.TransientRepository=WARN
+#log4j.logger.org.apache.sling.discovery.impl=DEBUG
+
+log4j.appender.stdout=org.apache.log4j.ConsoleAppender
+log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
+#log4j.appender.stdout.layout.ConversionPattern=%d{dd.MM.yyyy HH:mm:ss} *%-5p* [%t] %c{1}: %m (%F, line %L)\n
+log4j.appender.stdout.layout.ConversionPattern=%d{dd.MM.yyyy HH:mm:ss} *%-5p* [%t] %c{1}: %m\n
--
To stop receiving notification emails like this one, please contact
"commits@sling.apache.org" <co...@sling.apache.org>.