001/*
002 * (C) Copyright 2016 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Nuxeo
018 */
019
020package org.nuxeo.ecm.admin.operation;
021
022import java.util.ArrayList;
023import java.util.List;
024
025import net.sf.json.JSONArray;
026import net.sf.json.JSONObject;
027
028import org.apache.commons.lang3.ArrayUtils;
029import org.apache.commons.logging.Log;
030import org.apache.commons.logging.LogFactory;
031import org.nuxeo.common.utils.ExceptionUtils;
032import org.nuxeo.connect.client.we.StudioSnapshotHelper;
033import org.nuxeo.connect.connector.ConnectServerError;
034import org.nuxeo.connect.data.DownloadablePackage;
035import org.nuxeo.connect.data.DownloadingPackage;
036import org.nuxeo.connect.packages.PackageManager;
037import org.nuxeo.connect.packages.dependencies.DependencyResolution;
038import org.nuxeo.connect.packages.dependencies.TargetPlatformFilterHelper;
039import org.nuxeo.connect.update.LocalPackage;
040import org.nuxeo.connect.update.PackageDependency;
041import org.nuxeo.connect.update.PackageException;
042import org.nuxeo.connect.update.PackageState;
043import org.nuxeo.connect.update.PackageUpdateService;
044import org.nuxeo.connect.update.ValidationStatus;
045import org.nuxeo.connect.update.task.Task;
046import org.nuxeo.ecm.admin.runtime.PlatformVersionHelper;
047import org.nuxeo.ecm.admin.runtime.ReloadHelper;
048import org.nuxeo.ecm.automation.core.Constants;
049import org.nuxeo.ecm.automation.core.annotations.Context;
050import org.nuxeo.ecm.automation.core.annotations.Operation;
051import org.nuxeo.ecm.automation.core.annotations.OperationMethod;
052import org.nuxeo.ecm.automation.core.annotations.Param;
053import org.nuxeo.ecm.core.api.Blob;
054import org.nuxeo.ecm.core.api.Blobs;
055import org.nuxeo.ecm.core.api.CoreSession;
056import org.nuxeo.ecm.core.api.NuxeoException;
057import org.nuxeo.ecm.core.api.NuxeoPrincipal;
058import org.nuxeo.runtime.api.Framework;
059import org.nuxeo.runtime.reload.ReloadService;
060
061/**
062 * Operation to trigger a Hot reload of the Studio Snapshot package. You must be an administrator to trigger it.
063 *
064 * @since 8.2
065 */
066@Operation(id = HotReloadStudioSnapshot.ID, category = Constants.CAT_SERVICES, label = "Hot Reload Studio Snapshot Package", description = "Updates Studio project with latest snapshot.")
067public class HotReloadStudioSnapshot {
068
069    protected static final String inProgress = "updateInProgress";
070
071    protected static final String success = "success";
072
073    protected static final String error = "error";
074
075    protected static final String dependencyMismatch = "dependencyMismatch";
076
077    public static final String ID = "Service.HotReloadStudioSnapshot";
078
079    protected static boolean updateInProgress = false;
080
081    private static final Log log = LogFactory.getLog(HotReloadStudioSnapshot.class);
082
083    @Context
084    protected CoreSession session;
085
086    @Context
087    protected PackageManager pm;
088
089    @Param(name = "validate", required = false)
090    protected boolean validate = true;
091
092    @OperationMethod
093    public Blob run() {
094
095        if (updateInProgress) {
096            return jsonHelper(inProgress, "Update in progress.", null);
097        }
098
099        if (!((NuxeoPrincipal) session.getPrincipal()).isAdministrator()) {
100            return jsonHelper(error, "Must be Administrator to use this function.", null);
101        }
102
103        if (!Framework.isDevModeSet()) {
104            return jsonHelper(error, "You must enable Dev mode to Hot reload your Studio Snapshot package.", null);
105        }
106
107        List<DownloadablePackage> pkgs = pm.listRemoteAssociatedStudioPackages();
108        DownloadablePackage snapshotPkg = StudioSnapshotHelper.getSnapshot(pkgs);
109
110        if (snapshotPkg == null) {
111            return jsonHelper(error, "No Snapshot Package was found.", null);
112        }
113
114        try {
115            updateInProgress = true;
116            return hotReloadPackage(snapshotPkg);
117        } finally {
118            updateInProgress = false;
119        }
120    }
121
122    public Blob hotReloadPackage(DownloadablePackage remotePkg) {
123
124        if (validate) {
125            pm.flushCache();
126
127            String targetPlatform = PlatformVersionHelper.getPlatformFilter();
128            if (!TargetPlatformFilterHelper.isCompatibleWithTargetPlatform(remotePkg, targetPlatform)) {
129                return jsonHelper(error,
130                        String.format("This package is not validated for your current platform: %s", targetPlatform),
131                        null);
132            }
133
134            PackageDependency[] pkgDeps = remotePkg.getDependencies();
135            if (log.isDebugEnabled()) {
136                log.debug(String.format("%s target platforms: %s", remotePkg,
137                        ArrayUtils.toString(remotePkg.getTargetPlatforms())));
138                log.debug(String.format("%s dependencies: %s", remotePkg, ArrayUtils.toString(pkgDeps)));
139            }
140
141            String packageId = remotePkg.getId();
142
143            // check deps requirements
144            if (pkgDeps != null && pkgDeps.length > 0) {
145                DependencyResolution resolution = pm.resolveDependencies(packageId, targetPlatform);
146                if (resolution.isFailed() && targetPlatform != null) {
147                    // retry without PF filter in case it gives more information
148                    resolution = pm.resolveDependencies(packageId, null);
149                }
150                if (resolution.isFailed()) {
151                    return jsonHelper(dependencyMismatch,
152                            String.format("Dependency check has failed for package '%s' (%s)", packageId, resolution),
153                            null);
154                } else {
155                    List<String> pkgToInstall = resolution.getInstallPackageIds();
156                    if (pkgToInstall != null && pkgToInstall.size() == 1 && packageId.equals(pkgToInstall.get(0))) {
157                        // ignore
158                    } else if (resolution.requireChanges()) {
159                        // do not install needed deps: they may not be hot-reloadable and that's not what the
160                        // "update snapshot" button is for.
161                        // Returns missing dependencies in message instead of status
162                        List<String> dependencies = new ArrayList<>();
163                        for (String dependency : resolution.getInstallPackageNames()) {
164                            if (!dependency.contains(remotePkg.getName())) {
165                                dependencies.add(dependency);
166                            }
167                        }
168                        return jsonHelper(dependencyMismatch,
169                                "A dependency mismatch has been detected. Please check your Studio project settings and your server configuration.",
170                                dependencies);
171                    }
172                }
173            }
174        }
175
176        boolean useCompatReload = Framework.isBooleanPropertyTrue(ReloadService.USE_COMPAT_HOT_RELOAD);
177        if (!useCompatReload) {
178            log.info("Use hot reload update mechanism");
179            ReloadHelper.hotReloadPackage(remotePkg.getId());
180            return jsonHelper(success, "Studio package installed.", null);
181        }
182        // Install
183        try {
184            PackageUpdateService pus = Framework.getService(PackageUpdateService.class);
185            String packageId = remotePkg.getId();
186            LocalPackage pkg = pus.getPackage(packageId);
187
188            // Uninstall and/or remove if needed
189            if (pkg != null) {
190                removePackage(pus, pkg);
191            }
192
193            // Download
194            DownloadingPackage downloadingPkg = pm.download(packageId);
195            while (!downloadingPkg.isCompleted()) {
196                log.debug("Downloading studio snapshot package: " + packageId);
197                Thread.sleep(100);
198            }
199
200            log.info("Installing " + packageId);
201            pkg = pus.getPackage(packageId);
202            if (pkg == null || PackageState.DOWNLOADED != pkg.getPackageState()) {
203                throw new NuxeoException("Error while downloading studio snapshot " + pkg);
204            }
205            Task installTask = pkg.getInstallTask();
206            try {
207                performTask(installTask);
208                return jsonHelper(success, "Studio package installed.", null);
209            } catch (PackageException e) {
210                installTask.rollback();
211                throw e;
212            }
213        } catch (InterruptedException e) {
214            ExceptionUtils.checkInterrupt(e);
215            throw new NuxeoException("Error while downloading studio snapshot", e);
216        } catch (PackageException | ConnectServerError e) {
217            throw new NuxeoException("Error while installing studio snapshot", e);
218        }
219
220    }
221
222    protected static void removePackage(PackageUpdateService pus, LocalPackage pkg) throws PackageException {
223        log.info(String.format("Removing package %s before update...", pkg.getId()));
224        if (pkg.getPackageState().isInstalled()) {
225            // First remove it to allow SNAPSHOT upgrade
226            log.info("Uninstalling " + pkg.getId());
227            Task uninstallTask = pkg.getUninstallTask();
228            try {
229                performTask(uninstallTask);
230            } catch (PackageException e) {
231                uninstallTask.rollback();
232                throw e;
233            }
234        }
235        pus.removePackage(pkg.getId());
236    }
237
238    protected static void performTask(Task task) throws PackageException {
239        ValidationStatus validationStatus = task.validate();
240        if (validationStatus.hasErrors()) {
241            throw new PackageException(
242                    "Failed to validate package " + task.getPackage().getId() + " -> " + validationStatus.getErrors());
243        }
244        if (validationStatus.hasWarnings()) {
245            log.warn("Got warnings on package validation " + task.getPackage().getId() + " -> "
246                    + validationStatus.getWarnings());
247        }
248        task.run(null);
249    }
250
251    protected static Blob jsonHelper(String status, String message, List<String> dependencies) {
252        JSONObject resultJSON = new JSONObject();
253        JSONArray result = new JSONArray();
254        resultJSON.put("status", status);
255        resultJSON.put("message", message);
256        if (dependencies != null) {
257            resultJSON.put("deps", dependencies);
258        }
259        result.add(resultJSON);
260        return Blobs.createJSONBlob(result.toString());
261    }
262}