SLING-7871 - Proposal for RepositoryInitializer installing precalculated execution plans from FSRegistry into repository
diff --git a/packageinit/pom.xml b/packageinit/pom.xml
new file mode 100644
index 0000000..92116dc
--- /dev/null
+++ b/packageinit/pom.xml
@@ -0,0 +1,143 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>sling</artifactId>
+ <version>34</version>
+ <relativePath />
+ </parent>
+
+ <artifactId>org.apache.sling.jcr.packageinit</artifactId>
+ <packaging>bundle</packaging>
+ <version>0.0.1-SNAPSHOT</version>
+ <name>Apache Sling JCR Package Initializer module</name>
+ <description>
+ Installs packages into a JCR repository as SlingRepositoryInitializer based on a FileVault ExecutionPlan
+ </description>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>maven-bundle-plugin</artifactId>
+ <extensions>true</extensions>
+ <executions>
+ <!-- Configure extra execution of 'manifest' in process-classes phase to make sure SCR metadata is generated before unit test runs -->
+ <execution>
+ <id>scr-metadata</id>
+ <goals>
+ <goal>manifest</goal>
+ </goals>
+ <configuration>
+ <supportIncrementalBuild>true</supportIncrementalBuild>
+ </configuration>
+ </execution>
+ </executions>
+ <configuration>
+ <!-- Export SCR metadata to classpath to have them available in unit tests -->
+ <exportScr>true</exportScr>
+ <instructions>
+ <!-- Enable processing of OSGI DS component annotations -->
+ <_dsannotations>*</_dsannotations>
+ <!-- Enable processing of OSGI metatype annotations -->
+ <_metatypeannotations>*</_metatypeannotations>
+ <Private-Package>
+ org.apache.sling.jcr.packageinit.impl
+ </Private-Package>
+ </instructions>
+ </configuration>
+ </plugin>
+
+ </plugins>
+ </build>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.osgi</groupId>
+ <artifactId>osgi.core</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.osgi</groupId>
+ <artifactId>osgi.cmpn</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.osgi</groupId>
+ <artifactId>org.osgi.service.component.annotations</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.osgi</groupId>
+ <artifactId>org.osgi.service.metatype.annotations</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>javax.jcr</groupId>
+ <artifactId>jcr</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>org.apache.sling.jcr.api</artifactId>
+ <version>2.4.0</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>org.apache.sling.launchpad.api</artifactId>
+ <version>1.1.0</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>org.apache.sling.installer.core</artifactId>
+ <version>3.5.0</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.jackrabbit.vault</groupId>
+ <artifactId>org.apache.jackrabbit.vault</artifactId>
+ <version>3.2.1-SNAPSHOT</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>org.apache.sling.testing.sling-mock.junit4</artifactId>
+ <version>2.3.2</version>
+ <scope>testing</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-core</artifactId>
+ <version>2.21.0</version>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+</project>
diff --git a/packageinit/src/main/java/org/apache/sling/jcr/packageinit/impl/ExecutionPlanRepoInitializer.java b/packageinit/src/main/java/org/apache/sling/jcr/packageinit/impl/ExecutionPlanRepoInitializer.java
new file mode 100644
index 0000000..58633f7
--- /dev/null
+++ b/packageinit/src/main/java/org/apache/sling/jcr/packageinit/impl/ExecutionPlanRepoInitializer.java
@@ -0,0 +1,183 @@
+/*
+ * 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.packageinit.impl;
+
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.jackrabbit.vault.packaging.registry.ExecutionPlan;
+import org.apache.jackrabbit.vault.packaging.registry.ExecutionPlanBuilder;
+import org.apache.jackrabbit.vault.packaging.registry.PackageRegistry;
+import org.apache.sling.jcr.api.SlingRepository;
+import org.apache.sling.jcr.api.SlingRepositoryInitializer;
+import org.osgi.framework.BundleContext;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.metatype.annotations.AttributeDefinition;
+import org.osgi.service.metatype.annotations.Designate;
+import org.osgi.service.metatype.annotations.ObjectClassDefinition;
+import org.osgi.util.tracker.ServiceTracker;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.jcr.Session;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+
+@Component(service = {SlingRepositoryInitializer.class},
+ property = {"service.ranking:Integer=200"})
+@Designate(ocd = ExecutionPlanRepoInitializer.Config.class)
+public class ExecutionPlanRepoInitializer implements SlingRepositoryInitializer {
+
+ private static final String EXECUTEDPLANS_FILE = "executedplans.file";
+ private List<String> executionPlans = new ArrayList<>();
+
+ private File statusFile;
+
+ @ObjectClassDefinition(
+ name = "Executionplan based Repository Initializer"
+ )
+ @interface Config {
+
+ @AttributeDefinition
+ String statusfilepath() default "";
+
+ @AttributeDefinition
+ String[] executionplans() default {};
+ }
+
+ /**
+ * The logger.
+ */
+ private final Logger logger = LoggerFactory.getLogger(getClass());
+ private BundleContext context;
+
+ @Activate
+ private void activate(BundleContext context, Config config) throws FileNotFoundException, IOException {
+ List<String> epCandidates = Arrays.asList(config.executionplans());
+ if (!epCandidates.isEmpty()) {
+ if(StringUtils.isEmpty(config.statusfilepath())) {
+ // if no path is configured lookup default file in bundledata
+ statusFile = context.getDataFile(EXECUTEDPLANS_FILE);
+ } else {
+ Path statusFilePath = Paths.get(config.statusfilepath());
+ if (statusFilePath.isAbsolute()) {
+ // only absolute references are considered for lookup of
+ // external statusfile
+ statusFile = statusFilePath.toFile();
+ } else {
+ throw new IllegalStateException("Only absolute paths supported");
+ }
+ }
+ if (statusFile.exists()) {
+ // in case statusFile already exists read all hashes
+ List<Integer> executedHashes = new ArrayList<>();
+ try (BufferedReader br = new BufferedReader(new FileReader(statusFile))) {
+ for (String line; (line = br.readLine()) != null;) {
+ executedHashes.add(Integer.parseInt(line));
+ }
+ }
+ processCandidates(epCandidates, executedHashes);
+ } else {
+ this.executionPlans.addAll(epCandidates);
+ }
+ }
+ this.context = context;
+ }
+
+ private void processCandidates(List<String> epCandidates, List<Integer> executedHashes) {
+ Iterator<String> candidateIt = epCandidates.iterator();
+ Iterator<Integer> executedHashesIt = executedHashes.iterator();
+ // iterate over candidates and crosscheck next found hash
+ while (candidateIt.hasNext()) {
+ String candidate = candidateIt.next();
+ if (!executedHashesIt.hasNext()) {
+ // if no further hashes are present add candidate
+ // (will iterate over rest and add rest)
+ executionPlans.add(candidate);
+ } else {
+ // if another hash was found check if it matches the
+ // next candidate
+ Integer executedHash = executedHashesIt.next();
+ if (isCandidateProcessed(candidate, executedHash)) {
+ // already processed so no need to add - check
+ // next plan
+ continue;
+ } else {
+ String msg = "Different content installed then configured - repository needs to be reset.";
+ logger.error(msg);
+ throw new IllegalStateException(msg);
+ }
+ }
+ }
+ }
+
+ private boolean isCandidateProcessed(String candidate, Integer executedHash) {
+ return executedHash.equals(Integer.valueOf(candidate.hashCode()));
+ }
+
+ @Override
+ public void processRepository(SlingRepository slingRepository) throws Exception {
+ if (executionPlans != null) {
+ ServiceTracker<PackageRegistry, ?> st = new ServiceTracker<>(context, PackageRegistry.class, null);
+ try {
+ st.open();
+ logger.info("Waiting for PackageRegistry.");
+ PackageRegistry registry = (PackageRegistry) st.waitForService(0);
+ logger.info("PackageRegistry found - starting execution of executionplan");
+
+ @SuppressWarnings("deprecation")
+ Session session = slingRepository.loginAdministrative(null);
+ ExecutionPlanBuilder builder = registry.createExecutionPlan();
+ BufferedWriter writer = null;
+ try {
+ writer = new BufferedWriter(new FileWriter(statusFile));
+ for (String plan : executionPlans) {
+ builder.load(new ByteArrayInputStream(plan.getBytes("UTF-8")));
+ builder.with(session);
+ ExecutionPlan xplan = builder.execute();
+ logger.info("executionplan executed with {} entries", xplan.getTasks().size());
+ // save hashes to file for crosscheck on subsequent startup to avoid double processing
+ writer.write(String.valueOf(plan.hashCode()));
+ writer.newLine();
+
+ }
+ } finally {
+ if (writer != null) {
+ writer.close();
+ }
+ }
+ } finally {
+ st.close();
+ }
+ } else {
+ logger.info("No executionplans configured skipping init.");
+ }
+ }
+}
diff --git a/packageinit/src/test/java/org/apache/sling/jcr/packageinit/ExecutionPlanRepoInitializerTest.java b/packageinit/src/test/java/org/apache/sling/jcr/packageinit/ExecutionPlanRepoInitializerTest.java
new file mode 100644
index 0000000..5dd457a
--- /dev/null
+++ b/packageinit/src/test/java/org/apache/sling/jcr/packageinit/ExecutionPlanRepoInitializerTest.java
@@ -0,0 +1,184 @@
+/*
+ * 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.packageinit;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.StringWriter;
+import java.util.Dictionary;
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.UUID;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.jackrabbit.vault.packaging.PackageException;
+import org.apache.jackrabbit.vault.packaging.registry.ExecutionPlan;
+import org.apache.jackrabbit.vault.packaging.registry.ExecutionPlanBuilder;
+import org.apache.jackrabbit.vault.packaging.registry.PackageRegistry;
+import org.apache.jackrabbit.vault.packaging.registry.impl.FSPackageRegistry;
+import org.apache.sling.jcr.api.SlingRepository;
+import org.apache.sling.jcr.packageinit.impl.ExecutionPlanRepoInitializer;
+import org.apache.sling.testing.mock.osgi.MockOsgi;
+import org.apache.sling.testing.mock.sling.junit.SlingContext;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.Spy;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class ExecutionPlanRepoInitializerTest {
+
+ static String EXECUTIONPLAN_1 =
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
+ "<executionPlan version=\"1.0\">\n" +
+ " <task cmd=\"extract\" packageId=\"my_packages:test_a:1.0\"/>\n" +
+ "</executionPlan>\n";
+
+ static String EXECUTIONPLAN_2 =
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
+ "<executionPlan version=\"1.0\">\n" +
+ " <task cmd=\"extract\" packageId=\"my_packages:test_b:1.0\"/>\n" +
+ "</executionPlan>\n";
+
+ static String[] EXECUTIONSPLANS = {EXECUTIONPLAN_1, EXECUTIONPLAN_2};
+
+ static String STATUSFILE_NAME = "executedplans.file";
+
+ @Rule
+ public final SlingContext context = new SlingContext();
+
+ @Rule
+ public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+
+ @Mock
+ SlingRepository slingRepo;
+
+ @Spy
+ PackageRegistry registry = new FSPackageRegistry();
+
+ @Mock
+ ExecutionPlanBuilder builder;
+
+ @Mock
+ ExecutionPlanBuilder builder2;
+
+
+ @Mock
+ ExecutionPlan xplan;
+
+ private File statusFile;
+
+
+ @Before
+ public void setup() throws IOException, PackageException {
+ when(registry.createExecutionPlan()).thenReturn(builder);
+ when(builder.execute()).thenReturn(xplan);
+ when(builder2.execute()).thenReturn(xplan);
+ this.statusFile = temporaryFolder.newFile(STATUSFILE_NAME + UUID.randomUUID());
+ }
+
+ @Test
+ public void waitForRegistryAndInstall() throws Exception {
+ ExecutionPlanRepoInitializer initializer = registerRepoInitializer();
+
+ CountDownLatch cdl = new CountDownLatch(1);
+ processRepository(initializer, cdl);
+
+ assertTrue("processRespository() should not be completed before FSRegistry is available", cdl.getCount() > 0);
+ ArgumentCaptor<InputStream> captor = ArgumentCaptor.forClass(InputStream.class);
+
+ context.bundleContext().registerService(PackageRegistry.class.getName(), registry, null);
+ cdl.await(500, TimeUnit.MILLISECONDS);
+ verify(builder, times(2)).load(captor.capture());
+
+ Iterator<InputStream> isIt = captor.getAllValues().iterator();
+ for (String ep : EXECUTIONSPLANS) {
+ StringWriter writer = new StringWriter();
+ IOUtils.copy(isIt.next(), writer, "UTF-8");
+ assertEquals(writer.toString(), ep);
+ }
+ }
+
+ @Test
+ public void doubleExecute() throws Exception {
+ ExecutionPlanRepoInitializer initializer = registerRepoInitializer();
+
+ CountDownLatch cdl = new CountDownLatch(1);
+ processRepository(initializer, cdl);
+
+ assertTrue("processRespository() should not be completed before FSRegistry is available", cdl.getCount() > 0);
+ ArgumentCaptor<InputStream> captor = ArgumentCaptor.forClass(InputStream.class);
+
+ context.bundleContext().registerService(PackageRegistry.class.getName(), registry, null);
+ cdl.await(500, TimeUnit.MILLISECONDS);
+ verify(builder, times(2)).load(captor.capture());
+
+ // use different builder to reset captor
+ when(registry.createExecutionPlan()).thenReturn(builder2);
+
+ MockOsgi.deactivate(initializer, context.bundleContext());
+ initializer = registerRepoInitializer();
+ processRepository(initializer, cdl);;
+
+ cdl.await(500, TimeUnit.MILLISECONDS);
+ verify(builder2, never()).load(captor.capture());
+
+ }
+
+ private ExecutionPlanRepoInitializer registerRepoInitializer() {
+ ExecutionPlanRepoInitializer initializer = new ExecutionPlanRepoInitializer();
+ Dictionary<String, Object> props = new Hashtable<String, Object>();
+ props.put("executionplans", EXECUTIONSPLANS);
+ props.put("statusfilepath", statusFile.getAbsolutePath());
+ context.registerInjectActivateService(initializer, props);
+ return initializer;
+ }
+
+
+ private void processRepository(ExecutionPlanRepoInitializer initializer, CountDownLatch cdl) {
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ initializer.processRepository(slingRepo);
+ cdl.countDown();
+ } catch (Exception e) {
+ fail("Should not have thrown any exception");
+ }
+
+ }
+ }).start();
+ }
+
+}