This is an automated email from the ASF dual-hosted git repository.

dklco pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sling-whiteboard.git


The following commit(s) were added to refs/heads/master by this push:
     new a312420  Initial commit of the repository maintainaince jobs
     new 89d9819  Merge branch 'master' of github.com:apache/sling-whiteboard
a312420 is described below

commit a312420f643fcb3d7f2f0924835ff9c1c1caa897
Author: Dan Klco <[email protected]>
AuthorDate: Thu Dec 10 21:10:33 2020 -0500

    Initial commit of the repository maintainaince jobs
---
 .gitignore                                         |   1 +
 .../pom.xml                                        | 184 ++++++++++++++++
 .../DataStoreCleanupConfig.java                    |  31 +++
 .../RepositoryManagementUtil.java                  |  41 ++++
 .../RevisionCleanupConfig.java                     |  31 +++
 .../VersionCleanupConfig.java                      |  28 +++
 .../VersionCleanupPathConfig.java                  |  37 ++++
 .../internal/DataStoreCleanupScheduler.java        |  54 +++++
 .../internal/RevisionCleanupScheduler.java         |  54 +++++
 .../internal/VersionCleanup.java                   | 232 +++++++++++++++++++++
 .../internal/VersionCleanupMBean.java              |  47 +++++
 .../internal/VersionCleanupPath.java               |  75 +++++++
 .../main/resources/OSGI-INF/l10n/bundle.properties |  56 +++++
 .../repositorymaintainance/CompositeDataMock.java  |  55 +++++
 .../internal/DataStoreCleanupSchedulerTest.java    |  66 ++++++
 .../internal/RevisionCleanupSchedulerTest.java     |  65 ++++++
 .../internal/VersionCleanupPathTest.java           |  94 +++++++++
 .../internal/VersionCleanupTest.java               | 216 +++++++++++++++++++
 .../src/test/resources/nodetypes.cnd               |  89 ++++++++
 .../src/test/resources/version-content.json        |  44 ++++
 20 files changed, 1500 insertions(+)

diff --git a/.gitignore b/.gitignore
index 08082a5..a99da84 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,3 +10,4 @@ dependency-reduced-pom.xml
 .vscode
 node_modules
 openwhisk_action.zip
+.java-version
diff --git a/org.apache.sling.jcr.repositorymaintainance/pom.xml 
b/org.apache.sling.jcr.repositorymaintainance/pom.xml
new file mode 100644
index 0000000..7858422
--- /dev/null
+++ b/org.apache.sling.jcr.repositorymaintainance/pom.xml
@@ -0,0 +1,184 @@
+<?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-bundle-parent</artifactId>
+        <version>40</version>
+        <relativePath />
+    </parent>
+    <artifactId>org.apache.sling.jcr.repositorymaintainance</artifactId>
+    <version>1.0.0-SNAPSHOT</version>
+    <name>Apache Sling JCR Repository Maintainance</name>
+
+    <properties>
+        <sling.java.version>8</sling.java.version>
+    </properties>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>biz.aQute.bnd</groupId>
+                <artifactId>bnd-maven-plugin</artifactId>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <configuration>
+                    <source>8</source>
+                    <target>8</target>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <configuration>
+                    <useSystemClassLoader>false</useSystemClassLoader>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.jacoco</groupId>
+                <artifactId>jacoco-maven-plugin</artifactId>
+                <version>0.8.2</version>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>prepare-agent</goal>
+                        </goals>
+                    </execution>
+                    <!-- attached to Maven test phase -->
+                    <execution>
+                        <id>report</id>
+                        <phase>test</phase>
+                        <goals>
+                            <goal>report</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.geronimo.specs</groupId>
+            <artifactId>geronimo-atinject_1.0_spec</artifactId>
+            <version>1.2</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.service.component.annotations</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.service.metatype.annotations</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>osgi.core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>osgi.cmpn</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>osgi.annotation</artifactId>
+            <version>6.0.1</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.sling</groupId>
+            <artifactId>org.apache.sling.api</artifactId>
+            <version>2.20.0</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-simple</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.jackrabbit</groupId>
+            <artifactId>oak-core</artifactId>
+            <version>1.36</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-core</artifactId>
+            <version>3.6.28</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.testing.sling-mock.junit4</artifactId>
+            <version>2.6.2</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>javax.annotation</groupId>
+            <artifactId>javax.annotation-api</artifactId>
+            <version>1.3.2</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.jetbrains</groupId>
+            <artifactId>annotations</artifactId>
+            <version>20.1.0</version>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.testing.sling-mock-oak</artifactId>
+            <version>2.1.10-1.16.0</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.jackrabbit</groupId>
+            <artifactId>oak-core</artifactId>
+            <version>1.36</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.jackrabbit</groupId>
+            <artifactId>oak-jcr</artifactId>
+            <version>1.36</version>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
+</project>
diff --git 
a/org.apache.sling.jcr.repositorymaintainance/src/main/java/org/apache/sling/repositorymaintainance/DataStoreCleanupConfig.java
 
b/org.apache.sling.jcr.repositorymaintainance/src/main/java/org/apache/sling/repositorymaintainance/DataStoreCleanupConfig.java
new file mode 100644
index 0000000..8dbb1b5
--- /dev/null
+++ 
b/org.apache.sling.jcr.repositorymaintainance/src/main/java/org/apache/sling/repositorymaintainance/DataStoreCleanupConfig.java
@@ -0,0 +1,31 @@
+/*
+ * 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.repositorymaintainance;
+
+import org.osgi.service.metatype.annotations.ObjectClassDefinition;
+import org.osgi.service.metatype.annotations.AttributeDefinition;
+
+/**
+ * Configuration for the DataStore Cleanup Service
+ */
+@ObjectClassDefinition(name = "%datastore.cleanup.name", description = 
"%datastore.cleanup.description", localization = "OSGI-INF/l10n/bundle")
+public @interface DataStoreCleanupConfig {
+
+    @AttributeDefinition(name = "%scheduler.expression.name", description = 
"%scheduler.expression.description")
+    String scheduler_expression();
+
+}
\ No newline at end of file
diff --git 
a/org.apache.sling.jcr.repositorymaintainance/src/main/java/org/apache/sling/repositorymaintainance/RepositoryManagementUtil.java
 
