View Javadoc
1   /*
2    * #%L
3    * wcm.io
4    * %%
5    * Copyright (C) 2017 wcm.io
6    * %%
7    * Licensed under the Apache License, Version 2.0 (the "License");
8    * you may not use this file except in compliance with the License.
9    * You may obtain a copy of the License at
10   *
11   *      http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing, software
14   * distributed under the License is distributed on an "AS IS" BASIS,
15   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   * See the License for the specific language governing permissions and
17   * limitations under the License.
18   * #L%
19   */
20  package io.wcm.tooling.commons.packmgr.install.crx;
21  
22  import static io.wcm.tooling.commons.packmgr.PackageManagerHelper.CRX_PACKAGE_EXISTS_ERROR_MESSAGE_PREFIX;
23  
24  import java.io.IOException;
25  import java.net.URISyntaxException;
26  import java.util.Map;
27  
28  import org.apache.commons.lang3.StringUtils;
29  import org.apache.http.client.methods.HttpGet;
30  import org.apache.http.client.methods.HttpPost;
31  import org.apache.http.client.protocol.HttpClientContext;
32  import org.apache.http.client.utils.URIBuilder;
33  import org.apache.http.entity.mime.MultipartEntityBuilder;
34  import org.apache.http.impl.client.CloseableHttpClient;
35  import org.apache.jackrabbit.vault.packaging.PackageProperties;
36  import org.json.JSONObject;
37  import org.slf4j.Logger;
38  import org.slf4j.LoggerFactory;
39  
40  import io.wcm.tooling.commons.packmgr.PackageManagerException;
41  import io.wcm.tooling.commons.packmgr.PackageManagerHelper;
42  import io.wcm.tooling.commons.packmgr.PackageManagerProperties;
43  import io.wcm.tooling.commons.packmgr.install.PackageFile;
44  import io.wcm.tooling.commons.packmgr.install.VendorInstallerFactory;
45  import io.wcm.tooling.commons.packmgr.install.VendorPackageInstaller;
46  import io.wcm.tooling.commons.packmgr.util.ContentPackageProperties;
47  import io.wcm.tooling.commons.packmgr.util.HttpClientUtil;
48  
49  /**
50   * Package Installer for AEM's CRX Package Manager
51   */
52  public class CrxPackageInstaller implements VendorPackageInstaller {
53  
54    private final String url;
55  
56    private static final Logger log = LoggerFactory.getLogger(CrxPackageInstaller.class);
57  
58    /**
59     * @param url URL
60     */
61    public CrxPackageInstaller(String url) {
62      this.url = url;
63    }
64  
65    @Override
66    public void installPackage(PackageFile packageFile, boolean replicate, PackageManagerHelper pkgmgr,
67        CloseableHttpClient httpClient, HttpClientContext packageManagerHttpClientContext, HttpClientContext consoleHttpClientContext,
68        PackageManagerProperties props) throws IOException, PackageManagerException {
69  
70      boolean force = packageFile.isForce();
71  
72      if (force) {
73        // in force mode, just check that package manager is available and then start uploading
74        ensurePackageManagerAvailability(pkgmgr, httpClient, packageManagerHttpClientContext);
75      }
76      else {
77        // otherwise check if package is already installed first, and skip further processing if it is
78        // this implicitly also checks the availability of the package manager
79        PackageInstalledStatus status = getPackageInstalledStatus(packageFile, pkgmgr, httpClient, packageManagerHttpClientContext);
80        switch (status) {
81          case NOT_FOUND:
82            log.debug("Package is not found in package list: proceed with install.");
83            break;
84          case INSTALLED:
85            log.info("Package skipped because it was already uploaded.");
86            return;
87          case UPLOADED:
88            log.info("Package was already uploaded but not installed: proceed with install and switch to force mode.");
89            force = true;
90            break;
91          case INSTALLED_OTHER_VERSION:
92            log.info("Package was already uploaded, but another version was installed more recently: proceed with install and switch to force mode.");
93            force = true;
94            break;
95          default:
96            throw new PackageManagerException("Unexpected status: " + status);
97        }
98      }
99  
100     // prepare post method
101     HttpPost post = new HttpPost(url + "/.json?cmd=upload");
102     HttpClientUtil.applyRequestConfig(post, packageFile, props);
103     MultipartEntityBuilder entityBuilder = MultipartEntityBuilder.create()
104         .addBinaryBody("package", packageFile.getFile());
105     if (force) {
106       entityBuilder.addTextBody("force", "true");
107     }
108     post.setEntity(entityBuilder.build());
109 
110     // execute post
111     JSONObject jsonResponse = pkgmgr.executePackageManagerMethodJson(httpClient, packageManagerHttpClientContext, post);
112     boolean success = jsonResponse.optBoolean("success", false);
113     String msg = jsonResponse.optString("msg", null);
114     String path = jsonResponse.optString("path", null);
115     if (success) {
116       if (packageFile.isInstall()) {
117         log.info("Package uploaded to {}, now installing...", path);
118 
119         try {
120           post = new HttpPost(url + "/console.html" + new URIBuilder().setPath(path).build().getRawPath() + "?cmd=install"
121               + (packageFile.isRecursive() ? "&recursive=true" : ""));
122           HttpClientUtil.applyRequestConfig(post, packageFile, props);
123         }
124         catch (URISyntaxException ex) {
125           throw new PackageManagerException("Invalid path: " + path, ex);
126         }
127 
128         // execute post
129         pkgmgr.executePackageManagerMethodHtmlOutputResponse(httpClient, packageManagerHttpClientContext, post);
130 
131         // delay further processing after install (if activated)
132         delay(packageFile.getDelayAfterInstallSec());
133 
134         // after install: if bundles are still stopping/starting, wait for completion
135         pkgmgr.waitForBundlesActivation(httpClient, consoleHttpClientContext);
136         // after install: if packages are still installing, wait for completion
137         pkgmgr.waitForPackageManagerInstallStatusFinished(httpClient, packageManagerHttpClientContext);
138       }
139       else {
140         log.info("Package uploaded successfully to {} (without installing).", path);
141       }
142     }
143     else if (StringUtils.startsWith(msg, CRX_PACKAGE_EXISTS_ERROR_MESSAGE_PREFIX) && !force) {
144       log.info("Package skipped because it was already uploaded.");
145     }
146     else {
147       throw new PackageManagerException("Package upload failed: " + msg);
148     }
149 
150     // replicate content package
151     if (success && replicate) {
152       log.info("Replicate package {}...", path);
153 
154       try {
155         post = new HttpPost(url + "/console.html" + new URIBuilder().setPath(path).build().getRawPath() + "?cmd=replicate");
156         HttpClientUtil.applyRequestConfig(post, packageFile, props);
157       }
158       catch (URISyntaxException ex) {
159         throw new PackageManagerException("Invalid path: " + path, ex);
160       }
161 
162       // execute post
163       pkgmgr.executePackageManagerMethodHtmlOutputResponse(httpClient, packageManagerHttpClientContext, post);
164     }
165   }
166 
167   @SuppressWarnings("PMD.GuardLogStatement")
168   private void delay(int seconds) {
169     if (seconds > 0) {
170       log.info("Wait {} seconds after package install...", seconds);
171       try {
172         Thread.sleep(seconds * 1000L);
173       }
174       catch (InterruptedException ex) {
175         // ignore
176       }
177     }
178   }
179 
180   private void ensurePackageManagerAvailability(PackageManagerHelper pkgmgr, CloseableHttpClient httpClient, HttpClientContext context) {
181     // do a help GET call before upload to ensure package manager is running
182     HttpGet get = new HttpGet(url + ".jsp?cmd=help");
183     pkgmgr.executePackageManagerMethodStatus(httpClient, context, get);
184   }
185 
186   private PackageInstalledStatus getPackageInstalledStatus(PackageFile packageFile, PackageManagerHelper pkgmgr,
187       CloseableHttpClient httpClient, HttpClientContext context) throws IOException {
188     // list packages in AEM instances and check for exact match
189     String baseUrl = VendorInstallerFactory.getBaseUrl(url);
190     String packageListUrl = baseUrl + PackageInstalledChecker.PACKMGR_LIST_URL;
191     HttpGet get = new HttpGet(packageListUrl);
192     JSONObject result = pkgmgr.executePackageManagerMethodJson(httpClient, context, get);
193 
194     Map<String, Object> props = ContentPackageProperties.get(packageFile.getFile());
195     String group = (String)props.get(PackageProperties.NAME_GROUP);
196     String name = (String)props.get(PackageProperties.NAME_NAME);
197     String version = (String)props.get(PackageProperties.NAME_VERSION);
198 
199     PackageInstalledChecker checker = new PackageInstalledChecker(result);
200     return checker.getStatus(group, name, version);
201   }
202 
203 }