SLING-3019 - Provide a mechanism to install a bundle based on a
directory

Allow updating a bundle from an uploaded jar file.

git-svn-id: https://svn.apache.org/repos/asf/sling/trunk@1534645 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/pom.xml b/pom.xml
index 0ec7b01..99d5b37 100644
--- a/pom.xml
+++ b/pom.xml
@@ -99,5 +99,11 @@
             <version>2.4</version>
             <scope>provided</scope>
         </dependency>
+        <dependency>
+            <groupId>commons-fileupload</groupId>
+            <artifactId>commons-fileupload</artifactId>
+            <version>1.2.2</version>
+            <scope>provided</scope>
+        </dependency>
     </dependencies>
 </project>
diff --git a/src/main/java/org/apache/sling/tooling/support/install/impl/InstallServlet.java b/src/main/java/org/apache/sling/tooling/support/install/impl/InstallServlet.java
index 765eda7..47b3bc0 100644
--- a/src/main/java/org/apache/sling/tooling/support/install/impl/InstallServlet.java
+++ b/src/main/java/org/apache/sling/tooling/support/install/impl/InstallServlet.java
@@ -22,7 +22,9 @@
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.util.List;
 import java.util.jar.JarFile;
+import java.util.jar.JarInputStream;
 import java.util.jar.JarOutputStream;
 import java.util.jar.Manifest;
 import java.util.zip.Deflater;
@@ -38,13 +40,20 @@
 import org.apache.felix.scr.annotations.Activate;
 import org.apache.felix.scr.annotations.Component;
 import org.apache.felix.scr.annotations.Property;
+import org.apache.felix.scr.annotations.Reference;
 import org.apache.felix.scr.annotations.Service;
 import org.osgi.framework.Bundle;
 import org.osgi.framework.BundleContext;
 import org.osgi.framework.BundleException;
 import org.osgi.framework.Constants;