b/org.apache.sling.jcr.repositorymaintainance/src/main/java/org/apache/sling/repositorymaintainance/RepositoryManagementUtil.java
new file mode 100644
index 0000000..95e75fe
--- /dev/null
+++ 
b/org.apache.sling.jcr.repositorymaintainance/src/main/java/org/apache/sling/repositorymaintainance/RepositoryManagementUtil.java
@@ -0,0 +1,41 @@
+/*
+ * 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.repositorymaintainance;
+
+import java.util.Arrays;
+
+import javax.management.openmbean.CompositeData;
+
+import org.apache.jackrabbit.oak.api.jmx.RepositoryManagementMBean.StatusCode;
+
+/**
+ * Utilities for interacting with the RepositoryManagementMBean
+ * 
+ * @see org.apache.jackrabbit.oak.api.jmx.RepositoryManagementMBean
+ */
+public class RepositoryManagementUtil {
+
+    private RepositoryManagementUtil() {
+    }
+
+    public static boolean isRunning(CompositeData status) {
+        int c = ((Integer) status.get("code"));
+        return StatusCode.RUNNING == 
Arrays.stream(StatusCode.values()).filter(sc -> sc.ordinal() == c).findFirst()
+                .orElse(StatusCode.NONE);
+    }
+
+}
diff --git 
a/org.apache.sling.jcr.repositorymaintainance/src/main/java/org/apache/sling/repositorymaintainance/RevisionCleanupConfig.java
 
b/org.apache.sling.jcr.repositorymaintainance/src/main/java/org/apache/sling/repositorymaintainance/RevisionCleanupConfig.java
new file mode 100644
index 0000000..f172b59
--- /dev/null
+++ 
b/org.apache.sling.jcr.repositorymaintainance/src/main/java/org/apache/sling/repositorymaintainance/RevisionCleanupConfig.java
@@ -0,0 +1,31 @@
+/*
+ * 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.repositorymaintainance;
+
+import org.osgi.service.metatype.annotations.ObjectClassDefinition;
+import org.osgi.service.metatype.annotations.AttributeDefinition;
+
+/**
+ * Configuration for the Reference Mapping Transformer
+ */
+@ObjectClassDefinition(name = "%revision.cleanup.name", description = 
"%revision.cleanup.description", localization = "OSGI-INF/l10n/bundle")
+public @interface RevisionCleanupConfig {
+
+    @AttributeDefinition(name = "%scheduler.expression.name", description = 
"%scheduler.expression.description")
+    String scheduler_expression();
+
+}
\ No newline at end of file
diff --git 
a/org.apache.sling.jcr.repositorymaintainance/src/main/java/org/apache/sling/repositorymaintainance/VersionCleanupConfig.java
 
b/org.apache.sling.jcr.repositorymaintainance/src/main/java/org/apache/sling/repositorymaintainance/VersionCleanupConfig.java
new file mode 100644
index 0000000..5c5c8e1
--- /dev/null
+++ 
b/org.apache.sling.jcr.repositorymaintainance/src/main/java/org/apache/sling/repositorymaintainance/VersionCleanupConfig.java
@@ -0,0 +1,28 @@
+/*
+ * 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.repositorymaintainance;
+
+import org.osgi.service.metatype.annotations.AttributeDefinition;
+import org.osgi.service.metatype.annotations.ObjectClassDefinition;
+
+@ObjectClassDefinition(name = "%version.cleanup.name", description = 
"%version.cleanup.description", localization = "OSGI-INF/l10n/bundle")
+public @interface VersionCleanupConfig {
+
+    @AttributeDefinition(name = "%scheduler.expression.name", description = 
"%scheduler.expression.description")
+    String scheduler_expression();
+
+}
\ No newline at end of file
diff --git 
a/org.apache.sling.jcr.repositorymaintainance/src/main/java/org/apache/sling/repositorymaintainance/VersionCleanupPathConfig.java
 
b/org.apache.sling.jcr.repositorymaintainance/src/main/java/org/apache/sling/repositorymaintainance/VersionCleanupPathConfig.java
new file mode 100644
index 0000000..6c9c8f9
--- /dev/null
+++ 
b/org.apache.sling.jcr.repositorymaintainance/src/main/java/org/apache/sling/repositorymaintainance/VersionCleanupPathConfig.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.repositorymaintainance;
+
+import org.osgi.service.metatype.annotations.AttributeDefinition;
+import org.osgi.service.metatype.annotations.ObjectClassDefinition;
+
+/**
+ * Configuration to configure how version cleanup works on a per-path basis
+ */
+@ObjectClassDefinition(name = "%version.cleanup.path.name", description = 
"%version.cleanup.path.description", localization = "OSGI-INF/l10n/bundle")
+public @interface VersionCleanupPathConfig {
+
+    @AttributeDefinition(name = "%version.path.name", description = 
"%version.path.description")
+    String path();
+
+    @AttributeDefinition(name = "%version.limit.name", description = 
"%version.limit.description")
+    int limit();
+
+    @AttributeDefinition(name = "%version.keepVersions.name", description = 
"%version.keepVersions.description")
+    boolean keepVersions();
+
+}
diff --git 
a/org.apache.sling.jcr.repositorymaintainance/src/main/java/org/apache/sling/repositorymaintainance/internal/DataStoreCleanupScheduler.java
 
b/org.apache.sling.jcr.repositorymaintainance/src/main/java/org/apache/sling/repositorymaintainance/internal/DataStoreCleanupScheduler.java
new file mode 100644
index 0000000..b15dfaa
--- /dev/null
+++ 
b/org.apache.sling.jcr.repositorymaintainance/src/main/java/org/apache/sling/repositorymaintainance/internal/DataStoreCleanupScheduler.java
@@ -0,0 +1,54 @@
+/*
+ * 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.repositorymaintainance.internal;
+
+import org.apache.jackrabbit.oak.api.jmx.RepositoryManagementMBean;
+import org.apache.sling.repositorymaintainance.DataStoreCleanupConfig;
+import org.apache.sling.repositorymaintainance.RepositoryManagementUtil;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.ConfigurationPolicy;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.metatype.annotations.Designate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Service for running the Jackrabbit OAK Blob Store cleanup on a schedule.
+ */
+@Component(service = { Runnable.class }, configurationPolicy = 
ConfigurationPolicy.REQUIRE, immediate = true)
+@Designate(ocd = DataStoreCleanupConfig.class)
+public class DataStoreCleanupScheduler implements Runnable {
+
+    private static final Logger log = 
LoggerFactory.getLogger(DataStoreCleanupScheduler.class);
+
+    private RepositoryManagementMBean repositoryManager;
+
+    @Reference
+    public void setRepositoryManager(final RepositoryManagementMBean 
repositoryManager) {
+        this.repositoryManager = repositoryManager;
+    }
+
+    public void run() {
+        if 
(!RepositoryManagementUtil.isRunning(repositoryManager.getDataStoreGCStatus())) 
{
+            log.info("Starting DataStore Garbage Collection");
+            repositoryManager.startDataStoreGC(false);
+        } else {
+            log.warn("DataStore Garbage Collection already running!");
+        }
+    }
+
+}
diff --git 
a/org.apache.sling.jcr.repositorymaintainance/src/main/java/org/apache/sling/repositorymaintainance/internal/RevisionCleanupScheduler.java
 
