| /* |
| * 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)); |
| } |
| } |
| } |
| |
| } |