SLING-7871 - Proposal for RepositoryInitializer installing precalculated execution...
authorDominik Suess <suess@adobe.com>
Thu, 6 Sep 2018 10:07:29 +0000 (12:07 +0200)
committerKarl Pauls <kpauls@adobe.com>
Wed, 10 Oct 2018 10:27:31 +0000 (12:27 +0200)
packageinit/pom.xml [new file with mode: 0644]
packageinit/src/main/java/org/apache/sling/jcr/packageinit/impl/ExecutionPlanRepoInitializer.java [new file with mode: 0644]
packageinit/src/test/java/org/apache/sling/jcr/packageinit/ExecutionPlanRepoInitializerTest.java [new file with mode: 0644]

diff --git a/packageinit/pom.xml b/packageinit/pom.xml
new file mode 100644 (file)
index 0000000..92116dc
--- /dev/null
@@ -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 (file)
index 0000000..58633f7
--- /dev/null
@@ -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 (file)
index 0000000..5dd457a
--- /dev/null
@@ -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();
+    }
+
+}