[KARAF-3937] Karaf Cave produces incomplete indexes
diff --git a/server/storage/src/main/java/org/apache/karaf/cave/server/storage/CaveRepositoryImpl.java b/server/storage/src/main/java/org/apache/karaf/cave/server/storage/CaveRepositoryImpl.java
index f78c515..ebc210c 100644
--- a/server/storage/src/main/java/org/apache/karaf/cave/server/storage/CaveRepositoryImpl.java
+++ b/server/storage/src/main/java/org/apache/karaf/cave/server/storage/CaveRepositoryImpl.java
@@ -16,7 +16,9 @@
  */
 package org.apache.karaf.cave.server.storage;
 
+import javax.xml.stream.XMLStreamException;
 import java.io.File;
+import java.io.FilterInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.Writer;
@@ -28,6 +30,8 @@
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.nio.file.StandardCopyOption;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
@@ -38,8 +42,6 @@
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipInputStream;
 
-import javax.xml.stream.XMLStreamException;
-
 import org.apache.karaf.cave.server.api.CaveRepository;
 import org.apache.karaf.features.internal.resolver.ResolverUtil;
 import org.apache.karaf.features.internal.resolver.ResourceBuilder;
@@ -56,6 +58,8 @@
 import org.slf4j.LoggerFactory;
 
 import static java.util.jar.JarFile.MANIFEST_NAME;
+import static org.osgi.service.repository.ContentNamespace.CAPABILITY_MIME_ATTRIBUTE;
+import static org.osgi.service.repository.ContentNamespace.CAPABILITY_SIZE_ATTRIBUTE;
 import static org.osgi.service.repository.ContentNamespace.CAPABILITY_URL_ATTRIBUTE;
 import static org.osgi.service.repository.ContentNamespace.CONTENT_NAMESPACE;
 
@@ -289,37 +293,107 @@
         }
     }
 
