001/*
002 * (C) Copyright 2016-2018 Nuxeo (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.io.IOException;
023import java.util.ArrayList;
024import java.util.LinkedHashMap;
025import java.util.List;
026import java.util.Map;
027
028import org.apache.commons.lang3.ArrayUtils;
029import org.apache.logging.log4j.LogManager;
030import org.apache.logging.log4j.Logger;
031import org.nuxeo.connect.client.ConnectClientComponent;
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.OperationException;
049import org.nuxeo.ecm.automation.core.Constants;
050import org.nuxeo.ecm.automation.core.annotations.Context;
051import org.nuxeo.ecm.automation.core.annotations.Operation;
052import org.nuxeo.ecm.automation.core.annotations.OperationMethod;
053import org.nuxeo.ecm.automation.core.annotations.Param;
054import org.nuxeo.ecm.core.api.Blob;
055import org.nuxeo.ecm.core.api.Blobs;
056import org.nuxeo.ecm.core.api.CoreSession;
057import org.nuxeo.ecm.core.api.NuxeoException;
058import org.nuxeo.runtime.api.Framework;
059import org.nuxeo.runtime.reload.ReloadService;
060import org.nuxeo.runtime.services.config.ConfigurationService;
061
062/**
063 * Operation to trigger a Hot reload of the Studio Snapshot package. You must be an administrator to trigger it.
064 *
065 * @since 8.2
066 */
067@Operation(id = HotReloadStudioSnapshot.ID, category = Constants.CAT_SERVICES, //
068        label = "Hot Reload Studio Snapshot Package", description = "Updates Studio project with latest snapshot.")
069public class HotReloadStudioSnapshot {
070
071    protected static final String IN_PROGRESS = "updateInProgress";
072
073    protected static final String SUCCESS = "success";
074
075    protected static final String ERROR = "error";
076
077    protected static final String DEPENDENCY_MISMATCH = "DEPENDENCY_MISMATCH";
078
079    public static final String ID = "Service.HotReloadStudioSnapshot";
080
081    protected static volatile boolean updateInProgress = false;
082
083    protected static synchronized boolean setInProgress(boolean inProgress) {
084        if (updateInProgress == inProgress) {
085            return false;
086        }
087        updateInProgress = inProgress;
088        return true;
089    }
090
091    private static final Logger log = LogManager.getLogger(HotReloadStudioSnapshot.class);
092
093    @Context
094    protected CoreSession session;
095
096    @Context
097    protected PackageManager pm;
098
099    @Param(name = "validate", required = false)
100    protected boolean validate = true;
101
102    @OperationMethod
103    public Blob run() throws Exception {
104        try {
105            if (!setInProgress(true)) {
106                return jsonHelper(IN_PROGRESS, "Update in progress.", null);
107            }
108
109            if (!session.getPrincipal().isAdministrator()) {
110                return jsonHelper(ERROR, "Must be Administrator to use this function.", null);
111            }
112
113            if (!Framework.isDevModeSet()) {
114                return jsonHelper(ERROR, "You must enable Dev mode to Hot reload your Studio Snapshot package.", null);
115            }
116
117            List<DownloadablePackage> pkgs = pm.listRemoteAssociatedStudioPackages();
118            DownloadablePackage snapshotPkg = StudioSnapshotHelper.getSnapshot(pkgs);
119
120            if (snapshotPkg == null) {
121                return jsonHelper(ERROR, "No Snapshot Package was found.", null);
122            }
123
124            return hotReloadPackage(snapshotPkg);
125        } catch (RuntimeException e) {
126            throw new OperationException(e);
127        } finally {
128            setInProgress(false);
129        }
130    }
131
132    protected boolean shouldValidate() {
133        ConfigurationService cs = Framework.getService(ConfigurationService.class);
134        if (cs.isBooleanPropertyTrue(ConnectClientComponent.STUDIO_SNAPSHOT_DISABLE_VALIDATION_PROPERTY)) {
135            return false;
136        }
137        return validate;
138    }
139
140    public Blob hotReloadPackage(DownloadablePackage remotePkg) {
141
142        if (shouldValidate()) {
143            pm.flushCache();
144
145            String targetPlatform = PlatformVersionHelper.getPlatformFilter();
146            if (!TargetPlatformFilterHelper.isCompatibleWithTargetPlatform(remotePkg, targetPlatform)) {
147                return jsonHelper(ERROR,
148                        String.format("This package is not validated for your current platform: %s", targetPlatform),
149                        null);
150            }
151
152            PackageDependency[] pkgDeps = remotePkg.getDependencies();
153            log.debug("{} target platforms: {}", () -> remotePkg,
154                    () -> ArrayUtils.toString(remotePkg.getTargetPlatforms()));
155            log.debug("{} dependencies: {}", () -> remotePkg, () -> ArrayUtils.toString(pkgDeps));
156
157            String packageId = remotePkg.getId();
158
159            // check deps requirements
160            if (pkgDeps != null && pkgDeps.length > 0) {
161                DependencyResolution resolution = pm.resolveDependencies(packageId, targetPlatform);
162                if (resolution.isFailed() && targetPlatform != null) {
163                    // retry without PF filter in case it gives more information
164                    resolution = pm.resolveDependencies(packageId, null);
165                }
166                if (resolution.isFailed()) {
167                    return jsonHelper(DEPENDENCY_MISMATCH,
168                            String.format("Dependency check has failed for package '%s' (%s)", packageId, resolution),
169                            null);
170                } else {
171                    List<String> pkgToInstall = resolution.getInstallPackageIds();
172                    if (pkgToInstall != null && pkgToInstall.size() == 1 && packageId.equals(pkgToInstall.get(0))) {
173                        // ignore
174                    } else if (resolution.requireChanges()) {
175                        // do not install needed deps: they may not be hot-reloadable and that's not what the
176                        // "update snapshot" button is for.
177                        // Returns missing dependencies in message instead of status
178                        List<String> dependencies = new ArrayList<>();
179                        for (String dependency : resolution.getInstallPackageNames()) {
180                            if (!dependency.contains(remotePkg.getName())) {
181                                dependencies.add(dependency);
182                            }
183                        }
184                        return jsonHelper(DEPENDENCY_MISMATCH,
185                                "A dependency mismatch has been detected. Please check your Studio project settings and your server configuration.",
186                                dependencies);
187                    }
188                }
189            }
190        }
191
192        boolean useCompatReload = Framework.isBooleanPropertyTrue(ReloadService.USE_COMPAT_HOT_RELOAD);
193        if (!useCompatReload) {
194            log.info("Use hot reload update mechanism");
195            ReloadHelper.hotReloadPackage(remotePkg.getId());
196            return jsonHelper(SUCCESS, "Studio package installed.", null);
197        }
198        // Install
199        try {
200            PackageUpdateService pus = Framework.getService(PackageUpdateService.class);
201            String packageId = remotePkg.getId();
202            LocalPackage pkg = pus.getPackage(packageId);
203
204            // Uninstall and/or remove if needed
205            if (pkg != null) {
206                removePackage(pus, pkg);
207            }
208
209            // Download
210            DownloadingPackage downloadingPkg = pm.download(packageId);
211            while (!downloadingPkg.isCompleted()) {
212                log.debug("Downloading studio snapshot package: {}", packageId);
213                Thread.sleep(100);
214            }
215
216            log.info("Installing {}", packageId);
217            pkg = pus.getPackage(packageId);
218            if (pkg == null || PackageState.DOWNLOADED != pkg.getPackageState()) {
219                throw new NuxeoException("Error while downloading studio snapshot " + pkg);
220            }
221            Task installTask = pkg.getInstallTask();
222            try {
223                performTask(installTask);
224                return jsonHelper(SUCCESS, "Studio package installed.", null);
225            } catch (PackageException e) {
226                installTask.rollback();
227                throw e;
228            }
229        } catch (InterruptedException e) {
230            Thread.currentThread().interrupt();
231            throw new NuxeoException(e);
232        } catch (PackageException | ConnectServerError e) {
233            throw new NuxeoException("Error while installing studio snapshot", e);
234        }
235
236    }
237
238    protected static void removePackage(PackageUpdateService pus, LocalPackage pkg) throws PackageException {
239        log.info("Removing package {} before update...", pkg.getId());
240        if (pkg.getPackageState().isInstalled()) {
241            // First remove it to allow SNAPSHOT upgrade
242            log.info("Uninstalling {}", pkg.getId());
243            Task uninstallTask = pkg.getUninstallTask();
244            try {
245                performTask(uninstallTask);
246            } catch (PackageException e) {
247                uninstallTask.rollback();
248                throw e;
249            }
250        }
251        pus.removePackage(pkg.getId());
252    }
253
254    protected static void performTask(Task task) throws PackageException {
255        ValidationStatus validationStatus = task.validate();
256        if (validationStatus.hasErrors()) {
257            throw new PackageException(
258                    "Failed to validate package " + task.getPackage().getId() + " -> " + validationStatus.getErrors());
259        }
260        if (validationStatus.hasWarnings()) {
261            log.warn("Got warnings on package validation {} -> {}", () -> task.getPackage().getId(),
262                    validationStatus::getWarnings);
263        }
264        task.run(null);
265    }
266
267    protected static Blob jsonHelper(String status, String message, List<String> dependencies) {
268        List<Map<String, Object>> result = new ArrayList<>();
269        Map<String, Object> resultJSON = new LinkedHashMap<>();
270        resultJSON.put("status", status);
271        resultJSON.put("message", message);
272        if (dependencies != null) {
273            resultJSON.put("deps", dependencies);
274        }
275        result.add(resultJSON);
276        try {
277            return Blobs.createJSONBlobFromValue(result);
278        } catch (IOException e) {
279            throw new NuxeoException("Unable to create json response", e);
280        }
281    }
282}