b/org.apache.sling.jcr.repositorymaintainance/src/main/java/org/apache/sling/repositorymaintainance/internal/RevisionCleanupScheduler.java
new file mode 100644
index 0000000..f2f06f5
--- /dev/null
+++ 
b/org.apache.sling.jcr.repositorymaintainance/src/main/java/org/apache/sling/repositorymaintainance/internal/RevisionCleanupScheduler.java
@@ -0,0 +1,54 @@
+/*
+ * 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.repositorymaintainance.internal;
+
+import org.apache.jackrabbit.oak.api.jmx.RepositoryManagementMBean;
+import org.apache.sling.repositorymaintainance.RepositoryManagementUtil;
+import org.apache.sling.repositorymaintainance.RevisionCleanupConfig;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.ConfigurationPolicy;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.metatype.annotations.Designate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Service for running the Jackrabbit OAK Segment Store cleanup on a schedule.
+ */
+@Component(service = { Runnable.class }, configurationPolicy = 
ConfigurationPolicy.REQUIRE, immediate = true)
+@Designate(ocd = RevisionCleanupConfig.class)
+public class RevisionCleanupScheduler implements Runnable {
+
+    private static final Logger log = 
LoggerFactory.getLogger(RevisionCleanupScheduler.class);
+
+    private RepositoryManagementMBean repositoryManager;
+
+    @Reference
+    public void setRepositoryManager(final RepositoryManagementMBean 
repositoryManager) {
+        this.repositoryManager = repositoryManager;
+    }
+
+    public void run() {
+        if 
(!RepositoryManagementUtil.isRunning(repositoryManager.getRevisionGCStatus())) {
+            log.info("Starting Revision Garbage Collection");
+            repositoryManager.startRevisionGC();
+        } else {
+            log.warn("Revision Garbage Collection already running!");
+        }
+    }
+
+}
diff --git 
a/org.apache.sling.jcr.repositorymaintainance/src/main/java/org/apache/sling/repositorymaintainance/internal/VersionCleanup.java
 
