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