001/*
002 * (C) Copyright 2006-2015 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 *     Thierry Delprat
018 */
019package org.nuxeo.apidoc.browse;
020
021import static org.nuxeo.apidoc.snapshot.DistributionSnapshot.PROP_RELEASED;
022
023import java.io.File;
024import java.io.FileOutputStream;
025import java.io.IOException;
026import java.io.OutputStream;
027import java.io.Serializable;
028import java.sql.Date;
029import java.text.ParseException;
030import java.time.Instant;
031import java.time.LocalDate;
032import java.time.ZoneId;
033import java.util.HashMap;
034import java.util.List;
035import java.util.Map;
036import java.util.Optional;
037import java.util.regex.Matcher;
038import java.util.regex.Pattern;
039import java.util.stream.Collectors;
040
041import javax.naming.NamingException;
042import javax.transaction.HeuristicMixedException;
043import javax.transaction.HeuristicRollbackException;
044import javax.transaction.NotSupportedException;
045import javax.transaction.RollbackException;
046import javax.transaction.SystemException;
047import javax.transaction.UserTransaction;
048import javax.ws.rs.GET;
049import javax.ws.rs.POST;
050import javax.ws.rs.Path;
051import javax.ws.rs.PathParam;
052import javax.ws.rs.Produces;
053import javax.ws.rs.WebApplicationException;
054import javax.ws.rs.core.Response;
055
056import org.apache.commons.lang3.StringUtils;
057import org.apache.commons.logging.Log;
058import org.apache.commons.logging.LogFactory;
059import org.nuxeo.apidoc.documentation.DocumentationService;
060import org.nuxeo.apidoc.export.ArchiveFile;
061import org.nuxeo.apidoc.listener.AttributesExtractorStater;
062import org.nuxeo.apidoc.snapshot.DistributionSnapshot;
063import org.nuxeo.apidoc.snapshot.DistributionSnapshotDesc;
064import org.nuxeo.apidoc.snapshot.SnapshotFilter;
065import org.nuxeo.apidoc.snapshot.SnapshotManager;
066import org.nuxeo.apidoc.snapshot.SnapshotManagerComponent;
067import org.nuxeo.apidoc.snapshot.SnapshotResolverHelper;
068import org.nuxeo.apidoc.worker.ExtractXmlAttributesWorker;
069import org.nuxeo.common.Environment;
070import org.nuxeo.ecm.core.api.Blob;
071import org.nuxeo.ecm.core.api.CoreSession;
072import org.nuxeo.ecm.core.api.DocumentModel;
073import org.nuxeo.ecm.core.api.IterableQueryResult;
074import org.nuxeo.ecm.core.api.NuxeoException;
075import org.nuxeo.ecm.core.api.NuxeoPrincipal;
076import org.nuxeo.ecm.core.query.QueryFilter;
077import org.nuxeo.ecm.core.query.sql.NXQL;
078import org.nuxeo.ecm.core.work.api.Work;
079import org.nuxeo.ecm.core.work.api.WorkManager;
080import org.nuxeo.ecm.webengine.forms.FormData;
081import org.nuxeo.ecm.webengine.model.Resource;
082import org.nuxeo.ecm.webengine.model.WebObject;
083import org.nuxeo.ecm.webengine.model.exceptions.WebResourceNotFoundException;
084import org.nuxeo.ecm.webengine.model.impl.ModuleRoot;
085import org.nuxeo.runtime.api.Framework;
086import org.nuxeo.runtime.transaction.TransactionHelper;
087
088@Path("/distribution")
089// needed for 5.4.1
090@WebObject(type = "distribution")
091public class Distribution extends ModuleRoot {
092
093    public static final String DIST_ID = "distId";
094
095    protected static final Log log = LogFactory.getLog(Distribution.class);
096
097    protected static final Pattern VERSION_REGEX = Pattern.compile("^(\\d+)(?:\\.(\\d+))?(?:\\.(\\d+))?(?:-.*)?",
098            Pattern.CASE_INSENSITIVE);
099
100    // handle errors
101    @Override
102    public Object handleError(WebApplicationException e) {
103        if (e instanceof WebResourceNotFoundException) {
104            return Response.status(404).entity(getTemplate("error/error_404.ftl")).type("text/html").build();
105        } else {
106            return super.handleError(e);
107        }
108    }
109
110    protected SnapshotManager getSnapshotManager() {
111        return Framework.getLocalService(SnapshotManager.class);
112    }
113
114    public String getNavigationPoint() {
115        String currentUrl = getContext().getURL();
116        String navPoint = "somewhere";
117
118        if (currentUrl.contains("/listBundles")) {
119            navPoint = "listBundles";
120        } else if (currentUrl.contains("/listSeamComponents")) {
121            navPoint = "listSeamComponents";
122        } else if (currentUrl.contains("/viewSeamComponent")) {
123            navPoint = "viewSeamComponent";
124        } else if (currentUrl.contains("/listComponents")) {
125            navPoint = "listComponents";
126        } else if (currentUrl.contains("/listServices")) {
127            navPoint = "listServices";
128        } else if (currentUrl.contains("/listExtensionPoints")) {
129            navPoint = "listExtensionPoints";
130        } else if (currentUrl.contains("/listContributions")) {
131            navPoint = "listContributions";
132        } else if (currentUrl.contains("/listBundleGroups")) {
133            navPoint = "listBundleGroups";
134        } else if (currentUrl.contains("/viewBundleGroup")) {
135            navPoint = "viewBundleGroup";
136        } else if (currentUrl.contains("/viewComponent")) {
137            navPoint = "viewComponent";
138        } else if (currentUrl.contains("/viewService")) {
139            navPoint = "viewService";
140        } else if (currentUrl.contains("/viewExtensionPoint")) {
141            navPoint = "viewExtensionPoint";
142        } else if (currentUrl.contains("/viewContribution")) {
143            navPoint = "viewContribution";
144        } else if (currentUrl.contains("/viewBundle")) {
145            navPoint = "viewBundle";
146        } else if (currentUrl.contains("/listOperations")) {
147            navPoint = "listOperations";
148        } else if (currentUrl.contains("/viewOperation")) {
149            navPoint = "viewOperation";
150        } else if (currentUrl.contains("/doc")) {
151            navPoint = "documentation";
152        }
153        return navPoint;
154    }
155
156    @GET
157    @Produces("text/html")
158    public Object doGet() {
159        return getView("index").arg("hideNav", Boolean.TRUE);
160    }
161
162    @Path("latest")
163    public Resource getLatest() {
164        List<DistributionSnapshot> snaps = listPersistedDistributions();
165        Optional<DistributionSnapshot> distribution = snaps.stream()
166                                                           .filter(snap -> snap.getName()
167                                                                               .toLowerCase()
168                                                                               .startsWith("nuxeo platform"))
169                                                           .findFirst();
170
171        String latest = "current";
172        if (distribution.isPresent()) {
173            latest = distribution.get().getKey();
174        }
175        return ctx.newObject("redirectWO", "latest", latest);
176    }
177
178    @Path("{distributionId}")
179    public Resource viewDistribution(@PathParam("distributionId") String distributionId) {
180        if (distributionId == null || "".equals(distributionId)) {
181            return this;
182        }
183        String orgDistributionId = distributionId;
184        Boolean embeddedMode = Boolean.FALSE;
185        if ("adm".equals(distributionId)) {
186            embeddedMode = Boolean.TRUE;
187        } else {
188            List<DistributionSnapshot> snaps = getSnapshotManager().listPersistentSnapshots((ctx.getCoreSession()));
189            snaps.add(getSnapshotManager().getRuntimeSnapshot());
190            distributionId = SnapshotResolverHelper.findBestMatch(snaps, distributionId);
191        }
192        if (distributionId == null || "".equals(distributionId)) {
193            distributionId = "current";
194        }
195
196        if (!orgDistributionId.equals(distributionId)) {
197            return ctx.newObject("redirectWO", orgDistributionId, distributionId);
198        }
199
200        ctx.setProperty("embeddedMode", embeddedMode);
201        ctx.setProperty("distribution", getSnapshotManager().getSnapshot(distributionId, ctx.getCoreSession()));
202        ctx.setProperty(DIST_ID, distributionId);
203        return ctx.newObject("apibrowser", distributionId, embeddedMode);
204    }
205
206    public List<DistributionSnapshotDesc> getAvailableDistributions() {
207        return getSnapshotManager().getAvailableDistributions(ctx.getCoreSession());
208    }
209
210    public String getRuntimeDistributionName() {
211        return SnapshotManagerComponent.RUNTIME;
212    }
213
214    public DistributionSnapshot getRuntimeDistribution() {
215        return getSnapshotManager().getRuntimeSnapshot();
216    }
217
218    public List<DistributionSnapshot> listPersistedDistributions() {
219        SnapshotManager sm = getSnapshotManager();
220        return sm.listPersistentSnapshots(ctx.getCoreSession())
221                 .stream()
222                 .sorted((o1, o2) -> {
223                     Matcher m1 = VERSION_REGEX.matcher(o1.getVersion());
224                     Matcher m2 = VERSION_REGEX.matcher(o2.getVersion());
225
226                     if (m1.matches() && m2.matches()) {
227                         for (int i = 0; i < 3; i++) {
228                             String s1 = m1.group(i + 1);
229                             int c1 = s1 != null ? Integer.parseInt(s1) : 0;
230                             String s2 = m2.group(i + 1);
231                             int c2 = s2 != null ? Integer.parseInt(s2) : 0;
232
233                             if (c1 != c2 || i == 2) {
234                                 return Integer.compare(c2, c1);
235                             }
236                         }
237                     }
238                     log.info(String.format("Comparing version using String between %s - %s", o1.getVersion(),
239                             o2.getVersion()));
240                     return o2.getVersion().compareTo(o1.getVersion());
241                 })
242                 .filter(s -> !s.isHidden())
243                 .collect(Collectors.toList());
244    }
245
246    public Map<String, DistributionSnapshot> getPersistedDistributions() {
247        return getSnapshotManager().getPersistentSnapshots(ctx.getCoreSession());
248    }
249
250    public DistributionSnapshot getCurrentDistribution() {
251        String distId = (String) ctx.getProperty(DIST_ID);
252        DistributionSnapshot currentDistribution = (DistributionSnapshot) ctx.getProperty("currentDistribution");
253        if (currentDistribution == null || !currentDistribution.getKey().equals(distId)) {
254            currentDistribution = getSnapshotManager().getSnapshot(distId, ctx.getCoreSession());
255            ctx.setProperty("currentDistribution", currentDistribution);
256        }
257        return currentDistribution;
258    }
259
260    @POST
261    @Path("save")
262    @Produces("text/html")
263    public Object doSave() throws NamingException, NotSupportedException, SystemException, RollbackException,
264            HeuristicMixedException, HeuristicRollbackException, ParseException {
265        if (!canAddDocumentation()) {
266            return null;
267        }
268        FormData formData = getContext().getForm();
269        String distribLabel = formData.getString("name");
270
271        log.info("Start Snapshot...");
272        boolean startedTx = false;
273        UserTransaction tx = TransactionHelper.lookupUserTransaction();
274        if (tx != null && !TransactionHelper.isTransactionActiveOrMarkedRollback()) {
275            tx.begin();
276            startedTx = true;
277        }
278
279        Map<String, Serializable> otherProperties = readFormData(formData);
280        try {
281            getSnapshotManager().persistRuntimeSnapshot(getContext().getCoreSession(), distribLabel, otherProperties);
282
283        } catch (NuxeoException e) {
284            log.error("Error during storage", e);
285            if (tx != null) {
286                tx.rollback();
287            }
288            return getView("savedKO").arg("message", e.getMessage());
289        }
290        log.info("Snapshot saved.");
291        if (tx != null && startedTx) {
292            tx.commit();
293        }
294
295        String redirectUrl = getContext().getBaseURL() + getPath();
296        log.debug("Path => " + redirectUrl);
297        return getView("saved");
298    }
299
300    protected Map<String, Serializable> readFormData(FormData formData) {
301        Map<String, Serializable> properties = new HashMap<>();
302
303        // Release date
304        String released = formData.getString("released");
305        if (StringUtils.isNotBlank(released)) {
306            LocalDate date = LocalDate.parse(released);
307            Instant instant = date.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant();
308            properties.put(PROP_RELEASED, Date.from(instant));
309        }
310
311        return properties;
312    }
313
314    @POST
315    @Path("saveExtended")
316    @Produces("text/html")
317    public Object doSaveExtended() throws NamingException, NotSupportedException, SystemException, SecurityException,
318            RollbackException, HeuristicMixedException, HeuristicRollbackException {
319        if (!canAddDocumentation()) {
320            return null;
321        }
322
323        FormData formData = getContext().getForm();
324
325        String distribLabel = formData.getString("name");
326        String bundleList = formData.getString("bundles");
327        String pkgList = formData.getString("packages");
328        SnapshotFilter filter = new SnapshotFilter(distribLabel);
329
330        if (bundleList != null) {
331            String[] bundles = bundleList.split("\n");
332            for (String bundleId : bundles) {
333                filter.addBundlePrefix(bundleId);
334            }
335        }
336
337        if (pkgList != null) {
338            String[] packages = pkgList.split("\\r?\\n");
339            for (String pkg : packages) {
340                filter.addPackagesPrefix(pkg);
341            }
342        }
343
344        Map<String, Serializable> otherProperties = readFormData(formData);
345
346        log.info("Start Snapshot...");
347        boolean startedTx = false;
348        UserTransaction tx = TransactionHelper.lookupUserTransaction();
349        if (tx != null && !TransactionHelper.isTransactionActiveOrMarkedRollback()) {
350            tx.begin();
351            startedTx = true;
352        }
353        try {
354            getSnapshotManager().persistRuntimeSnapshot(getContext().getCoreSession(), distribLabel, otherProperties,
355                    filter);
356        } catch (NuxeoException e) {
357            log.error("Error during storage", e);
358            if (tx != null) {
359                tx.rollback();
360            }
361            return getView("savedKO").arg("message", e.getMessage());
362        }
363        log.info("Snapshot saved.");
364        if (tx != null && startedTx) {
365            tx.commit();
366        }
367        return getView("saved");
368    }
369
370    public String getDocumentationInfo() {
371        DocumentationService ds = Framework.getService(DocumentationService.class);
372        return ds.getDocumentationStats(getContext().getCoreSession());
373    }
374
375    protected File getExportTmpFile() {
376        File tmpFile = new File(Environment.getDefault().getTemp(), "export.zip");
377        if (tmpFile.exists()) {
378            tmpFile.delete();
379        }
380        tmpFile.deleteOnExit();
381        return tmpFile;
382    }
383
384    @GET
385    @Path("downloadDoc")
386    public Response downloadDoc() throws IOException {
387        DocumentationService ds = Framework.getService(DocumentationService.class);
388        File tmp = getExportTmpFile();
389        tmp.createNewFile();
390        OutputStream out = new FileOutputStream(tmp);
391        ds.exportDocumentation(getContext().getCoreSession(), out);
392        out.flush();
393        out.close();
394        ArchiveFile aFile = new ArchiveFile(tmp.getAbsolutePath());
395        return Response.ok(aFile)
396                       .header("Content-Disposition", "attachment;filename=" + "nuxeo-documentation.zip")
397                       .type("application/zip")
398                       .build();
399    }
400
401    @GET
402    @Path("download/{distributionId}")
403    public Response downloadDistrib(@PathParam("distributionId") String distribId) throws IOException {
404        File tmp = getExportTmpFile();
405        tmp.createNewFile();
406        OutputStream out = new FileOutputStream(tmp);
407        getSnapshotManager().exportSnapshot(getContext().getCoreSession(), distribId, out);
408        out.close();
409        String fName = "nuxeo-distribution-" + distribId + ".zip";
410        fName = fName.replace(" ", "_");
411        ArchiveFile aFile = new ArchiveFile(tmp.getAbsolutePath());
412        return Response.ok(aFile)
413                       .header("Content-Disposition", "attachment;filename=" + fName)
414                       .type("application/zip")
415                       .build();
416    }
417
418    /**
419     * Use to allow authorized users to upload distribution even in site mode
420     *
421     * @since 8.3
422     */
423    @GET
424    @Path("_admin")
425    public Object getForms() {
426        NuxeoPrincipal principal = (NuxeoPrincipal) getContext().getPrincipal();
427        if (SecurityHelper.canEditDocumentation(principal)) {
428            return getView("forms").arg("hideNav", Boolean.TRUE);
429        } else {
430            return Response.status(401).build();
431        }
432    }
433
434    @POST
435    @Path("uploadDistrib")
436    @Produces("text/html")
437    public Object uploadDistrib() throws IOException {
438        if (!canAddDocumentation()) {
439            return null;
440        }
441        Blob blob = getContext().getForm().getFirstBlob();
442
443        getSnapshotManager().importSnapshot(getContext().getCoreSession(), blob.getStream());
444        getSnapshotManager().readPersistentSnapshots(getContext().getCoreSession());
445
446        return getView("index");
447    }
448
449    @POST
450    @Path("uploadDistribTmp")
451    @Produces("text/html")
452    public Object uploadDistribTmp() throws IOException {
453        if (!canAddDocumentation()) {
454            return null;
455        }
456        Blob blob = getContext().getForm().getFirstBlob();
457        if (blob == null || blob.getLength() == 0) {
458            return null;
459        }
460        DocumentModel snap = getSnapshotManager().importTmpSnapshot(getContext().getCoreSession(), blob.getStream());
461        if (snap == null) {
462            log.error("Unable to import archive");
463            return null;
464        }
465        DistributionSnapshot snapObject = snap.getAdapter(DistributionSnapshot.class);
466        return getView("uploadEdit").arg("tmpSnap", snap).arg("snapObject", snapObject);
467    }
468
469    @POST
470    @Path("uploadDistribTmpValid")
471    @Produces("text/html")
472    public Object uploadDistribTmpValid() {
473        if (!canAddDocumentation()) {
474            return null;
475        }
476
477        FormData formData = getContext().getForm();
478        String name = formData.getString("name");
479        String version = formData.getString("version");
480        String pathSegment = formData.getString("pathSegment");
481        String title = formData.getString("title");
482
483        getSnapshotManager().validateImportedSnapshot(getContext().getCoreSession(), name, version, pathSegment, title);
484        getSnapshotManager().readPersistentSnapshots(getContext().getCoreSession());
485        return getView("importDone");
486    }
487
488    @POST
489    @Path("uploadDoc")
490    @Produces("text/html")
491    public Object uploadDoc() throws IOException {
492        if (!canAddDocumentation()) {
493            return null;
494        }
495
496        Blob blob = getContext().getForm().getFirstBlob();
497        if (blob == null || blob.getLength() == 0) {
498            return null;
499        }
500
501        DocumentationService ds = Framework.getService(DocumentationService.class);
502        ds.importDocumentation(getContext().getCoreSession(), blob.getStream());
503
504        log.info("Documents imported.");
505
506        return getView("docImportDone");
507    }
508
509    @GET
510    @Path("_reindex")
511    @Produces("text/plain")
512    public Object reindex() {
513        NuxeoPrincipal nxPrincipal = (NuxeoPrincipal) getContext().getPrincipal();
514        if (!nxPrincipal.isAdministrator()) {
515            return Response.status(404).build();
516        }
517
518        CoreSession coreSession = getContext().getCoreSession();
519        String query = String.format(
520                "SELECT ecm:uuid FROM Document WHERE ecm:primaryType in ('%s') AND ecm:isProxy = 0 AND ecm:currentLifeCycleState <> 'deleted'",
521                StringUtils.join(AttributesExtractorStater.DOC_TYPES, "','"));
522
523        try (IterableQueryResult it = coreSession.queryAndFetch(query, NXQL.NXQL, QueryFilter.EMPTY);) {
524            for (Map<String, Serializable> map : it) {
525                String id = (String) map.get(NXQL.ECM_UUID);
526                Work work = new ExtractXmlAttributesWorker(coreSession.getRepositoryName(), nxPrincipal.getName(), id);
527                Framework.getLocalService(WorkManager.class).schedule(work);
528            }
529        }
530
531        return Response.ok().build();
532    }
533
534    public boolean isEmbeddedMode() {
535        Boolean embed = (Boolean) getContext().getProperty("embeddedMode", Boolean.FALSE);
536        return embed != null && embed;
537    }
538
539    public boolean isEditor() {
540        if (isEmbeddedMode() || isSiteMode()) {
541            return false;
542        }
543        NuxeoPrincipal principal = (NuxeoPrincipal) getContext().getPrincipal();
544        return SecurityHelper.canEditDocumentation(principal);
545    }
546
547    public boolean canAddDocumentation() {
548        NuxeoPrincipal principal = (NuxeoPrincipal) getContext().getPrincipal();
549        return !isEmbeddedMode() && SecurityHelper.canEditDocumentation(principal);
550    }
551
552    public static boolean showCurrentDistribution() {
553        return !(Framework.isBooleanPropertyTrue("org.nuxeo.apidoc.hide.current.distribution") || isSiteMode());
554    }
555
556    public static boolean showSeamComponent() {
557        return !(Framework.isBooleanPropertyTrue("org.nuxeo.apidoc.hide.seam.components") || isSiteMode());
558    }
559
560    public static boolean isSiteMode() {
561        return Framework.isBooleanPropertyTrue("org.nuxeo.apidoc.site.mode");
562    }
563}