You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by ro...@apache.org on 2022/03/15 09:51:48 UTC

[sling-org-apache-sling-feature-cpconverter] 03/04: SLING-11134 - Extract Oak index definitions and package them as an additional file

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

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

commit 4439a9f0e455df61c67852ce3173264ea4517825
Author: Robert Munteanu <ro...@apache.org>
AuthorDate: Tue Mar 1 14:33:57 2022 +0200

    SLING-11134 - Extract Oak index definitions and package them as an additional file
    
    - add an IndexManager that coordinates the various components working on index definitions
      parsing and storage
    - add an IndexDefinitionsEntryHandler that is able to parse Oak index definitions and store them
      for later use in the IndexManager
    - add add IndexDefinitionsJsonWriter that ouputs the index definitions in a format known to the
      oak-run tool
    - wire the IndexManager to the ContentPackage2FeatureModelConverter and store the discovered index
      definitions in a feature model extension
---
 pom.xml                                            |   4 +-
 .../ContentPackage2FeatureModelConverter.java      |  16 +-
 ...ntentPackage2FeatureModelConverterLauncher.java |   2 +
 .../features/DefaultFeaturesManager.java           |  24 ++-
 .../cpconverter/features/FeaturesManager.java      |   2 +
 .../handlers/IndexDefinitionsEntryHandler.java     | 128 ++++++++++++
 .../cpconverter/index/DefaultIndexManager.java     |  52 +++++
 .../cpconverter/index/IndexDefinitions.java        | 163 +++++++++++++++
 .../index/IndexDefinitionsJsonWriter.java          | 154 ++++++++++++++
 .../feature/cpconverter/index/IndexManager.java    |  60 ++++++
 .../cpconverter/vltpkg/JcrNamespaceRegistry.java   |  35 ++--
 ...sling.feature.cpconverter.handlers.EntryHandler |   1 +
 .../feature/cpconverter/AdjustedFilterTest.java    |   4 +-
 .../ContentPackage2FeatureModelConverterTest.java  |   4 +-
 .../ConverterUserAndPermissionTest.java            |   2 +
 .../handlers/IndexDefinitionsEntryHandlerTest.java | 223 +++++++++++++++++++++
 .../index/IndexDefinitionsJsonWriterTest.java      | 190 ++++++++++++++++++
 .../index_multiple_files/META-INF/vault/filter.xml |  21 ++
 .../META-INF/vault/nodetypes.cnd                   |  25 +++
 .../META-INF/vault/properties.xml                  |  34 ++++
 .../jcr_root/_oak_index/baz/.content.xml           |  24 +++
 .../jcr_root/_oak_index/lucene_custom/.content.xml |  64 ++++++
 .../index_nested_tika/META-INF/vault/filter.xml    |  20 ++
 .../index_nested_tika/META-INF/vault/nodetypes.cnd |  24 +++
 .../META-INF/vault/properties.xml                  |  34 ++++
 .../index/index_nested_tika/jcr_root/.content.xml  |  21 ++
 .../jcr_root/_oak_index/.content.xml               |  89 ++++++++
 .../_oak_index/lucene-custom/tika/config.xml       |  26 +++
 .../lucene-custom/tika/config.xml.dir/.content.xml |  24 +++
 .../index_single_file/META-INF/vault/filter.xml    |  20 ++
 .../index_single_file/META-INF/vault/nodetypes.cnd |  25 +++
 .../META-INF/vault/properties.xml                  |  34 ++++
 .../index/index_single_file/jcr_root/.content.xml  |  32 +++
 .../jcr_root/_oak_index/.content.xml               |  35 ++++
 34 files changed, 1590 insertions(+), 26 deletions(-)

diff --git a/pom.xml b/pom.xml
index 945207d..40c17c7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -35,7 +35,7 @@
     <sling.java.version>8</sling.java.version>
     <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
     <picocli.version>3.6.0</picocli.version>
-    <org.apache.jackrabbit.vault.version>3.4.10</org.apache.jackrabbit.vault.version>
+    <org.apache.jackrabbit.vault.version>3.6.0</org.apache.jackrabbit.vault.version>
     <jackrabbit-api.version>2.18.4</jackrabbit-api.version>
     <jackrabbit-spi-commons.version>2.18.4</jackrabbit-spi-commons.version>
     <mockito-core.version>3.2.0</mockito-core.version>
@@ -253,7 +253,7 @@
     <dependency>
       <groupId>org.apache.jackrabbit.vault</groupId>
       <artifactId>vault-validation</artifactId>
-      <version>3.4.6</version>
+      <version>3.6.0</version>
       <scope>compile</scope>
     </dependency>
 
diff --git a/src/main/java/org/apache/sling/feature/cpconverter/ContentPackage2FeatureModelConverter.java b/src/main/java/org/apache/sling/feature/cpconverter/ContentPackage2FeatureModelConverter.java
index c077ab0..f6c5507 100644
--- a/src/main/java/org/apache/sling/feature/cpconverter/ContentPackage2FeatureModelConverter.java
+++ b/src/main/java/org/apache/sling/feature/cpconverter/ContentPackage2FeatureModelConverter.java
@@ -57,6 +57,7 @@ import org.apache.sling.feature.cpconverter.handlers.EntryHandler;
 import org.apache.sling.feature.cpconverter.handlers.EntryHandlersManager;
 import org.apache.sling.feature.cpconverter.handlers.NodeTypesEntryHandler;
 import org.apache.sling.feature.cpconverter.handlers.slinginitialcontent.BundleSlingInitialContentExtractor;
+import org.apache.sling.feature.cpconverter.index.IndexManager;
 import org.apache.sling.feature.cpconverter.vltpkg.BaseVaultPackageScanner;
 import org.apache.sling.feature.cpconverter.vltpkg.PackagesEventsEmitter;
 import org.apache.sling.feature.cpconverter.vltpkg.RecollectorVaultPackageScanner;
@@ -110,6 +111,8 @@ public class ContentPackage2FeatureModelConverter extends BaseVaultPackageScanne
     
     private BundleSlingInitialContentExtractor bundleSlingInitialContentExtractor = new BundleSlingInitialContentExtractor();
 
