001/*
002 * (C) Copyright 2006-2014 Nuxeo SA (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl-2.1.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     Nuxeo - initial API and implementation
016 *
017 */
018package org.nuxeo.connect.client.we;
019
020import java.util.ArrayList;
021import java.util.HashMap;
022import java.util.List;
023import java.util.Map;
024import java.util.Map.Entry;
025
026import javax.ws.rs.GET;
027import javax.ws.rs.POST;
028import javax.ws.rs.Path;
029import javax.ws.rs.PathParam;
030import javax.ws.rs.Produces;
031import javax.ws.rs.QueryParam;
032
033import org.apache.commons.logging.Log;
034import org.apache.commons.logging.LogFactory;
035import org.nuxeo.connect.client.vindoz.InstallAfterRestart;
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.Package;
041import org.nuxeo.connect.update.PackageException;
042import org.nuxeo.connect.update.PackageUpdateService;
043import org.nuxeo.connect.update.ValidationStatus;
044import org.nuxeo.connect.update.Version;
045import org.nuxeo.connect.update.model.Field;
046import org.nuxeo.connect.update.model.Form;
047import org.nuxeo.connect.update.task.Task;
048import org.nuxeo.ecm.admin.runtime.PlatformVersionHelper;
049import org.nuxeo.ecm.core.api.NuxeoException;
050import org.nuxeo.ecm.webengine.forms.FormData;
051import org.nuxeo.ecm.webengine.model.WebObject;
052import org.nuxeo.ecm.webengine.model.impl.DefaultObject;
053import org.nuxeo.runtime.api.Framework;
054
055/**
056 * Provides REST bindings for {@link Package} install management.
057 *
058 * @author <a href="mailto:td@nuxeo.com">Thierry Delprat</a>
059 */
060@WebObject(type = "installHandler")
061public class InstallHandler extends DefaultObject {
062
063    protected static final Log log = LogFactory.getLog(InstallHandler.class);
064
065    protected static final String INSTALL_PARAM_MAPS = "org.nuxeo.connect.updates.install.params";
066
067    protected String getStorageKey(String pkgId) {
068        return INSTALL_PARAM_MAPS + "_" + pkgId;
069    }
070
071    @SuppressWarnings("unchecked")
072    protected Map<String, String> getInstallParameters(String pkgId) {
073        Map<String, String> params = (Map<String, String>) getContext().getRequest().getAttribute(getStorageKey(pkgId));
074        if (params == null) {
075            params = new HashMap<>();
076        }
077        return params;
078    }
079
080    protected void storeInstallParameters(String pkgId, Map<String, String> params) {
081        getContext().getRequest().setAttribute(getStorageKey(pkgId), params);
082    }
083
084    protected void clearInstallParameters(String pkgId) {
085        getContext().getRequest().setAttribute(getStorageKey(pkgId), null);
086    }
087
088    @GET
089    @Produces("text/html")
090    @Path(value = "showTermsAndConditions/{pkgId}")
091    public Object showTermsAndConditions(@PathParam("pkgId") String pkgId, @QueryParam("source") String source,
092            @QueryParam("depCheck") Boolean depCheck) {
093        if (depCheck == null) {
094            depCheck = true;
095        }
096        try {
097            PackageUpdateService pus = Framework.getLocalService(PackageUpdateService.class);
098            LocalPackage pkg = pus.getPackage(pkgId);
099            String content = pkg.getTermsAndConditionsContent();
100            return getView("termsAndConditions").arg("pkg", pkg).arg("source", source).arg("content", content).arg(
101                    "depCheck", depCheck);
102        } catch (PackageException e) {
103            log.error("Error during terms and conditions phase ", e);
104            return getView("installError").arg("e", e).arg("source", source);
105        }
106    }
107
108    @GET
109    @Produces("text/html")
110    @Path(value = "start/{pkgId}")
111    public Object startInstall(@PathParam("pkgId") String pkgId, @QueryParam("source") String source,
112            @QueryParam("tacAccepted") Boolean acceptedTAC, @QueryParam("depCheck") Boolean depCheck,
113            @QueryParam("autoMode") Boolean autoMode) {
114        try {
115            PackageUpdateService pus = Framework.getLocalService(PackageUpdateService.class);
116            LocalPackage pkg = pus.getPackage(pkgId);
117            if (pkg == null) {
118                throw new NuxeoException("Can not find package " + pkgId);
119            }
120            if (pkg.requireTermsAndConditionsAcceptance() && !Boolean.TRUE.equals(acceptedTAC)) {
121                return showTermsAndConditions(pkgId, source, depCheck);
122            }
123            if (!Boolean.FALSE.equals(depCheck)) {
124                // check deps requirements
125                if (pkg.getDependencies() != null && pkg.getDependencies().length > 0) {
126                    PackageManager pm = Framework.getLocalService(PackageManager.class);
127                    DependencyResolution resolution = pm.resolveDependencies(pkgId,
128                            PlatformVersionHelper.getPlatformFilter());
129                    if (resolution.isFailed() && PlatformVersionHelper.getPlatformFilter() != null) {
130                        // retry without PF filter ...
131                        resolution = pm.resolveDependencies(pkgId, null);
132                    }
133                    if (resolution.isFailed()) {
134                        return getView("dependencyError").arg("resolution", resolution).arg("pkg", pkg).arg("source",
135                                source);
136                    } else {
137                        if (resolution.requireChanges()) {
138                            if (autoMode == null) {
139                                autoMode = true;
140                            }
141                            return getView("displayDependencies").arg("resolution", resolution).arg("pkg", pkg).arg(
142                                    "source", source).arg("autoMode", autoMode);
143                        }
144                        // no dep changes => can continue standard install
145                        // process
146                    }
147                }
148            }
149            Task installTask = pkg.getInstallTask();
150            ValidationStatus status = installTask.validate();
151            String targetPlatform = PlatformVersionHelper.getPlatformFilter();
152            if (!TargetPlatformFilterHelper.isCompatibleWithTargetPlatform(pkg, targetPlatform)) {
153                status.addWarning("This package is not validated for you current platform: " + targetPlatform);
154            }
155            if (status.hasErrors()) {
156                return getView("canNotInstall").arg("status", status).arg("pkg", pkg).arg("source", source);
157            }
158
159            boolean needWizard = false;
160            Form[] forms = installTask.getPackage().getInstallForms();
161            if (forms != null && forms.length > 0) {
162                needWizard = true;
163            }
164            return getView("startInstall").arg("status", status).arg("needWizard", needWizard).arg("installTask",
165                    installTask).arg("pkg", pkg).arg("source", source);
166        } catch (PackageException e) {
167            log.error("Error during first step of installation", e);
168            return getView("installError").arg("e", e).arg("source", source);
169        }
170    }
171
172    @GET
173    @Produces("text/html")
174    @Path(value = "form/{pkgId}/{formId}")
175    public Object showInstallForm(@PathParam("pkgId") String pkgId, @PathParam("formId") int formId,
176            @QueryParam("source") String source) {
177        PackageUpdateService pus = Framework.getLocalService(PackageUpdateService.class);
178        try {
179            LocalPackage pkg = pus.getPackage(pkgId);
180            Task installTask = pkg.getInstallTask();
181            Form[] forms = installTask.getPackage().getInstallForms();
182            if (forms == null || forms.length < formId - 1) {
183                return getView("installError").arg("e",
184                        new NuxeoException("No form with Id " + formId + " for package " + pkgId)).arg("source",
185                        source);
186            }
187            return getView("showInstallForm").arg("form", forms[formId]).arg("pkg", pkg).arg("source", source).arg(
188                    "step", formId + 1).arg("steps", forms.length);
189        } catch (PackageException e) {
190            log.error("Error during displaying Form nb " + formId, e);
191            return getView("installError").arg("e", e).arg("source", source);
192        }
193    }
194
195    @POST
196    @Produces("text/html")
197    @Path(value = "form/{pkgId}/{formId}")
198    public Object processInstallForm(@PathParam("pkgId") String pkgId, @PathParam("formId") int formId,
199            @QueryParam("source") String source) {
200        PackageUpdateService pus = Framework.getLocalService(PackageUpdateService.class);
201        try {
202            LocalPackage pkg = pus.getPackage(pkgId);
203            Task installTask = pkg.getInstallTask();
204            Form[] forms = installTask.getPackage().getInstallForms();
205            if (forms == null || forms.length < formId - 1) {
206                return getView("installError").arg("e",
207                        new NuxeoException("No form with Id " + formId + " for package " + pkgId)).arg("source",
208                        source);
209            }
210
211            Form form = forms[formId];
212            FormData fdata = getContext().getForm();
213            Map<String, String> params = getInstallParameters(pkgId);
214            for (Field field : form.getFields()) {
215                String data = fdata.getString(field.getName());
216                if (data != null) {
217                    params.put(field.getName(), data);
218                }
219                // XXX validation, and type checking ...
220            }
221            storeInstallParameters(pkgId, params);
222            if (formId + 1 == forms.length) {
223                // this was the last form screen : start the install
224                return doInstall(pkgId, source);
225            } else {
226                return showInstallForm(pkgId, formId + 1, source);
227            }
228        } catch (PackageException e) {
229            log.error("Error during processing Form nb " + formId, e);
230            return getView("installError").arg("e", e).arg("source", source);
231        }
232    }
233
234    @GET
235    @Produces("text/html")
236    @Path(value = "bulkRun/{pkgId}")
237    public Object doBulkInstall(@PathParam("pkgId") String pkgId, @QueryParam("source") String source,
238            @QueryParam("confirm") Boolean confirm) {
239        if (!RequestHelper.isInternalLink(getContext())) {
240            return getView("installError").arg("e",
241                    new NuxeoException("Installation seems to have been started from an external link.")).arg(
242                    "source", source);
243        }
244        PackageManager pm = Framework.getLocalService(PackageManager.class);
245        PackageUpdateService pus = Framework.getLocalService(PackageUpdateService.class);
246        try {
247            DependencyResolution resolution = pm.resolveDependencies(pkgId, PlatformVersionHelper.getPlatformFilter());
248            if (resolution.isFailed() && PlatformVersionHelper.getPlatformFilter() != null) {
249                // retry without PF filter ...
250                resolution = pm.resolveDependencies(pkgId, null);
251            }
252            List<String> downloadPackagesIds = resolution.getDownloadPackageIds();
253            if (downloadPackagesIds.size() > 0) {
254                return getView("installError").arg("e",
255                        new NuxeoException("Some packages need to be downloaded before running bulk installation")).arg(
256                        "source", source);
257            }
258
259            List<String> pkgIds = resolution.getOrderedPackageIdsToInstall();
260            List<String> warns = new ArrayList<>();
261            List<String> descs = new ArrayList<>();
262            if (!pkgIds.contains(pkgId)) {
263                pkgIds.add(pkgId);
264            }
265            List<String> rmPkgIds = new ArrayList<>();
266            for (Entry<String, Version> rmEntry : resolution.getLocalPackagesToRemove().entrySet()) {
267                String id = rmEntry.getKey() + "-" + rmEntry.getValue().toString();
268                rmPkgIds.add(id);
269            }
270            for (String id : pkgIds) {
271                Package pkg = pus.getPackage(id);
272                if (pkg == null) {
273                    return getView("installError").arg("e", new NuxeoException("Unable to find local package " + id)).arg(
274                            "source", source);
275                }
276                String targetPlatform = PlatformVersionHelper.getPlatformFilter();
277                if (!TargetPlatformFilterHelper.isCompatibleWithTargetPlatform(pkg, targetPlatform)) {
278                    warns.add("Package " + id + " is not validated for your current platform: " + targetPlatform);
279                }
280                descs.add(pkg.getDescription());
281            }
282            if (confirm != null && true == confirm) {
283                for (String id : rmPkgIds) {
284                    InstallAfterRestart.addPackageForUnInstallation(id);
285                }
286                for (String id : pkgIds) {
287                    InstallAfterRestart.addPackageForInstallation(id);
288                }
289                return getView("bulkInstallOnRestart").arg("pkgIds", pkgIds).arg("rmPkgIds", rmPkgIds).arg("source",
290                        source);
291            } else {
292                return getView("bulkInstallOnRestartConfirm").arg("pkgIds", pkgIds).arg("rmPkgIds", rmPkgIds).arg(
293                        "warns", warns).arg("descs", descs).arg("source", source).arg("pkgId", pkgId);
294            }
295        } catch (PackageException e) {
296            log.error("Error during installation of " + pkgId, e);
297            return getView("installError").arg("e", e).arg("source", source);
298        }
299    }
300
301    @GET
302    @Produces("text/html")
303    @Path(value = "run/{pkgId}")
304    public Object doInstall(@PathParam("pkgId") String pkgId, @QueryParam("source") String source) {
305        if (!RequestHelper.isInternalLink(getContext())) {
306            return getView("installError").arg("e",
307                    new NuxeoException("Installation seems to have been started from an external link.")).arg(
308                    "source", source);
309        }
310        PackageUpdateService pus = Framework.getLocalService(PackageUpdateService.class);
311        try {
312            LocalPackage pkg = pus.getPackage(pkgId);
313            if (InstallAfterRestart.isNeededForPackage(pkg)) {
314                InstallAfterRestart.addPackageForInstallation(pkg.getId());
315                return getView("installOnRestart").arg("pkg", pkg).arg("source", source);
316            }
317            Task installTask = pkg.getInstallTask();
318            Map<String, String> params = getInstallParameters(pkgId);
319            try {
320                installTask.run(params);
321            } catch (PackageException e) {
322                log.error("Error during installation of " + pkgId, e);
323                installTask.rollback();
324                return getView("installError").arg("e", e).arg("source", source);
325            }
326            clearInstallParameters(pkgId);
327            return getView("installedOK").arg("installTask", installTask).arg("pkg", pkg).arg("source", source);
328        } catch (PackageException e) {
329            log.error("Error during installation of " + pkgId, e);
330            return getView("installError").arg("e", e).arg("source", source);
331        }
332    }
333
334}