+import org.osgi.service.packageadmin.PackageAdmin;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.apache.commons.fileupload.FileItem;
+import org.apache.commons.fileupload.FileUploadException;
+import org.apache.commons.fileupload.disk.DiskFileItemFactory;
+import org.apache.commons.fileupload.servlet.ServletFileUpload;
+import org.apache.commons.io.IOUtils;
 
 /**
  * Prototype for installing/updating a bundle from a directory
@@ -60,8 +69,13 @@
 
     private static final String DIR = "dir";
 
+    private static final int UPLOAD_IN_MEMORY_SIZE_THRESHOLD = 512 * 1024 * 1024;
+
     private BundleContext bundleContext;
 
+    @Reference
+    private PackageAdmin packageAdmin;
+
     @Activate
     protected void activate(final BundleContext bundleContext) {
         this.bundleContext = bundleContext;
@@ -71,16 +85,98 @@
     protected void doPost(HttpServletRequest req, HttpServletResponse resp)
             throws ServletException, IOException {
         final String dirPath = req.getParameter(DIR);
-        if ( dirPath == null ) {
-            logger.error("No dir parameter specified : {}", req.getParameterMap());
+
+        boolean isMultipart = ServletFileUpload.isMultipartContent(req);
+
+        if (dirPath == null && !isMultipart) {
+            logger.error("No dir parameter specified : {} and no multipart content found", req.getParameterMap());
             resp.setStatus(500);
             InstallationResult result = new InstallationResult(false, "No dir parameter specified: "
-                    + req.getParameterMap());
+                    + req.getParameterMap() + " and no multipart content found");
             result.render(resp.getWriter());
             return;
         }
-        final File dir = new File(dirPath);
-        installBasedOnDirectory(resp, dir);
+
+        if (isMultipart) {
+            installBasedOnUploadedJar(req, resp);
+        } else {
+            installBasedOnDirectory(resp, new File(dirPath));
+        }
+    }
+
+    private void installBasedOnUploadedJar(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+
+        InstallationResult result = null;
+
+        try {
+            DiskFileItemFactory factory = new DiskFileItemFactory();
+            // try to hold even largish bundles in memory to potentially improve performance
+            factory.setSizeThreshold(UPLOAD_IN_MEMORY_SIZE_THRESHOLD);
+
+            ServletFileUpload upload = new ServletFileUpload();
+            upload.setFileItemFactory(factory);
+
+            @SuppressWarnings("unchecked")
+            List<FileItem> items = upload.parseRequest(req);
+            if (items.size() != 1) {
+                logAndWriteError("Found " + items.size() + " items to process, but only updating 1 bundle is supported", resp);
+                return;
+            }
+
+            FileItem item = items.get(0);
+
+            JarInputStream jar = null;
+            InputStream rawInput = null;
+            try {
+                jar = new JarInputStream(item.getInputStream());
+                Manifest manifest = jar.getManifest();
+                if (manifest == null) {
+                    logAndWriteError("Uploaded jar file does not contain a manifest", resp);
+                    return;
+                }
+
+                final String symbolicName = manifest.getMainAttributes().getValue(Constants.BUNDLE_SYMBOLICNAME);
+                if (symbolicName == null) {
+                    logAndWriteError("Manifest does not have a " + Constants.BUNDLE_SYMBOLICNAME, resp);
+                    return;
+                }
+
+                // the JarInputStream is used only for validation, we need a fresh input stream for updating
+                rawInput = item.getInputStream();
+
+                Bundle found = getBundle(symbolicName);
+                try {
+                    installOrUpdateBundle(found, rawInput, null);
+
+                    result = new InstallationResult(true, null);
+                    resp.setStatus(200);
+                    result.render(resp.getWriter());
+                    return;
+                } catch (BundleException e) {
+                    logAndWriteError("Unable to install/update bundle " + symbolicName, e, resp);
+                    return;
+                }
+            } finally {
+                IOUtils.closeQuietly(jar);
+                IOUtils.closeQuietly(rawInput);
+            }
+
+        } catch (FileUploadException e) {
+            logAndWriteError("Failed parsing uploaded bundle", e, resp);
+            return;
+        }
+    }
+
+    private void logAndWriteError(String message, HttpServletResponse resp) throws IOException {
+        logger.info(message);
+        resp.setStatus(500);
+        new InstallationResult(false, message).render(resp.getWriter());
+    }
+
+    private void logAndWriteError(String message, Exception e, HttpServletResponse resp) throws IOException {
+        logger.info(message, e);
+        resp.setStatus(500);
+        new InstallationResult(false, message + " : " + e.getMessage()).render(resp.getWriter());
     }
 
     private void installBasedOnDirectory(HttpServletResponse resp, final File dir) throws FileNotFoundException,
@@ -100,13 +196,7 @@
                     final String symbolicName = mf.getMainAttributes().getValue(Constants.BUNDLE_SYMBOLICNAME);
                     if ( symbolicName != null ) {
                         // search bundle
-                        Bundle found = null;
-                        for(final Bundle b : this.bundleContext.getBundles() ) {
-                            if ( symbolicName.equals(b.getSymbolicName()) ) {
-                                found = b;
-                                break;
-                            }
-                        }
+                        Bundle found = getBundle(symbolicName);
 
                         final File tempFile = File.createTempFile(dir.getName(), "bundle");
                         try {
@@ -114,14 +204,9 @@
 
                             final InputStream in = new FileInputStream(tempFile);
                             try {
-                                if ( found != null ) {
-                                    // update
-                                    found.update(in);
-                                } else {
-                                    // install
-                                    final Bundle b = bundleContext.installBundle(dir.getAbsolutePath(), in);
-                                    b.start();
-                                }
+                                String location = dir.getAbsolutePath();
+
+                                installOrUpdateBundle(found, in, location);
                                 result = new InstallationResult(true, null);
                                 resp.setStatus(200);
                                 result.render(resp.getWriter());
@@ -157,6 +242,30 @@
         }
     }
 
+    private void installOrUpdateBundle(Bundle bundle, final InputStream in, String location) throws BundleException {
+        if (bundle != null) {
+            // update
+            bundle.update(in);
+
+            packageAdmin.refreshPackages(new Bundle[] { bundle });
+        } else {
+            // install
+            final Bundle b = bundleContext.installBundle(location, in);
+            b.start();
+        }
+    }
+
+    private Bundle getBundle(final String symbolicName) {
+        Bundle found = null;
+        for (final Bundle b : this.bundleContext.getBundles()) {
+            if (symbolicName.equals(b.getSymbolicName())) {
+                found = b;
+                break;
+            }
+        }
+        return found;
+    }
+
     private static void createJar(final File sourceDir, final File jarFile, final Manifest mf)
     throws IOException {
         final JarOutputStream zos = new JarOutputStream(new FileOutputStream(jarFile));