blob: 4e2827528519772b3dff9e4de9c4c51866c0bff2 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.sling.tooling.support.install.impl;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import java.util.stream.Stream;
import java.util.zip.Deflater;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleException;
import org.osgi.framework.Constants;
import org.osgi.framework.wiring.FrameworkWiring;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.http.whiteboard.HttpWhiteboardConstants;
import org.osgi.service.http.whiteboard.propertytypes.HttpWhiteboardContextSelect;
import org.osgi.service.http.whiteboard.propertytypes.HttpWhiteboardServletPattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* ReST endpoint for installing/updating a bundle from a directory
*/
@Component(service = Servlet.class)
@HttpWhiteboardServletPattern("/system/sling/tooling/install")
//choose another servlet context with a higher ranking than the Sling one (https://issues.apache.org/jira/browse/SLING-11677)
@HttpWhiteboardContextSelect("(" + HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_NAME + "=org.osgi.service.http)")
public class InstallServlet extends HttpServlet {
private static final long serialVersionUID = -8820366266126231409L;
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private static final String DIR = "dir";
private final BundleContext bundleContext;
@Activate
public InstallServlet(final BundleContext bundleContext) {
this.bundleContext = bundleContext;
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
final String dirPath = req.getParameter(DIR);
final boolean refreshPackages = Boolean.parseBoolean(req.getParameter(dirPath));
try {
if (dirPath == null) {
logger.error("No mandatory dir parameter specified");
resp.setStatus(500);
InstallationResult result = new InstallationResult(false, "No mandatory dir parameter specified");
result.render(resp.getWriter());
return;
}
installBundleFromDirectory(resp, Paths.get(dirPath), refreshPackages);
} catch (IOException e) {
throw new ServletException(e);
}
}
private void logAndWriteError(Exception e, HttpServletResponse resp) throws IOException {
logger.info(e.getMessage(), e);
resp.setStatus(500);
new InstallationResult(false, e.getMessage()).render(resp.getWriter());
}
private void installBundleFromDirectory(HttpServletResponse resp, final Path dir, boolean refreshPackages) throws IOException {
try {
installBundleFromDirectory(dir, refreshPackages);
InstallationResult result = new InstallationResult(true, null);
resp.setStatus(200);
result.render(resp.getWriter());
} catch (IllegalArgumentException e) {
logAndWriteError(e, resp);
}
}
/**
*
* @param dir
* @param refreshPackages
* @throws IOException
* @throws IllegalArgumentException if the provided directory does not contain a valid exploded OSGi bundle
*/
Bundle installBundleFromDirectory(final Path dir, boolean refreshPackages) throws IOException {
if (Files.isDirectory(dir)) {
logger.info("Checking dir {} for bundle install", dir);
final Path manifestFile = dir.resolve(JarFile.MANIFEST_NAME);
if (Files.exists(dir)) {
try (InputStream fis = Files.newInputStream(manifestFile)) {
final Manifest mf = new Manifest(fis);
final String symbolicName = mf.getMainAttributes().getValue(Constants.BUNDLE_SYMBOLICNAME);
if (symbolicName != null) {
// search bundle
Bundle found = getBundle(symbolicName);
Path tmpJarFile = Files.createTempFile(dir.getFileName().toString(), "bundle");
try {
createJar(dir, tmpJarFile, mf);
try (InputStream in = Files.newInputStream(tmpJarFile)) {
String location = dir.toAbsolutePath().toString();
return installOrUpdateBundle(found, in, location, refreshPackages);
} catch (final BundleException be) {
throw new IllegalArgumentException("Unable to install/update bundle from dir " + dir, be);
}
} finally {
Files.delete(tmpJarFile);
}
} else {
throw new IllegalArgumentException("Manifest in " + dir + " does not have a symbolic name");
}
}
} else {
throw new IllegalArgumentException("Dir " + dir + " does not have a manifest");
}
} else {
throw new IllegalArgumentException("Dir " + dir + " does not exist");
}
}
private Bundle installOrUpdateBundle(Bundle bundle, final InputStream in, String location, boolean refreshPackages) throws BundleException {
if (bundle != null) {
// update
bundle.update(in);
} else {
// install
bundle = bundleContext.installBundle(location, in);
bundle.start();
}
if (refreshPackages) {
refreshBundle(bundle);
}
return bundle;
}
private void refreshBundle(Bundle bundle) {
FrameworkWiring frameworkWiring = bundleContext.getBundle(Constants.SYSTEM_BUNDLE_ID).adapt(FrameworkWiring.class);
// take into account added/removed packages for updated bundles and newly satisfied optional package imports
// for new installed bundles
frameworkWiring.refreshBundles(Collections.singleton(bundle));
}
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 Path sourceDir, final Path jarFile, final Manifest mf) throws IOException {
try (JarOutputStream zos = new JarOutputStream(Files.newOutputStream(jarFile))) {
zos.setLevel(Deflater.NO_COMPRESSION);
// manifest first
final ZipEntry anEntry = new ZipEntry(JarFile.MANIFEST_NAME);
zos.putNextEntry(anEntry);
mf.write(zos);
zos.closeEntry();
zipDir(sourceDir, zos, "");
}
}
public static void zipDir(final Path sourceDir, final ZipOutputStream zos, final String prefix) throws IOException {
try (Stream<Path> stream = Files.list(sourceDir)) {
stream.forEach(p ->
{
try {
zipFileOrDir(p, zos, prefix);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
} catch (UncheckedIOException ioe) {
throw ioe.getCause();
}
}
private static void zipFileOrDir(final Path sourceFileOrDir, final ZipOutputStream zos, final String prefix) throws IOException {
if (Files.isDirectory(sourceFileOrDir)) {
final String newPrefix = prefix + sourceFileOrDir.getFileName() + "/";
zos.putNextEntry(new ZipEntry(newPrefix));
zipDir(sourceFileOrDir, zos, newPrefix);
} else {
final String entry = prefix + sourceFileOrDir.getFileName();
if (!JarFile.MANIFEST_NAME.equals(entry)) {
final ZipEntry anEntry = new ZipEntry(entry);
zos.putNextEntry(anEntry);
Files.copy(sourceFileOrDir, zos);
}
}
}
}