You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by dk...@apache.org on 2021/01/19 16:17:45 UTC

[sling-org-apache-sling-jcr-maintenance] 01/01: SLING-9985: Migrating JCR Maintenance code from whiteboard

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-org-apache-sling-jcr-maintenance.git

commit 82bb34d76a7dfa6d65e5d3546eb3c83b00cf1be1
Author: Dan Klco <da...@perficient.com>
AuthorDate: Tue Jan 19 11:16:26 2021 -0500

    SLING-9985: Migrating JCR Maintenance code from whiteboard
---
 .gitignore                                         |  26 +++
 CODE_OF_CONDUCT.md                                 |  22 ++
 CONTRIBUTING.md                                    |  30 +++
 Jenkinsfile                                        |  20 ++
 NOTICE                                             |   5 +
 README.md                                          |  30 +++
 pom.xml                                            | 221 +++++++++++++++++++
 src/main/features/base.json                        |  22 ++
 src/main/features/configuration.json               |  17 ++
 .../jcr/maintenance/DataStoreCleanupConfig.java    |  31 +++
 .../jcr/maintenance/RepositoryManagementUtil.java  |  53 +++++
 .../jcr/maintenance/RevisionCleanupConfig.java     |  31 +++
 .../apache/sling/jcr/maintenance/RunnableJob.java  |  24 +++
 .../jcr/maintenance/VersionCleanupConfig.java      |  28 +++
 .../jcr/maintenance/VersionCleanupPathConfig.java  |  37 ++++
 .../internal/DataStoreCleanupScheduler.java        |  68 ++++++
 .../internal/RepositoryMaintenanceHealthCheck.java | 104 +++++++++
 .../internal/RevisionCleanupScheduler.java         |  67 ++++++
 .../jcr/maintenance/internal/VersionCleanup.java   | 238 +++++++++++++++++++++
 .../maintenance/internal/VersionCleanupMBean.java  |  47 ++++
 .../maintenance/internal/VersionCleanupPath.java   |  91 ++++++++
 src/main/resources/OSGI-INF/l10n/bundle.properties |  56 +++++
 .../sling/jcr/maintenance/CompositeDataMock.java   |  55 +++++
 .../internal/DataStoreCleanupSchedulerTest.java    |  91 ++++++++
 .../RepositoryMaintenanceHealthCheckTest.java      | 168 +++++++++++++++
 .../internal/RevisionCleanupSchedulerTest.java     |  91 ++++++++
 .../internal/VersionCleanupPathTest.java           |  94 ++++++++
 .../maintenance/internal/VersionCleanupTest.java   | 216 +++++++++++++++++++
 src/test/resources/nodetypes.cnd                   |  89 ++++++++
 src/test/resources/version-content.json            |  44 ++++
 30 files changed, 2116 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..230229a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,26 @@
