This is an automated email from the ASF dual-hosted git repository. pauls pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-jcr-packageinit.git
commit 001844045b171596647acba31b76bf01592ac3b3 Author: Dominik Suess <[email protected]> AuthorDate: Thu Sep 6 12:07:29 2018 +0200 SLING-7871 - Proposal for RepositoryInitializer installing precalculated execution plans from FSRegistry into repository --- packageinit/pom.xml | 143 ++++++++++++++++ .../impl/ExecutionPlanRepoInitializer.java | 183 ++++++++++++++++++++ .../ExecutionPlanRepoInitializerTest.java | 184 +++++++++++++++++++++ 3 files changed, 510 insertions(+) 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(); + } + +}