-    private ResourceImpl createResource(URL url) throws BundleException, IOException {
-        return createResource(url, url.toExternalForm());
+    private ResourceImpl createResource(URL url) throws BundleException, IOException, NoSuchAlgorithmException {
+        return createResource(url, url.toExternalForm(), true);
     }
 
-    private ResourceImpl createResource(URL url, String uri) throws BundleException, IOException {
-        Map<String, String> headers = getHeaders(url);
+    private ResourceImpl createResource(URL url, String uri, boolean readFully) throws BundleException, IOException, NoSuchAlgorithmException {
+        Map<String, String> headers = null;
+        String digest = null;
+        long size = -1;
+        // Find headers, compute length and checksum
+        try (ContentInputStream is = new ContentInputStream(url.openStream())) {
+            ZipInputStream zis = new ZipInputStream(is);
+            ZipEntry entry;
+            while ((entry = zis.getNextEntry()) != null) {
+                if (MANIFEST_NAME.equals(entry.getName())) {
+                    Attributes attributes = new Manifest(zis).getMainAttributes();
+                    headers = new HashMap<>();
+                    for (Map.Entry attr : attributes.entrySet()) {
+                        headers.put(attr.getKey().toString(), attr.getValue().toString());
+                    }
+                    if (!readFully) {
+                        break;
+                    }
+                }
+            }
+            if (readFully) {
+                digest = is.getDigest();
+                size = is.getSize();
+            }
+        }
+        if (headers == null) {
+            throw new BundleException("Resource " + url + " does not contain a manifest");
+        }
+        // Fix the content directive
         try {
             ResourceImpl resource = ResourceBuilder.build(uri, headers);
-            useResourceRelativeUri(resource);
+            for (Capability cap : resource.getCapabilities(null)) {
+                if (cap.getNamespace().equals(CONTENT_NAMESPACE)) {
+                    String resourceURI = cap.getAttributes().get(CAPABILITY_URL_ATTRIBUTE).toString();
+                    String locationURI = "file:" + getLocation();
+                    LOGGER.debug("Converting resource URI {} relatively to repository URI {}", resourceURI, locationURI);
+                    if (resourceURI.startsWith(locationURI)) {
+                        resourceURI = resourceURI.substring(locationURI.length() + 1);
+                        LOGGER.debug("Resource URI converted to " + resourceURI);
+                        // This is a bit hacky, but the map is not read only
+                        cap.getAttributes().put(CAPABILITY_URL_ATTRIBUTE, resourceURI);
+                    }
+                    if (readFully) {
+                        cap.getAttributes().put(CONTENT_NAMESPACE, digest);
+                        cap.getAttributes().put(CAPABILITY_SIZE_ATTRIBUTE, size);
+                    }
+                    cap.getAttributes().put(CAPABILITY_MIME_ATTRIBUTE, "application/vnd.osgi.bundle");
+                    break;
+                }
+            }
             return resource;
         } catch (BundleException e) {
             throw new BundleException("Unable to create resource from " + uri + ": " + e.getMessage(), e);
         }
     }
 
-    Map<String, String> getHeaders(URL url) throws IOException, BundleException {
-        try (InputStream is = url.openStream()) {
-            ZipInputStream zis = new ZipInputStream(is);
-            ZipEntry entry;
-            while ((entry = zis.getNextEntry()) != null) {
-                if (MANIFEST_NAME.equals(entry.getName())) {
-                    Attributes attributes = new Manifest(zis).getMainAttributes();
-                    Map<String, String> headers = new HashMap<>();
-                    for (Map.Entry attr : attributes.entrySet()) {
-                        headers.put(attr.getKey().toString(), attr.getValue().toString());
-                    }
-                    return headers;
-                }
-            }
+    private static class ContentInputStream extends FilterInputStream {
+        final MessageDigest md;
+        long size = 0;
+
+        public ContentInputStream(InputStream in) throws NoSuchAlgorithmException {
+            super(in);
+            md = MessageDigest.getInstance("SHA-256");
         }
-        throw new BundleException("Resource " + url + " does not contain a manifest");
+
+        @Override
+        public int read() throws IOException {
+            int b = super.read();
+            if (b >= 0) {
+                md.update((byte) b);
+                size++;
+            }
+            return b;
+        }
+
+        @Override
+        public int read(byte[] b, int off, int len) throws IOException {
+            int length = super.read(b, off, len);
+            if (length > 0) {
+                md.update(b, off, length);
+                this.size += length;
+            }
+            return length;
+        }
+
+        public String getDigest() {
+            byte[] digest = md.digest();
+            StringBuilder sb = new StringBuilder();
+            for (byte b : digest) {
+                sb.append(String.format("%02x", b));
+            }
+            return sb.toString();
+        }
+
+        public long getSize() {
+            return size;
+        }
     }
 
     /**
@@ -335,7 +409,9 @@
         HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
         try (InputStream is = conn.getInputStream()) {
             String type = conn.getContentType();
-            if ("application/java-archive".equals(type) || "application/octet-stream".equals(type)) {
+            if ("application/java-archive".equals(type)
+                    || "application/octet-stream".equals(type)
+                    || "application/vnd.osgi.bundle".equals(type)) {
                 // I have a jar/binary, potentially a resource
                 try {
                     if ((filter == null) || (url.matches(filter))) {
@@ -448,9 +524,12 @@
         HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
         try (InputStream is = conn.getInputStream()) {
             String type = conn.getContentType();
-            if ("application/java-archive".equals(type) || "application/octet-stream".equals(type)) {
+            if ("application/java-archive".equals(type)
+                    || "application/octet-stream".equals(type)
+                    || "application/vnd.osgi.bundle".equals(type)) {
                 try {
                     if ((filter == null) || (url.matches(filter))) {
+                        // Make sure this is a valid bundle
                         ResourceImpl resource = createResource(new URL(url));
                         LOGGER.debug("Copy {} into the Cave repository storage", url);
                         int index = url.lastIndexOf("/");
@@ -482,32 +561,9 @@
     }
 
     /**
-     * Convert the Resource absolute URI to an URI relative to the repository one.
-     *
-     * @param resource the Resource to manipulate.
-     */
-    private void useResourceRelativeUri(ResourceImpl resource) {
-        for (Capability cap : resource.getCapabilities(null)) {
-            if (cap.getNamespace().equals(CONTENT_NAMESPACE)) {
-                String resourceURI = cap.getAttributes().get(CAPABILITY_URL_ATTRIBUTE).toString();
-                String locationURI = "file:" + getLocation();
-                LOGGER.debug("Converting resource URI {} relatively to repository URI {}", resourceURI, locationURI);
-                if (resourceURI.startsWith(locationURI)) {
-                    resourceURI = resourceURI.substring(locationURI.length() + 1);
-                    LOGGER.debug("Resource URI converted to " + resourceURI);
-                    // This is a bit hacky, but the map is not read only
-                    cap.getAttributes().put(CAPABILITY_URL_ATTRIBUTE, resourceURI);
-                }
-                break;
-            }
-        }
-    }
-
-    /**
      * Get the File object of the repository.xml file.
      *
      * @return the File corresponding to the repository.xml.
-     * @throws Exception
      */
     private Path getRepositoryXmlFile() {
         return getLocationPath().resolve("repository.xml");