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
184        List<DistributionSnapshot> snaps = getSnapshotManager().listPersistentSnapshots((ctx.getCoreSession()));
185        if (distributionId.matches(VERSION_REGEX.toString())) {
186            String finalDistributionId = distributionId;
187            String distribution = snaps.stream()
188                                       .filter(s -> s.getVersion().equals(finalDistributionId))
189                                       .findFirst()
190                                       .map(DistributionSnapshot::getKey)
191                                       .orElse("current");
192
193            return ctx.newObject("redirectWO", finalDistributionId, distribution);
194        }
195
196        String orgDistributionId = distributionId;
197        Boolean embeddedMode = Boolean.FALSE;
198        if ("adm".equals(distributionId)) {
199            embeddedMode = Boolean.TRUE;
200        } else {
201
202            snaps.add(getSnapshotManager().getRuntimeSnapshot());
203            distributionId = SnapshotResolverHelper.findBestMatch(snaps, distributionId);
204        }
205        if (distributionId == null || "".equals(distributionId)) {
206            distributionId = "current";
207        }
208
209        if (!orgDistributionId.equals(distributionId)) {
210            return ctx.newObject("redirectWO", orgDistributionId, distributionId);
211        }
212
213        ctx.setProperty("embeddedMode", embeddedMode);
214        ctx.setProperty("distribution", getSnapshotManager().getSnapshot(distributionId, ctx.getCoreSession()));
215        ctx.setProperty(DIST_ID, distributionId);
216        return ctx.newObject("apibrowser", distributionId, embeddedMode);
217    }
218
219    public List<DistributionSnapshotDesc> getAvailableDistributions() {
220        return getSnapshotManager().getAvailableDistributions(ctx.getCoreSession());
221    }
222
223    public String getRuntimeDistributionName() {
224        return SnapshotManagerComponent.RUNTIME;
225    }
226
227    public DistributionSnapshot getRuntimeDistribution() {
228        return getSnapshotManager().getRuntimeSnapshot();
229    }
230
231    public List<DistributionSnapshot> listPersistedDistributions() {
232        SnapshotManager sm = getSnapshotManager();
233        return sm.listPersistentSnapshots(ctx.getCoreSession())
234                 .stream()
235                 .sorted((o1, o2) -> {
236                     Matcher m1 = VERSION_REGEX.matcher(o1.getVersion());
237                     Matcher m2 = VERSION_REGEX.matcher(o2.getVersion());
238
239                     if (m1.matches() && m2.matches()) {
240                         for (int i = 0; i < 3; i++) {
241                             String s1 = m1.group(i + 1);
242                             int c1 = s1 != null ? Integer.parseInt(s1) : 0;
243                             String s2 = m2.group(i + 1);
244                             int c2 = s2 != null ? Integer.parseInt(s2) : 0;
245
246                             if (c1 != c2 || i == 2) {
247                                 return Integer.compare(c2, c1);
248                             }
249                         }
250                     }
251                     log.info(String.format("Comparing version using String between %s - %s", o1.getVersion(),
252                             o2.getVersion()));
253                     return o2.getVersion().compareTo(o1.getVersion());
254                 })
255                 .filter(s -> !s.isHidden())
256                 .collect(Collectors.toList());
257    }
258
259    public Map<String, DistributionSnapshot> getPersistedDistributions() {
260        return getSnapshotManager().getPersistentSnapshots(ctx.getCoreSession());
261    }
262
263    public DistributionSnapshot getCurrentDistribution() {
264        String distId = (String) ctx.getProperty(DIST_ID);
265        DistributionSnapshot currentDistribution = (DistributionSnapshot) ctx.getProperty("currentDistribution");
266        if (currentDistribution == null || !currentDistribution.getKey().equals(distId)) {
267            currentDistribution = getSnapshotManager().getSnapshot(distId, ctx.getCoreSession());
268            ctx.setProperty("currentDistribution", currentDistribution);
269        }
270        return currentDistribution;
271    }
272
273    @POST
274    @Path("save")
275    @Produces("text/html")
276    public Object doSave() throws NamingException, NotSupportedException, SystemException, RollbackException,
277            HeuristicMixedException, HeuristicRollbackException, ParseException {
278        if (!canAddDocumentation()) {
279            return null;
280        }
281        FormData formData = getContext().getForm();
282        String distribLabel = formData.getString("name");
283
284        log.info("Start Snapshot...");
285        boolean startedTx = false;
286        UserTransaction tx = TransactionHelper.lookupUserTransaction();
287        if (tx != null && !TransactionHelper.isTransactionActiveOrMarkedRollback()) {
288            tx.begin();
289            startedTx = true;
290        }
291
292        Map<String, Serializable> otherProperties = readFormData(formData);
293        try {
294            getSnapshotManager().persistRuntimeSnapshot(getContext().getCoreSession(), distribLabel, otherProperties);
295
296        } catch (NuxeoException e) {
297            log.error("Error during storage", e);
298            if (tx != null) {
299                tx.rollback();
300            }
301            return getView("savedKO").arg("message", e.getMessage());
302        }
303        log.info("Snapshot saved.");
304        if (tx != null && startedTx) {
305            tx.commit();
306        }
307
308        String redirectUrl = getContext().getBaseURL() + getPath();
309        log.debug("Path => " + redirectUrl);
310        return getView("saved");
311    }
312
313    protected Map<String, Serializable> readFormData(FormData formData) {
314        Map<String, Serializable> properties = new HashMap<>();
315
316        // Release date
317        String released = formData.getString("released");
318        if (StringUtils.isNotBlank(released)) {
319            LocalDate date = LocalDate.parse(released);
320            Instant instant = date.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant();
321            properties.put(PROP_RELEASED, Date.from(instant));
322        }
323
324        return properties;
325    }
326
327    @POST
328    @Path("saveExtended")
329    @Produces("text/html")
330    public Object doSaveExtended() throws NamingException, NotSupportedException, SystemException, SecurityException,
331            RollbackException, HeuristicMixedException, HeuristicRollbackException {
332        if (!canAddDocumentation()) {
333            return null;
334        }
335
336        FormData formData = getContext().getForm();
337
338        String distribLabel = formData.getString("name");
339        String bundleList = formData.getString("bundles");
340        String pkgList = formData.getString("packages");
341        SnapshotFilter filter = new SnapshotFilter(distribLabel);
342
343        if (bundleList != null) {
344            String[] bundles = bundleList.split("\n");
345            for (String bundleId : bundles) {
346                filter.addBundlePrefix(bundleId);
347            }
348        }
349
350        if (pkgList != null) {
351            String[] packages = pkgList.split("\\r?\\n");
352            for (String pkg : packages) {
353                filter.addPackagesPrefix(pkg);
354            }
355        }
356
357        Map<String, Serializable> otherProperties = readFormData(formData);
358
359        log.info("Start Snapshot...");
360        boolean startedTx = false;
361        UserTransaction tx = TransactionHelper.lookupUserTransaction();
362        if (tx != null && !TransactionHelper.isTransactionActiveOrMarkedRollback()) {
363            tx.begin();
364            startedTx = true;
365        }
366        try {
367            getSnapshotManager().persistRuntimeSnapshot(getContext().getCoreSession(), distribLabel, otherProperties,
368                    filter);
369        } catch (NuxeoException e) {
370            log.error("Error during storage", e);
371            if (tx != null) {
372                tx.rollback();
373            }
374            return getView("savedKO").arg("message", e.getMessage());
375        }
376        log.info("Snapshot saved.");
377        if (tx != null && startedTx) {
378            tx.commit();
379        }
380        return getView("saved");
381    }
382
383    public String getDocumentationInfo() {
384        DocumentationService ds = Framework.getService(DocumentationService.class);
385        return ds.getDocumentationStats(getContext().getCoreSession());
386    }
387
388    protected File getExportTmpFile() {
389        File tmpFile = new File(Environment.getDefault().getTemp(), "export.zip");
390        if (tmpFile.exists()) {
391            tmpFile.delete();
392        }
393        tmpFile.deleteOnExit();
394        return tmpFile;
395    }
396
397    @GET
398    @Path("downloadDoc")
399    public Response downloadDoc() throws IOException {
400        DocumentationService ds = Framework.getService(DocumentationService.class);
401        File tmp = getExportTmpFile();
402        tmp.createNewFile();
403        OutputStream out = new FileOutputStream(tmp);
404        ds.exportDocumentation(getContext().getCoreSession(), out);
405        out.flush();
406        out.close();
407        ArchiveFile aFile = new ArchiveFile(tmp.getAbsolutePath());
408        return Response.ok(aFile)
409                       .header("Content-Disposition", "attachment;filename=" + "nuxeo-documentation.zip")
410                       .type("application/zip")
411                       .build();
412    }
413
414    @GET
415    @Path("download/{distributionId}")
416    public Response downloadDistrib(@PathParam("distributionId") String distribId) throws IOException {
417        File tmp = getExportTmpFile();
418        tmp.createNewFile();
419        OutputStream out = new FileOutputStream(tmp);
420        getSnapshotManager().exportSnapshot(getContext().getCoreSession(), distribId, out);
421        out.close();
422        String fName = "nuxeo-distribution-" + distribId + ".zip";
423        fName = fName.replace(" ", "_");
424        ArchiveFile aFile = new ArchiveFile(tmp.getAbsolutePath());
425        return Response.ok(aFile)
426                       .header("Content-Disposition", "attachment;filename=" + fName)
427                       .type("application/zip")
428                       .build();
429    }
430
431    /**
432     * Use to allow authorized users to upload distribution even in site mode
433     *
434     * @since 8.3
435     */
436    @GET
437    @Path("_admin")
438    public Object getForms() {
439        NuxeoPrincipal principal = (NuxeoPrincipal) getContext().getPrincipal();
440        if (SecurityHelper.canEditDocumentation(principal)) {
441            return getView("forms").arg("hideNav", Boolean.TRUE);
442        } else {
443            return Response.status(401).build();
444        }
445    }
446
447    @POST
448    @Path("uploadDistrib")
449    @Produces("text/html")
450    public Object uploadDistrib() throws IOException {
451        if (!canAddDocumentation()) {
452            return null;
453        }
454        Blob blob = getContext().getForm().getFirstBlob();
455
456        getSnapshotManager().importSnapshot(getContext().getCoreSession(), blob.getStream());
457        getSnapshotManager().readPersistentSnapshots(getContext().getCoreSession());
458
459        return getView("index");
460    }
461
462    @POST
463    @Path("uploadDistribTmp")
464    @Produces("text/html")
465    public Object uploadDistribTmp() throws IOException {
466        if (!canAddDocumentation()) {
467            return null;
468        }
469        Blob blob = getContext().getForm().getFirstBlob();
470        if (blob == null || blob.getLength() == 0) {
471            return null;
472        }
473        DocumentModel snap = getSnapshotManager().importTmpSnapshot(getContext().getCoreSession(), blob.getStream());
474        if (snap == null) {
475            log.error("Unable to import archive");
476            return null;
477        }
478        DistributionSnapshot snapObject = snap.getAdapter(DistributionSnapshot.class);
479        return getView("uploadEdit").arg("tmpSnap", snap).arg("snapObject", snapObject);
480    }
481
482    @POST
483    @Path("uploadDistribTmpValid")
484    @Produces("text/html")
485    public Object uploadDistribTmpValid() {
486        if (!canAddDocumentation()) {
487            return null;
488        }
489
490        FormData formData = getContext().getForm();
491        String name = formData.getString("name");
492        String version = formData.getString("version");
493        String pathSegment = formData.getString("pathSegment");
494        String title = formData.getString("title");
495
496        getSnapshotManager().validateImportedSnapshot(getContext().getCoreSession(), name, version, pathSegment, title);
497        getSnapshotManager().readPersistentSnapshots(getContext().getCoreSession());
498        return getView("importDone");
499    }
500
501    @POST
502    @Path("uploadDoc")
503    @Produces("text/html")
504    public Object uploadDoc() throws IOException {
505        if (!canAddDocumentation()) {
506            return null;
507        }
508
509        Blob blob = getContext().getForm().getFirstBlob();
510        if (blob == null || blob.getLength() == 0) {
511            return null;
512        }
513
514        DocumentationService ds = Framework.getService(DocumentationService.class);
515        ds.importDocumentation(getContext().getCoreSession(), blob.getStream());
516
517        log.info("Documents imported.");
518
519        return getView("docImportDone");
520    }
521
522    @GET
523    @Path("_reindex")
524    @Produces("text/plain")
525    public Object reindex() {
526        NuxeoPrincipal nxPrincipal = (NuxeoPrincipal) getContext().getPrincipal();
527        if (!nxPrincipal.isAdministrator()) {
528            return Response.status(404).build();
529        }
530
531        CoreSession coreSession = getContext().getCoreSession();
532        String query = String.format(
533                "SELECT ecm:uuid FROM Document WHERE ecm:primaryType in ('%s') AND ecm:isProxy = 0 AND ecm:currentLifeCycleState <> 'deleted'",
534                StringUtils.join(AttributesExtractorStater.DOC_TYPES, "','"));
535
536        try (IterableQueryResult it = coreSession.queryAndFetch(query, NXQL.NXQL, QueryFilter.EMPTY);) {
537            for (Map<String, Serializable> map : it) {
538                String id = (String) map.get(NXQL.ECM_UUID);
539                Work work = new ExtractXmlAttributesWorker(coreSession.getRepositoryName(), nxPrincipal.getName(), id);
540                Framework.getLocalService(WorkManager.class).schedule(work);
541            }
542        }
543
544        return Response.ok().build();
545    }
546
547    public boolean isEmbeddedMode() {
548        Boolean embed = (Boolean) getContext().getProperty("embeddedMode", Boolean.FALSE);
549        return embed != null && embed;
550    }
551
552    public boolean isEditor() {
553        if (isEmbeddedMode() || isSiteMode()) {
554            return false;
555        }
556        NuxeoPrincipal principal = (NuxeoPrincipal) getContext().getPrincipal();
557        return SecurityHelper.canEditDocumentation(principal);
558    }
559
560    public boolean canAddDocumentation() {
561        NuxeoPrincipal principal = (NuxeoPrincipal) getContext().getPrincipal();
562        return !isEmbeddedMode() && SecurityHelper.canEditDocumentation(principal);
563    }
564
565    public static boolean showCurrentDistribution() {
566        return !(Framework.isBooleanPropertyTrue("org.nuxeo.apidoc.hide.current.distribution") || isSiteMode());
567    }
568
569    public static boolean showSeamComponent() {
570        return !(Framework.isBooleanPropertyTrue("org.nuxeo.apidoc.hide.seam.components") || isSiteMode());
571    }
572
573    public static boolean isSiteMode() {
574        return Framework.isBooleanPropertyTrue("org.nuxeo.apidoc.site.mode");
575    }
576}