You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jackrabbit.apache.org by kw...@apache.org on 2021/05/05 06:38:53 UTC

[jackrabbit-filevault] branch master updated: JCRVLT-517 fix package cache init in OSGi containers (#136)

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

kwin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/jackrabbit-filevault.git


The following commit(s) were added to refs/heads/master by this push:
     new 3dba5a2  JCRVLT-517 fix package cache init in OSGi containers (#136)
3dba5a2 is described below

commit 3dba5a20e2aff2aee45ef8fb9c963372bb6d7d98
Author: Konrad Windszus <kw...@apache.org>
AuthorDate: Wed May 5 08:38:47 2021 +0200

    JCRVLT-517 fix package cache init in OSGi containers (#136)
    
    improve test coverage
    fix another issue in FSInstallState not correctly persisting excludes
    extract cache into dedicated class
---
 vault-core/pom.xml                                 |  28 +-
 .../jackrabbit/vault/fs/io/MemoryArchive.java      |   7 +-
 .../packaging/impl/SubPackageExportProcessor.java  |   7 +-
 .../vault/packaging/impl/ZipVaultPackage.java      |   3 +-
 .../vault/packaging/registry/PackageRegistry.java  |   8 +-
 .../registry/impl/AbstractPackageRegistry.java     |   6 +-
 .../packaging/registry/impl/FSInstallState.java    | 116 ++++--
 .../registry/impl/FSInstallStateCache.java         | 219 ++++++++++++
 .../packaging/registry/impl/FSPackageRegistry.java | 396 ++++++---------------
 .../packaging/registry/impl/FSPackageStatus.java   |   3 +-
 .../registry/impl/FSRegisteredPackage.java         |   9 +-
 .../registry/impl/JcrPackageRegistry.java          |  19 +-
 .../jackrabbit/vault/util/InputStreamPump.java     |  22 +-
 .../packaging/integration/PackageInstallIT.java    |  29 +-
 .../vault/packaging/integration/SubPackagesIT.java |   2 +-
 .../impl}/FSInstallStateTest.java                  |  39 +-
 .../impl}/FSPackageRegistryIT.java                 | 102 ++++--
 .../registry/impl/FSPackageRegistryTest.java       |  97 +++++
 .../registry/impl/FSRegisteredPackageTest.java     |   7 +-
 .../packaging/registry/impl/invalid-metadata.xml   | 174 +++++++++
 .../vault/packaging/registry/impl/test-package.xml |  28 ++
 .../test-packages/properties-with-0mtime.zip       | Bin 0 -> 453 bytes
 22 files changed, 908 insertions(+), 413 deletions(-)

diff --git a/vault-core/pom.xml b/vault-core/pom.xml
index 4466650..2bd9e1d 100644
--- a/vault-core/pom.xml
+++ b/vault-core/pom.xml
@@ -330,6 +330,12 @@
                                     <goal>integration-test</goal>
                                 </goals>
                             </execution>
+                            <execution>
+                                <id>verify-its</id>
+                                <goals>
+                                    <goal>verify</goal>
+                                </goals>
+                            </execution>
                         </executions>
                     </plugin>
                 </plugins>
@@ -351,6 +357,12 @@
                                     <goal>integration-test</goal>
                                 </goals>
                             </execution>
+                            <execution>
+                                <id>verify-its</id>
+                                <goals>
+                                    <goal>verify</goal>
+                                </goals>
+                            </execution>
                         </executions>
                     </plugin>
                 </plugins>
@@ -367,7 +379,7 @@
                         <artifactId>maven-failsafe-plugin</artifactId>
                         <executions>
                             <execution>
-                                <id>oak</id>
+                                <id>it-with-oak</id>
                                 <goals>
                                     <goal>integration-test</goal>
                                 </goals>
@@ -380,12 +392,24 @@
                                 </configuration>
                             </execution>
                             <execution>
-                                <id>jr</id>
+                                <id>it-with-jr</id>
                                 <goals>
                                     <goal>integration-test</goal>
                                 </goals>
                                 <configuration>
                                     <reportNameSuffix>JR</reportNameSuffix>
+                                    <summaryFile>${project.build.directory}/failsafe-reports/failsafe-summary-jr.xml</summaryFile>
+                                </configuration>
+                            </execution>
+                            <execution>
+                                <id>verify-its</id>
+                                <goals>
+                                    <goal>verify</goal>
+                                </goals>
+                                <configuration>
+                                    <summaryFiles>
+                                        <summaryFile>${project.build.directory}/failsafe-reports/failsafe-summary-jr.xml</summaryFile>
+                                    </summaryFiles>
                                 </configuration>
                             </execution>
                         </executions>
diff --git a/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/io/MemoryArchive.java b/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/io/MemoryArchive.java
index ad93d9a..7d21d7b 100644
--- a/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/io/MemoryArchive.java
+++ b/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/io/MemoryArchive.java
@@ -28,21 +28,20 @@ import java.util.zip.ZipEntry;
 import java.util.zip.ZipInputStream;
 
 import org.apache.commons.io.IOUtils;
+import org.apache.jackrabbit.util.Text;
 import org.apache.jackrabbit.vault.fs.api.VaultInputSource;
 import org.apache.jackrabbit.vault.fs.config.DefaultMetaInf;
 import org.apache.jackrabbit.vault.fs.config.MetaInf;
 import org.apache.jackrabbit.vault.fs.config.VaultSettings;
 import org.apache.jackrabbit.vault.util.Constants;
 import org.apache.jackrabbit.vault.util.InputStreamPump;
-import org.apache.jackrabbit.util.Text;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 /**
- * Implements a input stream pump that analyzes the stream copies the stream content into memory.
- * The memory archive is initialized via the {@link #run(InputStream)}.
+ * Implements an {@link org.apache.jackrabbit.vault.util.InputStreamPump.Pump} that extracts the relevant parts from the input stream into memory.
+ * The memory archive is initialized via the {@link #run(InputStream)} being called from {@link InputStreamPump}.
  */
 public class MemoryArchive extends AbstractArchive implements InputStreamPump.Pump {
 
diff --git a/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/impl/SubPackageExportProcessor.java b/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/impl/SubPackageExportProcessor.java
index 9a57b73..586c1eb 100644
--- a/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/impl/SubPackageExportProcessor.java
+++ b/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/impl/SubPackageExportProcessor.java
@@ -38,6 +38,7 @@ import org.apache.jackrabbit.vault.fs.io.AbstractExporter;
 import org.apache.jackrabbit.vault.packaging.ExportPostProcessor;
 import org.apache.jackrabbit.vault.packaging.JcrPackage;
 import org.apache.jackrabbit.vault.packaging.PackageId;
+import org.apache.jackrabbit.vault.packaging.registry.impl.AbstractPackageRegistry;
 import org.apache.jackrabbit.util.Text;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -73,7 +74,8 @@ public class SubPackageExportProcessor implements ExportPostProcessor {
                 if (Text.isDescendantOrEqual(DEFAULT_PACKAGE_ROOT_PATH, nodePath)) {
                     continue;
                 }
-                String etcPath = DEFAULT_PACKAGE_ROOT_PATH + mgr.getInternalRegistry().getRelativeInstallationPath(pkg.getKey()) + ".zip";
+                mgr.getInternalRegistry();
+                String etcPath = DEFAULT_PACKAGE_ROOT_PATH + "/" + AbstractPackageRegistry.getRelativeInstallationPath(pkg.getKey()) + ".zip";
                 etcPath = Text.getRelativeParent(etcPath, 1);
 
                 // define a workspace filter for the package at the real location
@@ -148,7 +150,8 @@ public class SubPackageExportProcessor implements ExportPostProcessor {
 
             // re-add all the packages in /etc/packages
             for (Map.Entry<PackageId, String> pkg : subPackages.entrySet()) {
-                String path = DEFAULT_PACKAGE_ROOT_PATH + mgr.getInternalRegistry().getRelativeInstallationPath(pkg.getKey()) + ".zip";
+                mgr.getInternalRegistry();
+                String path = DEFAULT_PACKAGE_ROOT_PATH + "/" + AbstractPackageRegistry.getRelativeInstallationPath(pkg.getKey()) + ".zip";
                 newFilter.add(new PathFilterSet(path));
             }
 
diff --git a/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/impl/ZipVaultPackage.java b/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/impl/ZipVaultPackage.java
index 8ff075a..85d9df8 100644
--- a/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/impl/ZipVaultPackage.java
+++ b/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/impl/ZipVaultPackage.java
@@ -17,6 +17,7 @@
 
 package org.apache.jackrabbit.vault.packaging.impl;
 
+import java.io.Closeable;
 import java.io.File;
 import java.io.IOException;
 import java.util.List;
@@ -48,7 +49,7 @@ import org.slf4j.LoggerFactory;
  * Implements a vault package that is a zipped representation of a file vault
  * export.
  */
-public class ZipVaultPackage extends PackagePropertiesImpl implements VaultPackage {
+public class ZipVaultPackage extends PackagePropertiesImpl implements VaultPackage, Closeable {
 
     private static final Logger log = LoggerFactory.getLogger(ZipVaultPackage.class);
 
diff --git a/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/registry/PackageRegistry.java b/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/registry/PackageRegistry.java
index 6f3f480..2a799ef 100644
--- a/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/registry/PackageRegistry.java
+++ b/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/registry/PackageRegistry.java
@@ -42,14 +42,14 @@ public interface PackageRegistry {
      * Checks if this registry contains the package with the given id.
      * @param id the package id.
      * @return {@code true} if the package is registered.
-     * @throws IOException if an I/O error occurrs.
+     * @throws IOException if an I/O error occurs.
      */
     boolean contains(@NotNull PackageId id) throws IOException;
 
     /**
      * Returns as set of all packages registered in this registry.
      * @return a set of package ids.
-     * @throws IOException if an I/O error occurrs.
+     * @throws IOException if an I/O error occurs.
      */
     @NotNull
     Set<PackageId> packages() throws IOException;
@@ -58,7 +58,7 @@ public interface PackageRegistry {
      * Opens the package with the given id.
      * @param id the package id
      * @return the package or {@code null} if it does not exists.
-     * @throws IOException if an I/O error occurrs.
+     * @throws IOException if an I/O error occurs.
      */
     @Nullable
     RegisteredPackage open(@NotNull PackageId id) throws IOException;
@@ -70,7 +70,7 @@ public interface PackageRegistry {
      * @param in the input stream to the package data
      * @param replace {@code true} if existing package should be replaced.
      * @return the new package id.
-     * @throws IOException if an I/O error occurrs.
+     * @throws IOException if an I/O error occurs.
      * @throws PackageExistsException if the package exists and {@code replace} is {@code false}.
      */
     @NotNull
diff --git a/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/registry/impl/AbstractPackageRegistry.java b/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/registry/impl/AbstractPackageRegistry.java
index 6125339..858a9ad 100644
--- a/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/registry/impl/AbstractPackageRegistry.java
+++ b/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/registry/impl/AbstractPackageRegistry.java
@@ -192,7 +192,7 @@ public abstract class AbstractPackageRegistry implements PackageRegistry, Intern
     @NotNull
     @Override
     public PackageId[] usage(PackageId id) throws IOException {
-        TreeSet<PackageId> usages = new TreeSet<PackageId>();
+        TreeSet<PackageId> usages = new TreeSet<>();
         for (PackageId pid : packages()) {
             try (RegisteredPackage pkg = open(pid)) {
                 if (pkg == null || !pkg.isInstalled()) {
@@ -227,8 +227,8 @@ public abstract class AbstractPackageRegistry implements PackageRegistry, Intern
      * @return the relative path of this package
      * @since 2.2
      */
-    public String getRelativeInstallationPath(PackageId id) {
-        StringBuilder b = new StringBuilder("/");
+    public static String getRelativeInstallationPath(PackageId id) {
+        StringBuilder b = new StringBuilder();
         if (id.getGroup().length() > 0) {
             b.append(id.getGroup());
             b.append("/");
diff --git a/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/registry/impl/FSInstallState.java b/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/registry/impl/FSInstallState.java
index 2435beb..1946f3f 100644
--- a/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/registry/impl/FSInstallState.java
+++ b/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/registry/impl/FSInstallState.java
@@ -16,12 +16,13 @@
  */
 package org.apache.jackrabbit.vault.packaging.registry.impl;
 
-import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -34,7 +35,6 @@ import javax.xml.parsers.DocumentBuilderFactory;
 import javax.xml.parsers.ParserConfigurationException;
 import javax.xml.stream.XMLStreamException;
 
-import org.apache.commons.io.FileUtils;
 import org.apache.jackrabbit.vault.fs.api.FilterSet.Entry;
 import org.apache.jackrabbit.vault.fs.api.PathFilter;
 import org.apache.jackrabbit.vault.fs.api.PathFilterSet;
@@ -60,7 +60,7 @@ import org.w3c.dom.NodeList;
 import org.xml.sax.SAXException;
 
 /**
- * Internal (immutable) State object to cache and pass the relevant metadata around.
+ * Internal (immutable) state object for a package to cache and pass the relevant metadata around.
  */
 public class FSInstallState {
 
@@ -98,9 +98,9 @@ public class FSInstallState {
 
     private static final String TAG_PACKAGEPROPERTIES = "packageproperties";
 
-    private final PackageId packageId;
-    private final FSPackageStatus status;
-    private Path filePath;
+    private final @NotNull PackageId packageId;
+    private final @NotNull FSPackageStatus status;
+    private final @NotNull Path filePath;
     private boolean external;
     private Set<Dependency> dependencies = Collections.emptySet();
     private Map<PackageId, SubPackageHandling.Option> subPackages = Collections.emptyMap();
@@ -109,14 +109,10 @@ public class FSInstallState {
     private WorkspaceFilter filter;
     private Properties properties = new Properties();
 
-    public FSInstallState(@NotNull PackageId pid, @NotNull FSPackageStatus status) {
+    public FSInstallState(@NotNull PackageId pid, @NotNull FSPackageStatus status, @NotNull Path filePath) {
         this.packageId = pid;
         this.status = status;
-    }
-
-    public FSInstallState withFilePath(Path filePath) {
         this.filePath = filePath;
-        return this;
     }
 
     public FSInstallState withExternal(boolean external) {
@@ -167,12 +163,12 @@ public class FSInstallState {
      * @throws IOException in case root tag is correct but structure not parsable as expected
      */
     @Nullable
-    public static FSInstallState fromFile(File metaFile) throws IOException {
-        if (!metaFile.exists()) {
+    public static FSInstallState fromFile(Path metaFile) throws IOException {
+        if (!Files.exists(metaFile)) {
             return null;
         }
-        try (InputStream in = FileUtils.openInputStream(metaFile)) {
-            return fromStream(in, metaFile.getPath());
+        try (InputStream in = Files.newInputStream(metaFile)) {
+            return fromStream(in, metaFile.toString());
         }
     }
 
@@ -232,8 +228,7 @@ public class FSInstallState {
                 }
 
             }
-            return new FSInstallState(PackageId.fromString(packageId), status)
-                    .withFilePath(filePath)
+            return new FSInstallState(PackageId.fromString(packageId), status, filePath)
                     .withExternal(external)
                     .withSize(size)
                     .withFilter(filter)
@@ -287,7 +282,7 @@ public class FSInstallState {
                             DefaultPathFilter pf = new DefaultPathFilter(((Element) rule).getAttribute(ATTR_INCLUDE));
                             pfs.addInclude(pf);
                         } else if (((Element) rule).hasAttribute(ATTR_EXCLUDE)) {
-                            DefaultPathFilter pf = new DefaultPathFilter(((Element) rule).getAttribute(ATTR_INCLUDE));
+                            DefaultPathFilter pf = new DefaultPathFilter(((Element) rule).getAttribute(ATTR_EXCLUDE));
                             pfs.addExclude(pf);
                         }
                     }
@@ -305,8 +300,9 @@ public class FSInstallState {
      * @param file The files to save the state to
      * @throws IOException if an error occurs.
      */
-    public void save(File file) throws IOException {
-        try (OutputStream out = FileUtils.openOutputStream(file)) {
+    public void save(Path file) throws IOException {
+        Files.createDirectories(file.getParent());
+        try (OutputStream out = Files.newOutputStream(file, StandardOpenOption.CREATE)) {
             save(out);
         }
     }
@@ -417,4 +413,84 @@ public class FSInstallState {
     public Properties getProperties() {
         return properties;
     }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((dependencies == null) ? 0 : dependencies.hashCode());
+        result = prime * result + (external ? 1231 : 1237);
+        result = prime * result + ((filePath == null) ? 0 : filePath.hashCode());
+        result = prime * result + ((filter == null) ? 0 : filter.hashCode());
+        result = prime * result + ((installTime == null) ? 0 : installTime.hashCode());
+        result = prime * result + ((packageId == null) ? 0 : packageId.hashCode());
+        result = prime * result + ((properties == null) ? 0 : properties.hashCode());
+        result = prime * result + (int) (size ^ (size >>> 32));
+        result = prime * result + ((status == null) ? 0 : status.hashCode());
+        result = prime * result + ((subPackages == null) ? 0 : subPackages.hashCode());
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj)
+            return true;
+        if (obj == null)
+            return false;
+        if (getClass() != obj.getClass())
+            return false;
+        FSInstallState other = (FSInstallState) obj;
+        if (dependencies == null) {
+            if (other.dependencies != null)
+                return false;
+        } else if (!dependencies.equals(other.dependencies))
+            return false;
+        if (external != other.external)
+            return false;
+        if (filePath == null) {
+            if (other.filePath != null)
+                return false;
+        } else if (!filePath.equals(other.filePath))
+            return false;
+        if (filter == null) {
+            if (other.filter != null)
+                return false;
+        } else if (!filter.equals(other.filter))
+            return false;
+        if (installTime == null) {
+            if (other.installTime != null)
+                return false;
+        } else if (!installTime.equals(other.installTime))
+            return false;
+        if (packageId == null) {
+            if (other.packageId != null)
+                return false;
+        } else if (!packageId.equals(other.packageId))
+            return false;
+        if (properties == null) {
+            if (other.properties != null)
+                return false;
+        } else if (!properties.equals(other.properties))
+            return false;
+        if (size != other.size)
+            return false;
+        if (status != other.status)
+            return false;
+        if (subPackages == null) {
+            if (other.subPackages != null)
+                return false;
+        } else if (!subPackages.equals(other.subPackages))
+            return false;
+        return true;
+    }
+
+    @Override
+    public String toString() {
+        return "FSInstallState [" + (packageId != null ? "packageId=" + packageId + ", " : "")
+                + (status != null ? "status=" + status + ", " : "") + (filePath != null ? "filePath=" + filePath + ", " : "") + "external="
+                + external + ", " + (dependencies != null ? "dependencies=" + dependencies + ", " : "")
+                + (subPackages != null ? "subPackages=" + subPackages + ", " : "")
+                + (installTime != null ? "installTime=" + installTime + ", " : "") + "size=" + size + ", "
+                + (filter != null ? "filter=" + filter + ", " : "") + (properties != null ? "properties=" + properties : "") + "]";
+    }
 }
\ No newline at end of file
diff --git a/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/registry/impl/FSInstallStateCache.java b/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/registry/impl/FSInstallStateCache.java
new file mode 100644
index 0000000..fb019d2
--- /dev/null
+++ b/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/registry/impl/FSInstallStateCache.java
@@ -0,0 +1,219 @@
+/*
+ * 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.jackrabbit.vault.packaging.registry.impl;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.AbstractMap;
+import java.util.Calendar;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Stream;
+
+import org.apache.jackrabbit.vault.packaging.PackageId;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Persisted cache of all {@link FSInstallState} objects for all packages in a registry.
+ * Populated on demand and written back immediately for every modifying operation.
+ * Is thread-safe.
+ */
+class FSInstallStateCache extends AbstractMap<PackageId, FSInstallState> {
+
+    /** Wraps a checked IOExceptioin in a unchecked exception, this is potentially thrown from all Map operations */
+    final class UncheckedIOException extends RuntimeException {
+        /**
+         * 
+         */
+        private static final long serialVersionUID = 1188317232809121358L;
+        private final IOException ioException;
+
+        public UncheckedIOException(IOException ioException) {
+            super(ioException.getMessage(), ioException);
+            this.ioException = ioException;
+        }
+
+        public IOException getIOException() {
+            return ioException;
+        }
+    }
+
+    /**
+     * Extension for metadata files
+     */
+    private static final String META_EXTENSION = ".xml";
+
+    private final Map<PackageId, FSInstallState> cache = new ConcurrentHashMap<>();
+    private boolean isInitialized = false;
+
+    /**
+     * Contains a map of all filesystem paths to package IDs
+     */
+    private Map<Path, PackageId> pathIdMapping = new ConcurrentHashMap<>();
+
+    private final Path homeDir;
+    
+    public FSInstallStateCache(Path homeDir) throws IOException {
+        this.homeDir = homeDir;
+        Files.createDirectories(homeDir);
+    }
+
+    /**
+     * Loads all state from files persisted in configured homeDir, adds to cache and returns all cached {@code PackageId}s.
+     * @throws IOException 
+     */
+    private synchronized void load() throws IOException {
+        Map<PackageId, FSInstallState> cacheEntries = new HashMap<>();
+        Map<Path, PackageId> idMapping = new HashMap<>();
+
+        // recursively find meta file
+        try (Stream<Path> stream = Files.walk(homeDir, 10)) {
+            stream.filter(Files::isRegularFile).filter(p -> p.toString().endsWith(META_EXTENSION)).forEach(
+                p -> {
+                    FSInstallState state;
+                    try {
+                        state = FSInstallState.fromFile(p);
+                    } catch (IOException e) {
+                        throw new UncheckedIOException(e);
+                    }
+                    if (state != null) {
+                        PackageId id = state.getPackageId();
+                        if (id != null) {
+                            cacheEntries.put(id, state);
+                            idMapping.put(state.getFilePath(), id);
+                        }
+                    }
+                }
+            );
+        }
+
+        cache.putAll(cacheEntries);
+        pathIdMapping.putAll(idMapping);
+        isInitialized = true;
+    }
+
+    @Override
+    public Set<Entry<PackageId, FSInstallState>> entrySet() {
+        if (!isInitialized) {
+            try {
+                load();
+            } catch (IOException e) {
+                throw new UncheckedIOException(e);
+            }
+        }
+        return cache.entrySet();
+    }
+
+    /**
+     * Returns the meta data file of the package with the given Id.
+     *
+     * @param id The package Id.
+     * @return the meta data file.
+     */
+    @NotNull
+    private Path getPackageMetaDataFile(@NotNull PackageId id) {
+        final String path = AbstractPackageRegistry.getRelativeInstallationPath(id);
+        return homeDir.resolve(path + ".xml");
+    }
+
+    @NotNull
+    public Path getPackageFile(@NotNull PackageId id) {
+        String path = AbstractPackageRegistry.getRelativeInstallationPath(id);
+        return homeDir.resolve(path + ".zip");
+    }
+
+
+    /**
+     * Shortcut to just change the status of a package - implicitly sets the installtime when switching to EXTRACTED
+     *
+     * @param pid PackageId of the package to update
+     * @param targetStatus Status to update
+     * @throws IOException If an I/O error occurs.
+     */
+    public void updatePackageStatus(PackageId pid, FSPackageStatus targetStatus) throws IOException {
+        FSInstallState state = get(pid);
+        if (state == null) {
+            throw new IllegalArgumentException("No package with pid " + pid + " registered");
+        }
+        Long installTime = state.getInstallationTime();
+        if (FSPackageStatus.EXTRACTED == targetStatus) {
+            installTime = Calendar.getInstance().getTimeInMillis();
+        }
+        FSInstallState targetState = new FSInstallState(pid, targetStatus, state.getFilePath())
+              .withDependencies(state.getDependencies())
+              .withSubPackages(state.getSubPackages())
+              .withInstallTime(installTime)
+              .withSize(state.getSize())
+              .withProperties(state.getProperties())
+              .withExternal(state.isExternal());
+        put(pid, targetState);
+    }
+
+    @Override
+    public FSInstallState get(Object key) {
+        FSInstallState state = super.get(key);
+        if (state == null) {
+            PackageId pid = (PackageId) key;
+            // fallback (only for get(..), but does not affect size(), entrySet(), hasKey(), keys(), values()), detects changes on the filesystem done outside this class
+            Path metaFile = getPackageMetaDataFile(pid);
+            try {
+                state = FSInstallState.fromFile(metaFile);
+            } catch (IOException e) {
+                throw new UncheckedIOException(e);
+            }
+            if (state != null) {
+                cache.put(pid, state);
+                pathIdMapping.put(state.getFilePath(), pid);
+            }
+        }
+        return state;
+    }
+
+    @Override
+    public FSInstallState put(PackageId key, FSInstallState value) {
+        FSInstallState state = cache.put(key, value);
+        // persist changes
+        try {
+            value.save(getPackageMetaDataFile(key));
+        } catch (IOException e) {
+            throw new UncheckedIOException(e);
+        }
+        return state;
+    }
+
+    @Override
+    public FSInstallState remove(Object key) {
+        FSInstallState state = super.remove(key);
+        if (state != null) {
+            PackageId pid = (PackageId) key;
+            Path metaData = getPackageMetaDataFile(pid);
+            try {
+                Files.delete(metaData);
+            } catch (IOException e) {
+                throw new UncheckedIOException(e);
+            }
+        }
+        return state;
+    }
+
+    public PackageId getIdForFile(Path file) {
+        return pathIdMapping.get(file);
+    }
+}
diff --git a/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/registry/impl/FSPackageRegistry.java b/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/registry/impl/FSPackageRegistry.java
index f8c19c6..b2c793d 100644
--- a/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/registry/impl/FSPackageRegistry.java
+++ b/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/registry/impl/FSPackageRegistry.java
@@ -19,9 +19,10 @@ package org.apache.jackrabbit.vault.packaging.registry.impl;
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
+import java.nio.file.Files;
 import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
 import java.util.Arrays;
-import java.util.Calendar;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -29,12 +30,11 @@ import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
 
 import javax.jcr.RepositoryException;
 import javax.jcr.Session;
 
-import org.apache.commons.io.FileUtils;
+import org.apache.jackrabbit.util.Text;
 import org.apache.jackrabbit.vault.fs.api.PathFilterSet;
 import org.apache.jackrabbit.vault.fs.api.WorkspaceFilter;
 import org.apache.jackrabbit.vault.fs.config.DefaultWorkspaceFilter;
@@ -60,7 +60,6 @@ import org.apache.jackrabbit.vault.packaging.registry.PackageRegistry;
 import org.apache.jackrabbit.vault.packaging.registry.RegisteredPackage;
 import org.apache.jackrabbit.vault.util.InputStreamPump;
 import org.apache.jackrabbit.vault.util.PlatformNameFormat;
-import org.apache.jackrabbit.util.Text;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 import org.osgi.framework.BundleContext;
@@ -87,39 +86,21 @@ import org.slf4j.LoggerFactory;
 @Designate(ocd = FSPackageRegistry.Config.class)
 public class FSPackageRegistry extends AbstractPackageRegistry {
 
-    private static final String REPOSITORY_HOME = "repository.home";
+    protected static final String REPOSITORY_HOME = "repository.home";
 
     /**
      * default logger
      */
     private static final Logger log = LoggerFactory.getLogger(FSPackageRegistry.class);
 
-    /**
-     * Suffixes for metadata files
-     */
-    private final String[] META_SUFFIXES = {"xml"};
-
-    private Map<PackageId, FSInstallState> stateCache = new ConcurrentHashMap<>();
-
-    /**
-     * Contains a map of all filesystem paths to package IDs
-     */
-    private Map<Path, PackageId> pathIdMapping = new ConcurrentHashMap<>();
-
+    private FSInstallStateCache stateCache;
 
-    private boolean packagesInitializied = false;
 
     @Reference
     private PackageEventDispatcher dispatcher;
 
-    private File homeDir;
-
     private InstallationScope scope = InstallationScope.UNSCOPED;
 
-    private File getHomeDir() {
-        return homeDir;
-    }
-
     /**
      * Creates a new FSPackageRegistry based on the given home directory.
      *
@@ -160,10 +141,9 @@ public class FSPackageRegistry extends AbstractPackageRegistry {
 
     public FSPackageRegistry(@NotNull File homeDir, InstallationScope scope, @Nullable AbstractPackageRegistry.SecurityConfig securityConfig, boolean isStrict) throws IOException {
         super(securityConfig, isStrict);
-        this.homeDir = homeDir;
-        log.info("Jackrabbit Filevault FS Package Registry initialized with home location {}", this.homeDir.getPath());
+        log.info("Jackrabbit Filevault FS Package Registry initialized with home location {}", homeDir.getPath());
         this.scope = scope;
-        loadPackageCache();
+        this.stateCache = new FSInstallStateCache(homeDir.toPath());
     }
 
     /**
@@ -175,16 +155,17 @@ public class FSPackageRegistry extends AbstractPackageRegistry {
     }
 
     @Activate
-    public void activate(BundleContext context, Config config) {
-        this.homeDir = context.getProperty(REPOSITORY_HOME) != null ? ( 
+    public void activate(BundleContext context, Config config) throws IOException {
+        File homeDir = context.getProperty(REPOSITORY_HOME) != null ? ( 
                 new File(config.homePath()).isAbsolute() ? new File(config.homePath()) : new File(context.getProperty(REPOSITORY_HOME) + "/" + config.homePath())) : 
                 context.getDataFile(config.homePath());
         if (!homeDir.exists()) {
             homeDir.mkdirs();
         }
-        log.info("Jackrabbit Filevault FS Package Registry initialized with home location {}", this.homeDir.getPath());
+        log.info("Jackrabbit Filevault FS Package Registry initialized with home location {}", homeDir.getPath());
         this.scope = InstallationScope.valueOf(config.scope());
         this.securityConfig = new AbstractPackageRegistry.SecurityConfig(config.authIdsForHookExecution(), config.authIdsForRootInstallation());
+        this.stateCache = new FSInstallStateCache(homeDir.toPath());
     }
 
     @ObjectClassDefinition(
@@ -199,7 +180,7 @@ public class FSPackageRegistry extends AbstractPackageRegistry {
                 description = "Allows to limit the installation scope of this Apache Jackrabbit FS Package Registry Service. "
                         + "Packages installed from this registry may be unscoped (unfiltered), "
                         + "application scoped (only content for /apps & /libs) "
-                        + "or content scoped (all content despite of /libs & /apps)",
+                        + "or content scoped (all content except for /libs & /apps)",
                 options = {
                     @Option(label = "Unscoped", value = "UNSCOPED"),
                     @Option(label = "Application Scoped", value = "APPLICATION_SCOPED"),
@@ -241,68 +222,57 @@ public class FSPackageRegistry extends AbstractPackageRegistry {
     @Override
     public RegisteredPackage open(@NotNull PackageId id) throws IOException {
         FSInstallState state = getInstallState(id);
-        return FSPackageStatus.NOTREGISTERED != state.getStatus() ? new FSRegisteredPackage(this, state) : null;
+        return state != null ? new FSRegisteredPackage(this, state) : null;
     }
 
     @Override
     public boolean contains(@NotNull PackageId id) throws IOException {
-        return stateCache.containsKey(id);
+        return getInstallState(id) != null; // don't use hasKey as otherwise there is no fallback for lazily loading metadata files
     }
 
-    @Nullable
-    private File getPackageFile(@NotNull PackageId id) {
+    @Nullable 
+    FSInstallState getInstallState(@NotNull PackageId id) throws IOException {
         try {
-            FSInstallState state = getInstallState(id);
-            if (FSPackageStatus.NOTREGISTERED == state.getStatus()) {
-                return buildPackageFile(id);
-            } else {
-                return state.getFilePath().toFile();
-            }
-        } catch (IOException e) {
-            log.error("Couldn't get install state of packageId {}", id, e);
+            return stateCache.get(id);
+        } catch (FSInstallStateCache.UncheckedIOException e) {
+            throw e.getIOException();
         }
-        return null;
-    }
-
-    private File buildPackageFile(@NotNull PackageId id) {
-        String path = getInstallationPath(id);
-        return new File(getHomeDir(), path + ".zip");
     }
 
     /**
-     * Returns the meta data file of the package with the given Id.
-     *
-     * @param id The package Id.
-     * @return the meta data file.
+     * 
+     * @param id
+     * @return the file pointing to an existing or new package with the given id
+     * @throws IOException
      */
     @NotNull
-    private File getPackageMetaDataFile(@NotNull PackageId id) {
-        final String path = getInstallationPath(id);
-        return new File(getHomeDir(), path + ".xml");
+    private Path getPackageFile(@NotNull PackageId id) throws IOException {
+        FSInstallState state = getInstallState(id);
+        if (state == null) {
+            return stateCache.getPackageFile(id);
+        } else {
+            return state.getFilePath();
+        }
     }
 
     /**
      * Opens the package of a file with the given Id.
      * @param id The Id of package file.
      * @return the package
-     * @throws IOException if an I/O error occurrs.
+     * @throws IOException if an I/O error occurs.
      */
     @NotNull
-    protected VaultPackage openPackageFile(@NotNull PackageId id) throws IOException {
-        File pkg = getPackageFile(id);
-        if (pkg == null) {
-            throw new IOException("Could not find package file for id " + id);
-        }
+    protected VaultPackage openPackageFile(@NotNull PackageId id) throws IOException, NoSuchPackageException {
+        Path pkg = getPackageFile(id);
 
-        if (pkg.exists() && pkg.length() > 0) {
-            try {
-                return new ZipVaultPackage(pkg, false, true);
-            } catch (IOException e) {
-                log.error("Cloud not open file {} as ZipVaultPackage.", pkg.getPath(), e);
-                throw e;
-            }
+        if (Files.exists(pkg) && Files.size(pkg) > 0) {
+            return new ZipVaultPackage(pkg.toFile(), false, true);
         } else {
-            return new HollowVaultPackage(getInstallState(id).getProperties());
+            FSInstallState state = getInstallState(id);
+            if (state == null) {
+                throw new NoSuchPackageException().setId(id);
+            }
+            return new HollowVaultPackage(state.getProperties());
         }
     }
 
@@ -314,8 +284,8 @@ public class FSPackageRegistry extends AbstractPackageRegistry {
     public DependencyReport analyzeDependencies(@NotNull PackageId id, boolean onlyInstalled) throws IOException, NoSuchPackageException {
         List<Dependency> unresolved = new LinkedList<>();
         List<PackageId> resolved = new LinkedList<>();
-        FSInstallState state = getInstallState(id);
-        if (FSPackageStatus.NOTREGISTERED == state.getStatus()) {
+        FSInstallState state = stateCache.get(id);
+        if (state == null) {
             throw new NoSuchPackageException().setId(id);
         }
 
@@ -369,8 +339,11 @@ public class FSPackageRegistry extends AbstractPackageRegistry {
      * @throws IOException If an I/O error occurs.
      */
     boolean isInstalled(PackageId id) throws IOException {
-        FSPackageStatus status = getInstallState(id).getStatus();
-        return FSPackageStatus.EXTRACTED == status;
+        FSInstallState state = getInstallState(id);
+        if (state != null) {
+            return FSPackageStatus.EXTRACTED == state.getStatus();
+        }
+        return false;
     }
 
     /**
@@ -381,30 +354,26 @@ public class FSPackageRegistry extends AbstractPackageRegistry {
     public PackageId register(@NotNull InputStream in, boolean replace) throws IOException, PackageExistsException {
       return register(in, replace, null);
     }
-    
-    /**
-     * {@inheritDoc}
-     */
+
     @NotNull
     private PackageId register(@NotNull InputStream in, boolean replace, Dependency autoDependency) throws IOException, PackageExistsException {
         ZipVaultPackage pkg = upload(in, replace);
 
         Map<PackageId, SubPackageHandling.Option> subpackages = registerSubPackages(pkg, replace);
-        File pkgFile = buildPackageFile(pkg.getId());
+        Path pkgFile = getPackageFile(pkg.getId());
         HashSet<Dependency> dependencies = new HashSet<>();
         dependencies.addAll(Arrays.asList(pkg.getDependencies()));
         if (autoDependency != null) {
             dependencies.add(autoDependency);
         }
-        FSInstallState state = new FSInstallState(pkg.getId(), FSPackageStatus.REGISTERED)
-                .withFilePath(pkgFile.toPath())
+        FSInstallState state = new FSInstallState(pkg.getId(), FSPackageStatus.REGISTERED, pkgFile)
                 .withDependencies(dependencies)
                 .withSubPackages(subpackages)
                 .withFilter(pkg.getArchive().getMetaInf().getFilter())
                 .withSize(pkg.getSize())
                 .withProperties(pkg.getArchive().getMetaInf().getProperties())
                 .withExternal(false);
-        setInstallState(state);
+        stateCache.put(pkg.getId(), state);
         return pkg.getId();
     }
 
@@ -480,63 +449,57 @@ public class FSPackageRegistry extends AbstractPackageRegistry {
         }
     }
 
-    /**
-     * {@inheritDoc}
-     */
-    public ZipVaultPackage upload(InputStream in, boolean replace)
+    protected ZipVaultPackage upload(InputStream in, boolean replace)
             throws IOException, PackageExistsException {
 
+        Path tempFile = Files.createTempFile("upload", ".zip");
         MemoryArchive archive = new MemoryArchive(false);
-        File tempFile = File.createTempFile("upload", ".zip");
 
         try (InputStreamPump pump = new InputStreamPump(in, archive)) {
             // this will cause the input stream to be consumed and the memory
             // archive being initialized.
             try {
-
-                FileUtils.copyInputStreamToFile(pump, tempFile);
-            } catch (Exception e) {
-                String msg = "Stream could be read successfully.";
-                log.error(msg);
+                Files.copy(pump, tempFile, StandardCopyOption.REPLACE_EXISTING);
+            } catch (IOException e) {
+                String msg = "Stream could not be read successfully.";
                 throw new IOException(msg, e);
             }
+        }
+        if (archive.getJcrRoot() == null) {
+            String msg = "Stream is not a content package. Missing 'jcr_root'.";
+            throw new IOException(msg);
+        }
 
-            if (archive.getJcrRoot() == null) {
-                String msg = "Stream is not a content package. Missing 'jcr_root'.";
-                log.error(msg);
-                throw new IOException(msg);
-            }
-
-            final MetaInf inf = archive.getMetaInf();
-            PackageId pid = inf.getPackageProperties().getId();
+        final MetaInf inf = archive.getMetaInf();
+        PackageId pid = inf.getPackageProperties().getId();
 
-            // invalidate pid if path is unknown
-            if (pid == null) {
-                throw new IllegalArgumentException("Unable to create package. No package pid set.");
-            }
-            if (!pid.isValid()) {
-                throw new IllegalArgumentException("Unable to create package. Illegal package name.");
-            }
+        // invalidate pid if path is unknown
+        if (pid == null) {
+            throw new IllegalArgumentException("Unable to create package. No package pid set.");
+        }
+        if (!pid.isValid()) {
+            throw new IllegalArgumentException("Unable to create package. Illegal package name.");
+        }
 
-            File oldPkgFile = getPackageFile(pid);
-            FSInstallState state = getInstallState(pid);
+        Path pkgFile = getPackageFile(pid);
+        FSInstallState state = getInstallState(pid);
 
-            if (oldPkgFile != null && oldPkgFile.exists()) {
-                if (replace && !state.isExternal()) {
-                    oldPkgFile.delete();
-                } else {
-                    throw new PackageExistsException("Package already exists: " + pid).setId(pid);
-                }
+        if (Files.exists(pkgFile)) {
+            if (replace && !state.isExternal()) {
+                Files.delete(pkgFile);
+            } else {
+                throw new PackageExistsException("Package already exists: " + pid).setId(pid);
             }
-
-            ZipVaultPackage pkg = new ZipVaultPackage(archive, true);
-            registerSubPackages(pkg, replace);
-            File pkgFile = buildPackageFile(pid);
-            FileUtils.moveFile(tempFile, pkgFile);
-            dispatch(Type.UPLOAD, pid, null);
-            return pkg;
+        } else {
+            Files.createDirectories(pkgFile.getParent());
         }
 
+        ZipVaultPackage pkg = new ZipVaultPackage(archive, true);
+        registerSubPackages(pkg, replace);
+        Files.move(tempFile, pkgFile);
+        dispatch(Type.UPLOAD, pid, null);
+        return pkg;
+
     }
 
     /**
@@ -545,69 +508,55 @@ public class FSPackageRegistry extends AbstractPackageRegistry {
     @NotNull
     @Override
     public PackageId register(@NotNull File file, boolean replace) throws IOException, PackageExistsException {
-        try (ZipVaultPackage pack = new ZipVaultPackage(file, false, true)) {
-            File pkgFile = buildPackageFile(pack.getId());
-            if (pkgFile.exists()) {
-                if (replace) {
-                    pkgFile.delete();
-                } else {
-                    throw new PackageExistsException("Package already exists: " + pack.getId()).setId(pack.getId());
-                }
-            }
-            Map<PackageId, SubPackageHandling.Option> subpackages = registerSubPackages(pack, replace);
-            FileUtils.copyFile(file, pkgFile);
-            Set<Dependency> dependencies = new HashSet<>(Arrays.asList(pack.getDependencies()));
-            FSInstallState state = new FSInstallState(pack.getId(), FSPackageStatus.REGISTERED)
-                    .withFilePath(pkgFile.toPath())
-                    .withDependencies(dependencies)
-                    .withSubPackages(subpackages)
-                    .withFilter(pack.getArchive().getMetaInf().getFilter())
-                    .withSize(pack.getSize())
-                    .withProperties(pack.getArchive().getMetaInf().getProperties())
-                    .withExternal(false);
-            setInstallState(state);
-            return pack.getId();
-        }
+        return doRegister(file, replace, false);
     }
 
     @NotNull
     @Override
     public PackageId registerExternal(@NotNull File file, boolean replace) throws IOException, PackageExistsException {
-        if (!replace && pathIdMapping.containsKey(file.toPath())) {
-            PackageId pid = pathIdMapping.get(file.toPath());
-            throw new PackageExistsException("Package already exists: " + pid).setId(pid);
-        }
-        ZipVaultPackage pack = new ZipVaultPackage(file, false, true);
-        try {
+        return doRegister(file, replace, true);
+    }
 
+    @NotNull
+    private PackageId doRegister(@NotNull File file, boolean replace, boolean external) throws IOException, PackageExistsException {
+        // detect collisions without parsing the package to speed things up
+        PackageId oldPackageId = stateCache.getIdForFile(file.toPath());
+        if (!replace && oldPackageId != null) {
+            throw new PackageExistsException("Package already exists: " + oldPackageId).setId(oldPackageId);
+        }
+        try (ZipVaultPackage pack = new ZipVaultPackage(file, false, true)) {
             FSInstallState state = getInstallState(pack.getId());
-            if (!(FSPackageStatus.NOTREGISTERED == state.getStatus())) {
+            if (state != null) {
                 if (replace) {
                     try {
                         remove(pack.getId());
                     } catch (NoSuchPackageException e) {
-                        log.error("Status isn't NOTREGISTERD but no metafile exists to remove", e);
+                        log.error("No metafile exists to remove", e);
                     }
                 } else {
                     throw new PackageExistsException("Package already exists: " + pack.getId()).setId(pack.getId());
                 }
             }
+            final Path newPackageFile;
+            if (!external) {
+                // copy to registry path
+                newPackageFile = getPackageFile(pack.getId());
+                Files.createDirectories(newPackageFile.getParent());
+                Files.copy(file.toPath(), newPackageFile);
+            } else {
+                newPackageFile = file.toPath();
+            }
             Map<PackageId, SubPackageHandling.Option> subpackages = registerSubPackages(pack, replace);
             Set<Dependency> dependencies = new HashSet<>(Arrays.asList(pack.getDependencies()));
-            FSInstallState targetState = new FSInstallState(pack.getId(), FSPackageStatus.REGISTERED)
-                    .withFilePath(file.toPath())
+            FSInstallState targetState = new FSInstallState(pack.getId(), FSPackageStatus.REGISTERED, newPackageFile)
                     .withDependencies(dependencies)
                     .withSubPackages(subpackages)
                     .withFilter(pack.getArchive().getMetaInf().getFilter())
                     .withSize(pack.getSize())
                     .withProperties(pack.getArchive().getMetaInf().getProperties())
-                    .withExternal(true);
-            setInstallState(targetState);
+                    .withExternal(external);
+            stateCache.put(pack.getId(), targetState);
             return pack.getId();
-        } finally {
-            if (!pack.isClosed()) {
-                pack.close();
-            }
         }
     }
 
@@ -616,18 +565,13 @@ public class FSPackageRegistry extends AbstractPackageRegistry {
      */
     @Override
     public void remove(@NotNull PackageId id) throws IOException, NoSuchPackageException {
-        FSInstallState state = getInstallState(id);
-        File metaData = getPackageMetaDataFile(id);
-
-        if (!metaData.exists()) {
+        FSInstallState state = stateCache.remove(id);
+        if (state == null) {
             throw new NoSuchPackageException().setId(id);
         }
-        metaData.delete();
-
         if (!state.isExternal()) {
-            getPackageFile(id).delete();
+            Files.delete(state.getFilePath());
         }
-        updateInstallState(id, FSPackageStatus.NOTREGISTERED);
         dispatch(PackageEvent.Type.REMOVE, id, null);
     }
 
@@ -637,48 +581,7 @@ public class FSPackageRegistry extends AbstractPackageRegistry {
     @NotNull
     @Override
     public Set<PackageId> packages() throws IOException {
-        return packagesInitializied ? stateCache.keySet() : loadPackageCache();
-    }
-
-    /**
-     * Loads all state from files persisted in configured homeDir, adds to cache and returns all cached {@code PackageId}s.
-     *
-     * @return {@code Set} of all cached {@code PackageId}s
-     *
-     * @throws IOException If an I/O error occurs
-     */
-    private Set<PackageId> loadPackageCache() throws IOException {
-        Map<PackageId, FSInstallState> cacheEntries = new HashMap<>();
-        Map<Path, PackageId> idMapping = new HashMap<>();
-
-
-        Collection<File> files = FileUtils.listFiles(getHomeDir(), META_SUFFIXES, true);
-        for (File file : files) {
-            FSInstallState state = FSInstallState.fromFile(file);
-            if (state != null) {
-                PackageId id = state.getPackageId();
-                if (id != null) {
-                    cacheEntries.put(id, state);
-                    idMapping.put(state.getFilePath(), id);
-                    
-                }
-            }
-        }
-        stateCache.putAll(cacheEntries);
-        pathIdMapping.putAll(idMapping);
-        packagesInitializied = true;
-        return cacheEntries.keySet();
-    }
-
-    /**
-     * Returns the path of this package.this also includes the version, but
-     * never the extension (.zip).
-     *
-     * @param id the package id
-     * @return the path of this package
-     */
-    public String getInstallationPath(PackageId id) {
-        return getRelativeInstallationPath(id);
+        return stateCache.keySet();
     }
 
     /**
@@ -721,7 +624,7 @@ public class FSPackageRegistry extends AbstractPackageRegistry {
             }
             ((ZipVaultPackage)vltPkg).extract(session, opts, getSecurityConfig(), isStrictByDefault());
             dispatch(PackageEvent.Type.EXTRACT, pkg.getId(), null);
-            updateInstallState(vltPkg.getId(), FSPackageStatus.EXTRACTED);
+            stateCache.updatePackageStatus(vltPkg.getId(), FSPackageStatus.EXTRACTED);
 
         } catch (RepositoryException e) {
             throw new IOException(e);
@@ -738,73 +641,4 @@ public class FSPackageRegistry extends AbstractPackageRegistry {
         throw new PackageException(msg);
     }
 
-    /**
-     * Shortcut to just change the status of a package - implicitly sets the installtime when switching to EXTRACTED
-     *
-     * @param pid PackageId of the package to update
-     * @param targetStatus Status to update
-     * @throws IOException If an I/O error occurs.
-     */
-    private void updateInstallState(PackageId pid, FSPackageStatus targetStatus) throws IOException {
-        FSInstallState state = getInstallState(pid);
-        Long installTime = state.getInstallationTime();
-        if (FSPackageStatus.EXTRACTED == targetStatus) {
-            installTime = Calendar.getInstance().getTimeInMillis();
-        }
-        FSInstallState targetState = new FSInstallState(pid, targetStatus)
-              .withFilePath(state.getFilePath())
-              .withDependencies(state.getDependencies())
-              .withSubPackages(state.getSubPackages())
-              .withInstallTime(installTime)
-              .withSize(state.getSize())
-              .withProperties(state.getProperties())
-              .withExternal(state.isExternal());
-        setInstallState(targetState);
-    }
-
-    /**
-     * Persists the installState to a metadatafile and adds current state to cache
-     * @param state
-     * @throws IOException
-     */
-    private void setInstallState(@NotNull FSInstallState state) throws IOException {
-        PackageId pid = state.getPackageId();
-        File metaData = getPackageMetaDataFile(pid);
-
-        if (state.getStatus() == FSPackageStatus.NOTREGISTERED) {
-            pathIdMapping.remove(stateCache.get(pid).getFilePath());
-            metaData.delete();
-            stateCache.remove(pid);
-        } else {
-            state.save(metaData);
-            stateCache.put(pid, state);
-            pathIdMapping.put(state.getFilePath(), pid);
-        }
-    }
-
-    /**
-     * Retrieves {@code InstallState} from cache, falls back to reading from metafile and returns state for {@code FSPackageStatus.NOTREGISTERED} in case not found.
-     *
-     * @param pid the PackageId of the package to retrieve the install state from.
-     * @return {@code InstallState} found for given {@code PackageId} or a fresh one with status {@code FSPackageStatus.NOTREGISTERED}
-     *
-     * @throws IOException if an I/O error occurs.
-     */
-    @NotNull
-    public FSInstallState getInstallState(PackageId pid) throws IOException {
-        if (stateCache.containsKey(pid)) {
-            return stateCache.get(pid);
-        } else {
-            File metaFile = getPackageMetaDataFile(pid);
-            FSInstallState state = FSInstallState.fromFile(metaFile);
-            if (state != null) {
-                //theoretical file - should only be feasible when manipulating on filesystem, writing metafile automatically updates cache
-                stateCache.put(pid, state);
-                pathIdMapping.put(state.getFilePath(), pid);
-            }
-            return state != null ? state : new FSInstallState(pid, FSPackageStatus.NOTREGISTERED);
-        }
-    }
-
-
 }
diff --git a/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/registry/impl/FSPackageStatus.java b/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/registry/impl/FSPackageStatus.java
index d340228..9bc02e3 100644
--- a/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/registry/impl/FSPackageStatus.java
+++ b/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/registry/impl/FSPackageStatus.java
@@ -22,7 +22,6 @@ package org.apache.jackrabbit.vault.packaging.registry.impl;
 public enum FSPackageStatus {
     
     REGISTERED,
-    EXTRACTED,
-    NOTREGISTERED
+    EXTRACTED
 
 }
diff --git a/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/registry/impl/FSRegisteredPackage.java b/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/registry/impl/FSRegisteredPackage.java
index c7fa409..f22bbdf 100644
--- a/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/registry/impl/FSRegisteredPackage.java
+++ b/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/registry/impl/FSRegisteredPackage.java
@@ -21,6 +21,7 @@ import java.util.Calendar;
 
 import org.apache.jackrabbit.vault.fs.api.WorkspaceFilter;
 import org.apache.jackrabbit.vault.packaging.Dependency;
+import org.apache.jackrabbit.vault.packaging.NoSuchPackageException;
 import org.apache.jackrabbit.vault.packaging.PackageId;
 import org.apache.jackrabbit.vault.packaging.PackageProperties;
 import org.apache.jackrabbit.vault.packaging.VaultPackage;
@@ -38,7 +39,7 @@ public class FSRegisteredPackage implements RegisteredPackage {
     /**
      * default logger
      */
-    private static final Logger log = LoggerFactory.getLogger(FSPackageRegistry.class);
+    private static final Logger log = LoggerFactory.getLogger(FSRegisteredPackage.class);
 
     private FSPackageRegistry registry;
 
@@ -69,7 +70,11 @@ public class FSRegisteredPackage implements RegisteredPackage {
     @Override
     public VaultPackage getPackage() throws IOException {
         if (this.vltPkg == null) {
-            this.vltPkg = registry.openPackageFile(getId());
+            try {
+                this.vltPkg = registry.openPackageFile(getId());
+            } catch (NoSuchPackageException e) {
+                throw new IOException("Registry does not/no longer know package with id " + getId(), e);
+            }
         }
         return this.vltPkg;
     }
diff --git a/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/registry/impl/JcrPackageRegistry.java b/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/registry/impl/JcrPackageRegistry.java
index cfc1419..0c5ac0f 100644
--- a/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/registry/impl/JcrPackageRegistry.java
+++ b/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/registry/impl/JcrPackageRegistry.java
@@ -272,7 +272,7 @@ public class JcrPackageRegistry extends AbstractPackageRegistry {
     private Node getPackageNode(@NotNull PackageId id) throws RepositoryException {
         String relPath = getRelativeInstallationPath(id);
         for (String pfx: packRootPaths) {
-            String path = pfx + relPath;
+            String path = pfx + "/" + relPath;
             String[] exts = new String[]{"", ".zip", ".jar"};
             for (String ext: exts) {
                 if (session.nodeExists(path + ext)) {
@@ -357,21 +357,14 @@ public class JcrPackageRegistry extends AbstractPackageRegistry {
             throws RepositoryException, IOException, PackageExistsException {
 
         MemoryArchive archive = new MemoryArchive(true);
-        InputStreamPump pump = new InputStreamPump(in , archive);
-
-        // this will cause the input stream to be consumed and the memory archive being initialized.
-        Binary bin = session.getValueFactory().createBinary(pump);
-        if (pump.getError() != null) {
-            Exception error = pump.getError();
-            log.error("Error while reading from input stream.", error);
-            bin.dispose();
-            archive.close();
-            throw new IOException("Error while reading from input stream", error);
+        Binary bin;
+        try (InputStreamPump pump = new InputStreamPump(in , archive)) {
+            // this will cause the input stream to be consumed and the memory archive being initialized.
+            bin = session.getValueFactory().createBinary(pump);
         }
 
         if (archive.getJcrRoot() == null) {
             String msg = "Stream is not a content package. Missing 'jcr_root'.";
-            log.error(msg);
             bin.dispose();
             archive.close();
             throw new IOException(msg);
@@ -771,7 +764,7 @@ public class JcrPackageRegistry extends AbstractPackageRegistry {
      * @since 2.2
      */
     public String getInstallationPath(PackageId id) {
-        return packRootPaths[0] + getRelativeInstallationPath(id);
+        return packRootPaths[0] + "/" + getRelativeInstallationPath(id);
     }
 
     @Override
diff --git a/vault-core/src/main/java/org/apache/jackrabbit/vault/util/InputStreamPump.java b/vault-core/src/main/java/org/apache/jackrabbit/vault/util/InputStreamPump.java
index 63027ee..b776692 100644
--- a/vault-core/src/main/java/org/apache/jackrabbit/vault/util/InputStreamPump.java
+++ b/vault-core/src/main/java/org/apache/jackrabbit/vault/util/InputStreamPump.java
@@ -22,11 +22,19 @@ import java.io.PipedInputStream;
 import java.io.PipedOutputStream;
 
 import org.apache.commons.io.input.CloseShieldInputStream;
+import org.apache.commons.io.input.TeeInputStream;
+import org.apache.jackrabbit.vault.fs.io.MemoryArchive;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 /**
- * {@code InputStreamPump}...
+ * An input stream pump feeds a {@link InputStreamPump.Pump} in a dedicated thread with the input read from
+ * the given input stream.
+ * This is similar to a {@link TeeInputStream} but leverages {@link PipedInputStream} and {@link PipedOutputStream}
+ * and can execute additional tasks in the additional thread consuming the PipedInputStream.
+ * Only after calling {@link #close()} the PipedInputStream has been fully consumed (as it waits for the pump's thread to complete).
+ * 
+ * @see MemoryArchive
  */
 public class InputStreamPump extends InputStream {
 
@@ -43,7 +51,7 @@ public class InputStreamPump extends InputStream {
 
     private Thread pumpThread;
 
-    private Exception error;
+    private volatile Exception error;
 
     public InputStreamPump(InputStream source, final Pump pump) throws IOException {
         this.source = source;
@@ -76,6 +84,13 @@ public class InputStreamPump extends InputStream {
         void run(InputStream in) throws Exception;
     }
 
+    /**
+     * 
+     * @return exception which has occurred in the pump thread or {@code null}.
+     * @deprecated Rather call {@link #close()}, as otherwise this might be called too early (before the thread finished).
+     * {@code close()} will automatically wrap the potential exception from the pump in an IOException and throws it as well
+     */
+    @Deprecated
     public Exception getError() {
         return error;
     }
@@ -128,6 +143,9 @@ public class InputStreamPump extends InputStream {
         } catch (InterruptedException e) {
             throw new IOException(e);
         }
+        if (error != null) {
+            throw new IOException(error);
+        }
     }
 
     @Override
diff --git a/vault-core/src/test/java/org/apache/jackrabbit/vault/packaging/integration/PackageInstallIT.java b/vault-core/src/test/java/org/apache/jackrabbit/vault/packaging/integration/PackageInstallIT.java
index 303d81b..3282d71 100644
--- a/vault-core/src/test/java/org/apache/jackrabbit/vault/packaging/integration/PackageInstallIT.java
+++ b/vault-core/src/test/java/org/apache/jackrabbit/vault/packaging/integration/PackageInstallIT.java
@@ -24,16 +24,10 @@ import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.IOException;
-import java.lang.reflect.Field;
 import java.security.Principal;
 import java.util.Collections;
-import java.util.Properties;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipOutputStream;
 
 import javax.jcr.NodeIterator;
 import javax.jcr.Property;
@@ -832,28 +826,7 @@ public class PackageInstallIT extends IntegrationTestBase {
      */
     @Test
     public void testPackageInstallWith0MtimeZipEntry() throws IOException, RepositoryException, NoSuchFieldException, IllegalAccessException {
-        ByteArrayOutputStream out = new ByteArrayOutputStream();
-        ZipOutputStream zout = new ZipOutputStream(out);
-        Properties p = new Properties();
-        p.setProperty("name", TMP_PACKAGE_ID.getName());
-        p.setProperty("group", TMP_PACKAGE_ID.getGroup());
-        p.setProperty("version", TMP_PACKAGE_ID.getVersionString());
-        ZipEntry e = new ZipEntry("META-INF/vault/properties.xml");
-
-        Field field = ZipEntry.class.getDeclaredField("xdostime");
-        field.setAccessible(true);
-        field.setLong(e, 0);
-        zout.putNextEntry(e);
-        p.storeToXML(zout, "", "utf-8");
-        zout.closeEntry();
-
-        zout.putNextEntry(new ZipEntry("jcr_root/"));
-        zout.closeEntry();
-
-        zout.close();
-        out.close();
-
-        JcrPackage pack = packMgr.upload(new ByteArrayInputStream(out.toByteArray()), true);
+        JcrPackage pack = packMgr.upload(getStream("/test-packages/properties-with-0mtime.zip"), true, true);
         assertEquals("packageid", TMP_PACKAGE_ID, pack.getDefinition().getId());
     }
 
diff --git a/vault-core/src/test/java/org/apache/jackrabbit/vault/packaging/integration/SubPackagesIT.java b/vault-core/src/test/java/org/apache/jackrabbit/vault/packaging/integration/SubPackagesIT.java
index bf3912a..8868d7c 100644
--- a/vault-core/src/test/java/org/apache/jackrabbit/vault/packaging/integration/SubPackagesIT.java
+++ b/vault-core/src/test/java/org/apache/jackrabbit/vault/packaging/integration/SubPackagesIT.java
@@ -92,7 +92,7 @@ public class SubPackagesIT extends IntegrationTestBase {
      */
     public String getInstallationPath(PackageId id) {
         // make sure we use the one from the test parameter
-        return packageRoots[0] + ((JcrPackageRegistry)packMgr.getRegistry()).getRelativeInstallationPath(id) + ".zip";
+        return packageRoots[0] + "/" + ((JcrPackageRegistry)packMgr.getRegistry()).getRelativeInstallationPath(id) + ".zip";
     }
 
     /**
diff --git a/vault-core/src/test/java/org/apache/jackrabbit/vault/packaging/integration/FSInstallStateTest.java b/vault-core/src/test/java/org/apache/jackrabbit/vault/packaging/registry/impl/FSInstallStateTest.java
similarity index 66%
rename from vault-core/src/test/java/org/apache/jackrabbit/vault/packaging/integration/FSInstallStateTest.java
rename to vault-core/src/test/java/org/apache/jackrabbit/vault/packaging/registry/impl/FSInstallStateTest.java
index e140146..a96b408 100644
--- a/vault-core/src/test/java/org/apache/jackrabbit/vault/packaging/integration/FSInstallStateTest.java
+++ b/vault-core/src/test/java/org/apache/jackrabbit/vault/packaging/registry/impl/FSInstallStateTest.java
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-package org.apache.jackrabbit.vault.packaging.integration;
+package org.apache.jackrabbit.vault.packaging.registry.impl;
 
 import static org.junit.Assert.assertEquals;
 
@@ -23,17 +23,21 @@ import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.IOException;
+import java.io.InputStream;
 import java.nio.file.Paths;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
 
+import org.apache.jackrabbit.vault.fs.api.PathFilterSet;
+import org.apache.jackrabbit.vault.fs.config.ConfigurationException;
+import org.apache.jackrabbit.vault.fs.config.DefaultWorkspaceFilter;
+import org.apache.jackrabbit.vault.fs.filter.DefaultPathFilter;
 import org.apache.jackrabbit.vault.packaging.Dependency;
 import org.apache.jackrabbit.vault.packaging.PackageId;
 import org.apache.jackrabbit.vault.packaging.SubPackageHandling;
-import org.apache.jackrabbit.vault.packaging.registry.impl.FSInstallState;
-import org.apache.jackrabbit.vault.packaging.registry.impl.FSPackageStatus;
 import org.hamcrest.MatcherAssert;
 import org.junit.Test;
 import org.xmlunit.matchers.CompareMatcher;
@@ -60,8 +64,7 @@ public class FSInstallStateTest {
         Map<PackageId, SubPackageHandling.Option> subs = new HashMap<>();
         subs.put(TMP_PACKAGE_ID, SubPackageHandling.Option.ADD);
 
-        FSInstallState state = new FSInstallState(TMP_PACKAGE_ID, FSPackageStatus.EXTRACTED)
-                .withFilePath(testFile.toPath())
+        FSInstallState state = new FSInstallState(TMP_PACKAGE_ID, FSPackageStatus.EXTRACTED, testFile.toPath())
                 .withExternal(true)
                 .withDependencies(deps)
                 .withSubPackages(subs)
@@ -77,9 +80,10 @@ public class FSInstallStateTest {
 
     @Test
     public void testReadInstallStateNonExistent() throws IOException {
-        FSInstallState state = FSInstallState.fromFile(new File("nonexist.xml"));
+        FSInstallState state = FSInstallState.fromFile(Paths.get("nonexisting.xml"));
         assertEquals(null, state);
     }
+
     @Test
     public void testReadInstallState() throws IOException {
         Set<Dependency> deps = new HashSet<>();
@@ -96,6 +100,29 @@ public class FSInstallStateTest {
         assertEquals(subs, state.getSubPackages());
         assertEquals(1234L, state.getSize());
         assertEquals((Long) 1234L, state.getInstallationTime());
+    }
 
+    @Test
+    public void testSaveLoad() throws IOException, ConfigurationException {
+        PackageId packageId = new PackageId("group", "name", "1.1.1");
+        FSInstallState fsInstallState = new FSInstallState(packageId, FSPackageStatus.REGISTERED, Paths.get(""));
+        DefaultWorkspaceFilter filter = new DefaultWorkspaceFilter();
+        PathFilterSet pathFilterSet = new PathFilterSet("/apps/mytest");
+        pathFilterSet.addExclude(new DefaultPathFilter("/apps/mytest/exclude"));
+        pathFilterSet.addInclude(new DefaultPathFilter("/apps/mytest/include"));
+        pathFilterSet.seal();
+        filter.add(pathFilterSet);
+        fsInstallState.withFilter(filter);
+        fsInstallState.withDependencies(Collections.singleton(new Dependency(new PackageId("group", "other", "1.0.0"))));
+        fsInstallState.withInstallTime(1234L);
+        fsInstallState.withSize(333);
+        fsInstallState.withSubPackages(Collections.singletonMap(new PackageId("group", "subpackage", "1.0.0"), SubPackageHandling.Option.FORCE_EXTRACT));
+        try (ByteArrayOutputStream output = new ByteArrayOutputStream()) {
+            fsInstallState.save(output);
+            try (InputStream input = new ByteArrayInputStream(output.toByteArray())) {
+                FSInstallState fsInstallState2 = FSInstallState.fromStream(input, "systemId");
+                assertEquals(fsInstallState, fsInstallState2);
+            }
+        }
     }
 }
\ No newline at end of file
diff --git a/vault-core/src/test/java/org/apache/jackrabbit/vault/packaging/integration/FSPackageRegistryIT.java b/vault-core/src/test/java/org/apache/jackrabbit/vault/packaging/registry/impl/FSPackageRegistryIT.java
similarity index 90%
rename from vault-core/src/test/java/org/apache/jackrabbit/vault/packaging/integration/FSPackageRegistryIT.java
rename to vault-core/src/test/java/org/apache/jackrabbit/vault/packaging/registry/impl/FSPackageRegistryIT.java
index 34d9902..9ec6887 100644
--- a/vault-core/src/test/java/org/apache/jackrabbit/vault/packaging/integration/FSPackageRegistryIT.java
+++ b/vault-core/src/test/java/org/apache/jackrabbit/vault/packaging/registry/impl/FSPackageRegistryIT.java
@@ -15,18 +15,24 @@
  * limitations under the License.
  */
 
-package org.apache.jackrabbit.vault.packaging.integration;
+package org.apache.jackrabbit.vault.packaging.registry.impl;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 import java.io.File;
 import java.io.IOException;
-import java.lang.reflect.Field;
-import java.lang.reflect.Method;
 import java.util.Arrays;
 import java.util.Calendar;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Map;
 import java.util.UUID;
 
 import javax.jcr.RepositoryException;
@@ -41,27 +47,26 @@ import org.apache.jackrabbit.vault.packaging.NoSuchPackageException;
 import org.apache.jackrabbit.vault.packaging.PackageException;
 import org.apache.jackrabbit.vault.packaging.PackageExistsException;
 import org.apache.jackrabbit.vault.packaging.PackageId;
+import org.apache.jackrabbit.vault.packaging.VaultPackage;
+import org.apache.jackrabbit.vault.packaging.integration.IntegrationTestBase;
 import org.apache.jackrabbit.vault.packaging.registry.DependencyReport;
 import org.apache.jackrabbit.vault.packaging.registry.ExecutionPlan;
 import org.apache.jackrabbit.vault.packaging.registry.ExecutionPlanBuilder;
 import org.apache.jackrabbit.vault.packaging.registry.PackageTask;
-import org.apache.jackrabbit.vault.packaging.registry.RegisteredPackage;
 import org.apache.jackrabbit.vault.packaging.registry.PackageTask.Type;
-import org.apache.jackrabbit.vault.packaging.registry.impl.FSInstallState;
-import org.apache.jackrabbit.vault.packaging.registry.impl.FSPackageRegistry;
-import org.apache.jackrabbit.vault.packaging.registry.impl.FSPackageStatus;
-import org.apache.jackrabbit.vault.packaging.registry.impl.InstallationScope;
+import org.apache.jackrabbit.vault.packaging.registry.RegisteredPackage;
+import org.apache.jackrabbit.vault.packaging.registry.impl.FSPackageRegistry.Config;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.mockito.Mockito;
+import org.osgi.framework.BundleContext;
+import org.osgi.util.converter.Converter;
+import org.osgi.util.converter.Converters;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
 /**
  * Test the Package registry interface
  */
@@ -81,6 +86,11 @@ public class FSPackageRegistryIT extends IntegrationTestBase {
             "/tmp/foo"
     };
     
+    private static final PackageId TEST_PACKAGE_ID = new PackageId("test", "test-package-with-etc", "1.0");
+
+    @Rule
+    public TemporaryFolder tmpFolder = new TemporaryFolder();
+
     private FSPackageRegistry registry;
     private File registryHome;
 
@@ -208,6 +218,12 @@ public class FSPackageRegistryIT extends IntegrationTestBase {
         assertTrue("file should still exist", file.exists());
         registry.register(file, true);
         file.delete();
+        // make sure package is still accessible after original has been deleted
+        try (RegisteredPackage registeredPackage = registry.open(id)) {
+            try (VaultPackage pack = registeredPackage.getPackage()) {
+                assertNotEquals(file, pack.getFile());
+            }
+        }
     }
     
     /**
@@ -237,6 +253,7 @@ public class FSPackageRegistryIT extends IntegrationTestBase {
     /**
      * registers a file as external package twice with 
      */
+    @SuppressWarnings("deprecation")
     @Test
     public void testRegisterExternalFileTwiceFailsLoadedRegistry() throws IOException, PackageException {
         File file = getTempFile("/test-packages/tmp.zip");
@@ -312,11 +329,7 @@ public class FSPackageRegistryIT extends IntegrationTestBase {
         assertFalse(registry.open(PACKAGE_ID_SUB_A).isInstalled());
         assertFalse(registry.open(PACKAGE_ID_SUB_B).isInstalled());
     }
-    
-    
 
-
-    @SuppressWarnings("deprecation")
     @Test
     public void testInstallExternalUnScoped() throws IOException, PackageException, RepositoryException, org.apache.jackrabbit.oak.segment.file.InvalidFileStoreVersionException {
         File file = getTempFile("/test-packages/mixed_package.zip");
@@ -337,7 +350,6 @@ public class FSPackageRegistryIT extends IntegrationTestBase {
         checkFiltered(CONTENT_PATHS, new String[] {}, listener.paths);
     }
 
-    @SuppressWarnings("deprecation")
     @Test
     public void testInstallExternalContentScoped() throws IOException, PackageException, RepositoryException, org.apache.jackrabbit.oak.segment.file.InvalidFileStoreVersionException {
         File file = getTempFile("/test-packages/mixed_package.zip");
@@ -357,14 +369,12 @@ public class FSPackageRegistryIT extends IntegrationTestBase {
         checkFiltered(CONTENT_PATHS, APPLICATION_PATHS, listener.paths);
     }
 
-    @SuppressWarnings("deprecation")
     private void cleanPaths(String[] paths) throws IOException, RepositoryException, org.apache.jackrabbit.oak.segment.file.InvalidFileStoreVersionException  {
         for (String path : paths) {
             clean(path);
         }
     }
 
-    @SuppressWarnings("deprecation")
     @Test
     public void testInstallExternalApplicationScoped() throws IOException, PackageException, RepositoryException, org.apache.jackrabbit.oak.segment.file.InvalidFileStoreVersionException {
         File file = getTempFile("/test-packages/mixed_package.zip");
@@ -629,7 +639,7 @@ public class FSPackageRegistryIT extends IntegrationTestBase {
         assertTrue("Installation time for idC too early", registry.open(idC).getInstallationTime().compareTo(before) >= 0);
 
     }
-    
+
     @Test
     public void testUnsupportedUninstall() throws Exception {
         PackageId idC = registry.register(getStream(TEST_PACKAGE_C_10), false);
@@ -658,27 +668,41 @@ public class FSPackageRegistryIT extends IntegrationTestBase {
             //expected
         }
     }
-    
+
     @Test
-    public void testNonMetaXmlFile() throws Exception {
-        PackageId idC = registry.register(getStream(TEST_PACKAGE_C_10), false);
+    public void testInvalidMetaXmlFile() throws Exception {
+        getFreshRegistryWithDefaultConstructor("test-package.zip", "invalid-metadata.xml");
+        assertNull(registry.getInstallState(TEST_PACKAGE_ID));
+    }
 
-        assertEquals(idC, registry.getInstallState(idC).getPackageId());
-        assertEquals(FSPackageStatus.REGISTERED, registry.getInstallState(idC).getStatus());
+    @Test
+    public void testCacheInitializedAfterOSGiActivate() throws IOException {
+         new FSPackageRegistry();
+        getFreshRegistryWithDefaultConstructor("test-package.zip", "test-package.xml");
+        assertTrue(registry.contains(TEST_PACKAGE_ID));
+        assertEquals(Collections.singleton(TEST_PACKAGE_ID), registry.packages());
+    }
+
+    private void getFreshRegistryWithDefaultConstructor(String packageName, String packageMetadataName) throws IOException {
+        if (this.registryHome != null && this.registryHome.exists()) {
+            this.registryHome.delete();
+        }
+        this.registryHome = new File(DIR_REGISTRY_HOME, UUID.randomUUID().toString());
+        FileUtils.copyInputStreamToFile(getClass().getResourceAsStream(packageName), new File(this.registryHome, "package1.zip"));
+        FileUtils.copyInputStreamToFile(getClass().getResourceAsStream(packageMetadataName), new File(this.registryHome, "package1.zip.xml"));
         
-        Field stateCache = registry.getClass().getDeclaredField("stateCache");
-        stateCache.setAccessible(true);
-        stateCache.set(registry, new HashMap<String, FSInstallState>()  );
-        Method getPackageMetaDataFile = registry.getClass().getDeclaredMethod("getPackageMetaDataFile", new Class<?>[]{idC.getClass()});
-        getPackageMetaDataFile.setAccessible(true);
-        Method loadPackageCache = registry.getClass().getDeclaredMethod("loadPackageCache");
-        loadPackageCache.setAccessible(true);
-        File metaFile = (File)getPackageMetaDataFile.invoke(registry, idC);
-        FileUtils.copyToFile(getStream("repository.xml"), metaFile);
-        loadPackageCache.invoke(registry);
-        assertEquals(FSPackageStatus.NOTREGISTERED, registry.getInstallState(idC).getStatus());
+        BundleContext context = Mockito.mock(BundleContext.class);
+        Mockito.when(context.getProperty(FSPackageRegistry.REPOSITORY_HOME)).thenReturn(DIR_REGISTRY_HOME.toString());
+        Converter converter = Converters.standardConverter();
+        Map<String, Object> map = new HashMap<>();
+        map.put("homePath", registryHome.getName());
+        map.put("authIdsForHookExecution", new String[0]);
+        map.put("authIdsForRootInstallation", new String[0]);
+        Config config = converter.convert(map).to(Config.class);
+        registry.activate(context, config);
     }
-    
+
+    @SuppressWarnings("deprecation")
     private void getFreshRegistry(InstallationScope... scope) throws IOException {
         if (this.registryHome != null && this.registryHome.exists()) {
             this.registryHome.delete();
diff --git a/vault-core/src/test/java/org/apache/jackrabbit/vault/packaging/registry/impl/FSPackageRegistryTest.java b/vault-core/src/test/java/org/apache/jackrabbit/vault/packaging/registry/impl/FSPackageRegistryTest.java
new file mode 100644
index 0000000..880d326
--- /dev/null
+++ b/vault-core/src/test/java/org/apache/jackrabbit/vault/packaging/registry/impl/FSPackageRegistryTest.java
@@ -0,0 +1,97 @@
+/*
+ * 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.jackrabbit.vault.packaging.registry.impl;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.jackrabbit.vault.packaging.NoSuchPackageException;
+import org.apache.jackrabbit.vault.packaging.PackageExistsException;
+import org.apache.jackrabbit.vault.packaging.PackageId;
+import org.apache.jackrabbit.vault.packaging.registry.impl.FSPackageRegistry.Config;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.mockito.Mockito;
+import org.osgi.framework.BundleContext;
+import org.osgi.util.converter.Converter;
+import org.osgi.util.converter.Converters;
+
+public class FSPackageRegistryTest {
+
+    @Rule
+    public TemporaryFolder tmpFolder = new TemporaryFolder();
+
+    private static final PackageId TEST_PACKAGE_ID = new PackageId("test", "test-package-with-etc", "1.0");
+    
+    private void copyResourceStreamToFile(Path targetFile, String name) throws IOException {
+        try (InputStream in = getClass().getResourceAsStream(name)) {
+            Files.copy(in, targetFile, StandardCopyOption.REPLACE_EXISTING);
+        }
+    }
+
+    private Path getTempRegistryHomeWithPackage(String packageName, String packageMetadataName) throws IOException {
+        Path tmpDir = tmpFolder.newFolder().toPath();
+        copyResourceStreamToFile(tmpDir.resolve("package1.zip"), packageName);
+        copyResourceStreamToFile(tmpDir.resolve("package1.zip.xml"), packageMetadataName);
+        return tmpDir;
+    }
+
+    @Test
+    public void testRegisterAndRemove() throws IOException, PackageExistsException, NoSuchPackageException {
+        FSPackageRegistry registry = createRegistryWithDefaultConstructor(tmpFolder.newFolder().toPath());
+        try (InputStream in = getClass().getResourceAsStream("test-package.zip")) {
+            registry.register(in, false);
+        }
+        assertTrue(registry.contains(TEST_PACKAGE_ID));
+        registry.remove(TEST_PACKAGE_ID);
+        assertFalse(registry.contains(TEST_PACKAGE_ID));
+    }
+
+    @Test
+    public void testCacheInitializedAfterOSGiActivate() throws IOException {
+         new FSPackageRegistry();
+        Path registryHomeDir = getTempRegistryHomeWithPackage("test-package.zip", "test-package.xml");
+        FSPackageRegistry registry = createRegistryWithDefaultConstructor(registryHomeDir);
+        assertTrue(registry.contains(TEST_PACKAGE_ID));
+        assertEquals(Collections.singleton(TEST_PACKAGE_ID), registry.packages());
+    }
+
+    private FSPackageRegistry createRegistryWithDefaultConstructor(Path homePath) throws IOException {
+        FSPackageRegistry registry = new FSPackageRegistry();
+        BundleContext context = Mockito.mock(BundleContext.class);
+        Mockito.when(context.getProperty(FSPackageRegistry.REPOSITORY_HOME)).thenReturn(tmpFolder.getRoot().toString());
+        Converter converter = Converters.standardConverter();
+        Map<String, Object> map = new HashMap<>();
+        map.put("homePath", homePath.toString());
+        map.put("authIdsForHookExecution", new String[0]);
+        map.put("authIdsForRootInstallation", new String[0]);
+        Config config = converter.convert(map).to(Config.class);
+        registry.activate(context, config);
+        return registry;
+    }
+}
diff --git a/vault-core/src/test/java/org/apache/jackrabbit/vault/packaging/registry/impl/FSRegisteredPackageTest.java b/vault-core/src/test/java/org/apache/jackrabbit/vault/packaging/registry/impl/FSRegisteredPackageTest.java
index 9824490..222ce8d 100644
--- a/vault-core/src/test/java/org/apache/jackrabbit/vault/packaging/registry/impl/FSRegisteredPackageTest.java
+++ b/vault-core/src/test/java/org/apache/jackrabbit/vault/packaging/registry/impl/FSRegisteredPackageTest.java
@@ -29,6 +29,7 @@ import java.util.Properties;
 
 import org.apache.commons.io.FileUtils;
 import org.apache.commons.io.IOUtils;
+import org.apache.jackrabbit.vault.packaging.NoSuchPackageException;
 import org.apache.jackrabbit.vault.packaging.PackageId;
 import org.apache.jackrabbit.vault.packaging.VaultPackage;
 import org.apache.jackrabbit.vault.packaging.impl.HollowVaultPackage;
@@ -58,7 +59,7 @@ public class FSRegisteredPackageTest {
         }
     }
 
-    private FSPackageRegistry newRegistry(File packageFile) throws IOException {
+    private FSPackageRegistry newRegistry(File packageFile) throws IOException, NoSuchPackageException {
         FSPackageRegistry registry = Mockito.mock(FSPackageRegistry.class);
         Mockito.when(registry.openPackageFile(DUMMY_ID)).thenReturn(safeLoadVaultPackage(packageFile));
         return registry;
@@ -72,7 +73,7 @@ public class FSRegisteredPackageTest {
     }
 
     @Test
-    public void testGetPackageFromNonTruncatedFile() throws IOException {
+    public void testGetPackageFromNonTruncatedFile() throws IOException, NoSuchPackageException {
         File packageFile = getTempFile("test-package.zip");
         try (RegisteredPackage regPack = new FSRegisteredPackage(newRegistry(packageFile), newInstallState(packageFile));
                 VaultPackage vltPack = regPack.getPackage()) {
@@ -86,7 +87,7 @@ public class FSRegisteredPackageTest {
     }
 
     @Test
-    public void testGetPackageFromTruncatedFile() throws IOException {
+    public void testGetPackageFromTruncatedFile() throws IOException, NoSuchPackageException {
         File packageFile = getTempFile("test-package-truncated.zip");
         try (RegisteredPackage regPack = new FSRegisteredPackage(newRegistry(packageFile), newInstallState(packageFile));
                 VaultPackage vltPack = regPack.getPackage()) {
diff --git a/vault-core/src/test/resources/org/apache/jackrabbit/vault/packaging/registry/impl/invalid-metadata.xml b/vault-core/src/test/resources/org/apache/jackrabbit/vault/packaging/registry/impl/invalid-metadata.xml
new file mode 100644
index 0000000..c31e942
--- /dev/null
+++ b/vault-core/src/test/resources/org/apache/jackrabbit/vault/packaging/registry/impl/invalid-metadata.xml
@@ -0,0 +1,174 @@
+<?xml version="1.0"?>
+<!--
+   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 Repository
+          PUBLIC "-//The Apache Software Foundation//DTD Jackrabbit 2.0//EN"
+          "http://jackrabbit.apache.org/dtd/repository-2.0.dtd">
+
+<!-- Example Repository Configuration File
+     Used by
+     - org.apache.jackrabbit.core.config.RepositoryConfigTest.java
+     -
+-->
+<Repository>
+    <!--
+        virtual file system where the repository stores global state
+        (e.g. registered namespaces, custom node types, etc.)
+    -->
+    <FileSystem class="org.apache.jackrabbit.core.fs.local.LocalFileSystem">
+        <param name="path" value="${rep.home}/repository"/>
+    </FileSystem>
+
+    <!--
+        data store configuration
+    -->
+    <DataStore class="org.apache.jackrabbit.core.data.FileDataStore"/>
+
+    <!--
+        security configuration
+    -->
+    <Security appName="Jackrabbit">
+
+        <SecurityManager class="org.apache.jackrabbit.core.UserPerWorkspaceSecurityManager">
+            <!--
+            optional user manager configuration
+            -->
+            <UserManager class="org.apache.jackrabbit.core.security.user.UserPerWorkspaceUserManager">
+                <param name="usersPath" value="/home/users"/>
+                <param name="groupsPath" value="/home/groups"/>
+            </UserManager>
+
+            <!--
+            optional workspace access manager configuration
+           -->
+        </SecurityManager>
+
+        <!--
+            access manager:
+            class: FQN of class implementing the AccessManager interface
+        -->
+        <AccessManager class="org.apache.jackrabbit.core.security.DefaultAccessManager">
+            <!-- <param name="config" value="${rep.home}/access.xml"/> -->
+        </AccessManager>
+
+        <LoginModule class="org.apache.jackrabbit.core.security.authentication.DefaultLoginModule">
+           <!-- 
+              anonymous user name ('anonymous' is the default value)
+            -->
+           <param name="anonymousId" value="anonymous"/>
+           <!--
+              administrator user id (default value if param is missing is 'admin')
+            -->
+           <param name="adminId" value="admin"/>
+        </LoginModule>
+    </Security>
+
+    <!--
+        location of workspaces root directory and name of default workspace
+    -->
+    <Workspaces rootPath="${rep.home}/workspaces" defaultWorkspace="default"/>
+    <!--
+        workspace configuration template:
+        used to create the initial workspace if there's no workspace yet
+    -->
+    <Workspace name="${wsp.name}">
+        <!--
+            virtual file system of the workspace:
+            class: FQN of class implementing the FileSystem interface
+        -->
+        <FileSystem class="org.apache.jackrabbit.core.fs.local.LocalFileSystem">
+            <param name="path" value="${wsp.home}"/>
+        </FileSystem>
+        <!--
+            persistence manager of the workspace:
+            class: FQN of class implementing the PersistenceManager interface
+        -->
+        <PersistenceManager class="org.apache.jackrabbit.core.persistence.pool.DerbyPersistenceManager">
+          <param name="url" value="jdbc:derby:${wsp.home}/db;create=true"/>
+          <param name="schemaObjectPrefix" value="${wsp.name}_"/>
+        </PersistenceManager>
+        <!--
+            Search index and the file system it uses.
+            class: FQN of class implementing the QueryHandler interface
+        -->
+        <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+            <param name="path" value="${wsp.home}/index"/>
+            <param name="supportHighlighting" value="true"/>
+        </SearchIndex>
+
+        <!--
+            Workspace security configuration
+        -->
+        <WorkspaceSecurity>
+            <AccessControlProvider class="org.apache.jackrabbit.core.security.authorization.acl.ACLProvider">
+                <!-- param name="omit-default-permission" value="true"/-->
+                <param name="allow-unknown-principals" value="true"/>
+            </AccessControlProvider>
+        </WorkspaceSecurity>
+
+        <!--
+            XML Import configuration of the workspace
+        -->
+        <Import>
+            <ProtectedItemImporter class="org.apache.jackrabbit.core.xml.AccessControlImporter"/>
+            <ProtectedItemImporter class="org.apache.jackrabbit.core.security.user.UserImporter">
+                <param name="importBehavior" value="besteffort"/>
+            </ProtectedItemImporter>
+        </Import>
+    </Workspace>
+
+    <!--
+        Configures the versioning
+    -->
+    <Versioning rootPath="${rep.home}/version">
+        <!--
+            Configures the filesystem to use for versioning for the respective
+            persistence manager
+        -->
+        <FileSystem class="org.apache.jackrabbit.core.fs.local.LocalFileSystem">
+            <param name="path" value="${rep.home}/version" />
+        </FileSystem>
+
+        <!--
+            Configures the persistence manager to be used for persisting version state.
+            Please note that the current versioning implementation is based on
+            a 'normal' persistence manager, but this could change in future
+            implementations.
+        -->
+        <PersistenceManager class="org.apache.jackrabbit.core.persistence.pool.DerbyPersistenceManager">
+          <param name="url" value="jdbc:derby:${rep.home}/version/db;create=true"/>
+          <param name="schemaObjectPrefix" value="version_"/>
+        </PersistenceManager>
+    </Versioning>
+
+    <!--
+        Search index for content that is shared repository wide
+        (/jcr:system tree, contains mainly versions)
+    -->
+    <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+        <param name="path" value="${rep.home}/repository/index"/>
+        <param name="supportHighlighting" value="true"/>
+    </SearchIndex>
+
+    <!--
+        Run with a cluster journal
+    -->
+    <Cluster id="node1">
+        <Journal class="org.apache.jackrabbit.core.journal.MemoryJournal"/>
+    </Cluster>
+</Repository>
diff --git a/vault-core/src/test/resources/org/apache/jackrabbit/vault/packaging/registry/impl/test-package.xml b/vault-core/src/test/resources/org/apache/jackrabbit/vault/packaging/registry/impl/test-package.xml
new file mode 100644
index 0000000..d09709e
--- /dev/null
+++ b/vault-core/src/test/resources/org/apache/jackrabbit/vault/packaging/registry/impl/test-package.xml
@@ -0,0 +1,28 @@
+<?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.
+  -->
+<registryMetadata packageid="test:test-package-with-etc:1.0" size="-1" filepath="/var/folders/rm/vlg2h6m16mb0f65djmnb12xr0000gq/T/junit1443483155243547543/junit9008025097740758307/home/test/test-package-with-etc-1.0.zip" external="false" packagestatus="registered">
+    <workspacefilter>
+        <filter root="/etc">
+            <rule include="/etc"/>
+            <rule include="/etc/clientlibs"/>
+            <rule include="/etc/clientlibs/granite"/>
+            <rule include="/etc/clientlibs/granite/test(/.*)?"/>
+        </filter>
+    </workspacefilter>
+    <packageproperties createdBy="admin" name="test-package-with-etc" lastModified="2017-05-16T17:23:18.391+09:00" lastModifiedBy="admin" acHandling="MERGE_PRESERVE" created="2017-05-16T17:23:18.655+09:00" buildCount="1" version="1.0" dependencies="" packageFormatVersion="2" group="test" lastWrappedBy="admin"/>
+</registryMetadata>
diff --git a/vault-core/src/test/resources/test-packages/properties-with-0mtime.zip b/vault-core/src/test/resources/test-packages/properties-with-0mtime.zip
new file mode 100644
index 0000000..fc43677
Binary files /dev/null and b/vault-core/src/test/resources/test-packages/properties-with-0mtime.zip differ