+    private IndexManager indexManager;
+
     public enum PackagePolicy {
         /**
          * References the content package in the feature model and deploys via the {@link ContentPackage2FeatureModelConverter#artifactsDeployer}
@@ -236,6 +239,15 @@ public class ContentPackage2FeatureModelConverter extends BaseVaultPackageScanne
         return this;
     }
 
+    public @Nullable IndexManager getIndexManager() {
+        return indexManager;
+    }
+
+    public @NotNull ContentPackage2FeatureModelConverter setIndexManager(IndexManager indexManager) {
+        this.indexManager = indexManager;
+        return this;
+    }
+
     public @NotNull File getTempDirectory() {
         return this.tmpDirectory;
     }
@@ -315,6 +327,7 @@ public class ContentPackage2FeatureModelConverter extends BaseVaultPackageScanne
 
                     aclManager.addRepoinitExtension(assemblers, featuresManager);
                     bundleSlingInitialContentExtractor.addRepoInitExtension(assemblers, featuresManager);
+                    indexManager.addRepoinitExtension(featuresManager);
                     
                     logger.info("Conversion complete!");
 
@@ -326,6 +339,7 @@ public class ContentPackage2FeatureModelConverter extends BaseVaultPackageScanne
                 
                 aclManager.reset();
                 bundleSlingInitialContentExtractor.reset();
+                indexManager.reset();
                 assemblers.clear();
 
                 try {
@@ -379,7 +393,7 @@ public class ContentPackage2FeatureModelConverter extends BaseVaultPackageScanne
         // Please note: THIS IS A HACK to meet the new requirement without drastically change the original design
         // temporary swap the main handler to collect stuff
         VaultPackageAssembler handler = getMainPackageAssembler();
-      
+        
         Properties parentProps = handler.getPackageProperties();
         boolean isContainerPackage = PackageType.CONTAINER.equals(parentProps.get(PackageProperties.NAME_PACKAGE_TYPE));
         setMainPackageAssembler(clonedPackage);
diff --git a/src/main/java/org/apache/sling/feature/cpconverter/cli/ContentPackage2FeatureModelConverterLauncher.java b/src/main/java/org/apache/sling/feature/cpconverter/cli/ContentPackage2FeatureModelConverterLauncher.java
index 59098fb..14948ff 100644
--- a/src/main/java/org/apache/sling/feature/cpconverter/cli/ContentPackage2FeatureModelConverterLauncher.java
+++ b/src/main/java/org/apache/sling/feature/cpconverter/cli/ContentPackage2FeatureModelConverterLauncher.java
@@ -38,6 +38,7 @@ import org.apache.sling.feature.cpconverter.features.DefaultFeaturesManager;
 import org.apache.sling.feature.cpconverter.filtering.RegexBasedResourceFilter;
 import org.apache.sling.feature.cpconverter.handlers.DefaultEntryHandlersManager;
 import org.apache.sling.feature.cpconverter.handlers.slinginitialcontent.BundleSlingInitialContentExtractor;
+import org.apache.sling.feature.cpconverter.index.DefaultIndexManager;
 import org.apache.sling.feature.cpconverter.shared.ConverterConstants;
 import org.apache.sling.feature.cpconverter.vltpkg.DefaultPackagesEventsEmitter;
 import org.apache.sling.feature.io.json.FeatureJSONReader;
@@ -217,6 +218,7 @@ public final class ContentPackage2FeatureModelConverterLauncher implements Runna
                              .setBundleSlingInitialContentExtractor(bundleSlingInitialContentExtractor)
                              .setEntryHandlersManager(new DefaultEntryHandlersManager(entryHandlerConfigsMap, !disableInstallerPolicy, slingInitialContentPolicy, bundleSlingInitialContentExtractor, systemUserRelPath))
                              .setAclManager(aclManager)
+                             .setIndexManager(new DefaultIndexManager())
                              .setEmitter(DefaultPackagesEventsEmitter.open(featureModelsOutputDirectory))
                              .setFailOnMixedPackages(failOnMixedPackages)
                              .setContentTypePackagePolicy(contentTypePackagePolicy);
diff --git a/src/main/java/org/apache/sling/feature/cpconverter/features/DefaultFeaturesManager.java b/src/main/java/org/apache/sling/feature/cpconverter/features/DefaultFeaturesManager.java
index c29a9fb..3eb9e6d 100644
--- a/src/main/java/org/apache/sling/feature/cpconverter/features/DefaultFeaturesManager.java
+++ b/src/main/java/org/apache/sling/feature/cpconverter/features/DefaultFeaturesManager.java
@@ -50,6 +50,7 @@ import org.apache.sling.feature.Feature;
 import org.apache.sling.feature.cpconverter.ConverterException;
 import org.apache.sling.feature.cpconverter.accesscontrol.AclManager;
 import org.apache.sling.feature.cpconverter.accesscontrol.Mapping;
+import org.apache.sling.feature.cpconverter.index.IndexManager;
 import org.apache.sling.feature.cpconverter.interpolator.SimpleVariablesInterpolator;
 import org.apache.sling.feature.cpconverter.interpolator.VariablesInterpolator;
 import org.apache.sling.feature.cpconverter.repoinit.NoOpVisitor;
@@ -328,7 +329,7 @@ public class DefaultFeaturesManager implements FeaturesManager, PackagesEventsEm
         }
         return false;
     }
-    
+
     private List<String> convertMappings(@Nullable String[] mappings, @NotNull String pid, boolean enforceServiceMappingByPrincipal) throws ConverterException {
         if (mappings == null) {
             return Collections.emptyList();
@@ -393,8 +394,8 @@ public class DefaultFeaturesManager implements FeaturesManager, PackagesEventsEm
 
         adjustConfigurationProperties(configuration, configurationProperties);
     }
-    
-    private void adjustConfigurationProperties(@NotNull Configuration configuration, 
+
+    private void adjustConfigurationProperties(@NotNull Configuration configuration,
                                                @NotNull Dictionary<String, Object> configurationProperties) {
         Enumeration<String> keys = configurationProperties.keys();
         while (keys.hasMoreElements()) {
@@ -538,7 +539,22 @@ public class DefaultFeaturesManager implements FeaturesManager, PackagesEventsEm
             repoInitExtension.setText(repoInitExtension.getText().concat(System.lineSeparator()).concat(text));
         }
     }
-    
+
+    @Override
+    public void addOrAppendOakIndexDefinitionsExtension(String source, String text)
+            throws IOException, ConverterException {
+
+        Extension oakIndexDefsExtension = getRunMode(null).getExtensions().getByName(IndexManager.EXTENSION_NAME);
+        if (oakIndexDefsExtension == null) {
+            oakIndexDefsExtension = new Extension(ExtensionType.JSON, IndexManager.EXTENSION_NAME, ExtensionState.REQUIRED);
+            getRunMode(null).getExtensions().add(oakIndexDefsExtension);
+            oakIndexDefsExtension.setJSON(text);
+        } else {
+            oakIndexDefsExtension.setJSON(oakIndexDefsExtension.getText().concat(System.lineSeparator()).concat(text));
+        }
+
+    }
+
     private static void checkReferences(@NotNull final Dictionary<String, Object> configurationProperties, @NotNull final String pid) throws ConverterException {
         final String[] references = Converters.standardConverter().convert(configurationProperties.get("references")).to(String[].class);
         if (references != null && references.length > 0) {
diff --git a/src/main/java/org/apache/sling/feature/cpconverter/features/FeaturesManager.java b/src/main/java/org/apache/sling/feature/cpconverter/features/FeaturesManager.java
index 13e0377..028d1d8 100644
--- a/src/main/java/org/apache/sling/feature/cpconverter/features/FeaturesManager.java
+++ b/src/main/java/org/apache/sling/feature/cpconverter/features/FeaturesManager.java
@@ -70,6 +70,8 @@ public interface FeaturesManager {
     void addOrAppendRepoInitExtension(@NotNull String source, @NotNull String text, @Nullable String runMode)
             throws IOException, ConverterException;
 
+    void addOrAppendOakIndexDefinitionsExtension(String source, String text) throws IOException, ConverterException;
+
     @NotNull
     Map<String, String> getNamespaceUriByPrefix();
 
diff --git a/src/main/java/org/apache/sling/feature/cpconverter/handlers/IndexDefinitionsEntryHandler.java b/src/main/java/org/apache/sling/feature/cpconverter/handlers/IndexDefinitionsEntryHandler.java
new file mode 100644
index 0000000..b0770f8
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/cpconverter/handlers/IndexDefinitionsEntryHandler.java
@@ -0,0 +1,128 @@
+/*
+ * 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.feature.cpconverter.handlers;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Optional;
+
+import javax.jcr.RepositoryException;
+
+import org.apache.jackrabbit.util.Text;
+import org.apache.jackrabbit.vault.fs.api.WorkspaceFilter;
+import org.apache.jackrabbit.vault.fs.io.Archive;
+import org.apache.jackrabbit.vault.fs.io.Archive.Entry;
+import org.apache.jackrabbit.vault.fs.io.DocViewParser;
+import org.apache.jackrabbit.vault.fs.io.DocViewParser.XmlParseException;
+import org.apache.jackrabbit.vault.fs.io.DocViewParserHandler;
+import org.apache.jackrabbit.vault.util.DocViewNode2;
+import org.apache.jackrabbit.vault.util.PlatformNameFormat;
+import org.apache.sling.feature.cpconverter.ContentPackage2FeatureModelConverter;
+import org.apache.sling.feature.cpconverter.ConverterException;
+import org.apache.sling.feature.cpconverter.index.IndexDefinitions;
+import org.apache.sling.feature.cpconverter.index.IndexManager;
+import org.jetbrains.annotations.NotNull;
+import org.xml.sax.InputSource;
+
+/**
+ * Handler for Jackrabbit Oak index definitions
+ *
+ * <p>This implementation scans content packages for entries stored under <tt>/oak:index</tt>
+ * and exposes them to the {@link IndexManager} for further processing.
+ *
+ */
+public class IndexDefinitionsEntryHandler extends AbstractRegexEntryHandler {
+
+    private final class IndexDefinitionsParserHandler implements DocViewParserHandler {
+        private final WorkspaceFilter filter;
+        private IndexDefinitions definitions;
+
+        public IndexDefinitionsParserHandler(WorkspaceFilter filter, IndexDefinitions definitions) {
+            this.filter = filter;
+            this.definitions = definitions;
+        }
+
+        @Override
+        public void startDocViewNode(@NotNull String nodePath, @NotNull DocViewNode2 docViewNode,
+                @NotNull Optional<DocViewNode2> parentDocViewNode, int line, int column)
+                throws IOException, RepositoryException {
+
+            if ( nodePath.startsWith(IndexDefinitions.OAK_INDEX_PATH) && filter.contains(nodePath) ) {
+                definitions.addNode(Text.getRelativeParent(nodePath, 1), docViewNode);
+            }
+        }
+
+        @Override
+        public void endDocViewNode(@NotNull String nodePath, @NotNull DocViewNode2 docViewNode,
+                @NotNull Optional<DocViewNode2> parentDocViewNode, int line, int column)
+                throws IOException, RepositoryException {
+            // nothing to do
+        }
+
+        @Override
+        public void startPrefixMapping(String prefix, String uri) {
+            definitions.registerPrefixMapping(prefix, uri);
+        }
+    }
+
+    public IndexDefinitionsEntryHandler() {
+        super("/jcr_root/" + PlatformNameFormat.getPlatformName(IndexDefinitions.OAK_INDEX_NAME)+ "/.*(/)?/*.xml");
+    }
+
+    @Override
+    public void handle(@NotNull String path, @NotNull Archive archive, @NotNull Entry entry,
+            @NotNull ContentPackage2FeatureModelConverter converter) throws IOException, ConverterException {
+
+        IndexManager indexManager = converter.getIndexManager();
+        if ( indexManager == null ) {
+            logger.info("{} not present, will skip index definition extraction", IndexManager.class.getName());
+        } else {
+            try (InputStream is = archive.openInputStream(entry)) {
+
+                String platformPath = path.replaceAll("^/jcr_root", "")
+                        .replaceAll("/\\.content\\.xml$", "")
+                        .replace(".dir", "");
+                String repositoryPath = PlatformNameFormat.getRepositoryPath(platformPath);
+                InputSource inputSource = new InputSource(is);
+
+                boolean isDocView = false;
+                // DocViewParser.isDocView closes the input stream it is passed
+                try ( InputStream isCheck = archive.openInputStream(entry) ) {
+                    isDocView =  DocViewParser.isDocView(new InputSource(isCheck));
+                }
+                if ( isDocView ) {
+                    DocViewParser parser = new DocViewParser();
+                    IndexDefinitionsParserHandler handler = new IndexDefinitionsParserHandler(archive.getMetaInf().getFilter(), indexManager.getIndexes());
+
+                    parser.parse(repositoryPath, inputSource, handler);
+
+                } else {
+                    // binary file, should we attach?
+                    if ( archive.getMetaInf().getFilter().contains(repositoryPath)) {
+                        indexManager.getIndexes().registerBinary(repositoryPath, is);
+                    }
+                }
+
+
+            } catch (XmlParseException e) {
+                throw new ConverterException("Failed parsing the index definitions", e);
+            }
+        }
+
+        converter.getMainPackageAssembler().addEntry(path, archive, entry);
+    }
+}
diff --git a/src/main/java/org/apache/sling/feature/cpconverter/index/DefaultIndexManager.java b/src/main/java/org/apache/sling/feature/cpconverter/index/DefaultIndexManager.java
new file mode 100644
index 0000000..df68e01
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/cpconverter/index/DefaultIndexManager.java
@@ -0,0 +1,52 @@
+/*
+ * 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.feature.cpconverter.index;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+import org.apache.sling.feature.cpconverter.ConverterException;
+import org.apache.sling.feature.cpconverter.features.FeaturesManager;
+
+public class DefaultIndexManager implements IndexManager {
+
+    private IndexDefinitions indexDefinitions = new IndexDefinitions();
+    private IndexDefinitionsJsonWriter writer = new IndexDefinitionsJsonWriter(indexDefinitions);
+
+    @Override
+    public void addRepoinitExtension(FeaturesManager features) throws IOException, ConverterException {
+
+        if ( indexDefinitions.getIndexes().isEmpty() )
+            return;
+
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        writer.writeAsJson(out);
+        features.addOrAppendOakIndexDefinitionsExtension("content-package", out.toString(StandardCharsets.UTF_8.toString()));
+    }
+
+    @Override
+    public IndexDefinitions getIndexes() {
+        return indexDefinitions;
+    }
+
+    @Override
+    public void reset() {
+        indexDefinitions = new IndexDefinitions();
+        writer = new IndexDefinitionsJsonWriter(indexDefinitions);
+    }
+}
diff --git a/src/main/java/org/apache/sling/feature/cpconverter/index/IndexDefinitions.java b/src/main/java/org/apache/sling/feature/cpconverter/index/IndexDefinitions.java
new file mode 100644
index 0000000..300e42c
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/cpconverter/index/IndexDefinitions.java
@@ -0,0 +1,163 @@
+/*
+ * 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.feature.cpconverter.index;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.jackrabbit.spi.Name;
+import org.apache.jackrabbit.vault.util.DocViewNode2;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Holds information about discovered index definitions
+ *
+ */
+public class IndexDefinitions {
+
+    public static final String OAK_INDEX_NAME = "oak:index";
+    public static final String OAK_INDEX_PATH = "/" + OAK_INDEX_NAME; // NOSONAR - java:S1075 does not apply as this is not a filesystem path
+
+    private final Map<String, List<DocViewNode2>> children = new HashMap<>();
+    private final Map<String, byte[]> binaries = new HashMap<>();
+    private Map<String, String> prefixesToUris = new HashMap<>();
+    private Map<String, String> urisToPrefixes = new HashMap<>();
+
+    public void addNode(@NotNull String parentPath, @NotNull DocViewNode2 node) {
+        List<DocViewNode2> currentChildren = children.computeIfAbsent(parentPath, k -> new ArrayList<>());
+        DocViewNode2 existing = null;
+        for ( DocViewNode2 currentChild : currentChildren ) {
+
+            // prevent duplicates
+            if ( currentChild.getName().equals(node.getName() )) {
+                // new node holds less information. There should not be a scenario where we need to
+                // merge properties.
+                if ( node.getProperties().size() <= currentChild.getProperties().size() ) {
+                    return;
+                }
+
+                existing = currentChild;
+            }
+        }
+
+        // remove node marked as placeholder
+        if ( existing != null ) {
+            currentChildren.remove(existing);
+        }
+
+        // add new node
+        currentChildren.add(node);
+    }
+
+    public @NotNull List<DocViewNode2> getIndexes() {
+        return getChildren(OAK_INDEX_PATH);
+    }
+
+    public @NotNull List<DocViewNode2> getChildren(@NotNull String parentPath) {
+        return children.getOrDefault(parentPath, Collections.emptyList());
+    }
+
+
+    /**
+     * Returns a name in compact format
+     *
+     * <p>Maps a fully qualified {@link Name name}, e.g. ['http://jackrabbit.apache.org/oak/ns/1.0','index'] to a compact name
+     * like <tt>oak:index</tt></p>
+     *
+     * @param name The name to map
+     * @return the compact name
+     */
+    public @NotNull String toShortName(@NotNull Name name) {
+        if ( name.getNamespaceURI().length() == 0 )
+            return name.getLocalName();
+        return urisToPrefixes.get(name.getNamespaceURI()) + ":" + name.getLocalName();
+    }
+
+    /**
+     * Registers a binary entry at the specified repository path
+     *
+     * <p>The input stream may be fully read into memory, and therefore is not expected to be unreasonably large.</p>
+     *
+     * <p>The input stream will be fully read, but not closed.</p>
+     *
+     * @param repositoryPath The JCR repository path where the binary was found
+     * @param is the input stream for the binary
+     * @throws IOException in case of I/O problems
+     */
+    public void registerBinary(@NotNull String repositoryPath, @NotNull InputStream is) throws IOException {
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        IOUtils.copy(is, out);
+        binaries.put(repositoryPath, out.toByteArray());
+    }
+
+    /**
+     * Returns a potential binary registered for a repository path
+     *
+     * @param repositoryPath the path of the repository
+     * @return an optional wrapping the binary data, possibly {@link Optional#empty() empty}
+     */
+    public @NotNull Optional<byte[]> getBinary(@NotNull String repositoryPath) {
+        return Optional.ofNullable(binaries.get(repositoryPath));
+    }
+
+    /**
+     * Registers a prefix mapping for a specified uri
+     *
+     * @param prefix the prefix
+     * @param uri the uri
+     */
+    public void registerPrefixMapping(@NotNull String prefix, @NotNull String uri) {
+        prefixesToUris.put(prefix, uri);
+        urisToPrefixes.put(uri, prefix);
+     }
+
+    /**
+     * Dumps a compact representation of the data
+     *
+     * <p>Useful for debugging purposes only</p>
+     *
+     * @param out the PrintStream to use
+     */
+    public void dump(@NotNull PrintStream out) {
+        out.println("---------");
+        out.println(OAK_INDEX_NAME);
+        dumpChildren(out, OAK_INDEX_PATH);
+        out.println("---------");
+    }
+
+    private void dumpChildren(PrintStream out, String parentPath) {
+
+        StringBuilder padding = new StringBuilder();
+        int depth = parentPath.split("/").length - 1;
+        for ( int i = 0 ; i < 2 * depth; i++)
+            padding.append(' ');
+
+        for ( DocViewNode2 node : children.getOrDefault(parentPath, Collections.emptyList()) ) {
+            out.println(padding.toString() + toShortName(node.getName()));
+            dumpChildren(out, parentPath + '/' + node.getName().getLocalName());
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/feature/cpconverter/index/IndexDefinitionsJsonWriter.java b/src/main/java/org/apache/sling/feature/cpconverter/index/IndexDefinitionsJsonWriter.java
new file mode 100644
index 0000000..a14fab5
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/cpconverter/index/IndexDefinitionsJsonWriter.java
@@ -0,0 +1,154 @@
+/*
+ * 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.feature.cpconverter.index;
+
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Function;
+
+import javax.jcr.PropertyType;
+import javax.json.Json;
+import javax.json.JsonArrayBuilder;
+import javax.json.JsonValue;
+import javax.json.stream.JsonGenerator;
+
+import org.apache.jackrabbit.util.Base64;
+import org.apache.jackrabbit.vault.util.DocViewNode2;
+import org.apache.jackrabbit.vault.util.DocViewProperty2;
+import org.jetbrains.annotations.NotNull;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Writes index definitions in a JSON format that can be consumed by the <tt>oak-run</tt> tool.
+ *
+ * @see <a href=
+ *  "https://jackrabbit.apache.org/oak/docs/query/oak-run-indexing.html">Oak-Run
+ *   indexing</a>
+ */
+public class IndexDefinitionsJsonWriter {
+
+    private static final Function<String, JsonValue> BLOB_MAPPER =  s -> Json.createValue(":blobid:" + Base64.encode(s));
+
+    private final Logger logger = LoggerFactory.getLogger(getClass());
+
+    private final IndexDefinitions indexDefinitions;
+
+    public IndexDefinitionsJsonWriter(@NotNull IndexDefinitions indexDefinitions) {
+        this.indexDefinitions = indexDefinitions;
+    }
+
+    /**
+     * Writes the index definitions to the specified <tt>out</tt>
+     *
+     * @param out the output stream to write to
+     */
+    public void writeAsJson(@NotNull OutputStream out) {
+        try ( JsonGenerator root = Json.createGenerator(out) ) {
+            root.writeStartObject();
+            for ( DocViewNode2 index : indexDefinitions.getIndexes() ) {
+                write(root, index, IndexDefinitions.OAK_INDEX_PATH);
+            }
+            root.writeEnd(); // end object declaration
+        }
+    }
+
+    private void write(JsonGenerator json, DocViewNode2 index, String parentPath) {
+
+        String nodeName = indexDefinitions.toShortName(index.getName());
+        String objectKey = parentPath.equals(IndexDefinitions.OAK_INDEX_PATH) ?
+                IndexDefinitions.OAK_INDEX_PATH + "/" + nodeName : nodeName;
+
+        // 1. start object
+        json.writeStartObject(objectKey);
+
+        // 2. write properties
+        for ( DocViewProperty2 property : index.getProperties() ) {
+
+            String propertyName = indexDefinitions.toShortName(property.getName());
+
+            switch ( property.getType() ) {
+                case PropertyType.STRING:
+                case PropertyType.UNDEFINED:
+                    write(json, propertyName, property.getStringValues(), s -> Json.createValue("str:" + s ));
+                    break;
+                case PropertyType.LONG:
+                    write(json, propertyName, property.getStringValues(), s -> Json.createValue(Long.parseLong(s) ));
+                    break;
+                case PropertyType.BOOLEAN:
+                    write(json, propertyName, property.getStringValues(), s -> ( Boolean.parseBoolean(s) ? JsonValue.TRUE : JsonValue.FALSE)  );
+                    break;
+                case PropertyType.NAME:
+                    write(json, propertyName, property.getStringValues(), s -> Json.createValue("nam:" + s ));
+                    break;
+                case PropertyType.DOUBLE:
+                    write(json, propertyName, property.getStringValues(), s -> Json.createValue(Double.parseDouble(s) ));
+                    break;
+                case PropertyType.DATE:
+                    write(json, propertyName, property.getStringValues(), s -> Json.createValue("dat:" + s) );
+                    break;
+                case PropertyType.PATH:
+                    write(json, propertyName, property.getStringValues(), s -> Json.createValue("pat:" + s) );
+                    break;
+                case PropertyType.URI:
+                    write(json, propertyName, property.getStringValues(), s -> Json.createValue("uri:" + s) );
+                    break;
+                case PropertyType.BINARY:
+                    write(json, propertyName, property.getStringValues(), BLOB_MAPPER );
+                    break;
+                default:
+                    logger.warn("Skipping property {}, don't know how to handle type {}; values: {}", property.getName(), property.getType(), property.getStringValues());
+
+            }
+        }
+
+        // 3. write nt:data entries for nt:resource children of nt:files
+        // in this case, this is the nt:resource node
+        Optional<byte[]> binary = indexDefinitions.getBinary(parentPath);
+        if ( binary.isPresent() ) {
+            String blobAsString = new String(binary.get(), StandardCharsets.UTF_8);
+            write(json, "jcr:data", Collections.singletonList(blobAsString), BLOB_MAPPER);
+        };
+
+        // 4. write children
+        String nodePath = parentPath + "/" + nodeName;  // NOSONAR - java:S1075 does not apply as this is not a filesystem path
+        for ( DocViewNode2 child : indexDefinitions.getChildren(nodePath)) {
+            write(json, child, nodePath);
+        }
+
+        // 5. end object
+        json.writeEnd();
+    }
+
+    private void write(JsonGenerator json, String propertyName, List<String> propertyValues, Function<String, JsonValue> mapper) {
+        if ( propertyValues.size() == 1 ) {
+            json.write(propertyName, mapper.apply(propertyValues.get(0)));
+            return;
+        }
+
+        JsonArrayBuilder arrayBuilder = Json.createArrayBuilder();
+        propertyValues.stream()
+            .map( mapper )
+            .forEach( arrayBuilder::add );
+
+        json.write(propertyName, arrayBuilder.build());
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/feature/cpconverter/index/IndexManager.java b/src/main/java/org/apache/sling/feature/cpconverter/index/IndexManager.java
new file mode 100644
index 0000000..1f4573f
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/cpconverter/index/IndexManager.java
@@ -0,0 +1,60 @@
+/*
+ * 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.feature.cpconverter.index;
+
+import java.io.IOException;
+
+import org.apache.sling.feature.cpconverter.ConverterException;
+import org.apache.sling.feature.cpconverter.features.FeaturesManager;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Point of entry for logic related to handling Oak indexes
+ *
+ * @see <a href=
+ *  "https://jackrabbit.apache.org/oak/docs/query/oak-run-indexing.html">Oak-Run
+ *   indexing</a>
+ */
+public interface IndexManager {
+
+    public static final String EXTENSION_NAME = "oak-index-definitions";
+
+    /**
+     * Returns the index definitions managed by this instance
+     *
+     * <p>The returned object may be used to record data discovered about oak indexes</p>
+     *
+     * @return the index definitions
+     */
+    @NotNull IndexDefinitions getIndexes();
+
+    /**
+     * Records the Oak index data using the features manager
+     *
+     * <p>The index definitions will be recoreded as a JSON repoinit extension named {@value #EXTENSION_NAME} .</p>
+     *
+     * @param features
+     * @throws IOException
+     * @throws ConverterException
+     */
+    void addRepoinitExtension(FeaturesManager features) throws IOException, ConverterException;
+
+    /**
+     * Resets the internal state
+     */
+    void reset();
+}
diff --git a/src/main/java/org/apache/sling/feature/cpconverter/vltpkg/JcrNamespaceRegistry.java b/src/main/java/org/apache/sling/feature/cpconverter/vltpkg/JcrNamespaceRegistry.java
index e1ea0b5..d88f63e 100644
--- a/src/main/java/org/apache/sling/feature/cpconverter/vltpkg/JcrNamespaceRegistry.java
+++ b/src/main/java/org/apache/sling/feature/cpconverter/vltpkg/JcrNamespaceRegistry.java
@@ -16,37 +16,38 @@
  */
 package org.apache.sling.feature.cpconverter.vltpkg;
 
+import java.io.IOException;
+import java.io.Reader;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+
+import javax.jcr.NamespaceException;
+import javax.jcr.NamespaceRegistry;
+import javax.jcr.RepositoryException;
+import javax.jcr.ValueFactory;
+import javax.jcr.nodetype.NodeTypeManager;
+import javax.xml.namespace.NamespaceContext;
+
 import org.apache.commons.lang3.StringUtils;
 import org.apache.jackrabbit.commons.SimpleValueFactory;
 import org.apache.jackrabbit.commons.cnd.CndImporter;
 import org.apache.jackrabbit.commons.cnd.ParseException;
 import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver;
-import org.apache.jackrabbit.vault.validation.spi.impl.nodetype.NodeTypeManagerProvider;
+import org.apache.jackrabbit.vault.util.StandaloneManagerProvider;
 import org.jetbrains.annotations.NotNull;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import javax.jcr.NamespaceException;
-import javax.jcr.NamespaceRegistry;
-import javax.jcr.RepositoryException;
-import javax.jcr.ValueFactory;
-import javax.jcr.nodetype.NodeTypeManager;
-import javax.xml.namespace.NamespaceContext;
-import java.io.IOException;
-import java.io.Reader;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Iterator;
-
 /** Simple namespace registry backed by a map */
 public class JcrNamespaceRegistry implements NamespaceRegistry, NamespaceResolver, NamespaceContext {
-    
+
     private final Collection<String> registeredCndSystemIds = new ArrayList<>();
-    private final NodeTypeManagerProvider ntManagerProvider = new NodeTypeManagerProvider();
+    private final StandaloneManagerProvider ntManagerProvider = new StandaloneManagerProvider();
     private final NodeTypeManager ntManager = ntManagerProvider.getNodeTypeManager();
     private final Logger logger = LoggerFactory.getLogger(JcrNamespaceRegistry.class);
-    
+
     public JcrNamespaceRegistry() throws RepositoryException, ParseException, IOException {
         ntManagerProvider.registerNamespace(PREFIX_XML, NAMESPACE_XML);
         ntManagerProvider.registerNamespace("sling", "http://sling.apache.org/jcr/sling/1.0");
diff --git a/src/main/resources/META-INF/services/org.apache.sling.feature.cpconverter.handlers.EntryHandler b/src/main/resources/META-INF/services/org.apache.sling.feature.cpconverter.handlers.EntryHandler
index 46fb089..b8ad4c8 100644
--- a/src/main/resources/META-INF/services/org.apache.sling.feature.cpconverter.handlers.EntryHandler
+++ b/src/main/resources/META-INF/services/org.apache.sling.feature.cpconverter.handlers.EntryHandler
@@ -2,6 +2,7 @@ org.apache.sling.feature.cpconverter.handlers.BundleEntryHandler
 org.apache.sling.feature.cpconverter.handlers.ConfigurationEntryHandler
 org.apache.sling.feature.cpconverter.handlers.ContentPackageEntryHandler
 org.apache.sling.feature.cpconverter.handlers.GroupEntryHandler
+org.apache.sling.feature.cpconverter.handlers.IndexDefinitionsEntryHandler
 org.apache.sling.feature.cpconverter.handlers.JsonConfigurationEntryHandler
 org.apache.sling.feature.cpconverter.handlers.NodeTypesEntryHandler
 org.apache.sling.feature.cpconverter.handlers.PrivilegesHandler
diff --git a/src/test/java/org/apache/sling/feature/cpconverter/AdjustedFilterTest.java b/src/test/java/org/apache/sling/feature/cpconverter/AdjustedFilterTest.java
index 0792378..86fd426 100644
--- a/src/test/java/org/apache/sling/feature/cpconverter/AdjustedFilterTest.java
+++ b/src/test/java/org/apache/sling/feature/cpconverter/AdjustedFilterTest.java
@@ -27,6 +27,7 @@ import org.apache.sling.feature.cpconverter.features.FeaturesManager;
 import org.apache.sling.feature.cpconverter.handlers.DefaultEntryHandlersManager;
 import org.apache.sling.feature.cpconverter.handlers.EntryHandlersManager;
 import org.apache.sling.feature.cpconverter.handlers.slinginitialcontent.BundleSlingInitialContentExtractor;
+import org.apache.sling.feature.cpconverter.index.DefaultIndexManager;
 import org.apache.sling.feature.cpconverter.shared.ConverterConstants;
 import org.apache.sling.feature.cpconverter.vltpkg.DefaultPackagesEventsEmitter;
 import org.jetbrains.annotations.NotNull;
@@ -59,7 +60,8 @@ public class AdjustedFilterTest extends AbstractConverterTest {
 
         converter = new ContentPackage2FeatureModelConverter()
                 .setEntryHandlersManager(handlersManager)
-                .setAclManager(aclManager);
+                .setAclManager(aclManager)
+                .setIndexManager(new DefaultIndexManager());
 
         outputDirectory = new File(System.getProperty("java.io.tmpdir"), getClass().getName() + '_' + System.currentTimeMillis());
         FeaturesManager featuresManager = new DefaultFeaturesManager(true, 5, outputDirectory, null, null, null, aclManager);
diff --git a/src/test/java/org/apache/sling/feature/cpconverter/ContentPackage2FeatureModelConverterTest.java b/src/test/java/org/apache/sling/feature/cpconverter/ContentPackage2FeatureModelConverterTest.java
index 4aea947..f4e0441 100644
--- a/src/test/java/org/apache/sling/feature/cpconverter/ContentPackage2FeatureModelConverterTest.java
+++ b/src/test/java/org/apache/sling/feature/cpconverter/ContentPackage2FeatureModelConverterTest.java
@@ -68,6 +68,7 @@ import org.apache.sling.feature.cpconverter.filtering.RegexBasedResourceFilter;
 import org.apache.sling.feature.cpconverter.handlers.DefaultEntryHandlersManager;
 import org.apache.sling.feature.cpconverter.handlers.EntryHandlersManager;
 import org.apache.sling.feature.cpconverter.handlers.slinginitialcontent.BundleSlingInitialContentExtractor;
+import org.apache.sling.feature.cpconverter.index.DefaultIndexManager;
 import org.apache.sling.feature.cpconverter.shared.ConverterConstants;
 import org.apache.sling.feature.cpconverter.vltpkg.DefaultPackagesEventsEmitter;
 import org.apache.sling.feature.io.json.FeatureJSONReader;
@@ -100,7 +101,8 @@ public class ContentPackage2FeatureModelConverterTest extends AbstractConverterT
         converter = new ContentPackage2FeatureModelConverter()
                     .setEntryHandlersManager(handlersManager)
                     .setFeaturesManager(new DefaultFeaturesManager(new File("")))
-                    .setAclManager(new DefaultAclManager());
+                    .setAclManager(new DefaultAclManager())
+                    .setIndexManager(new DefaultIndexManager());
     }
 
     @After
diff --git a/src/test/java/org/apache/sling/feature/cpconverter/ConverterUserAndPermissionTest.java b/src/test/java/org/apache/sling/feature/cpconverter/ConverterUserAndPermissionTest.java
index 6ff8aea..9e46b80 100644
--- a/src/test/java/org/apache/sling/feature/cpconverter/ConverterUserAndPermissionTest.java
+++ b/src/test/java/org/apache/sling/feature/cpconverter/ConverterUserAndPermissionTest.java
@@ -35,6 +35,7 @@ import org.apache.sling.feature.cpconverter.features.FeaturesManager;
 import org.apache.sling.feature.cpconverter.handlers.DefaultEntryHandlersManager;
 import org.apache.sling.feature.cpconverter.handlers.EntryHandlersManager;
 import org.apache.sling.feature.cpconverter.handlers.slinginitialcontent.BundleSlingInitialContentExtractor;
+import org.apache.sling.feature.cpconverter.index.DefaultIndexManager;
 import org.apache.sling.feature.cpconverter.shared.ConverterConstants;
 import org.apache.sling.feature.cpconverter.vltpkg.DefaultPackagesEventsEmitter;
 import org.apache.sling.feature.io.json.FeatureJSONReader;
@@ -146,6 +147,7 @@ public class ConverterUserAndPermissionTest  extends AbstractConverterTest {
         converter = new ContentPackage2FeatureModelConverter()
                 .setEntryHandlersManager(handlersManager)
                 .setAclManager(aclManager)
+                .setIndexManager(new DefaultIndexManager())
                 .setContentTypePackagePolicy(PackagePolicy.REFERENCE);
 
         outputDirectory = new File(System.getProperty("java.io.tmpdir"), getClass().getName() + '_' + System.currentTimeMillis());
diff --git a/src/test/java/org/apache/sling/feature/cpconverter/handlers/IndexDefinitionsEntryHandlerTest.java b/src/test/java/org/apache/sling/feature/cpconverter/handlers/IndexDefinitionsEntryHandlerTest.java
new file mode 100644
index 0000000..6bf7c55
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/cpconverter/handlers/IndexDefinitionsEntryHandlerTest.java
@@ -0,0 +1,223 @@
+/*
+ * 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.feature.cpconverter.handlers;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.util.List;
+import java.util.Objects;
+
+import javax.jcr.NamespaceRegistry;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+
+import org.apache.jackrabbit.vault.fs.io.Archive;
+import org.apache.jackrabbit.vault.fs.io.Archive.Entry;
+import org.apache.jackrabbit.vault.fs.io.FileArchive;
+import org.apache.jackrabbit.vault.packaging.impl.ZipVaultPackage;
+import org.apache.jackrabbit.vault.util.DocViewNode2;
+import org.apache.sling.feature.cpconverter.ContentPackage2FeatureModelConverter;
+import org.apache.sling.feature.cpconverter.ConverterException;
+import org.apache.sling.feature.cpconverter.index.DefaultIndexManager;
+import org.apache.sling.feature.cpconverter.index.IndexDefinitions;
+import org.apache.sling.feature.cpconverter.vltpkg.BaseVaultPackageScanner;
+import org.apache.sling.feature.cpconverter.vltpkg.VaultPackageAssembler;
+import org.assertj.core.api.Condition;
+import org.jetbrains.annotations.NotNull;
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+public class IndexDefinitionsEntryHandlerTest {
+
+    @Test
+    public void matches() {
+        IndexDefinitionsEntryHandler handler = new IndexDefinitionsEntryHandler();
+        assertThat(handler.matches("/jcr_root/_oak_index/.content.xml")).isTrue();
+        assertThat(handler.matches("/jcr_root/_oak_index/bar/.content.xml")).isTrue();
+        assertThat(handler.matches("/jcr_root/_oak_index/lucene/tika/config.xml")).isTrue();
+        assertThat(handler.matches("/jcr_root/_oak_index/.vlt")).isFalse();
+        assertThat(handler.matches("/jcr_root/apps/_oak_index/.content.xml")).isFalse();
+    }
+
+    @Test
+    public void handleSingleFileIndexDefinition() throws IOException, ConverterException {
+
+        DefaultIndexManager manager = new DefaultIndexManager();
+
+        traverseForIndexing(manager, "index_single_file");
+
+        IndexDefinitions defs = manager.getIndexes();
+        List<DocViewNode2> indexes = defs.getIndexes();
+
+        assertThat(indexes).as("index definitions")
+            .hasSize(1)
+            .element(0)
+                .has( Conditions.localName("foo") )
+                .has( Conditions.property("type", "property") );
+
+    }
+
+    @Test
+    public void handleMultiFileIndexDefinition() throws IOException, ConverterException {
+
+        DefaultIndexManager manager = new DefaultIndexManager();
+
+        traverseForIndexing(manager, "index_multiple_files");
+
+        IndexDefinitions defs = manager.getIndexes();
+        List<DocViewNode2> indexes = defs.getIndexes();
+
+        assertThat(indexes).as("index definitions")
+            .hasSize(2);
+
+        assertThat(indexes).as("baz index")
+            .element(0).has( Conditions.localName("baz") );
+        assertThat(indexes).as("lucene_custom index")
+            .element(1)
+                .has( Conditions.localName("lucene_custom") )
+                .has( Conditions.property("type", "lucene") )
+                .has(Conditions.childWithLocalName("/oak:index/lucene_custom", "indexRules", defs));
+
+    }
+
+    @Test
+    public void handleIndexDefinitionWithNestedTikaXml() throws IOException, ConverterException, ParserConfigurationException, SAXException {
+        DefaultIndexManager manager = new DefaultIndexManager();
+
+        traverseForIndexing(manager, "index_nested_tika");
+
+        IndexDefinitions defs = manager.getIndexes();
+        List<DocViewNode2> indexes = defs.getIndexes();
+
+        assertThat(indexes).as("index definitions")
+            .hasSize(1)
+            .element(0)
+                .has(Conditions.localName("lucene-custom"));
+
+        DocViewNode2 luceneCustom = indexes.get(0);
+        assertThat(luceneCustom).as("lucene index definition")
+            .has(Conditions.childWithLocalName("/oak:index/lucene-custom", "indexRules", defs))
+            .has(Conditions.childWithLocalName("/oak:index/lucene-custom", "tika", defs));
+
+        List<DocViewNode2> luceneCustomChildren = defs.getChildren("/oak:index/lucene-custom");
+        assertThat(luceneCustomChildren).as("lucene index definition children")
+            .hasSize(2);
+
+        DocViewNode2 tikaConfigNode = luceneCustomChildren.stream()
+            .filter( c -> c.getName().getLocalName().equals("tika") )
+            .findFirst()
+            .get();
+
+        assertThat(tikaConfigNode).as("tika config node")
+            .has(Conditions.childWithLocalName("/oak:index/lucene-custom/tika","config.xml", defs));
+
+        List<DocViewNode2> children = defs.getChildren("/oak:index/lucene-custom/tika");
+        assertThat(children).as("tika config child nodes")
+            .hasSize(1)
+            .element(0)
+                .has( Conditions.localName("config.xml") )
+                .has( Conditions.property(NamespaceRegistry.NAMESPACE_JCR, "primaryType", "nt:file", defs) );
+
+        byte[] tikaConfig = defs.getBinary("/oak:index/lucene-custom/tika/config.xml").get();
+        assertIsValidXml(tikaConfig);
+    }
+
+
+    private void assertIsValidXml(byte[] tikeConfig) throws ParserConfigurationException, SAXException, IOException {
+
+        DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
+        DocumentBuilder documentBuilder = dbFactory.newDocumentBuilder();
+        documentBuilder.parse(new InputSource(new ByteArrayInputStream(tikeConfig)));
+    }
+
+    private void traverseForIndexing(DefaultIndexManager manager, String testPackageDirectory) throws IOException, ConverterException {
+
+        try ( Archive archive = new FileArchive(TestUtils.getPackageRelativeFile(getClass(), "index", testPackageDirectory)) ) {
+            archive.open(true);
+
+            try ( ContentPackage2FeatureModelConverter converter = new ContentPackage2FeatureModelConverter() ) {
+
+                converter.setMainPackageAssembler(Mockito.mock(VaultPackageAssembler.class))
+                    .setIndexManager(manager);
+                IndexDefinitionsEntryHandler handler = new IndexDefinitionsEntryHandler();
+
+                new BaseVaultPackageScanner(true) {
+                    @Override
+                    protected void onFile(@NotNull String path, @NotNull Archive archive, @NotNull Entry entry)
+                            throws IOException, ConverterException {
+                        if ( handler.matches(path) )
+                            handler.handle(path, archive, entry, converter);
+                    }
+                }.traverse(new ZipVaultPackage(archive, true));
+            }
+
+        }
+    }
+
+    static class Conditions {
+        static final Condition<DocViewNode2> localName(String localName) {
+            return new Condition<DocViewNode2>("Node with name " + localName) {
+                @Override
+                public boolean matches(DocViewNode2 value) {
+                    return value.getName().getLocalName().equals(localName);
+                }
+            };
+        }
+
+        static final Condition<DocViewNode2> childWithLocalName(String path, String childName, IndexDefinitions defs) {
+            return new Condition<DocViewNode2>("Node with a child with localName " + childName) {
+                @Override
+                public boolean matches(DocViewNode2 value) {
+                    return defs.getChildren(path).stream().filter( n -> n.getName().getLocalName().equals(childName)).findAny().isPresent();
+                }
+            };
+        }
+
+        static final Condition<DocViewNode2> property(String localPropertyName, String propertyValue) {
+            return new Condition<DocViewNode2>("Node with property '" + localPropertyName + "' equal to '" + propertyValue + "'") {
+                @Override
+                public boolean matches(DocViewNode2 value) {
+                    return value.getProperties().stream().
+                        anyMatch( p -> {
+                            return p.getName().getLocalName().equals(localPropertyName)
+                                    && p.getStringValue().isPresent() && Objects.equals(p.getStringValue().get(), propertyValue);
+                        });
+                }
+            };
+        }
+
+        static final Condition<DocViewNode2> property(String uri, String localPropertyName, String propertyValue, IndexDefinitions defs) {
+            return new Condition<DocViewNode2>("Node with property '{" + uri +"}" + localPropertyName + "' equal to '" + propertyValue + "'") {
+                @Override
+                public boolean matches(DocViewNode2 value) {
+                    return value.getProperties().stream().
+                            anyMatch( p -> {
+                                return p.getName().getLocalName().equals(localPropertyName)
+                                        && Objects.equals(p.getName().getNamespaceURI(), uri)
+                                        && p.getStringValue().isPresent() && Objects.equals(p.getStringValue().get(), propertyValue);
+                            });
+                }
+            };
+        }
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/feature/cpconverter/index/IndexDefinitionsJsonWriterTest.java b/src/test/java/org/apache/sling/feature/cpconverter/index/IndexDefinitionsJsonWriterTest.java
new file mode 100644
index 0000000..a68ccad
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/cpconverter/index/IndexDefinitionsJsonWriterTest.java
@@ -0,0 +1,190 @@
+/*
+ * 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.feature.cpconverter.index;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.entry;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import javax.jcr.NamespaceRegistry;
+import javax.jcr.PropertyType;
+import javax.json.Json;
+import javax.json.JsonObject;
+import javax.json.JsonString;
+import javax.json.JsonValue;
+import javax.json.stream.JsonParser;
+
+import org.apache.jackrabbit.spi.NameFactory;
+import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl;
+import org.apache.jackrabbit.util.Base64;
+import org.apache.jackrabbit.vault.util.DocViewNode2;
+import org.apache.jackrabbit.vault.util.DocViewProperty2;
+import org.assertj.core.api.Condition;
+import org.junit.Before;
+import org.junit.Test;
+
+public class IndexDefinitionsJsonWriterTest {
+
+    // copied from oak-spi-core/NamespaceConstants to not add a dependency on Oak
+    private static final String PREFIX_OAK = "oak";
+    private static final String NAMESPACE_OAK = "http://jackrabbit.apache.org/oak/ns/1.0";
+
+    private NameFactory nameFactory;
+    private IndexDefinitions definitions;
+
+    @Before
+    public void setUp() {
+        nameFactory = NameFactoryImpl.getInstance();
+        definitions = new IndexDefinitions();
+        definitions.registerPrefixMapping(NamespaceRegistry.PREFIX_NT, NamespaceRegistry.NAMESPACE_NT);
+        definitions.registerPrefixMapping(NamespaceRegistry.PREFIX_JCR, NamespaceRegistry.NAMESPACE_JCR);
+        definitions.registerPrefixMapping(PREFIX_OAK, NAMESPACE_OAK);
+
+    }
+
+    @Test
+    public void emptyInput() throws IOException {
+        JsonObject root = generateAndParse(definitions);
+        assertThat(root).as("index definitions").isEmpty();
+    }
+
+    @Test
+    public void propertyIndexDefinition() throws IOException {
+
+        Collection<DocViewProperty2> fooProps = new ArrayList<>();
+        fooProps.add(new DocViewProperty2(nameFactory.create("{}type"), "property"));
+        fooProps.add(new DocViewProperty2(nameFactory.create(NamespaceRegistry.NAMESPACE_JCR, "primaryType"), PREFIX_OAK+":QueryIndexDefinition"));
+        fooProps.add(new DocViewProperty2(nameFactory.create("{}reindex"), Boolean.FALSE.toString(), PropertyType.BOOLEAN));
+        fooProps.add(new DocViewProperty2(nameFactory.create("{}reindexCount"), "1", PropertyType.LONG));
+
+        definitions.addNode("/oak:index", new DocViewNode2(nameFactory.create("{}foo"), fooProps));
+
+        Collection<DocViewProperty2> barProps = new ArrayList<>();
+        fooProps.add(new DocViewProperty2(nameFactory.create("{}type"), "property"));
+        fooProps.add(new DocViewProperty2(nameFactory.create(NamespaceRegistry.NAMESPACE_JCR, "primaryType"), PREFIX_OAK+":QueryIndexDefinition"));
+        fooProps.add(new DocViewProperty2(nameFactory.create("{}reindex"), Boolean.TRUE.toString(), PropertyType.BOOLEAN));
+        fooProps.add(new DocViewProperty2(nameFactory.create("{}reindexCount"), "25", PropertyType.LONG));
+
+        definitions.addNode("/oak:index", new DocViewNode2(nameFactory.create("{}bar"), barProps));
+
+        JsonObject root = generateAndParse(definitions);
+        assertThat(root).as("indexDefinitions")
+            .hasSize(2)
+            .hasEntrySatisfying("/oak:index/foo", Conditions.isJsonObject())
+            .hasEntrySatisfying("/oak:index/bar", Conditions.isJsonObject());
+
+        JsonObject fooIndex = root.getJsonObject("/oak:index/foo");
+        assertThat(fooIndex).as("foo index")
+            .hasSize(4)
+            .contains(entry("type", Json.createValue("str:property")))
+            .contains(entry("jcr:primaryType", Json.createValue("nam:oak:QueryIndexDefinition")))
+            .contains(entry("reindex", JsonObject.FALSE))
+            .contains(entry("reindexCount", Json.createValue(1)));
+    }
+
+    @Test
+    public void luceneIndexDefinitionWithTikaConfig() throws IOException {
+
+        String configXmlFileContents = "<properties/>";
+
+        // lucene index
+        Collection<DocViewProperty2> luceneProps = new ArrayList<>();
+        luceneProps.add(new DocViewProperty2(nameFactory.create("{}type"), "lucene"));
+        luceneProps.add(new DocViewProperty2(nameFactory.create(NamespaceRegistry.NAMESPACE_JCR, "primaryType"), PREFIX_OAK+":QueryIndexDefinition"));
+        luceneProps.add(new DocViewProperty2(nameFactory.create("{}reindex"), Boolean.FALSE.toString(), PropertyType.BOOLEAN));
+        luceneProps.add(new DocViewProperty2(nameFactory.create("{}reindexCount"), "1", PropertyType.LONG));
+        luceneProps.add(new DocViewProperty2(nameFactory.create("{}includePropertyTypes"), Arrays.asList("String", "Binary"), PropertyType.STRING));
+
+        definitions.addNode("/oak:index", new DocViewNode2(nameFactory.create("{}lucene"), luceneProps));
+
+        // index rules node
+        List<DocViewProperty2> indexRulesProps = Collections.singletonList(new DocViewProperty2(nameFactory.create(NamespaceRegistry.NAMESPACE_JCR, "primaryType"), "nt:unstructured"));
+
+        definitions.addNode("/oak:index/lucene", new DocViewNode2(nameFactory.create("{}indexRules"), indexRulesProps));
+
+        // tika node
+        List<DocViewProperty2> tikaProps = Collections.singletonList(new DocViewProperty2(nameFactory.create(NamespaceRegistry.NAMESPACE_JCR, "primaryType"), "nt:unstructured"));
+
+        definitions.addNode("/oak:index/lucene", new DocViewNode2(nameFactory.create("{}tika"), tikaProps));
+
+        // tika config.xml node
+        List<DocViewProperty2> configXmlProps = Collections.singletonList(new DocViewProperty2(nameFactory.create(NamespaceRegistry.NAMESPACE_JCR, "primaryType"), "nt:file"));
+
+        definitions.addNode("/oak:index/lucene/tika", new DocViewNode2(nameFactory.create("{}config.xml"), configXmlProps));
+        definitions.registerBinary("/oak:index/lucene/tika/config.xml", new ByteArrayInputStream(configXmlFileContents.getBytes(StandardCharsets.UTF_8)));
+
+        // tika config.xml jcr:content node
+        List<DocViewProperty2> jcrContentProps = Collections.singletonList(new DocViewProperty2(nameFactory.create(NamespaceRegistry.NAMESPACE_JCR, "primaryType"), "nt:resource"));
+        definitions.addNode("/oak:index/lucene/tika/config.xml", new DocViewNode2(nameFactory.create(NamespaceRegistry.NAMESPACE_JCR, "resource"), jcrContentProps));
+
+        JsonObject root = generateAndParse(definitions);
+        System.out.println(root);
+
+        assertThat(root).as("root index")
+            .hasEntrySatisfying("/oak:index/lucene", Conditions.isJsonObject());
+
+        JsonObject lucene = root.getJsonObject("/oak:index/lucene");
+        assertThat(lucene).as("lucene index")
+            .hasEntrySatisfying("tika", Conditions.isJsonObject());
+
+        JsonObject tika = lucene.getJsonObject("tika");
+        assertThat(tika).as("tika node index")
+            .hasEntrySatisfying("config.xml", Conditions.isJsonObject());
+
+        JsonObject configNode = tika.getJsonObject("config.xml");
+        assertThat(configNode).as("config node")
+            .hasEntrySatisfying("jcr:resource", Conditions.isJsonObject());
+
+        JsonObject jcrResource = configNode.getJsonObject("jcr:resource");
+        JsonString binaryEntry = jcrResource.getJsonString("jcr:data");
+        assertThat(binaryEntry).as("config.xml blob")
+            .hasFieldOrPropertyWithValue("string", ":blobid:" + Base64.encode(configXmlFileContents));
+    }
+
+
+    private JsonObject generateAndParse(IndexDefinitions definitions) throws IOException {
+
+        IndexDefinitionsJsonWriter writer = new IndexDefinitionsJsonWriter(definitions);
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        writer.writeAsJson(out);
+
+        JsonParser parser = Json.createParser(new ByteArrayInputStream(out.toByteArray()));
+        JsonObject root = parser.getObject();
+        return root;
+    }
+
+    static class Conditions {
+        public static Condition<JsonValue> isJsonObject() {
+            return new Condition<JsonValue>("Is a " + JsonObject.class.getSimpleName()) {
+                @Override
+                public boolean matches(JsonValue value) {
+                    return value instanceof JsonObject;
+                }
+            };
+        }
+    }
+
+}
diff --git a/src/test/resources/org/apache/sling/feature/cpconverter/handlers/index/index_multiple_files/META-INF/vault/filter.xml b/src/test/resources/org/apache/sling/feature/cpconverter/handlers/index/index_multiple_files/META-INF/vault/filter.xml
new file mode 100644
index 0000000..9e1e22c
--- /dev/null
+++ b/src/test/resources/org/apache/sling/feature/cpconverter/handlers/index/index_multiple_files/META-INF/vault/filter.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+  -->
+<workspaceFilter version="1.0">
+    <filter root="/oak:index/baz"/>
+    <filter root="/oak:index/lucene_custom"/>
+</workspaceFilter>
diff --git a/src/test/resources/org/apache/sling/feature/cpconverter/handlers/index/index_multiple_files/META-INF/vault/nodetypes.cnd b/src/test/resources/org/apache/sling/feature/cpconverter/handlers/index/index_multiple_files/META-INF/vault/nodetypes.cnd
new file mode 100644
index 0000000..29762da
--- /dev/null
+++ b/src/test/resources/org/apache/sling/feature/cpconverter/handlers/index/index_multiple_files/META-INF/vault/nodetypes.cnd
@@ -0,0 +1,25 @@
+/*
+ * 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.
+ */
+
+<'sling'='http://sling.apache.org/jcr/sling/1.0'>
+<'nt'='http://www.jcp.org/jcr/nt/1.0'>
+
+[sling:Folder] > nt:folder
+  - * (undefined)
+  - * (undefined) multiple
+  + * (nt:base) = sling:Folder version
+
diff --git a/src/test/resources/org/apache/sling/feature/cpconverter/handlers/index/index_multiple_files/META-INF/vault/properties.xml b/src/test/resources/org/apache/sling/feature/cpconverter/handlers/index/index_multiple_files/META-INF/vault/properties.xml
new file mode 100644
index 0000000..8a7b22d
--- /dev/null
+++ b/src/test/resources/org/apache/sling/feature/cpconverter/handlers/index/index_multiple_files/META-INF/vault/properties.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8" standalone="no"?>
+<!--
+  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.
+  -->
+<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
+<properties>
+<comment>FileVault Package Properties</comment>
+<entry key="createdBy">admin</entry>
+<entry key="name">index_multiple_files</entry>
+<entry key="lastModified">2011-11-15T09:45:14.664+01:00</entry>
+<entry key="lastModifiedBy">admin</entry>
+<entry key="created">2011-11-15T09:45:14.685+01:00</entry>
+<entry key="buildCount">1</entry>
+<entry key="version"/>
+<entry key="dependencies"/>
+<entry key="packageFormatVersion">2</entry>
+<entry key="description"/>
+<entry key="lastWrapped">2011-11-15T09:45:14.664+01:00</entry>
+<entry key="group"/>
+<entry key="lastWrappedBy">admin</entry>
+</properties>
diff --git a/src/test/resources/org/apache/sling/feature/cpconverter/handlers/index/index_multiple_files/jcr_root/_oak_index/baz/.content.xml b/src/test/resources/org/apache/sling/feature/cpconverter/handlers/index/index_multiple_files/jcr_root/_oak_index/baz/.content.xml
new file mode 100644
index 0000000..2296ed9
--- /dev/null
+++ b/src/test/resources/org/apache/sling/feature/cpconverter/handlers/index/index_multiple_files/jcr_root/_oak_index/baz/.content.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+  -->
+<jcr:root xmlns:oak="http://jackrabbit.apache.org/oak/ns/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0" xmlns:rep="internal"
+    jcr:primaryType="oak:QueryIndexDefinition"
+    propertyNames="[foo]"
+    reindex="{Boolean}false"
+    reindexCount="{Long}1"
+    type="property">
+</jcr:root>
diff --git a/src/test/resources/org/apache/sling/feature/cpconverter/handlers/index/index_multiple_files/jcr_root/_oak_index/lucene_custom/.content.xml b/src/test/resources/org/apache/sling/feature/cpconverter/handlers/index/index_multiple_files/jcr_root/_oak_index/lucene_custom/.content.xml
new file mode 100644
index 0000000..cbb532c
--- /dev/null
+++ b/src/test/resources/org/apache/sling/feature/cpconverter/handlers/index/index_multiple_files/jcr_root/_oak_index/lucene_custom/.content.xml
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+  -->
+<jcr:root xmlns:oak="http://jackrabbit.apache.org/oak/ns/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0" xmlns:sling="http://sling.apache.org/jcr/sling/1.0"
+    jcr:primaryType="oak:QueryIndexDefinition"
+    async="[async]"
+    includePropertyTypes="[String,Binary]"
+    reindex="{Boolean}false"
+    reindexCount="{Long}1"
+    seed="{Long}4516265077047968953"
+    type="lucene">
+    <indexRules
+        jcr:primaryType="nt:unstructured">
+        <nt:base
+            jcr:primaryType="nt:unstructured"
+            includePropertyTypes="[String,Binary]">
+            <properties
+                jcr:primaryType="nt:unstructured">
+                <sling:alias
+                    jcr:primaryType="nt:unstructured"
+                    index="{Boolean}false"
+                    name="sling:alias"/>
+                <jcr:lastmodifiedby
+                    jcr:primaryType="nt:unstructured"
+                    index="{Boolean}false"
+                    name="jcr:lastmodifiedby"/>
+                <sling:resourcetype
+                    jcr:primaryType="nt:unstructured"
+                    index="{Boolean}false"
+                    name="sling:resourcetype"/>
+                <jcr:createdby
+                    jcr:primaryType="nt:unstructured"
+                    index="{Boolean}false"
+                    name="jcr:createdby"/>
+                <sling:vanitypath
+                    jcr:primaryType="nt:unstructured"
+                    index="{Boolean}false"
+                    name="sling:vanitypath"/>
+                <prop0
+                    jcr:primaryType="nt:unstructured"
+                    analyzed="{Boolean}true"
+                    isRegexp="{Boolean}true"
+                    name="^[^\\/]*$"
+                    nodeScopeIndex="{Boolean}true"
+                    propertyIndex="{Boolean}false"
+                    useInExcerpt="{Boolean}true"/>
+            </properties>
+        </nt:base>
+    </indexRules>
+</jcr:root>
diff --git a/src/test/resources/org/apache/sling/feature/cpconverter/handlers/index/index_nested_tika/META-INF/vault/filter.xml b/src/test/resources/org/apache/sling/feature/cpconverter/handlers/index/index_nested_tika/META-INF/vault/filter.xml
new file mode 100644
index 0000000..1da236d
--- /dev/null
+++ b/src/test/resources/org/apache/sling/feature/cpconverter/handlers/index/index_nested_tika/META-INF/vault/filter.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+  -->
+<workspaceFilter version="1.0">
+    <filter root="/oak:index/lucene-custom"/>
+</workspaceFilter>
diff --git a/src/test/resources/org/apache/sling/feature/cpconverter/handlers/index/index_nested_tika/META-INF/vault/nodetypes.cnd b/src/test/resources/org/apache/sling/feature/cpconverter/handlers/index/index_nested_tika/META-INF/vault/nodetypes.cnd
new file mode 100644
index 0000000..82c6bd6
--- /dev/null
+++ b/src/test/resources/org/apache/sling/feature/cpconverter/handlers/index/index_nested_tika/META-INF/vault/nodetypes.cnd
@@ -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.
+ */
+<'sling'='http://sling.apache.org/jcr/sling/1.0'>
+<'nt'='http://www.jcp.org/jcr/nt/1.0'>
+
+[sling:Folder] > nt:folder
+  - * (undefined)
+  - * (undefined) multiple
+  + * (nt:base) = sling:Folder version
+
diff --git a/src/test/resources/org/apache/sling/feature/cpconverter/handlers/index/index_nested_tika/META-INF/vault/properties.xml b/src/test/resources/org/apache/sling/feature/cpconverter/handlers/index/index_nested_tika/META-INF/vault/properties.xml
new file mode 100644
index 0000000..1032a7f
--- /dev/null
+++ b/src/test/resources/org/apache/sling/feature/cpconverter/handlers/index/index_nested_tika/META-INF/vault/properties.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8" standalone="no"?>
+<!--
+  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.
+  -->
+<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
+<properties>
+<comment>FileVault Package Properties</comment>
+<entry key="createdBy">admin</entry>
+<entry key="name">index_single_file</entry>
+<entry key="lastModified">2011-11-15T09:45:14.664+01:00</entry>
+<entry key="lastModifiedBy">admin</entry>
+<entry key="created">2011-11-15T09:45:14.685+01:00</entry>
+<entry key="buildCount">1</entry>
+<entry key="version"/>
+<entry key="dependencies"/>
+<entry key="packageFormatVersion">2</entry>
+<entry key="description"/>
+<entry key="lastWrapped">2011-11-15T09:45:14.664+01:00</entry>
+<entry key="group"/>
+<entry key="lastWrappedBy">admin</entry>
+</properties>
diff --git a/src/test/resources/org/apache/sling/feature/cpconverter/handlers/index/index_nested_tika/jcr_root/.content.xml b/src/test/resources/org/apache/sling/feature/cpconverter/handlers/index/index_nested_tika/jcr_root/.content.xml
new file mode 100644
index 0000000..741d31d
--- /dev/null
+++ b/src/test/resources/org/apache/sling/feature/cpconverter/handlers/index/index_nested_tika/jcr_root/.content.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+  -->
+<jcr:root xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:rep="internal" xmlns:sling="http://sling.apache.org/jcr/sling/1.0"
+    jcr:primaryType="rep:root"
+    sling:resourceType="sling:redirect"
+    sling:target="/starter.html"/>
diff --git a/src/test/resources/org/apache/sling/feature/cpconverter/handlers/index/index_nested_tika/jcr_root/_oak_index/.content.xml b/src/test/resources/org/apache/sling/feature/cpconverter/handlers/index/index_nested_tika/jcr_root/_oak_index/.content.xml
new file mode 100644
index 0000000..5db0614
--- /dev/null
+++ b/src/test/resources/org/apache/sling/feature/cpconverter/handlers/index/index_nested_tika/jcr_root/_oak_index/.content.xml
@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+  -->
+<jcr:root xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0" xmlns:oak="http://jackrabbit.apache.org/oak/ns/1.0" xmlns:sling="http://sling.apache.org/jcr/sling/1.0"
+    jcr:primaryType="nt:unstructured">
+    <jcrLanguage/>
+    <event.job.topic/>
+    <extensionType/>
+    <slingeventEventId/>
+    <repMembers/>
+    <counter/>
+    <acPrincipalName/>
+    <uuid/>
+    <slingVanityPath/>
+    <jcrLockOwner/>
+    <status/>
+    <type/>
+    <slingResource/>
+    <nodetype/>
+    <reference/>
+    <lucene-custom
+        jcr:primaryType="oak:QueryIndexDefinition"
+        async="async"
+        includePropertyTypes="[String,Binary]"
+        reindex="{Boolean}false"
+        reindexCount="{Long}3"
+        seed="{Long}8337987644672197141"
+        type="lucene">
+        <indexRules jcr:primaryType="nt:unstructured">
+            <nt:base
+                jcr:primaryType="nt:unstructured"
+                includePropertyTypes="[String,Binary]">
+                <properties jcr:primaryType="nt:unstructured">
+                    <sling:alias
+                        jcr:primaryType="nt:unstructured"
+                        index="{Boolean}false"
+                        name="sling:alias"/>
+                    <jcr:lastmodifiedby
+                        jcr:primaryType="nt:unstructured"
+                        index="{Boolean}false"
+                        name="jcr:lastmodifiedby"/>
+                    <sling:resourcetype
+                        jcr:primaryType="nt:unstructured"
+                        index="{Boolean}false"
+                        name="sling:resourcetype"/>
+                    <jcr:createdby
+                        jcr:primaryType="nt:unstructured"
+                        index="{Boolean}false"
+                        name="jcr:createdby"/>
+                    <sling:vanitypath
+                        jcr:primaryType="nt:unstructured"
+                        index="{Boolean}false"
+                        name="sling:vanitypath"/>
+                    <prop0
+                        jcr:primaryType="nt:unstructured"
+                        analyzed="{Boolean}true"
+                        isRegexp="{Boolean}true"
+                        name="^[^\\/]*$"
+                        nodeScopeIndex="{Boolean}true"
+                        propertyIndex="{Boolean}false"
+                        useInExcerpt="{Boolean}true"/>
+                </properties>
+            </nt:base>
+        </indexRules>
+        <tika jcr:primaryType="nt:unstructured">
+            <config.xml/>
+        </tika>
+    </lucene-custom>
+    <lockCreated/>
+    <principalName/>
+    <lucene/>
+    <slingAlias/>
+    <authorizableId/>
+    <slingResourceType/>
+</jcr:root>
diff --git a/src/test/resources/org/apache/sling/feature/cpconverter/handlers/index/index_nested_tika/jcr_root/_oak_index/lucene-custom/tika/config.xml b/src/test/resources/org/apache/sling/feature/cpconverter/handlers/index/index_nested_tika/jcr_root/_oak_index/lucene-custom/tika/config.xml
new file mode 100644
index 0000000..fa7c2be
--- /dev/null
+++ b/src/test/resources/org/apache/sling/feature/cpconverter/handlers/index/index_nested_tika/jcr_root/_oak_index/lucene-custom/tika/config.xml
@@ -0,0 +1,26 @@
+<!--
+  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.
+  -->
+<properties>
+  <parsers>
+    <parser class="org.apache.tika.parser.EmptyParser">
+      <mime>application/zip</mime>
+      <mime>application/msword</mime>
+      <mime>application/vnd.ms-excel</mime>
+      <mime>application/pdf</mime>
+    </parser>
+  </parsers>
+</properties>
diff --git a/src/test/resources/org/apache/sling/feature/cpconverter/handlers/index/index_nested_tika/jcr_root/_oak_index/lucene-custom/tika/config.xml.dir/.content.xml b/src/test/resources/org/apache/sling/feature/cpconverter/handlers/index/index_nested_tika/jcr_root/_oak_index/lucene-custom/tika/config.xml.dir/.content.xml
new file mode 100644
index 0000000..0436956
--- /dev/null
+++ b/src/test/resources/org/apache/sling/feature/cpconverter/handlers/index/index_nested_tika/jcr_root/_oak_index/lucene-custom/tika/config.xml.dir/.content.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+  -->
+<jcr:root xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
+    jcr:primaryType="nt:file">
+    <jcr:content
+        jcr:lastModifiedBy="admin"
+        jcr:mimeType="application/xml"
+        jcr:primaryType="nt:resource"/>
+</jcr:root>
diff --git a/src/test/resources/org/apache/sling/feature/cpconverter/handlers/index/index_single_file/META-INF/vault/filter.xml b/src/test/resources/org/apache/sling/feature/cpconverter/handlers/index/index_single_file/META-INF/vault/filter.xml
new file mode 100644
index 0000000..43772b3
--- /dev/null
+++ b/src/test/resources/org/apache/sling/feature/cpconverter/handlers/index/index_single_file/META-INF/vault/filter.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+  -->
+<workspaceFilter version="1.0">
+    <filter root="/oak:index/foo"/>
+</workspaceFilter>
diff --git a/src/test/resources/org/apache/sling/feature/cpconverter/handlers/index/index_single_file/META-INF/vault/nodetypes.cnd b/src/test/resources/org/apache/sling/feature/cpconverter/handlers/index/index_single_file/META-INF/vault/nodetypes.cnd
new file mode 100644
index 0000000..29762da
--- /dev/null
+++ b/src/test/resources/org/apache/sling/feature/cpconverter/handlers/index/index_single_file/META-INF/vault/nodetypes.cnd
@@ -0,0 +1,25 @@
+/*
+ * 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.
+ */
+
+<'sling'='http://sling.apache.org/jcr/sling/1.0'>
+<'nt'='http://www.jcp.org/jcr/nt/1.0'>
+
+[sling:Folder] > nt:folder
+  - * (undefined)
+  - * (undefined) multiple
+  + * (nt:base) = sling:Folder version
+
diff --git a/src/test/resources/org/apache/sling/feature/cpconverter/handlers/index/index_single_file/META-INF/vault/properties.xml b/src/test/resources/org/apache/sling/feature/cpconverter/handlers/index/index_single_file/META-INF/vault/properties.xml
new file mode 100644
index 0000000..1032a7f
--- /dev/null
+++ b/src/test/resources/org/apache/sling/feature/cpconverter/handlers/index/index_single_file/META-INF/vault/properties.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8" standalone="no"?>
+<!--
+  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.
+  -->
+<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
+<properties>
+<comment>FileVault Package Properties</comment>
+<entry key="createdBy">admin</entry>
+<entry key="name">index_single_file</entry>
+<entry key="lastModified">2011-11-15T09:45:14.664+01:00</entry>
+<entry key="lastModifiedBy">admin</entry>
+<entry key="created">2011-11-15T09:45:14.685+01:00</entry>
+<entry key="buildCount">1</entry>
+<entry key="version"/>
+<entry key="dependencies"/>
+<entry key="packageFormatVersion">2</entry>
+<entry key="description"/>
+<entry key="lastWrapped">2011-11-15T09:45:14.664+01:00</entry>
+<entry key="group"/>
+<entry key="lastWrappedBy">admin</entry>
+</properties>
diff --git a/src/test/resources/org/apache/sling/feature/cpconverter/handlers/index/index_single_file/jcr_root/.content.xml b/src/test/resources/org/apache/sling/feature/cpconverter/handlers/index/index_single_file/jcr_root/.content.xml
new file mode 100644
index 0000000..fd08de2
--- /dev/null
+++ b/src/test/resources/org/apache/sling/feature/cpconverter/handlers/index/index_single_file/jcr_root/.content.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+  -->
+<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:rep="internal"
+    jcr:mixinTypes="[rep:AccessControllable]"
+    jcr:primaryType="rep:root"
+    sling:resourceType="sling:redirect"
+    sling:target="/index.html">
+    <rep:policy/>
+    <jcr:system/>
+    <var/>
+    <libs/>
+    <etc/>
+    <apps/>
+    <content/>
+    <tmp/>
+    <home/>
+</jcr:root>
diff --git a/src/test/resources/org/apache/sling/feature/cpconverter/handlers/index/index_single_file/jcr_root/_oak_index/.content.xml b/src/test/resources/org/apache/sling/feature/cpconverter/handlers/index/index_single_file/jcr_root/_oak_index/.content.xml
new file mode 100644
index 0000000..3f1e4c7
--- /dev/null
+++ b/src/test/resources/org/apache/sling/feature/cpconverter/handlers/index/index_single_file/jcr_root/_oak_index/.content.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+  -->
+<jcr:root xmlns:oak="http://jackrabbit.apache.org/oak/ns/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0" xmlns:rep="internal"
+    jcr:mixinTypes="[rep:AccessControllable]"
+    jcr:primaryType="nt:unstructured">
+    <foo
+        jcr:primaryType="oak:QueryIndexDefinition"
+        propertyNames="[foo]"
+        reindex="{Boolean}false"
+        reindexCount="{Long}1"
+        type="property">
+    </foo>
+    <bar
+        jcr:primaryType="oak:QueryIndexDefinition"
+        propertyNames="[bar]"
+        reindex="{Boolean}false"
+        reindexCount="{Long}1"
+        type="property">
+    </bar>
+</jcr:root>