b/org.apache.sling.jcr.repositorymaintainance/src/main/java/org/apache/sling/repositorymaintainance/internal/VersionCleanup.java
new file mode 100644
index 0000000..7f6a31b
--- /dev/null
+++ 
b/org.apache.sling.jcr.repositorymaintainance/src/main/java/org/apache/sling/repositorymaintainance/internal/VersionCleanup.java
@@ -0,0 +1,232 @@
+/*
+ * 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.repositorymaintainance.internal;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+
+import javax.jcr.ItemNotFoundException;
+import javax.jcr.Node;
+import javax.jcr.PathNotFoundException;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.version.Version;
+import javax.jcr.version.VersionHistory;
+import javax.jcr.version.VersionIterator;
+import javax.jcr.version.VersionManager;
+import javax.management.DynamicMBean;
+
+import org.apache.jackrabbit.oak.commons.jmx.AnnotatedStandardMBean;
+import org.apache.sling.api.resource.LoginException;
+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.repositorymaintainance.VersionCleanupConfig;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.ConfigurationPolicy;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.metatype.annotations.Designate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * 
+ */
+@Component(service = { Runnable.class, DynamicMBean.class }, property = {
+        
"jmx.objectname=org.apache.sling.repositorymaintainance:type=VersionCleanup" }, 
configurationPolicy = ConfigurationPolicy.REQUIRE, immediate = true)
+@Designate(ocd = VersionCleanupConfig.class)
+public class VersionCleanup extends AnnotatedStandardMBean implements 
Runnable, VersionCleanupMBean {
+
+    private static final Logger log = 
LoggerFactory.getLogger(VersionCleanup.class);
+
+    private Thread cleanupThread;
+    private final ResourceResolverFactory factory;
+    private long lastCleanedVersions;
+    private String lastFailureMessage;
+    private final List<VersionCleanupPath> versionCleanupConfigs;
+
+    @Activate
+    public VersionCleanup(@Reference final List<VersionCleanupPath> 
versionCleanupConfigs,
+            @Reference final ResourceResolverFactory factory) {
+        super(VersionCleanupMBean.class);
+        this.factory = factory;
+        this.versionCleanupConfigs = versionCleanupConfigs;
+
+    }
+
+    private String getPath(final Session session, final VersionHistory 
versionHistory) throws RepositoryException {
+        String identifier = versionHistory.getVersionableIdentifier();
+        try {
+            Node versionableNode = session.getNodeByIdentifier(identifier);
+            return versionableNode.getPath();
+        } catch (ItemNotFoundException infe) {
+            log.debug("Unable to get versionable node by ID: {}, exception: 
{}", identifier, infe.getMessage());
+            return 
versionHistory.getProperty(session.getWorkspace().getName()).getString();
+        }
+    }
+
+    private void cleanupVersions(final Session session, final Resource 
history) {
+        try {
+            final VersionHistory versionHistory = (VersionHistory) 
session.getItem(history.getPath());
+            final String path = getPath(session, versionHistory);
+            final VersionCleanupPath config = 
VersionCleanupPath.getMatchingConfiguration(this.versionCleanupConfigs,
+                    path);
+            int limit = config.getLimit();
+
+            if (!isMatchingVersion(session, path, versionHistory) && 
!config.isKeepVersions() && limit > 0) {
+                log.debug("Deleted, removing all but last version");
+                limit = 1;
+            }
+            log.debug("Cleaning up versions for: {}", 
versionHistory.getPath());
+            final VersionIterator versionIterator = 
versionHistory.getAllVersions();
+            final List<String> versionNames = new ArrayList<>();
+            while (versionIterator.hasNext()) {
+                final Version version = versionIterator.nextVersion();
+                if (!version.getName().equals("jcr:rootVersion")) {
+                    versionNames.add(version.getName());
+                }
+            }
+            if (versionNames.size() > limit) {
+                final List<String> toCleanup = versionNames.subList(0, 
versionNames.size() - limit);
+                log.info("Cleaning up {} versions from {} at: {}", 
toCleanup.size(), path, versionHistory.getPath());
+                for (final String item : toCleanup) {
+                    versionHistory.removeVersion(item);
+                    log.trace("Cleaned up: {}", item);
+                    lastCleanedVersions++;
+                }
+            }
+        } catch (final RepositoryException re) {
+            log.warn("Failed to cleanup version history for: {}", 
history.getPath(), re);
+        }
+
+    }
+
+    private void findVersions(final Session session, final Resource resource)
+            throws RepositoryException, InterruptedException {
+        if (Thread.interrupted()) {
+            throw new InterruptedException("Process interrupted");
+        }
+        log.debug("Finding versions under: {}", resource.getPath());
+        if ("nt:versionHistory".equals(resource.getResourceType())) {
+            resource.getResourceResolver().refresh();
+            cleanupVersions(session, resource);
+        } else {
+            for (final Resource child : resource.getChildren()) {
+                findVersions(session, child);
+            }
+        }
+    }
+
+    private boolean isMatchingVersion(Session session, String path, 
VersionHistory versionHistory)
+            throws RepositoryException {
+        try {
+            VersionManager versionManager = 
session.getWorkspace().getVersionManager();
+            String baseVersionPath = 
versionManager.getBaseVersion(path).getParent().getPath();
+            String versionHistoryPath = versionHistory.getPath();
+
+            return session.nodeExists(path) && 
isVersionable(session.getNode(path))
+                    && baseVersionPath.equals(versionHistoryPath);
+        } catch (PathNotFoundException pnfe) {
+            log.debug("Path: {} not found: {}", path, pnfe.getMessage());
+            return false;
+        }
+    }
+
+    private boolean isVersionable(final Node node) throws RepositoryException {
+        return node != null && 
node.isNodeType("{http://www.jcp.org/jcr/mix/1.0}versionable";);
+    }
+
+    @Override
+    public void run() {
+        if (isRunning()) {
+            log.warn("Version cleanup already running!");
+        } else {
+            cleanupThread = new Thread((this::doRun));
+            cleanupThread.setDaemon(true);
+            cleanupThread.start();
+        }
+    }
+
+    private void doRun() {
+        boolean interrupted = false;
+        boolean succeeded = false;
+        String failureMessage = null;
+        lastCleanedVersions = 0;
+        try {
+            try (final ResourceResolver adminResolver = 
factory.getServiceResourceResolver(
+                    
Collections.singletonMap(ResourceResolverFactory.SUBSERVICE, 
"sling-cms-versionmgr"))) {
+                final Resource versionRoot = 
adminResolver.getResource("/jcr:system/jcr:versionStorage");
+                final Session session = 
Optional.ofNullable(versionRoot.getResourceResolver().adaptTo(Session.class))
+                        .orElseThrow(() -> new RepositoryException("Failed to 
get session"));
+                for (final Resource folder : versionRoot.getChildren()) {
+                    log.info("Traversing and cleaning: {}", folder.getPath());
+                    findVersions(session, folder);
+                }
+                succeeded = true;
+            }
+        } catch (final LoginException le) {
+            log.error("Failed to run version cleanup, cannot get service 
user", le);
+            failureMessage = "Failed to run version cleanup, cannot get 
service user";
+        } catch (final RepositoryException re) {
+            log.error("Failed to run version cleanup", re);
+            failureMessage = "Failed to run version cleanup";
+        } catch (final InterruptedException e) { // no need to do anything, at 
this point nearly done
+            log.info("Process interrupted, quitting");
+            interrupted = true;
+        } finally {
+            if (succeeded) {
+                this.lastFailureMessage = null;
+            } else if (!interrupted) {
+                lastFailureMessage = failureMessage != null ? failureMessage
+                        : "Failed due to unexpected exception, see logs";
+            }
+        }
+    }
+
+    @Override
+    public boolean isRunning() {
+        return cleanupThread != null && cleanupThread.isAlive();
+    }
+
+    @Override
+    public boolean isFailed() {
+        return lastFailureMessage != null;
+    }
+
+    @Override
+    public String getLastMessage() {
+        return lastFailureMessage;
+    }
+
+    @Override
+    public long getLastCleanedVersionsCount() {
+        return lastCleanedVersions;
+    }
+
+    @Override
+    public void start() {
+        this.run();
+    }
+
+    @Override
+    public void stop() {
+        Optional.ofNullable(cleanupThread).ifPresent(Thread::interrupt);
+    }
+}
diff --git 
a/org.apache.sling.jcr.repositorymaintainance/src/main/java/org/apache/sling/repositorymaintainance/internal/VersionCleanupMBean.java
 
b/org.apache.sling.jcr.repositorymaintainance/src/main/java/org/apache/sling/repositorymaintainance/internal/VersionCleanupMBean.java
new file mode 100644
index 0000000..1fa9082
--- /dev/null
+++ 
b/org.apache.sling.jcr.repositorymaintainance/src/main/java/org/apache/sling/repositorymaintainance/internal/VersionCleanupMBean.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.repositorymaintainance.internal;
+
+import org.apache.jackrabbit.oak.api.jmx.Description;
+
+/**
+ * JMX MBean interface for the version cleanup tool to enable introspection 
into
+ * the state of
+ */
+
+@Description("Cleanup versions")
+public interface VersionCleanupMBean {
+
+    @Description("Whether or not the service is running")
+    boolean isRunning();
+
+    @Description("Whether or not the service is failed")
+    boolean isFailed();
+
+    @Description("The last message")
+    String getLastMessage();
+
+    @Description("The count of the last cleaned versions")
+    long getLastCleanedVersionsCount();
+
+    @Description("Start running the job, will stop any running instances")
+    void start();
+
+    @Description("Stop the running instance or do nothing")
+    void stop();
+
+}
diff --git 
a/org.apache.sling.jcr.repositorymaintainance/src/main/java/org/apache/sling/repositorymaintainance/internal/VersionCleanupPath.java
 
b/org.apache.sling.jcr.repositorymaintainance/src/main/java/org/apache/sling/repositorymaintainance/internal/VersionCleanupPath.java
new file mode 100644
index 0000000..cf18efd
--- /dev/null
+++ 
b/org.apache.sling.jcr.repositorymaintainance/src/main/java/org/apache/sling/repositorymaintainance/internal/VersionCleanupPath.java
@@ -0,0 +1,75 @@
+/*
+ * 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.repositorymaintainance.internal;
+
+import java.util.List;
+
+import javax.jcr.RepositoryException;
+
+import org.apache.sling.repositorymaintainance.VersionCleanupPathConfig;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.metatype.annotations.Designate;
+
+@Component(service = VersionCleanupPath.class, immediate = true)
+@Designate(ocd = VersionCleanupPathConfig.class, factory = true)
+public class VersionCleanupPath implements Comparable<VersionCleanupPath> {
+
+    private final boolean keepVersions;
+    private final int limit;
+    private final String path;
+
+    @Activate
+    public VersionCleanupPath(VersionCleanupPathConfig config) {
+        this.keepVersions = config.keepVersions();
+        this.limit = config.limit();
+        this.path = config.path();
+    }
+
+    @Override
+    public int compareTo(VersionCleanupPath o) {
+        return path.compareTo(o.path) * -1;
+    }
+
+    /**
+     * @return the keepVersions
+     */
+    public boolean isKeepVersions() {
+        return keepVersions;
+    }
+
+    /**
+     * @return the limit
+     */
+    public int getLimit() {
+        return limit;
+    }
+
+    /**
+     * @return the path
+     */
+    public String getPath() {
+        return path;
+    }
+
+    public static final VersionCleanupPath getMatchingConfiguration(
+            final List<VersionCleanupPath> versionCleanupConfigs, final String 
path) throws RepositoryException {
+        return versionCleanupConfigs.stream().filter(c -> 
path.startsWith(c.getPath())).findFirst()
+                .orElseThrow(() -> new RepositoryException("Failed to find 
version cleanup configuration for " + path));
+    }
+
+}
diff --git 
a/org.apache.sling.jcr.repositorymaintainance/src/main/resources/OSGI-INF/l10n/bundle.properties
 
b/org.apache.sling.jcr.repositorymaintainance/src/main/resources/OSGI-INF/l10n/bundle.properties
new file mode 100644
index 0000000..5c35f25
--- /dev/null
+++ 
b/org.apache.sling.jcr.repositorymaintainance/src/main/resources/OSGI-INF/l10n/bundle.properties
@@ -0,0 +1,56 @@
+#
+#  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.
+#
+
+#
+# This file contains localization strings for configuration labels and
+# descriptions as used in the metatype.xml descriptor generated by the
+# the Sling SCR plugin
+
+## Global Entries
+scheduler.expression.name=Quartz Scheduler Expression
+scheduler.expression.description=A quartz expression for configuring when \
+a scheduler should be triggered
+
+# DataStore Cleanup Entries
+datastore.cleanup.name=Apache Sling JCR Repository Maintainance Oak DataStore 
Garbage Collection
+datastore.cleanup.description=A scheduler to initiate a Data Store garbage 
collection operation \
+the Jackrabbit OAK repository
+
+# Revision Cleanup Entries
+revision.cleanup.name=Apache Sling JCR Repository Maintainance Revision 
Garbage Collection
+revision.cleanup.description=A scheduler to initiate a revision garbage 
collection operation \
+the Jackrabbit OAK repository
+
+version.cleanup.name=Apache Sling JCR Repository Maintainance Version Cleanup
+version.cleanup.description=A scheduler and service to cleanup JCR versions
+
+version.cleanup.path.name=Apache Sling JCR Repository Maintainance Version 
Cleanup Path Configuration
+version.cleanup.description=A configuration for how to perform version cleanup 
based on the path \
+inside the repository
+
+version.keepVersions.name=Keep Deleted Versions
+version.keepVersions.description=If true, versions will be kept even if the 
associated content has been deleted \
+or orphaned, if not only the latest version of deleted or orphaned content 
will be kept
+
+version.limit.name=Version Limit
+version.limit.description=The number of versions to keep, any additional 
versions beyond this number will be deleted \
+with the oldest first
+
+version.path.name=Path
+version.path.description=The path for which this configuration applies
\ No newline at end of file
diff --git 
a/org.apache.sling.jcr.repositorymaintainance/src/test/java/org/apache/sling/repositorymaintainance/CompositeDataMock.java
 
b/org.apache.sling.jcr.repositorymaintainance/src/test/java/org/apache/sling/repositorymaintainance/CompositeDataMock.java
new file mode 100644
index 0000000..3ed3432
--- /dev/null
+++ 
b/org.apache.sling.jcr.repositorymaintainance/src/test/java/org/apache/sling/repositorymaintainance/CompositeDataMock.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.repositorymaintainance;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.management.openmbean.CompositeData;
+
+import org.mockito.Mockito;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+/**
+ * A mock for creating a fluent API version of a CompositeData Object.
+ */
+public class CompositeDataMock {
+
+    private Map<String, Object> data = new HashMap<>();
+
+    public static CompositeDataMock init() {
+        return new CompositeDataMock();
+    }
+
+    public CompositeDataMock put(String key, Object value) {
+        this.data.put(key, value);
+        return this;
+    }
+
+    public CompositeData build() {
+        CompositeData dc = Mockito.mock(CompositeData.class);
+        Mockito.when(dc.get(Mockito.anyString())).thenAnswer(new 
Answer<Object>() {
+            @Override
+            public Object answer(InvocationOnMock invocation) throws Throwable 
{
+                return data.get(invocation.getArguments()[0]);
+            }
+        });
+        return dc;
+    }
+
+}
\ No newline at end of file
diff --git 
a/org.apache.sling.jcr.repositorymaintainance/src/test/java/org/apache/sling/repositorymaintainance/internal/DataStoreCleanupSchedulerTest.java
 
b/org.apache.sling.jcr.repositorymaintainance/src/test/java/org/apache/sling/repositorymaintainance/internal/DataStoreCleanupSchedulerTest.java
new file mode 100644
index 0000000..74aa837
--- /dev/null
+++ 
b/org.apache.sling.jcr.repositorymaintainance/src/test/java/org/apache/sling/repositorymaintainance/internal/DataStoreCleanupSchedulerTest.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.repositorymaintainance.internal;
+
+import static org.mockito.Mockito.never;
+
+import javax.management.openmbean.CompositeData;
+
+import org.apache.jackrabbit.oak.api.jmx.RepositoryManagementMBean;
+import org.apache.jackrabbit.oak.api.jmx.RepositoryManagementMBean.StatusCode;
+import org.apache.sling.repositorymaintainance.CompositeDataMock;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+public class DataStoreCleanupSchedulerTest {
+
+    @Test
+    public void testRunnable() {
+        final DataStoreCleanupScheduler dscs = new DataStoreCleanupScheduler();
+
+        Integer id = 1;
+        final RepositoryManagementMBean repositoryManager = 
Mockito.mock(RepositoryManagementMBean.class);
+        CompositeData startingCd = CompositeDataMock.init().put("id", 
id).build();
+        
Mockito.when(repositoryManager.startDataStoreGC(false)).thenReturn(startingCd);
+        CompositeData doneCd = CompositeDataMock.init().put("id", 
id).put("code", StatusCode.SUCCEEDED.ordinal())
+                .build();
+        
Mockito.when(repositoryManager.getDataStoreGCStatus()).thenReturn(doneCd);
+        dscs.setRepositoryManager(repositoryManager);
+
+        dscs.run();
+
+        
Mockito.verify(repositoryManager).startDataStoreGC(Mockito.anyBoolean());
+    }
+
+    @Test
+    public void testRunCheck() {
+        final DataStoreCleanupScheduler dscs = new DataStoreCleanupScheduler();
+
+        Integer id = 1;
+        final RepositoryManagementMBean repositoryManager = 
Mockito.mock(RepositoryManagementMBean.class);
+        CompositeData startingCd = CompositeDataMock.init().put("id", 
id).build();
+        
Mockito.when(repositoryManager.startDataStoreGC(false)).thenReturn(startingCd);
+        CompositeData doneCd = CompositeDataMock.init().put("id", 
id).put("code", StatusCode.RUNNING.ordinal()).build();
+        
Mockito.when(repositoryManager.getDataStoreGCStatus()).thenReturn(doneCd);
+        dscs.setRepositoryManager(repositoryManager);
+
+        dscs.run();
+
+        Mockito.verify(repositoryManager, 
never()).startDataStoreGC(Mockito.anyBoolean());
+    }
+
+}
\ No newline at end of file
diff --git 
a/org.apache.sling.jcr.repositorymaintainance/src/test/java/org/apache/sling/repositorymaintainance/internal/RevisionCleanupSchedulerTest.java
 
b/org.apache.sling.jcr.repositorymaintainance/src/test/java/org/apache/sling/repositorymaintainance/internal/RevisionCleanupSchedulerTest.java
new file mode 100644
index 0000000..bfc38c9
--- /dev/null
+++ 
b/org.apache.sling.jcr.repositorymaintainance/src/test/java/org/apache/sling/repositorymaintainance/internal/RevisionCleanupSchedulerTest.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.repositorymaintainance.internal;
+
+import static org.mockito.Mockito.never;
+
+import javax.management.openmbean.CompositeData;
+
+import org.apache.jackrabbit.oak.api.jmx.RepositoryManagementMBean;
+import org.apache.jackrabbit.oak.api.jmx.RepositoryManagementMBean.StatusCode;
+import org.apache.sling.repositorymaintainance.CompositeDataMock;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+public class RevisionCleanupSchedulerTest {
+
+    @Test
+    public void testRunnable() {
+        final RevisionCleanupScheduler rcs = new RevisionCleanupScheduler();
+
+        Integer id = 1;
+        final RepositoryManagementMBean repositoryManager = 
Mockito.mock(RepositoryManagementMBean.class);
+        CompositeData startingCd = CompositeDataMock.init().put("id", 
id).build();
+        
Mockito.when(repositoryManager.startDataStoreGC(false)).thenReturn(startingCd);
+        CompositeData doneCd = CompositeDataMock.init().put("id", (Integer) id 
+ 1)
+                .put("code", StatusCode.SUCCEEDED.ordinal()).build();
+        
Mockito.when(repositoryManager.getRevisionGCStatus()).thenReturn(doneCd);
+        rcs.setRepositoryManager(repositoryManager);
+
+        rcs.run();
+
+        Mockito.verify(repositoryManager).startRevisionGC();
+    }
+
+    @Test
+    public void testRunning() {
+        final RevisionCleanupScheduler rcs = new RevisionCleanupScheduler();
+
+        Integer id = 1;
+        final RepositoryManagementMBean repositoryManager = 
Mockito.mock(RepositoryManagementMBean.class);
+        CompositeData runningCd = CompositeDataMock.init().put("id", 
id).put("code", StatusCode.RUNNING.ordinal())
+                .build();
+        
Mockito.when(repositoryManager.getRevisionGCStatus()).thenReturn(runningCd);
+        rcs.setRepositoryManager(repositoryManager);
+
+        rcs.run();
+
+        Mockito.verify(repositoryManager, never()).startRevisionGC();
+    }
+
+}
\ No newline at end of file
diff --git 
a/org.apache.sling.jcr.repositorymaintainance/src/test/java/org/apache/sling/repositorymaintainance/internal/VersionCleanupPathTest.java
 
b/org.apache.sling.jcr.repositorymaintainance/src/test/java/org/apache/sling/repositorymaintainance/internal/VersionCleanupPathTest.java
new file mode 100644
index 0000000..c2be51a
--- /dev/null
+++ 
b/org.apache.sling.jcr.repositorymaintainance/src/test/java/org/apache/sling/repositorymaintainance/internal/VersionCleanupPathTest.java
@@ -0,0 +1,94 @@
+/*
+ * 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.repositorymaintainance.internal;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import java.lang.annotation.Annotation;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import javax.jcr.RepositoryException;
+
+import org.apache.sling.repositorymaintainance.VersionCleanupPathConfig;
+import org.junit.Test;
+
+public class VersionCleanupPathTest {
+
+    private VersionCleanupPath simpleCreate(String path) {
+        return new VersionCleanupPath(new VersionCleanupPathConfig() {
+
+            @Override
+            public Class<? extends Annotation> annotationType() {
+                return null;
+            }
+
+            @Override
+            public boolean keepVersions() {
+                return false;
+            }
+
+            @Override
+            public int limit() {
+                return 5;
+            }
+
+            @Override
+            public String path() {
+                return path;
+            }
+
+        });
+    }
+
+    @Test
+    public void testNotFound() {
+        try {
+            
VersionCleanupPath.getMatchingConfiguration(Collections.emptyList(), "/");
+            fail();
+        } catch (RepositoryException re) {
+            // expected
+        }
+
+        try {
+            
VersionCleanupPath.getMatchingConfiguration(Collections.singletonList(simpleCreate("/subpath")),
 "/");
+            fail();
+        } catch (RepositoryException re) {
+            // expected
+        }
+    }
+
+    @Test
+    public void testSorting() throws RepositoryException {
+        List<VersionCleanupPath> configs = new ArrayList<>();
+        configs.add(simpleCreate("/content"));
+        configs.add(simpleCreate("/content/content2"));
+        Collections.sort(configs);
+
+        assertEquals("/content/content2,/content",
+                
configs.stream().map(VersionCleanupPath::getPath).collect(Collectors.joining(",")));
+        assertEquals("/content/content2",
+                VersionCleanupPath.getMatchingConfiguration(configs, 
"/content/content2/subitem").getPath());
+        assertEquals("/content",
+                VersionCleanupPath.getMatchingConfiguration(configs, 
"/content/content3/subitem").getPath());
+
+    }
+
+}
\ No newline at end of file
diff --git 
a/org.apache.sling.jcr.repositorymaintainance/src/test/java/org/apache/sling/repositorymaintainance/internal/VersionCleanupTest.java
 
b/org.apache.sling.jcr.repositorymaintainance/src/test/java/org/apache/sling/repositorymaintainance/internal/VersionCleanupTest.java
new file mode 100644
index 0000000..94b0867
--- /dev/null
+++ 
b/org.apache.sling.jcr.repositorymaintainance/src/test/java/org/apache/sling/repositorymaintainance/internal/VersionCleanupTest.java
@@ -0,0 +1,216 @@
+/*
+ * 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.repositorymaintainance.internal;
+
+import static org.junit.Assert.assertEquals;
+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 static org.mockito.Mockito.atLeast;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.UnsupportedEncodingException;
+import java.lang.annotation.Annotation;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import javax.jcr.InvalidItemStateException;
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.UnsupportedRepositoryOperationException;
+import javax.jcr.lock.LockException;
+import javax.jcr.nodetype.InvalidNodeTypeDefinitionException;
+import javax.jcr.nodetype.NodeTypeExistsException;
+import javax.jcr.version.VersionException;
+import javax.jcr.version.VersionManager;
+
+import org.apache.jackrabbit.commons.cnd.CndImporter;
+import org.apache.jackrabbit.commons.cnd.ParseException;
+import org.apache.sling.api.resource.LoginException;
+import org.apache.sling.api.resource.PersistenceException;
+import org.apache.sling.api.resource.ResourceResolverFactory;
+import org.apache.sling.repositorymaintainance.VersionCleanupPathConfig;
+import org.apache.sling.testing.mock.sling.ResourceResolverType;
+import org.apache.sling.testing.mock.sling.junit.SlingContext;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+public class VersionCleanupTest {
+
+    private VersionManager versionManager;
+
+    @Rule
+    public SlingContext context = new 
SlingContext(ResourceResolverType.JCR_OAK);
+
+    private List<VersionCleanupPath> globalConfig;
+
+    private Session session;
+
+    @Before
+    public void init() throws LoginException, 
InvalidNodeTypeDefinitionException, NodeTypeExistsException,
+            UnsupportedRepositoryOperationException, 
UnsupportedEncodingException, ParseException, RepositoryException,
+            IOException {
+
+        session = context.resourceResolver().adaptTo(Session.class);
+        versionManager = session.getWorkspace().getVersionManager();
+        InputStream cnd = getClass().getResourceAsStream("/nodetypes.cnd");
+        CndImporter.registerNodeTypes(new InputStreamReader(cnd, "UTF-8"), 
session);
+
+        context.load().json("/version-content.json", 
"/content/apache/sling-apache-org");
+
+        globalConfig = Collections.singletonList(new VersionCleanupPath(new 
VersionCleanupPathConfig() {
+
+            @Override
+            public Class<? extends Annotation> annotationType() {
+                return null;
+            }
+
+            @Override
+            public boolean keepVersions() {
+                return false;
+            }
+
+            @Override
+            public int limit() {
+                return 5;
+            }
+
+            @Override
+            public String path() {
+                return "/";
+            }
+
+        }));
+
+    }
+
+    private void doVersions(String path, int count) throws RepositoryException 
{
+        Node node = session.getNode(path);
+        node.addMixin("mix:versionable");
+        session.save();
+        for (int i = 0; i < count; i++) {
+            versionManager.checkpoint(path);
+        }
+    }
+
+    @Test(timeout = 5000)
+    public void testRunnable() throws InterruptedException, VersionException, 
UnsupportedRepositoryOperationException,
+            InvalidItemStateException, LockException, RepositoryException {
+
+        doVersions("/content/apache/sling-apache-org/index", 10);
+
+        final VersionCleanup vcs = new VersionCleanup(globalConfig, 
context.getService(ResourceResolverFactory.class));
+
+        vcs.start();
+        while (vcs.isRunning()) {
+            TimeUnit.SECONDS.sleep(2);
+        }
+
+        assertFalse(vcs.isFailed());
+        assertFalse(vcs.isRunning());
+        assertNull(vcs.getLastMessage());
+        assertEquals(5L, vcs.getLastCleanedVersionsCount());
+    }
+
+    @Test(timeout = 5000)
+    public void testStop() throws InterruptedException, VersionException, 
UnsupportedRepositoryOperationException,
+            InvalidItemStateException, LockException, RepositoryException {
+
+        doVersions("/content/apache/sling-apache-org/index", 100);
+
+        final VersionCleanup vcs = new VersionCleanup(globalConfig, 
context.getService(ResourceResolverFactory.class));
+
+        vcs.start();
+        assertTrue(vcs.isRunning());
+        vcs.stop();
+        while (vcs.isRunning()) {
+            TimeUnit.SECONDS.sleep(2);
+        }
+        assertFalse(vcs.isRunning());
+        assertNull(vcs.getLastMessage());
+    }
+
+    @Test(timeout = 5000)
+    public void testReRun() throws InterruptedException, VersionException, 
UnsupportedRepositoryOperationException,
+            InvalidItemStateException, LockException, RepositoryException {
+
+        doVersions("/content/apache/sling-apache-org/index", 100);
+
+        final VersionCleanup vcs = Mockito
+                .spy(new VersionCleanup(globalConfig, 
context.getService(ResourceResolverFactory.class)));
+
+        vcs.start();
+        vcs.start();
+
+        Mockito.verify(vcs, atLeast(2)).isRunning();
+        while (vcs.isRunning()) {
+            TimeUnit.SECONDS.sleep(2);
+        }
+        assertFalse(vcs.isRunning());
+        assertNull(vcs.getLastMessage());
+
+    }
+
+    @Test(timeout = 5000)
+    public void testMissingServiceUser()
+            throws InterruptedException, VersionException, 
UnsupportedRepositoryOperationException,
+            InvalidItemStateException, LockException, RepositoryException, 
LoginException {
+
+        ResourceResolverFactory factory = 
Mockito.mock(ResourceResolverFactory.class);
+        Mockito.when(factory.getServiceResourceResolver(Mockito.anyMap()))
+                .thenThrow(new LoginException("No service user"));
+        final VersionCleanup vcs = Mockito.spy(new 
VersionCleanup(globalConfig, factory));
+
+        vcs.start();
+
+        while (vcs.isRunning()) {
+            TimeUnit.SECONDS.sleep(2);
+        }
+        assertFalse(vcs.isRunning());
+        assertTrue(vcs.isFailed());
+        assertNotNull(vcs.getLastMessage());
+    }
+
+    @Test(timeout = 5000)
+    public void testDeleted() throws InterruptedException, VersionException, 
UnsupportedRepositoryOperationException,
+            InvalidItemStateException, LockException, RepositoryException, 
LoginException, PersistenceException {
+        doVersions("/content/apache/sling-apache-org/index", 10);
+        doVersions("/content/apache/sling-apache-org/test2", 3);
+        
context.resourceResolver().delete(context.resourceResolver().getResource("/content/apache/sling-apache-org/test2"));
+        context.resourceResolver().commit();
+
+        final VersionCleanup vcs = new VersionCleanup(globalConfig, 
context.getService(ResourceResolverFactory.class));
+
+        vcs.start();
+        while (vcs.isRunning()) {
+            TimeUnit.SECONDS.sleep(2);
+        }
+
+        assertFalse(vcs.isFailed());
+        assertFalse(vcs.isRunning());
+        assertNull(vcs.getLastMessage());
+        assertEquals(7L, vcs.getLastCleanedVersionsCount());
+    }
+
+}
\ No newline at end of file
diff --git 
a/org.apache.sling.jcr.repositorymaintainance/src/test/resources/nodetypes.cnd 
b/org.apache.sling.jcr.repositorymaintainance/src/test/resources/nodetypes.cnd
new file mode 100644
index 0000000..43edb8f
--- /dev/null
+++ 
b/org.apache.sling.jcr.repositorymaintainance/src/test/resources/nodetypes.cnd
@@ -0,0 +1,89 @@
+//  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.
+//  
+//  You can find out more documentation on this topic 
+//  by following these links:
+//
+//    -  http://sling.apache.org/site/content-loading.html
+//    -  http://jackrabbit.apache.org/node-type-notation.html
+
+<'sling'='http://sling.apache.org/jcr/sling/1.0'>
+<'nt'='http://www.jcp.org/jcr/nt/1.0'>
+<'mix'='http://www.jcp.org/jcr/mix/1.0'>
+<'jcr'='http://www.jcp.org/jcr/1.0'>
+
+[mix:publishable] mixin
+- sling:published (boolean)
+- sling:lastPublication (date)
+- sling:lastPublicationBy (string)
+- sling:lastPublicationType (string)
+
+[sling:Component] > nt:unstructured
+    - componentType (string)
+    - jcr:title (string)
+    
+[sling:Config] > nt:hierarchyNode, mix:lastModified, mix:publishable
+    orderable
+    - sling:resourceType (string)
+    - jcr:title (string)
+    + * (nt:unstructured) = nt:unstructured version
+    - * (UNDEFINED) multiple
+    - * (UNDEFINED)
+    
+[sling:File] > nt:file, mix:publishable
+     - * (undefined) copy
+     + jcr:content (sling:FileContent) = sling:FileContent copy primary 
autocreated
+    
+[sling:FileContent] > nt:resource, mix:publishable
+    - * (undefined) copy
+    - * (undefined) copy multiple
+    + metadata (nt:unstructured) = nt:unstructured copy primary
+    + renditions (nt:unstructured) = nt:unstructured copy primary
+
+[sling:Page] > nt:hierarchyNode, mix:lastModified
+    orderable
+    + jcr:content (nt:unstructured) = nt:unstructured copy primary
+    + * (nt:base) = nt:base version
+
+[sling:Site] > nt:hierarchyNode, mix:lastModified
+    orderable
+    - sling:configRef (string)
+    - sling:url (string)
+    - jcr:language (string)
+    - jcr:title (string)
+    - jcr:description (string)
+    + * (nt:base) = nt:base version
+
+[sling:Taxonomy] > nt:hierarchyNode, mix:lastModified
+    orderable
+    - sling:related (string)
+    - jcr:title (string)
+    + * (sling:Taxonomy) = sling:Taxonomy version
+
+[sling:UGC] > nt:unstructured
+    - approveaction (string)
+    - contenttype (string)
+    - finalpath (string)
+    - preview (string)
+    - published (boolean)
+    - referrer (string)
+    - user (string)
+    - useragent (string)
+    - userip (string)
+    - * (UNDEFINED) multiple
+    - * (UNDEFINED)
+    
\ No newline at end of file
diff --git 
a/org.apache.sling.jcr.repositorymaintainance/src/test/resources/version-content.json
 
b/org.apache.sling.jcr.repositorymaintainance/src/test/resources/version-content.json
new file mode 100644
index 0000000..f52ba84
--- /dev/null
+++ 
b/org.apache.sling.jcr.repositorymaintainance/src/test/resources/version-content.json
@@ -0,0 +1,44 @@
+{
+    "jcr:primaryType": "sling:Site",
+    "jcr:title": "Apache Sling",
+    "jcr:language": "en",
+    "sling:url": "https://sling.apache.org";,
+    "index": {
+        "jcr:primaryType": "sling:Page",
+        "jcr:content": {
+            "jcr:primaryType": "nt:unstructured",
+            "jcr:title": "Apache Sling - Bringing Back the Fun!",
+            "sling:template": "/conf/global/site/templates/base-page",
+            "sling:taxonomy": "/etc/taxonomy/reference/community",
+            "sling:resourceType": "reference/components/pages/base",
+            "published": true,
+            "hideInSitemap": false,
+            "container": {
+                "jcr:primaryType": "nt:unstructured",
+                "richtext": {
+                    "jcr:primaryType": "nt:unstructured",
+                    "text": "<p>Apache Sling™ is a framework for RESTful 
web-applications based on an extensible content tree.</p>\r\n<p>In a nutshell, 
Sling maps HTTP request URLs to content resources based on the request's path, 
extension and selectors. Using convention over configuration, requests are 
processed by scripts and servlets, dynamically selected based on the current 
resource. This fosters meaningful URLs and resource driven request processing, 
while the modular nature of Sl [...]
+                    "sling:resourceType": 
"sling-cms/components/general/richtext"
+                }
+            },
+            "menu": {
+                "jcr:primaryType": "nt:unstructured",
+                "richtext": {
+                    "jcr:primaryType": "nt:unstructured",
+                    "text": "<p>\r\n                <strong><a 
href=\"#\">Documentation</a></strong><br>\r\n                <a 
href=\"#\">Getting Started</a><br>\r\n                <a href=\"#\">The Sling 
Engine</a><br>\r\n                <a href=\"#\">Development</a><br>\r\n         
       <a href=\"#\">Bundles</a><br>\r\n                <a href=\"#\">Tutorials 
&amp; How-Tos</a><br>\r\n                <a 
href=\"http://sling.apache.org/components/\";>Maven Plugins</a><br>\r\n          
    [...]
+                    "sling:resourceType": 
"sling-cms/components/general/richtext"
+                }
+            }
+        }
+    },
+    "test2": {
+        "jcr:primaryType": "sling:Page",
+        "jcr:content": {
+            "jcr:primaryType": "nt:unstructured",
+            "jcr:title": "Test 2",
+            "sling:template": "/conf/global/site/templates/base-page",
+            "sling:resourceType": "reference/components/pages/base",
+            "published": false
+        }
+    }
+}
\ No newline at end of file

Reply via email to