+*.jar
+.metadata
+RemoteSystemsTempFiles
+target
+.grunt
+.idea
+.classpath
+.project
+.settings
+bin
+.vlt
+.DS_Store
+.vlt-sync-config.properties
+.vlt-sync.log
+.brackets.json
+.metadata/
+.vagrant/
+*.iml
+node_modules/
+node/
+.vscode/
+pom.xml.releaseBackup
+pom.xml.tag
+release.properties
+pom.xml.next
+.java-version
\ No newline at end of file
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..0fa18e5
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,22 @@
+<!--/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+  ~ 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.
+  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/-->
+Apache Software Foundation Code of Conduct
+====
+
+Being an Apache project, Apache Sling adheres to the Apache Software Foundation's [Code of Conduct](https://www.apache.org/foundation/policies/conduct.html).
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..880019b
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -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.
+  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/-->
+Contributing
+====
+
+Thanks for choosing to contribute!
+
+Here's some great places to get started:
+
+ - [Backlog](https://issues.apache.org/jira/issues/?jql=project%20%3D%20SLING%20AND%20status%20%3D%20Open%20AND%20component%20in%20(%22App%20CMS%22%2C%20%22App%20CMS%20Reference%22))
+ - [Good Starting Issues](https://issues.apache.org/jira/browse/SLING-8910?jql=project%20%3D%20SLING%20AND%20status%20%3D%20Open%20AND%20component%20in%20(%22App%20CMS%22%2C%20%22App%20CMS%20Reference%22)%20AND%20labels%20%3D%20newbie)
+
+
+You will find all the necessary details about how you can do this at https://sling.apache.org/contributing.html.
diff --git a/Jenkinsfile b/Jenkinsfile
new file mode 100644
index 0000000..f582519
--- /dev/null
+++ b/Jenkinsfile
@@ -0,0 +1,20 @@
+/**
+ * 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.
+ */
+
+slingOsgiBundleBuild()
diff --git a/NOTICE b/NOTICE
new file mode 100644
index 0000000..5de96ff
--- /dev/null
+++ b/NOTICE
@@ -0,0 +1,5 @@
+Apache Sling JCR Maintenance
+Copyright 2021 The Apache Software Foundation
+
+This product includes software developed at
+The Apache Software Foundation (http://www.apache.org/).
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..6615962
--- /dev/null
+++ b/README.md
@@ -0,0 +1,30 @@
+[![Apache Sling](https://sling.apache.org/res/logos/sling.png)](https://sling.apache.org)
+
+&#32;[![Build Status](https://ci-builds.apache.org/job/Sling/job/modules/job/sling-org-apache-sling-jcr-maintenance/job/master/badge/icon)](https://ci-builds.apache.org/job/Sling/job/modules/job/sling-org-apache-sling-jcr-maintenance/job/master/)&#32;[![Test Status](https://img.shields.io/jenkins/tests.svg?jobUrl=https://ci-builds.apache.org/job/Sling/job/modules/job/sling-org-apache-sling-jcr-maintenance/job/master/)](https://ci-builds.apache.org/job/Sling/job/modules/job/sling-org-apac [...]
+
+
+# Apache Sling JCR Maintenance
+
+This project provides reference implementation of Maintenance jobs for maintaining a Apache Jackrabbit OAK repository in Apache Sling.
+
+This includes the following Maintenance jobs:
+
+- [DataStoreCleanupScheduler](src/main/java/org/apache/sling/maintenance/internal/DataStoreCleanupScheduler.java) - Run the [RepositoryManagementMBean.startDataStoreGC(true)](https://jackrabbit.apache.org/oak/docs/apidocs/org/apache/jackrabbit/oak/api/jmx/RepositoryManagementMBean.html#startDataStoreGC-boolean-) method to perform a Garbage Collection of the Data Store
+- [RevisionCleanupScheduler](src/main/java/org/apache/sling/maintenance/internal/RevisionCleanupScheduler.java) - Run the [RepositoryManagementMBean.startRevisionGC()](https://jackrabbit.apache.org/oak/docs/apidocs/org/apache/jackrabbit/oak/api/jmx/RepositoryManagementMBean.html#startRevisionGC--) method to perform a Garbage Collection of the Revision Store
+- [VersionCleanup](src/main/java/org/apache/sling/maintenance/internal/VersionCleanup.java) - Job to traverse the JCR Version Store
+  and remove versions (oldest-first) exceeding a configurable limit
+
+As well as a [Health Check](src/main/java/org/apache/sling/maintenance/internal/RepositoryMaintenanceHealthCheck.java) to ensure the jobs are scheduled and have not failed.
+
+## Configuration
+
+To see a reference implementation, see the [Configuration Feature](src/main/features/configuration.json).
+
+## Features
+
+There are two primary features made by this project include:
+
+- **Base** - org.apache.sling:org.apache.sling.jcr.maintenance:slingosgifeature:base:${project.version} - only the bundle and service user
+- **Default** - org.apache.sling:org.apache.sling.jcr.maintenance:slingosgifeature:default:${project.version} - the bundle, service user and default configuration which keeps 5 versions and runs the jobs every night
+
+This module is part of the [Apache Sling](https://sling.apache.org) project.
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..15a2dfd
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,221 @@
+<?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.maintenance</artifactId>
+    <version>1.0.0-SNAPSHOT</version>
+    <name>Apache Sling JCR Maintenance</name>
+    <description>Maintenance jobs for the JCR</description>
+
+    <properties>
+        <bnd.baseline.fail.on.missing>false</bnd.baseline.fail.on.missing>
+    </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>
+            </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>
+            <plugin>
+                <groupId>org.apache.sling</groupId>
+                <artifactId>slingfeature-maven-plugin</artifactId>
+                <version>1.4.18</version>
+                <extensions>true</extensions>
+                <configuration>
+                    <framework>
+                        <groupId>org.apache.felix</groupId>
+                        <artifactId>org.apache.felix.framework</artifactId>
+                        <version>6.0.3</version>
+                    </framework>
+                    <aggregates>
+                        <aggregate>
+                            <classifier>default</classifier>
+                            <filesInclude>**/*.json</filesInclude>
+                            <title>Apache Sling JCR Maintenance - Default</title>
+                        </aggregate>
+                    </aggregates>
+                </configuration>
+                <executions>
+                    <execution>
+                        <id>attach-features</id>
+                        <phase>prepare-package</phase>
+                        <goals>
+                            <goal>aggregate-features</goal>
+                            <goal>attach-features</goal>
+                        </goals>
+                        <configuration>
+                            <replacePropertyVariables>project.version</replacePropertyVariables>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>javax.annotation</groupId>
+            <artifactId>javax.annotation-api</artifactId>
+            <version>1.3.2</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>javax.jcr</groupId>
+            <artifactId>jcr</artifactId>
+            <version>2.0</version>
+            <scope>provided</scope>
+        </dependency>
+
+        <!-- Apache Dependencies -->
+        <dependency>
+            <groupId>org.apache.felix</groupId>
+            <artifactId>org.apache.felix.healthcheck.api</artifactId>
+            <version>2.0.0</version>
+            <scope>provided</scope>
+        </dependency>
+        <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.apache.jackrabbit</groupId>
+            <artifactId>oak-core</artifactId>
+            <version>1.8.8</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.jackrabbit</groupId>
+            <artifactId>oak-jcr</artifactId>
+            <version>1.8.8</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.api</artifactId>
+            <version>2.18.4</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.jetbrains</groupId>
+            <artifactId>annotations</artifactId>
+            <version>20.1.0</version>
+            <scope>provided</scope>
+        </dependency>
+
+
+        <!-- OSGi Dependencies -->
+        <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>
+
+        <!-- Test dependencies -->
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <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>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.mockito</groupId>
+            <artifactId>mockito-core</artifactId>
+            <version>3.6.28</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-simple</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/src/main/features/base.json b/src/main/features/base.json
new file mode 100644
index 0000000..e4830f1
--- /dev/null
+++ b/src/main/features/base.json
@@ -0,0 +1,22 @@
+{
+    "bundles": [
+        {
+            "id": "org.apache.sling:org.apache.sling.jcr.maintenance:${project.version}",
+            "start-order": "20"
+        }
+    ],
+    "configurations":{
+        "org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended~sling-versionmgr":{
+            "user.mapping":[
+                "org.apache.sling.jcr.maintenance:sling-versionmgr=sling-versionmgr"
+            ]
+        }
+    },
+    "repoinit:TEXT|true": [
+        "create service user sling-versionmgr",
+        "set ACL for sling-versionmgr",
+        "allow   jcr:write,jcr:nodeTypeManagement,jcr:versionManagement    on /",
+        "allow   jcr:read    on /jcr:system/jcr:versionStorage",
+        "end"
+    ]
+}
\ No newline at end of file
diff --git a/src/main/features/configuration.json b/src/main/features/configuration.json
new file mode 100644
index 0000000..9fc1eef
--- /dev/null
+++ b/src/main/features/configuration.json
@@ -0,0 +1,17 @@
+{
+    "configurations": {
+        "org.apache.sling.jcr.maintenance.internal.DataStoreCleanupScheduler": {
+            "scheduler.expression": "0 0 2 ? * *"
+        },
+        "org.apache.sling.jcr.maintenance.internal.RevisionCleanupScheduler": {
+            "scheduler.expression": "0 0 2 ? * *"
+        },
+        "org.apache.sling.jcr.maintenance.internal.VersionCleanup": {
+            "scheduler.expression": "0 0 2 ? * *"
+        },
+        "org.apache.sling.jcr.maintenance.internal.VersionCleanupPath~default": {
+            "path": "/",
+            "limit": 5
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/jcr/maintenance/DataStoreCleanupConfig.java b/src/main/java/org/apache/sling/jcr/maintenance/DataStoreCleanupConfig.java
new file mode 100644
index 0000000..3374465
--- /dev/null
+++ b/src/main/java/org/apache/sling/jcr/maintenance/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.jcr.maintenance;
+
+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/src/main/java/org/apache/sling/jcr/maintenance/RepositoryManagementUtil.java b/src/main/java/org/apache/sling/jcr/maintenance/RepositoryManagementUtil.java
new file mode 100644
index 0000000..e6548c2
--- /dev/null
+++ b/src/main/java/org/apache/sling/jcr/maintenance/RepositoryManagementUtil.java
@@ -0,0 +1,53 @@
+/*
+ * 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.jcr.maintenance;
+
+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) {
+        return StatusCode.RUNNING == getStatusCode(status);
+    }
+
+    public static boolean isValid(CompositeData status) {
+        StatusCode statusCode = getStatusCode(status);
+        return statusCode != StatusCode.UNAVAILABLE && statusCode != StatusCode.FAILED;
+    }
+
+    public static StatusCode getStatusCode(CompositeData status) {
+        int c = ((Integer) status.get("code"));
+        return Arrays.stream(StatusCode.values()).filter(sc -> sc.ordinal() == c).findFirst().orElse(StatusCode.NONE);
+    }
+
+    public static String getMessage(CompositeData status) {
+        return ((String) status.get("message"));
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/jcr/maintenance/RevisionCleanupConfig.java b/src/main/java/org/apache/sling/jcr/maintenance/RevisionCleanupConfig.java
new file mode 100644
index 0000000..4fe54fe
--- /dev/null
+++ b/src/main/java/org/apache/sling/jcr/maintenance/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.jcr.maintenance;
+
+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/src/main/java/org/apache/sling/jcr/maintenance/RunnableJob.java b/src/main/java/org/apache/sling/jcr/maintenance/RunnableJob.java
new file mode 100644
index 0000000..137ad6e
--- /dev/null
+++ b/src/main/java/org/apache/sling/jcr/maintenance/RunnableJob.java
@@ -0,0 +1,24 @@
+/*
+ * 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.jcr.maintenance;
+
+/**
+ * Interface to add a method to get the scheduler expression for a runnable job
+ */
+public interface RunnableJob extends Runnable {
+    String getSchedulerExpression();
+}
diff --git a/src/main/java/org/apache/sling/jcr/maintenance/VersionCleanupConfig.java b/src/main/java/org/apache/sling/jcr/maintenance/VersionCleanupConfig.java
new file mode 100644
index 0000000..4a0c9ae
--- /dev/null
+++ b/src/main/java/org/apache/sling/jcr/maintenance/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.jcr.maintenance;
+
+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/src/main/java/org/apache/sling/jcr/maintenance/VersionCleanupPathConfig.java b/src/main/java/org/apache/sling/jcr/maintenance/VersionCleanupPathConfig.java
new file mode 100644
index 0000000..8f76745
--- /dev/null
+++ b/src/main/java/org/apache/sling/jcr/maintenance/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.jcr.maintenance;
+
+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/src/main/java/org/apache/sling/jcr/maintenance/internal/DataStoreCleanupScheduler.java b/src/main/java/org/apache/sling/jcr/maintenance/internal/DataStoreCleanupScheduler.java
new file mode 100644
index 0000000..ba59feb
--- /dev/null
+++ b/src/main/java/org/apache/sling/jcr/maintenance/internal/DataStoreCleanupScheduler.java
@@ -0,0 +1,68 @@
+/*
+ * 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.jcr.maintenance.internal;
+
+import org.apache.jackrabbit.oak.api.jmx.RepositoryManagementMBean;
+import org.apache.sling.jcr.maintenance.DataStoreCleanupConfig;
+import org.apache.sling.jcr.maintenance.RepositoryManagementUtil;
+import org.apache.sling.jcr.maintenance.RunnableJob;
+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;
+
+/**
+ * Service for running the Jackrabbit OAK Blob Store cleanup on a schedule.
+ */
+@Component(service = { Runnable.class }, property = {
+        "scheduler.concurrent:Boolean=false" }, configurationPolicy = ConfigurationPolicy.REQUIRE, immediate = true)
+@Designate(ocd = DataStoreCleanupConfig.class)
+public class DataStoreCleanupScheduler implements RunnableJob {
+
+    private static final Logger log = LoggerFactory.getLogger(DataStoreCleanupScheduler.class);
+
+    private final RepositoryManagementMBean repositoryManager;
+
+    private final String schedulerExpression;
+
+    @Activate
+    public DataStoreCleanupScheduler(final DataStoreCleanupConfig config,
+            @Reference final RepositoryManagementMBean repositoryManager) {
+        this.repositoryManager = repositoryManager;
+        this.schedulerExpression = config.scheduler_expression();
+    }
+
+    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!");
+        }
+    }
+
+    /**
+     * @return the schedulerExpression
+     */
+    public String getSchedulerExpression() {
+        return schedulerExpression;
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/jcr/maintenance/internal/RepositoryMaintenanceHealthCheck.java b/src/main/java/org/apache/sling/jcr/maintenance/internal/RepositoryMaintenanceHealthCheck.java
new file mode 100644
index 0000000..d6b1ad6
--- /dev/null
+++ b/src/main/java/org/apache/sling/jcr/maintenance/internal/RepositoryMaintenanceHealthCheck.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.jcr.maintenance.internal;
+
+import javax.management.openmbean.CompositeData;
+
+import org.apache.felix.hc.api.FormattingResultLog;
+import org.apache.felix.hc.api.HealthCheck;
+import org.apache.felix.hc.api.Result;
+import org.apache.jackrabbit.oak.api.jmx.RepositoryManagementMBean;
+import org.apache.sling.jcr.maintenance.RepositoryManagementUtil;
+import org.apache.sling.jcr.maintenance.RunnableJob;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.osgi.service.component.annotations.ReferencePolicyOption;
+
+@Component(service = HealthCheck.class, property = { HealthCheck.TAGS + "=oak", HealthCheck.TAGS + "=system-resource",
+        HealthCheck.NAME + "=Apache Sling JCR Maintenance" }, immediate = true)
+public class RepositoryMaintenanceHealthCheck implements HealthCheck {
+
+    private DataStoreCleanupScheduler dataStoreCleanupScheduler;
+
+    private RevisionCleanupScheduler revisionCleanupScheduler;
+
+    private RepositoryManagementMBean repositoryManagementMBean;
+
+    private VersionCleanupMBean versionCleanup;
+
+    @Reference(cardinality = ReferenceCardinality.OPTIONAL, policyOption = ReferencePolicyOption.GREEDY, service = Runnable.class, target = "(component.name=org.apache.sling.jcr.maintenance.internal.DataStoreCleanupScheduler)")
+    public void setDataStoreCleanupScheduler(Runnable dataStoreCleanupScheduler) {
+        this.dataStoreCleanupScheduler = (DataStoreCleanupScheduler) dataStoreCleanupScheduler;
+    }
+
+    @Reference
+    public void setRepositoryManagementMBean(RepositoryManagementMBean repositoryManagementMBean) {
+        this.repositoryManagementMBean = repositoryManagementMBean;
+    }
+
+    @Reference(cardinality = ReferenceCardinality.OPTIONAL, policyOption = ReferencePolicyOption.GREEDY, service = Runnable.class, target = "(component.name=org.apache.sling.jcr.maintenance.internal.RevisionCleanupScheduler)")
+    public void setRevisionCleanupScheduler(Runnable revisionCleanupScheduler) {
+        this.revisionCleanupScheduler = (RevisionCleanupScheduler) revisionCleanupScheduler;
+    }
+
+    @Reference(cardinality = ReferenceCardinality.OPTIONAL, policyOption = ReferencePolicyOption.GREEDY)
+    public void setVersionCleanup(VersionCleanupMBean versionCleanup) {
+        this.versionCleanup = versionCleanup;
+    }
+
+    private void evaluateJobStatus(FormattingResultLog log, String jobName, RunnableJob job, CompositeData status) {
+        if (job != null) {
+            log.debug("{} Schedule: {}", jobName, job.getSchedulerExpression());
+        } else {
+            log.warn("{} not registered", jobName);
+        }
+        if (RepositoryManagementUtil.isValid(status)) {
+            log.debug("{} Last Status: {}", jobName, RepositoryManagementUtil.getStatusCode(status).name());
+            log.debug("{} Last Message: {}", jobName, RepositoryManagementUtil.getMessage(status));
+        } else {
+            log.critical("{} Last Status: {}", jobName, RepositoryManagementUtil.getStatusCode(status).name());
+            log.critical("{} Last Message: {}", jobName, RepositoryManagementUtil.getMessage(status));
+        }
+    }
+
+    @Override
+    public Result execute() {
+        FormattingResultLog log = new FormattingResultLog();
+
+        evaluateJobStatus(log, "DataStoreCleanupScheduler", dataStoreCleanupScheduler,
+                repositoryManagementMBean.getDataStoreGCStatus());
+
+        evaluateJobStatus(log, "RevisionCleanupScheduler", revisionCleanupScheduler,
+                repositoryManagementMBean.getRevisionGCStatus());
+
+        if (versionCleanup != null) {
+            if (versionCleanup.isFailed()) {
+                log.critical("VersionCleanup Status: FAILED");
+                log.critical("VersionCleanup Message: {}", versionCleanup.getLastMessage());
+            } else {
+                log.debug("VersionCleanup Status: SUCCEEDED");
+            }
+            log.debug("VersionCleanup Last Cleaned: {}", versionCleanup.getLastCleanedVersionsCount());
+        } else {
+            log.warn("VersionCleanup not registered");
+        }
+
+        return new Result(log);
+    }
+
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/jcr/maintenance/internal/RevisionCleanupScheduler.java b/src/main/java/org/apache/sling/jcr/maintenance/internal/RevisionCleanupScheduler.java
new file mode 100644
index 0000000..0a81d0e
--- /dev/null
+++ b/src/main/java/org/apache/sling/jcr/maintenance/internal/RevisionCleanupScheduler.java
@@ -0,0 +1,67 @@
+/*
+ * 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.jcr.maintenance.internal;
+
+import org.apache.jackrabbit.oak.api.jmx.RepositoryManagementMBean;
+import org.apache.sling.jcr.maintenance.RepositoryManagementUtil;
+import org.apache.sling.jcr.maintenance.RevisionCleanupConfig;
+import org.apache.sling.jcr.maintenance.RunnableJob;
+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;
+
+/**
+ * Service for running the Jackrabbit OAK Segment Store cleanup on a schedule.
+ */
+@Component(service = { Runnable.class }, property = {
+        "scheduler.concurrent:Boolean=false" }, configurationPolicy = ConfigurationPolicy.REQUIRE, immediate = true)
+@Designate(ocd = RevisionCleanupConfig.class)
+public class RevisionCleanupScheduler implements RunnableJob {
+
+    private static final Logger log = LoggerFactory.getLogger(RevisionCleanupScheduler.class);
+
+    private final RepositoryManagementMBean repositoryManager;
+
+    private final String schedulerExpression;
+
+    @Activate
+    public RevisionCleanupScheduler(final RevisionCleanupConfig config,
+            @Reference final RepositoryManagementMBean repositoryManager) {
+        this.repositoryManager = repositoryManager;
+        this.schedulerExpression = config.scheduler_expression();
+    }
+
+    public void run() {
+        if (!RepositoryManagementUtil.isRunning(repositoryManager.getRevisionGCStatus())) {
+            log.info("Starting Revision Garbage Collection");
+            repositoryManager.startRevisionGC();
+        } else {
+            log.warn("Revision Garbage Collection already running!");
+        }
+    }
+
+    /**
+     * @return the schedulerExpression
+     */
+    public String getSchedulerExpression() {
+        return schedulerExpression;
+    }
+}
diff --git a/src/main/java/org/apache/sling/jcr/maintenance/internal/VersionCleanup.java b/src/main/java/org/apache/sling/jcr/maintenance/internal/VersionCleanup.java
new file mode 100644
index 0000000..31a7372
--- /dev/null
+++ b/src/main/java/org/apache/sling/jcr/maintenance/internal/VersionCleanup.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.jcr.maintenance.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.jcr.maintenance.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.component.annotations.ReferenceCardinality;
+import org.osgi.service.component.annotations.ReferencePolicyOption;
+import org.osgi.service.metatype.annotations.Designate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * 
+ */
+@Component(service = { VersionCleanupMBean.class, Runnable.class, DynamicMBean.class }, property = {
+        "jmx.objectname=org.apache.sling.jcr.maintenance:type=VersionCleanup",
+        "scheduler.concurrent:Boolean=false" }, 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(cardinality = ReferenceCardinality.AT_LEAST_ONE, policyOption = ReferencePolicyOption.GREEDY) final List<VersionCleanupPath> versionCleanupConfigs,
+            @Reference final ResourceResolverFactory factory) {
+        super(VersionCleanupMBean.class);
+        this.factory = factory;
+        this.versionCleanupConfigs = versionCleanupConfigs;
+        versionCleanupConfigs.sort((c1, c2) -> c1.getPath().compareTo(c2.getPath()) * -1);
+
+    }
+
+    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() {
+        log.info("Running version cleanup");
+        boolean interrupted = false;
+        boolean succeeded = false;
+        String failureMessage = null;
+        lastCleanedVersions = 0;
+        try {
+            try (final ResourceResolver adminResolver = factory.getServiceResourceResolver(
+                    Collections.singletonMap(ResourceResolverFactory.SUBSERVICE, "sling-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/src/main/java/org/apache/sling/jcr/maintenance/internal/VersionCleanupMBean.java b/src/main/java/org/apache/sling/jcr/maintenance/internal/VersionCleanupMBean.java
new file mode 100644
index 0000000..343bdf5
--- /dev/null
+++ b/src/main/java/org/apache/sling/jcr/maintenance/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.jcr.maintenance.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/src/main/java/org/apache/sling/jcr/maintenance/internal/VersionCleanupPath.java b/src/main/java/org/apache/sling/jcr/maintenance/internal/VersionCleanupPath.java
new file mode 100644
index 0000000..07bd3c3
--- /dev/null
+++ b/src/main/java/org/apache/sling/jcr/maintenance/internal/VersionCleanupPath.java
@@ -0,0 +1,91 @@
+/*
+ * 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.jcr.maintenance.internal;
+
+import java.util.List;
+
+import javax.jcr.RepositoryException;
+
+import org.apache.sling.jcr.maintenance.VersionCleanupPathConfig;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.metatype.annotations.Designate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Component(service = VersionCleanupPath.class, immediate = true)
+@Designate(ocd = VersionCleanupPathConfig.class, factory = true)
+public class VersionCleanupPath implements Comparable<VersionCleanupPath> {
+
+    private static final Logger log = LoggerFactory.getLogger(VersionCleanupPath.class);
+
+    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 {
+        log.trace("Evaluating configurations {} for path {}", versionCleanupConfigs, path);
+        return versionCleanupConfigs.stream().filter(c -> path.startsWith(c.getPath())).findFirst()
+                .orElseThrow(() -> new RepositoryException("Failed to find version cleanup configuration for " + path));
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see java.lang.Object#toString()
+     */
+
+    @Override
+    public String toString() {
+        return "VersionCleanupPath [keepVersions=" + keepVersions + ", limit=" + limit + ", path=" + path + "]";
+    }
+
+}
diff --git a/src/main/resources/OSGI-INF/l10n/bundle.properties b/src/main/resources/OSGI-INF/l10n/bundle.properties
new file mode 100644
index 0000000..3f8f0f3
--- /dev/null
+++ b/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 Maintenance 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 Maintenance 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 Maintenance Version Cleanup
+version.cleanup.description=A scheduler and service to cleanup JCR versions
+
+version.cleanup.path.name=Apache Sling JCR Maintenance 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/src/test/java/org/apache/sling/jcr/maintenance/CompositeDataMock.java b/src/test/java/org/apache/sling/jcr/maintenance/CompositeDataMock.java
new file mode 100644
index 0000000..abb7ca7
--- /dev/null
+++ b/src/test/java/org/apache/sling/jcr/maintenance/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.jcr.maintenance;
+
+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/src/test/java/org/apache/sling/jcr/maintenance/internal/DataStoreCleanupSchedulerTest.java b/src/test/java/org/apache/sling/jcr/maintenance/internal/DataStoreCleanupSchedulerTest.java
new file mode 100644
index 0000000..014ae38
--- /dev/null
+++ b/src/test/java/org/apache/sling/jcr/maintenance/internal/DataStoreCleanupSchedulerTest.java
@@ -0,0 +1,91 @@
+/*
+ * 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.jcr.maintenance.internal;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.never;
+
+import java.lang.annotation.Annotation;
+
+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.jcr.maintenance.CompositeDataMock;
+import org.apache.sling.jcr.maintenance.DataStoreCleanupConfig;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+public class DataStoreCleanupSchedulerTest {
+
+    @Test
+    public void testRunnable() {
+
+        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);
+        final DataStoreCleanupScheduler dscs = new DataStoreCleanupScheduler(Mockito.mock(DataStoreCleanupConfig.class),
+                repositoryManager);
+        dscs.run();
+
+        Mockito.verify(repositoryManager).startDataStoreGC(Mockito.anyBoolean());
+    }
+
+    @Test
+    public void testRunCheck() {
+
+        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);
+        final DataStoreCleanupScheduler dscs = new DataStoreCleanupScheduler(Mockito.mock(DataStoreCleanupConfig.class),
+                repositoryManager);
+
+        dscs.run();
+
+        Mockito.verify(repositoryManager, never()).startDataStoreGC(Mockito.anyBoolean());
+    }
+
+    @Test
+    public void testSheduledExpression() {
+        final String EXPECTED = "* * * * *";
+        final DataStoreCleanupScheduler dscs = new DataStoreCleanupScheduler(new DataStoreCleanupConfig() {
+
+            @Override
+            public Class<? extends Annotation> annotationType() {
+                // TODO Auto-generated method stub
+                return null;
+            }
+
+            @Override
+            public String scheduler_expression() {
+                return EXPECTED;
+            }
+
+        }, null);
+
+        assertEquals(EXPECTED, dscs.getSchedulerExpression());
+
+    }
+
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/jcr/maintenance/internal/RepositoryMaintenanceHealthCheckTest.java b/src/test/java/org/apache/sling/jcr/maintenance/internal/RepositoryMaintenanceHealthCheckTest.java
new file mode 100644
index 0000000..3315691
--- /dev/null
+++ b/src/test/java/org/apache/sling/jcr/maintenance/internal/RepositoryMaintenanceHealthCheckTest.java
@@ -0,0 +1,168 @@
+/*
+ * 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.jcr.maintenance.internal;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import javax.management.openmbean.CompositeData;
+
+import org.apache.felix.hc.api.Result;
+import org.apache.jackrabbit.oak.api.jmx.RepositoryManagementMBean;
+import org.apache.sling.jcr.maintenance.CompositeDataMock;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+public class RepositoryMaintenanceHealthCheckTest {
+
+    private CompositeData successCompositeData;
+    private CompositeData failedCompositeData;
+
+    @Before
+    public void init() {
+        successCompositeData = CompositeDataMock.init()
+                .put("code", RepositoryManagementMBean.StatusCode.SUCCEEDED.ordinal()).build();
+        failedCompositeData = CompositeDataMock.init()
+                .put("code", RepositoryManagementMBean.StatusCode.FAILED.ordinal()).build();
+    }
+
+    @Test
+    public void testNothingRegistered() {
+        RepositoryMaintenanceHealthCheck repositoryHealthCheck = new RepositoryMaintenanceHealthCheck();
+
+        RepositoryManagementMBean repositoryManagementMBean = Mockito.mock(RepositoryManagementMBean.class);
+        Mockito.when(repositoryManagementMBean.getRevisionGCStatus()).thenReturn(successCompositeData);
+        Mockito.when(repositoryManagementMBean.getDataStoreGCStatus()).thenReturn(successCompositeData);
+        repositoryHealthCheck.setRepositoryManagementMBean(repositoryManagementMBean);
+
+        Result result = repositoryHealthCheck.execute();
+        assertFalse(result.isOk());
+    }
+
+    @Test
+    public void testAllSuccessful() {
+        RepositoryMaintenanceHealthCheck repositoryHealthCheck = new RepositoryMaintenanceHealthCheck();
+
+        DataStoreCleanupScheduler dataStoreCleanupScheduler = Mockito.mock(DataStoreCleanupScheduler.class);
+        repositoryHealthCheck.setDataStoreCleanupScheduler(dataStoreCleanupScheduler);
+
+        RepositoryManagementMBean repositoryManagementMBean = Mockito.mock(RepositoryManagementMBean.class);
+        Mockito.when(repositoryManagementMBean.getRevisionGCStatus()).thenReturn(successCompositeData);
+        Mockito.when(repositoryManagementMBean.getDataStoreGCStatus()).thenReturn(successCompositeData);
+
+        repositoryHealthCheck.setRepositoryManagementMBean(repositoryManagementMBean);
+
+        RevisionCleanupScheduler revisionCleanupScheduler = Mockito.mock(RevisionCleanupScheduler.class);
+        repositoryHealthCheck.setRevisionCleanupScheduler(revisionCleanupScheduler);
+
+        VersionCleanupMBean versionCleanupMBean = Mockito.mock(VersionCleanupMBean.class);
+        Mockito.when(versionCleanupMBean.isFailed()).thenReturn(false);
+        repositoryHealthCheck.setVersionCleanup(versionCleanupMBean);
+
+        Result result = repositoryHealthCheck.execute();
+        assertTrue(result.isOk());
+    }
+
+    @Test
+    public void testDataStoreFailure() {
+        RepositoryMaintenanceHealthCheck repositoryHealthCheck = new RepositoryMaintenanceHealthCheck();
+
+        RepositoryManagementMBean repositoryManagementMBean = Mockito.mock(RepositoryManagementMBean.class);
+        Mockito.when(repositoryManagementMBean.getRevisionGCStatus()).thenReturn(successCompositeData);
+        Mockito.when(repositoryManagementMBean.getDataStoreGCStatus()).thenReturn(successCompositeData);
+
+        repositoryHealthCheck.setRepositoryManagementMBean(repositoryManagementMBean);
+
+        RevisionCleanupScheduler revisionCleanupScheduler = Mockito.mock(RevisionCleanupScheduler.class);
+        repositoryHealthCheck.setRevisionCleanupScheduler(revisionCleanupScheduler);
+
+        VersionCleanupMBean versionCleanupMBean = Mockito.mock(VersionCleanupMBean.class);
+        Mockito.when(versionCleanupMBean.isFailed()).thenReturn(false);
+        repositoryHealthCheck.setVersionCleanup(versionCleanupMBean);
+
+        Result result = repositoryHealthCheck.execute();
+        assertFalse(result.isOk());
+        assertEquals(Result.Status.WARN, result.getStatus());
+
+        DataStoreCleanupScheduler dataStoreCleanupScheduler = Mockito.mock(DataStoreCleanupScheduler.class);
+        repositoryHealthCheck.setDataStoreCleanupScheduler(dataStoreCleanupScheduler);
+        Mockito.when(repositoryManagementMBean.getDataStoreGCStatus()).thenReturn(failedCompositeData);
+        result = repositoryHealthCheck.execute();
+        assertFalse(result.isOk());
+        assertEquals(Result.Status.CRITICAL, result.getStatus());
+    }
+
+    @Test
+    public void testRevisionFailure() {
+        RepositoryMaintenanceHealthCheck repositoryHealthCheck = new RepositoryMaintenanceHealthCheck();
+
+        DataStoreCleanupScheduler dataStoreCleanupScheduler = Mockito.mock(DataStoreCleanupScheduler.class);
+        repositoryHealthCheck.setDataStoreCleanupScheduler(dataStoreCleanupScheduler);
+
+        RepositoryManagementMBean repositoryManagementMBean = Mockito.mock(RepositoryManagementMBean.class);
+        Mockito.when(repositoryManagementMBean.getRevisionGCStatus()).thenReturn(successCompositeData);
+        Mockito.when(repositoryManagementMBean.getDataStoreGCStatus()).thenReturn(successCompositeData);
+
+        repositoryHealthCheck.setRepositoryManagementMBean(repositoryManagementMBean);
+
+        VersionCleanupMBean versionCleanupMBean = Mockito.mock(VersionCleanupMBean.class);
+        Mockito.when(versionCleanupMBean.isFailed()).thenReturn(false);
+        repositoryHealthCheck.setVersionCleanup(versionCleanupMBean);
+
+        Result result = repositoryHealthCheck.execute();
+        assertFalse(result.isOk());
+        assertEquals(Result.Status.WARN, result.getStatus());
+
+        Mockito.when(repositoryManagementMBean.getRevisionGCStatus()).thenReturn(failedCompositeData);
+        RevisionCleanupScheduler revisionCleanupScheduler = Mockito.mock(RevisionCleanupScheduler.class);
+        repositoryHealthCheck.setRevisionCleanupScheduler(revisionCleanupScheduler);
+        result = repositoryHealthCheck.execute();
+        assertFalse(result.isOk());
+        assertEquals(Result.Status.CRITICAL, result.getStatus());
+    }
+
+    @Test
+    public void testVersionFailure() {
+        RepositoryMaintenanceHealthCheck repositoryHealthCheck = new RepositoryMaintenanceHealthCheck();
+
+        DataStoreCleanupScheduler dataStoreCleanupScheduler = Mockito.mock(DataStoreCleanupScheduler.class);
+        repositoryHealthCheck.setDataStoreCleanupScheduler(dataStoreCleanupScheduler);
+
+        RepositoryManagementMBean repositoryManagementMBean = Mockito.mock(RepositoryManagementMBean.class);
+        Mockito.when(repositoryManagementMBean.getRevisionGCStatus()).thenReturn(successCompositeData);
+        Mockito.when(repositoryManagementMBean.getDataStoreGCStatus()).thenReturn(successCompositeData);
+
+        repositoryHealthCheck.setRepositoryManagementMBean(repositoryManagementMBean);
+
+        RevisionCleanupScheduler revisionCleanupScheduler = Mockito.mock(RevisionCleanupScheduler.class);
+        repositoryHealthCheck.setRevisionCleanupScheduler(revisionCleanupScheduler);
+
+        Result result = repositoryHealthCheck.execute();
+        assertFalse(result.isOk());
+        assertEquals(Result.Status.WARN, result.getStatus());
+
+        VersionCleanupMBean versionCleanupMBean = Mockito.mock(VersionCleanupMBean.class);
+        Mockito.when(versionCleanupMBean.isFailed()).thenReturn(true);
+        repositoryHealthCheck.setVersionCleanup(versionCleanupMBean);
+        result = repositoryHealthCheck.execute();
+        assertFalse(result.isOk());
+        assertEquals(Result.Status.CRITICAL, result.getStatus());
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/jcr/maintenance/internal/RevisionCleanupSchedulerTest.java b/src/test/java/org/apache/sling/jcr/maintenance/internal/RevisionCleanupSchedulerTest.java
new file mode 100644
index 0000000..5797226
--- /dev/null
+++ b/src/test/java/org/apache/sling/jcr/maintenance/internal/RevisionCleanupSchedulerTest.java
@@ -0,0 +1,91 @@
+/*
+ * 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.jcr.maintenance.internal;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.never;
+
+import java.lang.annotation.Annotation;
+
+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.jcr.maintenance.CompositeDataMock;
+import org.apache.sling.jcr.maintenance.RevisionCleanupConfig;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+public class RevisionCleanupSchedulerTest {
+
+    @Test
+    public void testRunnable() {
+
+        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);
+        final RevisionCleanupScheduler rcs = new RevisionCleanupScheduler(Mockito.mock(RevisionCleanupConfig.class),
+                repositoryManager);
+
+        rcs.run();
+
+        Mockito.verify(repositoryManager).startRevisionGC();
+    }
+
+    @Test
+    public void testRunning() {
+
+        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);
+
+        final RevisionCleanupScheduler rcs = new RevisionCleanupScheduler(Mockito.mock(RevisionCleanupConfig.class),
+                repositoryManager);
+
+        rcs.run();
+
+        Mockito.verify(repositoryManager, never()).startRevisionGC();
+    }
+
+    @Test
+    public void testSheduledExpression() {
+        final String EXPECTED = "* * * * *";
+        final RevisionCleanupScheduler rcs = new RevisionCleanupScheduler(new RevisionCleanupConfig() {
+
+            @Override
+            public Class<? extends Annotation> annotationType() {
+                // TODO Auto-generated method stub
+                return null;
+            }
+
+            @Override
+            public String scheduler_expression() {
+                return EXPECTED;
+            }
+
+        }, null);
+
+        assertEquals(EXPECTED, rcs.getSchedulerExpression());
+
+    }
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/jcr/maintenance/internal/VersionCleanupPathTest.java b/src/test/java/org/apache/sling/jcr/maintenance/internal/VersionCleanupPathTest.java
new file mode 100644
index 0000000..f05d726
--- /dev/null
+++ b/src/test/java/org/apache/sling/jcr/maintenance/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.jcr.maintenance.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.jcr.maintenance.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/src/test/java/org/apache/sling/jcr/maintenance/internal/VersionCleanupTest.java b/src/test/java/org/apache/sling/jcr/maintenance/internal/VersionCleanupTest.java
new file mode 100644
index 0000000..07fa774
--- /dev/null
+++ b/src/test/java/org/apache/sling/jcr/maintenance/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.jcr.maintenance.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.jcr.maintenance.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/src/test/resources/nodetypes.cnd b/src/test/resources/nodetypes.cnd
new file mode 100644
index 0000000..43edb8f
--- /dev/null
+++ b/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/src/test/resources/version-content.json b/src/test/resources/version-content.json
new file mode 100644
index 0000000..f52ba84
--- /dev/null
+++ b/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