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 java.io.File;
022import java.io.FileOutputStream;
023import java.io.IOException;
024import java.io.OutputStream;
025import java.util.ArrayList;
026import java.util.Collections;
027import java.util.List;
028import java.util.Map;
029
030import javax.naming.NamingException;
031import javax.transaction.HeuristicMixedException;
032import javax.transaction.HeuristicRollbackException;
033import javax.transaction.NotSupportedException;
034import javax.transaction.RollbackException;
035import javax.transaction.SystemException;
036import javax.transaction.UserTransaction;
037import javax.ws.rs.GET;
038import javax.ws.rs.POST;
039import javax.ws.rs.Path;
040import javax.ws.rs.PathParam;
041import javax.ws.rs.Produces;
042import javax.ws.rs.WebApplicationException;
043import javax.ws.rs.core.Response;
044
045import org.apache.commons.logging.Log;
046import org.apache.commons.logging.LogFactory;
047
048import org.nuxeo.apidoc.documentation.DocumentationService;
049import org.nuxeo.apidoc.export.ArchiveFile;
050import org.nuxeo.apidoc.snapshot.DistributionSnapshot;
051import org.nuxeo.apidoc.snapshot.DistributionSnapshotDesc;
052import org.nuxeo.apidoc.snapshot.SnapshotFilter;
053import org.nuxeo.apidoc.snapshot.SnapshotManager;
054import org.nuxeo.apidoc.snapshot.SnapshotManagerComponent;
055import org.nuxeo.apidoc.snapshot.SnapshotResolverHelper;
056import org.nuxeo.common.Environment;
057import org.nuxeo.ecm.core.api.Blob;
058import org.nuxeo.ecm.core.api.DocumentModel;
059import org.nuxeo.ecm.core.api.NuxeoException;
060import org.nuxeo.ecm.core.api.NuxeoPrincipal;
061import org.nuxeo.ecm.webengine.forms.FormData;
062import org.nuxeo.ecm.webengine.model.Resource;
063import org.nuxeo.ecm.webengine.model.WebObject;
064import org.nuxeo.ecm.webengine.model.exceptions.WebResourceNotFoundException;
065import org.nuxeo.ecm.webengine.model.impl.ModuleRoot;
066import org.nuxeo.runtime.api.Framework;
067import org.nuxeo.runtime.transaction.TransactionHelper;
068
069@Path("/distribution")
070// needed for 5.4.1
071@WebObject(type = "distribution")
072public class Distribution extends ModuleRoot {
073
074    public static final String DIST_ID = "distId";
075
076    protected static final Log log = LogFactory.getLog(Distribution.class);
077
078    // handle errors
079    @Override
080    public Object handleError(WebApplicationException e) {
081        if (e instanceof WebResourceNotFoundException) {
082            return Response.status(404).entity(getTemplate("error/error_404.ftl")).type("text/html").build();
083        } else {
084            return super.handleError(e);
085        }
086    }
087
088    protected SnapshotManager getSnapshotManager() {
089        return Framework.getLocalService(SnapshotManager.class);
090    }
091
092    public String getNavigationPoint() {
093        String currentUrl = getContext().getURL();
094        String navPoint = "somewhere";
095
096        if (currentUrl.contains("/listBundles")) {
097            navPoint = "listBundles";
098        } else if (currentUrl.contains("/listSeamComponents")) {
099            navPoint = "listSeamComponents";
100        } else if (currentUrl.contains("/viewSeamComponent")) {
101            navPoint = "viewSeamComponent";
102        } else if (currentUrl.contains("/listComponents")) {
103            navPoint = "listComponents";
104        } else if (currentUrl.contains("/listServices")) {
105            navPoint = "listServices";
106        } else if (currentUrl.contains("/listExtensionPoints")) {
107            navPoint = "listExtensionPoints";
108        } else if (currentUrl.contains("/listContributions")) {
109            navPoint = "listContributions";
110        } else if (currentUrl.contains("/listBundleGroups")) {
111            navPoint = "listBundleGroups";
112        } else if (currentUrl.contains("/viewBundleGroup")) {
113            navPoint = "viewBundleGroup";
114        } else if (currentUrl.contains("/viewComponent")) {
115            navPoint = "viewComponent";
116        } else if (currentUrl.contains("/viewService")) {
117            navPoint = "viewService";
118        } else if (currentUrl.contains("/viewExtensionPoint")) {
119            navPoint = "viewExtensionPoint";
120        } else if (currentUrl.contains("/viewContribution")) {
121            navPoint = "viewContribution";
122        } else if (currentUrl.contains("/viewBundle")) {
123            navPoint = "viewBundle";
124        } else if (currentUrl.contains("/listOperations")) {
125            navPoint = "listOperations";
126        } else if (currentUrl.contains("/viewOperation")) {
127            navPoint = "viewOperation";
128        } else if (currentUrl.contains("/doc")) {
129            navPoint = "documentation";
130        }
131        return navPoint;
132    }
133
134    @GET
135    @Produces("text/html")
136    public Object doGet() {
137        return getView("index").arg("hideNav", Boolean.TRUE);
138    }
139
140    @Path("latest")
141    public Resource getLatest() {
142        List<DistributionSnapshot> snaps = getSnapshotManager().listPersistentSnapshots((ctx.getCoreSession()));
143
144        List<String> keys = new ArrayList<>();
145        for (DistributionSnapshot snap : snaps) {
146            if (snap.getName().equalsIgnoreCase("Nuxeo Platform")) {
147                keys.add(snap.getKey());
148            }
149            Collections.sort(keys);
150            Collections.reverse(keys);
151        }
152
153        String latest = "current";
154        if (keys.size() > 0) {
155            latest = keys.get(0);
156        }
157        return ctx.newObject("redirectWO", "latest", latest);
158    }
159
160    @Path("{distributionId}")
161    public Resource viewDistribution(@PathParam("distributionId") String distributionId) {
162        if (distributionId == null || "".equals(distributionId)) {
163            return this;
164        }
165        String orgDistributionId = distributionId;
166        Boolean embeddedMode = Boolean.FALSE;
167        if ("adm".equals(distributionId)) {
168            embeddedMode = Boolean.TRUE;
169        } else {
170            List<DistributionSnapshot> snaps = getSnapshotManager().listPersistentSnapshots((ctx.getCoreSession()));
171            snaps.add(getSnapshotManager().getRuntimeSnapshot());
172            distributionId = SnapshotResolverHelper.findBestMatch(snaps, distributionId);
173        }
174        if (distributionId == null || "".equals(distributionId)) {
175            distributionId = "current";
176        }
177
178        if (!orgDistributionId.equals(distributionId)) {
179            return ctx.newObject("redirectWO", orgDistributionId, distributionId);
180        }
181
182        ctx.setProperty("embeddedMode", embeddedMode);
183        ctx.setProperty("distribution", getSnapshotManager().getSnapshot(distributionId, ctx.getCoreSession()));
184        ctx.setProperty(DIST_ID, distributionId);
185        return ctx.newObject("apibrowser", distributionId, embeddedMode);
186    }
187
188    public List<DistributionSnapshotDesc> getAvailableDistributions() {
189        return getSnapshotManager().getAvailableDistributions(ctx.getCoreSession());
190    }
191
192    public String getRuntimeDistributionName() {
193        return SnapshotManagerComponent.RUNTIME;
194    }
195
196    public DistributionSnapshot getRuntimeDistribution() {
197        return getSnapshotManager().getRuntimeSnapshot();
198    }
199
200    public List<DistributionSnapshot> listPersistedDistributions() {
201        return getSnapshotManager().listPersistentSnapshots(ctx.getCoreSession());
202    }
203
204    public Map<String, DistributionSnapshot> getPersistedDistributions() {
205        return getSnapshotManager().getPersistentSnapshots(ctx.getCoreSession());
206    }
207
208    public DistributionSnapshot getCurrentDistribution() {
209        String distId = (String) ctx.getProperty(DIST_ID);
210        DistributionSnapshot currentDistribution = (DistributionSnapshot) ctx.getProperty("currentDistribution");
211        if (currentDistribution == null || !currentDistribution.getKey().equals(distId)) {
212            currentDistribution = getSnapshotManager().getSnapshot(distId, ctx.getCoreSession());
213            ctx.setProperty("currentDistribution", currentDistribution);
214        }
215        return currentDistribution;
216    }
217
218    @POST
219    @Path("save")
220    @Produces("text/html")
221    public Object doSave() throws NamingException, NotSupportedException, SystemException, RollbackException,
222            HeuristicMixedException, HeuristicRollbackException {
223        if (!isEditor()) {
224            return null;
225        }
226        FormData formData = getContext().getForm();
227        String distribLabel = formData.getString("name");
228
229        log.info("Start Snapshot...");
230        boolean startedTx = false;
231        UserTransaction tx = TransactionHelper.lookupUserTransaction();
232        if (tx != null && !TransactionHelper.isTransactionActiveOrMarkedRollback()) {
233            tx.begin();
234            startedTx = true;
235        }
236        try {
237            getSnapshotManager().persistRuntimeSnapshot(getContext().getCoreSession(), distribLabel);
238        } catch (NuxeoException e) {
239            log.error("Error during storage", e);
240            if (tx != null) {
241                tx.rollback();
242            }
243            return getView("savedKO").arg("message", e.getMessage());
244        }
245        log.info("Snapshot saved.");
246        if (tx != null && startedTx) {
247            tx.commit();
248        }
249
250        String redirectUrl = getContext().getBaseURL() + getPath();
251        log.debug("Path => " + redirectUrl);
252        return getView("saved");
253    }
254
255    @POST
256    @Path("saveExtended")
257    @Produces("text/html")
258    public Object doSaveExtended() throws NamingException, NotSupportedException, SystemException, SecurityException,
259            RollbackException, HeuristicMixedException, HeuristicRollbackException {
260        if (!isEditor()) {
261            return null;
262        }
263
264        FormData formData = getContext().getForm();
265
266        String distribLabel = formData.getString("name");
267        String bundleList = formData.getString("bundles");
268        String pkgList = formData.getString("packages");
269        SnapshotFilter filter = new SnapshotFilter(distribLabel);
270
271        if (bundleList != null) {
272            String[] bundles = bundleList.split("\n");
273            for (String bundleId : bundles) {
274                filter.addBundlePrefix(bundleId);
275            }
276        }
277
278        if (pkgList != null) {
279            String[] packages = pkgList.split("\\r?\\n");
280            for (String pkg : packages) {
281                filter.addPackagesPrefix(pkg);
282            }
283        }
284
285        log.info("Start Snapshot...");
286        boolean startedTx = false;
287        UserTransaction tx = TransactionHelper.lookupUserTransaction();
288        if (tx != null && !TransactionHelper.isTransactionActiveOrMarkedRollback()) {
289            tx.begin();
290            startedTx = true;
291        }
292        try {
293            getSnapshotManager().persistRuntimeSnapshot(getContext().getCoreSession(), distribLabel, filter);
294        } catch (NuxeoException e) {
295            log.error("Error during storage", e);
296            if (tx != null) {
297                tx.rollback();
298            }
299            return getView("savedKO").arg("message", e.getMessage());
300        }
301        log.info("Snapshot saved.");
302        if (tx != null && startedTx) {
303            tx.commit();
304        }
305        return getView("saved");
306    }
307
308    public String getDocumentationInfo() {
309        DocumentationService ds = Framework.getService(DocumentationService.class);
310        return ds.getDocumentationStats(getContext().getCoreSession());
311    }
312
313    protected File getExportTmpFile() {
314        File tmpFile = new File(Environment.getDefault().getTemp(), "export.zip");
315        if (tmpFile.exists()) {
316            tmpFile.delete();
317        }
318        tmpFile.deleteOnExit();
319        return tmpFile;
320    }
321
322    @GET
323    @Path("downloadDoc")
324    public Response downloadDoc() throws IOException {
325        DocumentationService ds = Framework.getService(DocumentationService.class);
326        File tmp = getExportTmpFile();
327        tmp.createNewFile();
328        OutputStream out = new FileOutputStream(tmp);
329        ds.exportDocumentation(getContext().getCoreSession(), out);
330        out.flush();
331        out.close();
332        ArchiveFile aFile = new ArchiveFile(tmp.getAbsolutePath());
333        return Response.ok(aFile)
334                       .header("Content-Disposition", "attachment;filename=" + "nuxeo-documentation.zip")
335                       .type("application/zip")
336                       .build();
337    }
338
339    @GET
340    @Path("download/{distributionId}")
341    public Response downloadDistrib(@PathParam("distributionId") String distribId) throws IOException {
342        File tmp = getExportTmpFile();
343        tmp.createNewFile();
344        OutputStream out = new FileOutputStream(tmp);
345        getSnapshotManager().exportSnapshot(getContext().getCoreSession(), distribId, out);
346        out.close();
347        String fName = "nuxeo-distribution-" + distribId + ".zip";
348        fName = fName.replace(" ", "_");
349        ArchiveFile aFile = new ArchiveFile(tmp.getAbsolutePath());
350        return Response.ok(aFile)
351                       .header("Content-Disposition", "attachment;filename=" + fName)
352                       .type("application/zip")
353                       .build();
354    }
355
356    @POST
357    @Path("uploadDistrib")
358    @Produces("text/html")
359    public Object uploadDistrib() throws IOException {
360        if (!isEditor()) {
361            return null;
362        }
363        Blob blob = getContext().getForm().getFirstBlob();
364
365        getSnapshotManager().importSnapshot(getContext().getCoreSession(), blob.getStream());
366        getSnapshotManager().readPersistentSnapshots(getContext().getCoreSession());
367
368        return getView("index");
369    }
370
371    @POST
372    @Path("uploadDistribTmp")
373    @Produces("text/html")
374    public Object uploadDistribTmp() throws IOException {
375        if (!isEditor()) {
376            return null;
377        }
378        Blob blob = getContext().getForm().getFirstBlob();
379        if (blob == null || blob.getLength() == 0) {
380            return null;
381        }
382        DocumentModel snap = getSnapshotManager().importTmpSnapshot(getContext().getCoreSession(), blob.getStream());
383        if (snap == null) {
384            log.error("Unable to import archive");
385            return null;
386        }
387        DistributionSnapshot snapObject = snap.getAdapter(DistributionSnapshot.class);
388        return getView("uploadEdit").arg("tmpSnap", snap).arg("snapObject", snapObject);
389    }
390
391    @POST
392    @Path("uploadDistribTmpValid")
393    @Produces("text/html")
394    public Object uploadDistribTmpValid() {
395        if (!isEditor()) {
396            return null;
397        }
398
399        FormData formData = getContext().getForm();
400        String name = formData.getString("name");
401        String version = formData.getString("version");
402        String pathSegment = formData.getString("pathSegment");
403        String title = formData.getString("title");
404
405        getSnapshotManager().validateImportedSnapshot(getContext().getCoreSession(), name, version, pathSegment, title);
406        getSnapshotManager().readPersistentSnapshots(getContext().getCoreSession());
407        return getView("importDone");
408    }
409
410    @POST
411    @Path("uploadDoc")
412    @Produces("text/html")
413    public Object uploadDoc() throws IOException {
414        if (!isEditor()) {
415            return null;
416        }
417
418        Blob blob = getContext().getForm().getFirstBlob();
419        if (blob == null || blob.getLength() == 0) {
420            return null;
421        }
422
423        DocumentationService ds = Framework.getService(DocumentationService.class);
424        ds.importDocumentation(getContext().getCoreSession(), blob.getStream());
425
426        log.info("Documents imported.");
427
428        return getView("docImportDone");
429    }
430
431    public boolean isEmbeddedMode() {
432        Boolean embed = (Boolean) getContext().getProperty("embeddedMode", Boolean.FALSE);
433        return embed == null ? false : embed.booleanValue();
434    }
435
436    public boolean isEditor() {
437        if (isEmbeddedMode()) {
438            return false;
439        }
440        NuxeoPrincipal principal = (NuxeoPrincipal) getContext().getPrincipal();
441        return SecurityHelper.canEditDocumentation(principal);
442    }
443
444}