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.Strings;
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    @SuppressWarnings("java:S3776") // complexity
67    public void installPackage(PackageFile packageFile, boolean replicate, PackageManagerHelper pkgmgr,
68        CloseableHttpClient httpClient, HttpClientContext packageManagerHttpClientContext, HttpClientContext consoleHttpClientContext,
69        PackageManagerProperties props) throws IOException, PackageManagerException {
70  
71      boolean force = packageFile.isForce();
72  
73      if (force) {
74        // in force mode, just check that package manager is available and then start uploading
75        ensurePackageManagerAvailability(pkgmgr, httpClient, packageManagerHttpClientContext);
76      }
77      else {
78        // otherwise check if package is already installed first, and skip further processing if it is
79        // this implicitly also checks the availability of the package manager
80        PackageInstalledStatus status = getPackageInstalledStatus(packageFile, pkgmgr, httpClient, packageManagerHttpClientContext);
81        switch (status) {
82          case NOT_FOUND:
83            log.debug("Package is not found in package list: proceed with install.");
84            break;
85          case INSTALLED:
86            log.info("Package skipped because it was already uploaded.");
87            return;
88          case UPLOADED:
89            log.info("Package was already uploaded but not installed: proceed with install and switch to force mode.");
90            force = true;
91            break;
92          case INSTALLED_OTHER_VERSION:
93            log.info("Package was already uploaded, but another version was installed more recently: proceed with install and switch to force mode.");
94            force = true;
95            break;
96          default:
97            throw new PackageManagerException("Unexpected status: " + status);
98        }
99      }
100 
101     // prepare post method
102     HttpPost post = new HttpPost(url + "/.json?cmd=upload");
103     HttpClientUtil.applyRequestConfig(post, packageFile, props);
104     MultipartEntityBuilder entityBuilder = MultipartEntityBuilder.create()
105       .addBinaryBody("package", packageFile.getFile());
106     if (force) {
107       entityBuilder.addTextBody("force", "true");
108     }
109     post.setEntity(entityBuilder.build());
110 
111     // execute post
112     JSONObject jsonResponse = pkgmgr.executePackageManagerMethodJson(httpClient, packageManagerHttpClientContext, post);
113     boolean success = jsonResponse.optBoolean("success", false);
114     String msg = jsonResponse.optString("msg", null);
115     String path = jsonResponse.optString("path", null);
116     if (success) {
117       if (packageFile.isInstall()) {
118         log.info("Package uploaded to {}, now installing...", path);
119 
120         try {
121           post = new HttpPost(url + "/console.html" + new URIBuilder().setPath(path).build().getRawPath() + "?cmd=install"
122               + (packageFile.isRecursive() ? "&recursive=true" : ""));
123           HttpClientUtil.applyRequestConfig(post, packageFile, props);
124         }
125         catch (URISyntaxException ex) {
126           throw new PackageManagerException("Invalid path: " + path, ex);
127         }
128 
129         // execute post
130         pkgmgr.executePackageManagerMethodHtmlOutputResponse(httpClient, packageManagerHttpClientContext, post);
131 
132         // delay further processing after install (if activated)
133         delay(packageFile.getDelayAfterInstallSec());
134 
135         // after install: if bundles are still stopping/starting, wait for completion
136         pkgmgr.waitForBundlesActivation(httpClient, consoleHttpClientContext);
137         // after install: if packages are still installing, wait for completion
138         pkgmgr.waitForPackageManagerInstallStatusFinished(httpClient, packageManagerHttpClientContext);
139         // after install: validate system ready status
140         pkgmgr.waitForSystemReady(httpClient, consoleHttpClientContext);
141       }
142       else {
143         log.info("Package uploaded successfully to {} (without installing).", path);
144       }
145     }
146     else if (Strings.CS.startsWith(msg, CRX_PACKAGE_EXISTS_ERROR_MESSAGE_PREFIX) && !force) {
147       log.info("Package skipped because it was already uploaded.");
148     }
149     else {
150       throw new PackageManagerException("Package upload failed: " + msg);
151     }
152 
153     // replicate content package
154     if (success && replicate) {
155       log.info("Replicate package {}...", path);
156 
157       try {
158         post = new HttpPost(url + "/console.html" + new URIBuilder().setPath(path).build().getRawPath() + "?cmd=replicate");
159         HttpClientUtil.applyRequestConfig(post, packageFile, props);
160       }
161       catch (URISyntaxException ex) {
162         throw new PackageManagerException("Invalid path: " + path, ex);
163       }
164 
165       // execute post
166       pkgmgr.executePackageManagerMethodHtmlOutputResponse(httpClient, packageManagerHttpClientContext, post);
167     }
168   }
169 
170   @SuppressWarnings("PMD.GuardLogStatement")
171   private void delay(int seconds) {
172     if (seconds > 0) {
173       log.info("Wait {} seconds after package install...", seconds);
174       try {
175         Thread.sleep(seconds * 1000L);
176       }
177       catch (InterruptedException ex) {
178         Thread.currentThread().interrupt();
179       }
180     }
181   }
182 
183   private void ensurePackageManagerAvailability(PackageManagerHelper pkgmgr, CloseableHttpClient httpClient, HttpClientContext context) {
184     // do a help GET call before upload to ensure package manager is running
185     HttpGet get = new HttpGet(url + ".jsp?cmd=help");
186     pkgmgr.executePackageManagerMethodStatus(httpClient, context, get);
187   }
188 
189   private PackageInstalledStatus getPackageInstalledStatus(PackageFile packageFile, PackageManagerHelper pkgmgr,
190       CloseableHttpClient httpClient, HttpClientContext context) throws IOException {
191     // list packages in AEM instances and check for exact match
192     String baseUrl = VendorInstallerFactory.getBaseUrl(url);
193     String packageListUrl = baseUrl + PackageInstalledChecker.PACKMGR_LIST_URL;
194     HttpGet get = new HttpGet(packageListUrl);
195     JSONObject result = pkgmgr.executePackageManagerMethodJson(httpClient, context, get);
196 
197     Map<String, Object> props = ContentPackageProperties.get(packageFile.getFile());
198     String group = (String)props.get(PackageProperties.NAME_GROUP);
199     String name = (String)props.get(PackageProperties.NAME_NAME);
200     String version = (String)props.get(PackageProperties.NAME_VERSION);
201 
202     PackageInstalledChecker checker = new PackageInstalledChecker(result);
203     return checker.getStatus(group, name, version);
204   }
205 
206 }