blob: cb7159e4bad2df4b241890e6a2e7663c0d1cd36a [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.maven.bundlesupport;
import static org.apache.sling.maven.bundlesupport.JsonSupport.JSON_MIME_TYPE;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.methods.FileRequestEntity;
import org.apache.commons.httpclient.methods.HeadMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.PutMethod;
import org.apache.commons.httpclient.methods.multipart.FilePart;
import org.apache.commons.httpclient.methods.multipart.FilePartSource;
import org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity;
import org.apache.commons.httpclient.methods.multipart.Part;
import org.apache.commons.httpclient.methods.multipart.StringPart;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.apache.sling.maven.bundlesupport.fsresource.SlingInitialContentMounter;
abstract class AbstractBundleInstallMojo extends AbstractBundlePostMojo {
/**
* If a PUT via WebDAV should be used instead of the standard POST to the
* Felix Web Console. In the <code>uninstall</code> goal, a HTTP DELETE will be
* used.
*
* @deprecated Use {@link #deploymentMethod} instead.
*/
@Parameter(property="sling.usePut", defaultValue = "false")
protected boolean usePut;
/**
* Possible methodologies for deploying (installing and uninstalling)
* bundles from the remote server.
* Use camel-case values because those are used when you configure the plugin (and uppercase with separators "_" just looks ugly in that context)
*/
enum BundleDeploymentMethod {
/** Via POST to Felix Web Console */
WebConsole,
/** Via WebDAV */
WebDAV,
/** Via POST to Sling directly */
SlingPostServlet;
}
/**
* Bundle deployment method. One of the following three values are allowed
* <ol>
* <li><strong>WebConsole</strong>, uses the <a href="http://felix.apache.org/documentation/subprojects/apache-felix-web-console/web-console-restful-api.html#post-requests">
* Felix Web Console REST API</a> for deployment (HTTP POST). This is the default.
* Make sure that {@link #slingUrl} points to the Felix Web Console in that case.</li>
* <li><strong>WebDAV</strong>, uses <a href="https://sling.apache.org/documentation/development/repository-based-development.html">
* WebDAV</a> for deployment (HTTP PUT). Make sure that {@link #slingUrl} points to the entry path of
* the Sling WebDAV bundle (usually below regular Sling root URL). Issues a HTTP Delete for the uninstall goal.
* <li><strong>SlingPostServlet</strong>, uses the
* <a href="https://sling.apache.org/documentation/bundles/manipulating-content-the-slingpostservlet-servlets-post.html">Sling Post Servlet</a> for deployment (HTTP POST).
* Make sure that {@link #slingUrl} points a path which is handled by the Sling POST Servlet (usually below regular Sling root URL).</li>
* </ol>
*
* This has precedence over the deprecated parameter {@link #usePut}.
*/
@Parameter(property="sling.deploy.method", required = false)
protected BundleDeploymentMethod deploymentMethod;
/**
* The content type / mime type used for WebDAV or Sling POST deployment.
*/
@Parameter(property="sling.mimeType", defaultValue = "application/java-archive", required = true)
protected String mimeType;
/**
* The start level to set on the installed bundle. If the bundle is already installed and therefore is only
* updated this parameter is ignored. The parameter is also ignored if the running Sling instance has no
* StartLevel service (which is unusual actually). Only applies when POSTing to Felix Web Console.
*/
@Parameter(property="sling.bundle.startlevel", defaultValue = "20", required = true)
private String bundleStartLevel;
/**
* Whether to start the uploaded bundle or not. Only applies when POSTing
* to Felix Web Console
*/
@Parameter(property="sling.bundle.start", defaultValue = "true", required = true)
private boolean bundleStart;
/**
* Whether to refresh the packages after installing the uploaded bundle.
* Only applies when POSTing to Felix Web Console
*/
@Parameter(property="sling.refreshPackages", defaultValue = "true", required = true)
private boolean refreshPackages;
/**
* Whether to add the mapping for the
* <a href="https://sling.apache.org/documentation/bundles/accessing-filesystem-resources-extensions-fsresource.html">Apache Sling File System Resource Provider</a>.
*/
@Parameter(property="sling.mountByFS", defaultValue = "false", required = true)
private boolean mountByFS;
/**
* The Maven project.
*/
@Parameter(defaultValue = "${project}", required = true, readonly = true)
protected MavenProject project;
public AbstractBundleInstallMojo() {
super();
}
protected abstract String getBundleFileName() throws MojoExecutionException;
/**
* Returns the URL with the filename appended to it.
* @param targetURL the original requested targetURL to append fileName to
* @param fileName the name of the file to append to the targetURL.
*/
protected String getURLWithFilename(String targetURL, String fileName) {
return targetURL + (targetURL.endsWith("/") ? "" : "/") + fileName;
}
@Override
public void execute() throws MojoExecutionException {
// get the file to upload
String bundleFileName = getBundleFileName();
// only upload if packaging as an osgi-bundle
File bundleFile = new File(bundleFileName);
if(!bundleFile.exists()) {
getLog().info(bundleFile + " does not exist, no uploading");
return;
}
String bundleName = getBundleSymbolicName(bundleFile);
if (bundleName == null) {
getLog().info(bundleFile + " is not an OSGi Bundle, not uploading");
return;
}
String targetURL = getTargetURL();
BundleDeploymentMethod deploymentMethod = getDeploymentMethod();
getLog().info(
"Installing Bundle " + bundleName + "(" + bundleFile + ") to "
+ targetURL + " via " + deploymentMethod);
switch (deploymentMethod) {
case SlingPostServlet:
postToSling(targetURL, bundleFile);
break;
case WebConsole:
postToFelix(targetURL, bundleFile);
break;
case WebDAV:
putViaWebDav(targetURL, bundleFile);
break;
// sanity check to make sure it gets handled in some fashion
default:
throw new MojoExecutionException("Unrecognized BundleDeployMethod " + deploymentMethod);
}
if ( mountByFS ) {
configure(getConsoleTargetURL(), bundleFile);
}
}
protected void configure(final String targetURL, final File file) throws MojoExecutionException {
new SlingInitialContentMounter(getLog(), getHttpClient(), project).mount(targetURL, file);
}
/**
* Retrieve the bundle deployment method matching the configuration.
* @return bundle deployment method matching the plugin configuration.
*/
protected BundleDeploymentMethod getDeploymentMethod() throws MojoExecutionException {
if (this.deploymentMethod == null) {
if (usePut) {
getLog().warn("Using deprecated configuration parameter 'usePut=true', please instead use the new parameter 'deploymentMethod=WebDAV'!");
return BundleDeploymentMethod.WebDAV;
} else {
return BundleDeploymentMethod.WebConsole;
}
} else {
return deploymentMethod;
}
}
/**
* Install the bundle via POST to the Felix Web Console
* @param targetURL the URL to the Felix Web Console Bundles listing
* @param file the file to POST
* @throws MojoExecutionException
* @see <a href="http://felix.apache.org/documentation/subprojects/apache-felix-web-console/web-console-restful-api.html#post-requests">Webconsole RESTful API</a>
* @see <a href="https://github.com/apache/felix/blob/trunk/webconsole/src/main/java/org/apache/felix/webconsole/internal/core/BundlesServlet.java">BundlesServlet@Github</a>
*/
protected void postToFelix(String targetURL, File file)
throws MojoExecutionException {
// append pseudo path after root URL to not get redirected for nothing
final PostMethod filePost = new PostMethod(targetURL + "/install");
try {
// set referrer
filePost.setRequestHeader("referer", "about:blank");
List<Part> partList = new ArrayList<Part>();
partList.add(new StringPart("action", "install"));
partList.add(new StringPart("_noredir_", "_noredir_"));
partList.add(new FilePart("bundlefile", new FilePartSource(
file.getName(), file)));
partList.add(new StringPart("bundlestartlevel", bundleStartLevel));
if (bundleStart) {
partList.add(new StringPart("bundlestart", "start"));
}
if (refreshPackages) {
partList.add(new StringPart("refreshPackages", "true"));
}
Part[] parts = partList.toArray(new Part[partList.size()]);
filePost.setRequestEntity(new MultipartRequestEntity(parts,
filePost.getParams()));
int status = getHttpClient().executeMethod(filePost);
if (status == HttpStatus.SC_OK) {
getLog().info("Bundle installed");
} else {
String msg = "Installation failed, cause: "
+ HttpStatus.getStatusText(status);
if (failOnError) {
throw new MojoExecutionException(msg);
} else {
getLog().error(msg);
}
}
} catch (Exception ex) {
throw new MojoExecutionException("Installation on " + targetURL
+ " failed, cause: " + ex.getMessage(), ex);
} finally {
filePost.releaseConnection();
}
}
/**
* Perform the operation via POST to SlingPostServlet
* @param targetURL the URL of the Sling instance to post the file to.
* @param file the file being interacted with the POST to Sling.
* @throws MojoExecutionException
*/
protected void postToSling(String targetURL, File file) throws MojoExecutionException {
/* truncate off trailing slash as this has special behaviorisms in
* the SlingPostServlet around created node name conventions */
if (targetURL.endsWith("/")) {
targetURL = targetURL.substring(0, targetURL.length()-1);
}
// append pseudo path after root URL to not get redirected for nothing
final PostMethod filePost = new PostMethod(targetURL);
try {
Part[] parts = new Part[2];
// Add content type to force the configured mimeType value
parts[0] = new FilePart("*", new FilePartSource(
file.getName(), file), mimeType, null);
// Add TypeHint to have jar be uploaded as file (not as resource)
parts[1] = new StringPart("*@TypeHint", "nt:file");
/* Request JSON response from Sling instead of standard HTML, to
* reduce the payload size (if the PostServlet supports it). */
filePost.setRequestHeader("Accept", JSON_MIME_TYPE);
filePost.setRequestEntity(new MultipartRequestEntity(parts,
filePost.getParams()));
int status = getHttpClient().executeMethod(filePost);
// SlingPostServlet may return 200 or 201 on creation, accept both
if (status == HttpStatus.SC_OK || status == HttpStatus.SC_CREATED) {
getLog().info("Bundle installed");
} else {
String msg = "Installation failed, cause: "
+ HttpStatus.getStatusText(status);
if (failOnError) {
throw new MojoExecutionException(msg);
} else {
getLog().error(msg);
}
}
} catch (Exception ex) {
throw new MojoExecutionException("Installation on " + targetURL
+ " failed, cause: " + ex.getMessage(), ex);
} finally {
filePost.releaseConnection();
}
}
/**
* Puts the file via PUT (leveraging WebDAV). Creates the intermediate folders as well.
* @param targetURL
* @param file
* @throws MojoExecutionException
* @see <a href="https://tools.ietf.org/html/rfc4918#section-9.7.1">RFC 4918</a>
*/
protected void putViaWebDav(String targetURL, File file) throws MojoExecutionException {
boolean success = false;
int status;
try {
status = performPut(targetURL, file);
if (status >= 200 && status < 300) {
success = true;
} else if (status == HttpStatus.SC_CONFLICT) {
getLog().debug("Bundle not installed due missing parent folders. Attempting to create parent structure.");
createIntermediaryPaths(targetURL);
getLog().debug("Re-attempting bundle install after creating parent folders.");
status = performPut(targetURL, file);
if (status >= 200 && status < 300) {
success = true;
}
}
if (!success) {
String msg = "Installation failed, cause: "
+ HttpStatus.getStatusText(status);
if (failOnError) {
throw new MojoExecutionException(msg);
} else {
getLog().error(msg);
}
}
} catch (Exception ex) {
throw new MojoExecutionException("Installation on " + targetURL
+ " failed, cause: " + ex.getMessage(), ex);
}
}
private int performPut(String targetURL, File file) throws HttpException, IOException {
PutMethod filePut = new PutMethod(getURLWithFilename(targetURL, file.getName()));
try {
filePut.setRequestEntity(new FileRequestEntity(file, mimeType));
return getHttpClient().executeMethod(filePut);
} finally {
filePut.releaseConnection();
}
}
private int performHead(String uri) throws HttpException, IOException {
HeadMethod head = new HeadMethod(uri);
try {
return getHttpClient().executeMethod(head);
} finally {
head.releaseConnection();
}
}
private int performMkCol(String uri) throws IOException {
MkColMethod mkCol = new MkColMethod(uri);
try {
return getHttpClient().executeMethod(mkCol);
} finally {
mkCol.releaseConnection();
}
}
private void createIntermediaryPaths(String targetURL) throws HttpException, IOException, MojoExecutionException {
// extract all intermediate URIs (longest one first)
List<String> intermediateUris = IntermediateUrisExtractor.extractIntermediateUris(targetURL);
// 1. go up to the node in the repository which exists already (HEAD request towards the root node)
String existingIntermediateUri = null;
// go through all intermediate URIs (longest first)
for (String intermediateUri : intermediateUris) {
// until one is existing
int result = performHead(intermediateUri) ;
if (result == HttpStatus.SC_OK) {
existingIntermediateUri = intermediateUri;
break;
} else if (result != HttpStatus.SC_NOT_FOUND) {
throw new MojoExecutionException("Failed getting intermediate path at " + intermediateUri + "."
+ " Reason: " + HttpStatus.getStatusText(result));
}
}
if (existingIntermediateUri == null) {
throw new MojoExecutionException(
"Could not find any intermediate path up until the root of " + targetURL + ".");
}
// 2. now create from that level on each intermediate node individually towards the target path
int startOfInexistingIntermediateUri = intermediateUris.indexOf(existingIntermediateUri);
if (startOfInexistingIntermediateUri == -1) {
throw new IllegalStateException(
"Could not find intermediate uri " + existingIntermediateUri + " in the list");
}
for (int index = startOfInexistingIntermediateUri - 1; index >= 0; index--) {
// use MKCOL to create the intermediate paths
String intermediateUri = intermediateUris.get(index);
int result = performMkCol(intermediateUri);
if (result == HttpStatus.SC_CREATED || result == HttpStatus.SC_OK) {
getLog().debug("Intermediate path at " + intermediateUri + " successfully created");
continue;
} else {
throw new MojoExecutionException("Failed creating intermediate path at '" + intermediateUri + "'."
+ " Reason: " + HttpStatus.getStatusText(result));
}
}
}
}