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