fr.ortolang.diffusion.api.content.ContentResource.java Source code

Java tutorial

Introduction

Here is the source code for fr.ortolang.diffusion.api.content.ContentResource.java

Source

package fr.ortolang.diffusion.api.content;

/*
 * #%L
 * ORTOLANG
 * A online network structure for hosting language resources and tools.
 * 
 * Jean-Marie Pierrel / ATILF UMR 7118 - CNRS / Universit de Lorraine
 * Etienne Petitjean / ATILF UMR 7118 - CNRS
 * Jrme Blanchard / ATILF UMR 7118 - CNRS
 * Bertrand Gaiffe / ATILF UMR 7118 - CNRS
 * Cyril Pestel / ATILF UMR 7118 - CNRS
 * Marie Tonnelier / ATILF UMR 7118 - CNRS
 * Ulrike Fleury / ATILF UMR 7118 - CNRS
 * Frdric Pierre / ATILF UMR 7118 - CNRS
 * Cline Moro / ATILF UMR 7118 - CNRS
 *  
 * This work is based on work done in the equipex ORTOLANG (http://www.ortolang.fr/), by several Ortolang contributors (mainly CNRTL and SLDR)
 * ORTOLANG is funded by the French State program "Investissements d'Avenir" ANR-11-EQPX-0032
 * %%
 * Copyright (C) 2013 - 2015 Ortolang Team
 * %%
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Lesser Public License for more details.
 * 
 * You should have received a copy of the GNU General Lesser Public
 * License along with this program.  If not, see
 * <http://www.gnu.org/licenses/lgpl-3.0.html>.
 * #L%
 */

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;

import javax.ejb.EJB;
import javax.ws.rs.Consumes;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.CacheControl;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.NewCookie;
import javax.ws.rs.core.Request;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.ResponseBuilder;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.SecurityContext;
import javax.ws.rs.core.StreamingOutput;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;

import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.archivers.ArchiveOutputStream;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream;
import org.apache.commons.io.IOUtils;

import fr.ortolang.diffusion.OrtolangConfig;
import fr.ortolang.diffusion.OrtolangException;
import fr.ortolang.diffusion.OrtolangObject;
import fr.ortolang.diffusion.OrtolangObjectInfos;
import fr.ortolang.diffusion.OrtolangObjectState;
import fr.ortolang.diffusion.api.ApiHelper;
import fr.ortolang.diffusion.api.auth.AuthResource;
import fr.ortolang.diffusion.browser.BrowserService;
import fr.ortolang.diffusion.browser.BrowserServiceException;
import fr.ortolang.diffusion.core.AliasNotFoundException;
import fr.ortolang.diffusion.core.CoreService;
import fr.ortolang.diffusion.core.CoreServiceException;
import fr.ortolang.diffusion.core.InvalidPathException;
import fr.ortolang.diffusion.core.PathBuilder;
import fr.ortolang.diffusion.core.PathNotFoundException;
import fr.ortolang.diffusion.core.entity.Collection;
import fr.ortolang.diffusion.core.entity.CollectionElement;
import fr.ortolang.diffusion.core.entity.DataObject;
import fr.ortolang.diffusion.core.entity.Link;
import fr.ortolang.diffusion.core.entity.MetadataObject;
import fr.ortolang.diffusion.core.entity.SnapshotElement;
import fr.ortolang.diffusion.core.entity.TagElement;
import fr.ortolang.diffusion.core.entity.Workspace;
import fr.ortolang.diffusion.membership.MembershipService;
import fr.ortolang.diffusion.message.MessageService;
import fr.ortolang.diffusion.message.MessageServiceException;
import fr.ortolang.diffusion.message.entity.Message;
import fr.ortolang.diffusion.message.entity.MessageAttachment;
import fr.ortolang.diffusion.registry.KeyNotFoundException;
import fr.ortolang.diffusion.security.SecurityService;
import fr.ortolang.diffusion.security.SecurityServiceException;
import fr.ortolang.diffusion.security.authorisation.AccessDeniedException;
import fr.ortolang.diffusion.store.binary.BinaryStoreService;
import fr.ortolang.diffusion.store.binary.BinaryStoreServiceException;
import fr.ortolang.diffusion.store.binary.DataNotFoundException;
import fr.ortolang.diffusion.template.TemplateEngine;
import fr.ortolang.diffusion.template.TemplateEngineException;

@Path("/content")
@Produces({ MediaType.TEXT_HTML })
public class ContentResource {

    private static final Logger LOGGER = Logger.getLogger(ContentResource.class.getName());

    private static final ClassLoader TEMPLATE_ENGINE_CL = ContentResource.class.getClassLoader();

    private static final String anonymousBase64;

    @EJB
    private CoreService core;
    @EJB
    private MessageService message;
    @EJB
    private BrowserService browser;
    @EJB
    private BinaryStoreService store;
    @EJB
    private SecurityService security;
    @Context
    private UriInfo uriInfo;

    private static Map<String, Map<String, Object>> exportations;

    static {
        exportations = new HashMap<>();
        anonymousBase64 = Base64.getEncoder().encodeToString("anonymous".getBytes());
    }

    @GET
    @Path("/exportations/{id}")
    @SuppressWarnings("unchecked")
    public Response resumeExportation(@PathParam("id") String id, @Context SecurityContext securityContext)
            throws UnsupportedEncodingException {
        Map<String, Object> params = exportations.get(id);
        Response response = export(false, (String) params.get("followsymlink"), (String) params.get("filename"),
                (String) params.get("format"), (List<String>) params.get("path"), (String) params.get("regex"),
                securityContext);
        exportations.remove(id);
        return response;
    }

    @POST
    @Path("/export")
    @Consumes({ MediaType.APPLICATION_FORM_URLENCODED })
    @Produces({ MediaType.TEXT_HTML, MediaType.WILDCARD })
    public Response exportPost(final @QueryParam("scope") @DefaultValue("") String scope,
            final @FormParam("followsymlink") @DefaultValue("false") String followSymlink,
            @FormParam("filename") @DefaultValue("download") String filename,
            @FormParam("format") @DefaultValue("zip") String format, final @FormParam("path") List<String> paths,
            @FormParam("regex") String regex, @Context SecurityContext securityContext)
            throws UnsupportedEncodingException {
        LOGGER.log(Level.INFO, "POST /export");
        return export(!scope.startsWith(anonymousBase64), followSymlink, filename, format, paths, regex,
                securityContext);
    }

    @GET
    @Path("/export")
    @Produces({ MediaType.TEXT_HTML, MediaType.WILDCARD })
    public Response exportGet(final @QueryParam("scope") @DefaultValue("") String scope,
            final @QueryParam("followsymlink") @DefaultValue("false") String followSymlink,
            @QueryParam("filename") @DefaultValue("download") String filename,
            @QueryParam("format") @DefaultValue("zip") String format, final @QueryParam("path") List<String> paths,
            final @QueryParam("regex") String regex, @Context SecurityContext securityContext)
            throws UnsupportedEncodingException {
        LOGGER.log(Level.INFO, "GET /export");
        return export(!scope.startsWith(anonymousBase64), followSymlink, filename, format, paths, regex,
                securityContext);
    }

    private Response export(boolean connected, String followSymlink, String filename, String format,
            List<String> paths, String regex, SecurityContext securityContext) throws UnsupportedEncodingException {
        if (connected && securityContext.getUserPrincipal() == null) {
            LOGGER.log(Level.FINE, "user is not authenticated, redirecting to authentication");
            Map<String, Object> params = new HashMap<>();
            params.put("followSymlink", followSymlink);
            params.put("filename", filename);
            params.put("format", format);
            params.put("path", paths);
            params.put("regex", regex);
            String id = UUID.randomUUID().toString();
            exportations.put(id, params);
            String redirect = "/content/exportations/" + id;
            String encodedRedirect = Base64.getUrlEncoder().encodeToString(redirect.getBytes());
            NewCookie rcookie = new NewCookie(AuthResource.REDIRECT_PATH_PARAM_NAME, encodedRedirect,
                    OrtolangConfig.getInstance().getProperty(OrtolangConfig.Property.API_CONTEXT),
                    uriInfo.getBaseUri().getHost(), 1, "Redirect path after authentication", 300,
                    new Date(System.currentTimeMillis() + 300000), false, false);
            UriBuilder builder = UriBuilder.fromResource(ContentResource.class);
            builder.queryParam("followsymlink", followSymlink).queryParam("filename", filename)
                    .queryParam("format", format).queryParam("path", paths);
            return Response
                    .seeOther(uriInfo.getBaseUriBuilder().path(AuthResource.class)
                            .queryParam(AuthResource.REDIRECT_PATH_PARAM_NAME, encodedRedirect).build())
                    .cookie(rcookie).build();
        }
        Pattern pattern = null;
        if (regex != null) {
            pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
        }
        ResponseBuilder builder = handleExport(false, filename, format, paths, pattern);
        return builder.build();
    }

    private ResponseBuilder handleExport(boolean followSymlink, String filename, String format,
            final List<String> paths, final Pattern pattern) throws UnsupportedEncodingException {
        ResponseBuilder builder;
        switch (format) {
        case "zip": {
            LOGGER.log(Level.FINE, "exporting using format zip");
            builder = Response.ok();
            builder.header("Content-Disposition",
                    "attachment; filename*=UTF-8''" + URLEncoder.encode(filename, "utf-8") + ".zip");
            builder.type("application/zip");
            StreamingOutput stream = output -> {
                try (ZipArchiveOutputStream out = new ZipArchiveOutputStream(output)) {
                    for (String path : paths) {
                        try {
                            String key = resolveContentPath(path);
                            ArchiveEntryFactory factory = (name, time, size) -> {
                                ZipArchiveEntry entry = new ZipArchiveEntry(name);
                                if (time != -1) {
                                    entry.setTime(time);
                                }
                                if (size != -1) {
                                    entry.setSize(size);
                                }
                                return entry;
                            };
                            exportToArchive(key, out, factory, PathBuilder.fromPath(path), false, pattern);
                        } catch (AccessDeniedException e) {
                            LOGGER.log(Level.FINEST, "access denied during export to zip", e);
                        } catch (BrowserServiceException | CoreServiceException | AliasNotFoundException
                                | KeyNotFoundException | OrtolangException e) {
                            LOGGER.log(Level.INFO, "unable to export path to zip", e);
                        } catch (InvalidPathException e) {
                            LOGGER.log(Level.FINEST, "invalid path during export to zip", e);
                        } catch (PathNotFoundException e) {
                            LOGGER.log(Level.FINEST, "path not found during export to zip", e);
                        } catch (ExportToArchiveIOException e) {
                            LOGGER.log(Level.SEVERE,
                                    "unexpected IO error during export to archive, stopping export", e);
                            break;
                        }
                    }
                }
            };
            builder.entity(stream);
            break;
        }
        case "tar": {
            LOGGER.log(Level.FINE, "exporting using format tar");
            builder = Response.ok();
            builder.header("Content-Disposition",
                    "attachment; filename*=UTF-8''" + URLEncoder.encode(filename, "utf-8") + ".tar.gz");
            builder.type("application/x-gzip");
            StreamingOutput stream = output -> {
                try (GzipCompressorOutputStream gout = new GzipCompressorOutputStream(output);
                        TarArchiveOutputStream out = new TarArchiveOutputStream(gout)) {
                    for (String path : paths) {
                        try {
                            String key = resolveContentPath(path);
                            ArchiveEntryFactory factory = (name, time, size) -> {
                                TarArchiveEntry entry = new TarArchiveEntry(name);
                                if (time != -1) {
                                    entry.setModTime(time);
                                }
                                if (size != -1) {
                                    entry.setSize(size);
                                }
                                return entry;
                            };
                            exportToArchive(key, out, factory, PathBuilder.fromPath(path), false, pattern);
                        } catch (BrowserServiceException | CoreServiceException | AliasNotFoundException
                                | KeyNotFoundException | OrtolangException e) {
                            LOGGER.log(Level.INFO, "unable to export path to tar", e);
                        } catch (InvalidPathException e) {
                            LOGGER.log(Level.FINEST, "invalid path during export to tar", e);
                        } catch (PathNotFoundException e) {
                            LOGGER.log(Level.FINEST, "path not found during export to tar", e);
                        } catch (ExportToArchiveIOException e) {
                            LOGGER.log(Level.SEVERE,
                                    "unexpected IO error during export to archive, stopping export", e);
                            break;
                        }
                    }
                }
            };
            builder.entity(stream);
            break;
        }
        default:
            builder = Response.status(Status.BAD_REQUEST).entity("export format [" + format + "] is not supported");
        }
        return builder;
    }

    private String resolveContentPath(String path) throws CoreServiceException, InvalidPathException,
            AccessDeniedException, AliasNotFoundException, KeyNotFoundException, PathNotFoundException {
        PathBuilder pbuilder = PathBuilder.fromPath(path);
        String[] pparts = pbuilder.buildParts();
        if (pparts.length < 2) {
            throw new InvalidPathException("invalid path, format is : /{alias}/{root}/{path}");
        }
        String wskey = core.resolveWorkspaceAlias(pparts[0]);
        if (pparts[1].equals(Workspace.LATEST)) {
            pparts[1] = core.findWorkspaceLatestPublishedSnapshot(wskey);
            if (pparts[1] == null) {
                throw new InvalidPathException(
                        "unable to find latest published snapshot for workspace alias: " + pparts[0]);
            }
        }
        return core.resolveWorkspacePath(wskey, pparts[1], pbuilder.relativize(2).build());
    }

    private void exportToArchive(String key, ArchiveOutputStream aos, ArchiveEntryFactory factory, PathBuilder path,
            boolean followsymlink, Pattern pattern)
            throws OrtolangException, KeyNotFoundException, BrowserServiceException, ExportToArchiveIOException {
        OrtolangObject object;
        try {
            object = browser.findObject(key);
        } catch (BrowserServiceException e) {
            return;
        }
        OrtolangObjectInfos infos = browser.getInfos(key);
        String type = object.getObjectIdentifier().getType();

        switch (type) {
        case Collection.OBJECT_TYPE:
            try {
                Set<CollectionElement> elements = ((Collection) object).getElements();
                ArchiveEntry centry = factory.createArchiveEntry(path.build() + "/",
                        infos.getLastModificationDate(), 0L);
                try {
                    aos.putArchiveEntry(centry);
                    for (CollectionElement element : elements) {
                        try {
                            PathBuilder pelement = path.clone().path(element.getName());
                            exportToArchive(element.getKey(), aos, factory, pelement, followsymlink, pattern);
                        } catch (InvalidPathException e) {
                            LOGGER.log(Level.SEVERE, "unexpected error during export to zip !!", e);
                        }
                    }
                } catch (IOException e) {
                    throw new ExportToArchiveIOException(
                            "unable to put archive entry for collection at path: " + path.build(), e);
                }
            } finally {
                try {
                    aos.closeArchiveEntry();
                } catch (IOException e) {
                    LOGGER.log(Level.FINEST, "unable to close archive entry for collection at path [" + path.build()
                            + "]: " + e.getMessage());
                }
            }
            break;
        case DataObject.OBJECT_TYPE:
            if (pattern != null && !pattern.matcher(object.getObjectName()).matches()) {
                return;
            }
            try (InputStream input = core.download(object.getObjectKey())) {
                DataObject dataObject = (DataObject) object;
                ArchiveEntry oentry = factory.createArchiveEntry(path.build(), infos.getLastModificationDate(),
                        dataObject.getSize());
                try {
                    aos.putArchiveEntry(oentry);
                    IOUtils.copy(input, aos);
                } catch (IOException e) {
                    throw new ExportToArchiveIOException("unable to export dataobject at path: " + path.build(), e);
                } finally {
                    try {
                        aos.closeArchiveEntry();
                    } catch (IOException e) {
                        throw new ExportToArchiveIOException(
                                "unable to close archive entry for collection at path: " + path.build(), e);
                    }
                }
            } catch (IOException e) {
                throw new ExportToArchiveIOException(
                        "unable to get input stream for dataobject at path: " + path.build(), e);
            } catch (AccessDeniedException e) {
                return;
            } catch (CoreServiceException | DataNotFoundException e) {
                LOGGER.log(Level.SEVERE, "unexpected error during export to zip", e);
            }
            break;
        case Link.OBJECT_TYPE:
            if (followsymlink) {
                LOGGER.log(Level.SEVERE, "link export is not managed yet");
                // TODO in case of following symlink, add cyclic detection
            }
            break;
        }
    }

    @GET
    @Path("/attachments/{mkey}/{hash}")
    @Produces({ MediaType.TEXT_HTML, MediaType.WILDCARD })
    public Response attachment(@PathParam(value = "mkey") String mkey, @PathParam(value = "hash") String hash,
            @Context SecurityContext securityContext) throws MessageServiceException, KeyNotFoundException,
            DataNotFoundException, UnsupportedEncodingException, BinaryStoreServiceException {
        LOGGER.log(Level.INFO, "GET /attachments/" + mkey + "/" + hash);
        try {
            Message msg = message.readMessage(mkey);
            MessageAttachment attachment = msg.findAttachmentByHash(hash);
            if (attachment == null) {
                throw new DataNotFoundException("unable to find attachment");
            }
            ResponseBuilder builder = Response.ok(store.getFile(attachment.getHash()));
            builder.header("Content-Disposition",
                    "attachment; filename*=UTF-8''" + URLEncoder.encode(attachment.getName(), "utf-8"));
            builder.header("Content-Length", attachment.getSize());
            builder.header("Accept-Ranges", "bytes");
            builder.type(attachment.getType());
            return builder.build();
        } catch (AccessDeniedException e) {
            return redirectToAuth("/content/attachments/" + mkey + "/" + hash, true, Collections.emptyMap(),
                    securityContext);
        }
    }

    @GET
    @Path("/key/{key}")
    @Produces({ MediaType.TEXT_HTML, MediaType.WILDCARD })
    public Response key(@PathParam("key") String key, @QueryParam("fd") boolean download,
            @QueryParam("O") @DefaultValue("A") String asc, @QueryParam("C") @DefaultValue("N") String order,
            @QueryParam("l") @DefaultValue("true") boolean login, @Context SecurityContext securityContext,
            @Context Request request) throws TemplateEngineException, CoreServiceException, KeyNotFoundException,
            InvalidPathException, OrtolangException, BinaryStoreServiceException, DataNotFoundException,
            URISyntaxException, BrowserServiceException, UnsupportedEncodingException, SecurityServiceException {
        LOGGER.log(Level.INFO, "GET /content/key/" + key);
        try {
            OrtolangObjectState state = browser.getState(key);
            CacheControl cc = new CacheControl();
            cc.setPrivate(true);
            ApiHelper.setCacheControlFromState(state, cc);
            Date lmd = new Date(state.getLastRefresh() / 1000 * 1000);
            ResponseBuilder builder = null;
            if (System.currentTimeMillis() - state.getLastRefresh() > 1000) {
                builder = request.evaluatePreconditions(lmd);
            }
            if (builder == null) {
                OrtolangObject object = browser.findObject(key);
                if (object instanceof DataObject) {
                    String sha1 = ((DataObject) object).getStream();
                    File content = store.getFile(sha1);
                    security.checkPermission(key, "download");
                    builder = Response.ok(content).header("Content-Type", ((DataObject) object).getMimeType())
                            .header("Content-Length", ((DataObject) object).getSize())
                            .header("Accept-Ranges", "bytes");
                    if (download) {
                        builder = builder.header("Content-Disposition", "attachment; filename*=UTF-8''"
                                + URLEncoder.encode(object.getObjectName(), "utf-8"));
                    } else {
                        builder = builder.header("Content-Disposition",
                                "filename*=UTF-8''" + URLEncoder.encode(object.getObjectName(), "utf-8"));
                    }
                    builder.lastModified(lmd);
                } else if (object instanceof MetadataObject) {
                    File content = store.getFile(((MetadataObject) object).getStream());
                    security.checkPermission(key, "download");
                    builder = Response.ok(content)
                            .header("Content-Type", ((MetadataObject) object).getContentType())
                            .header("Content-Length", ((MetadataObject) object).getSize())
                            .header("Accept-Ranges", "bytes");
                    if (download) {
                        builder = builder.header("Content-Disposition", "attachment; filename*=UTF-8''"
                                + URLEncoder.encode(object.getObjectName(), "utf-8"));
                    } else {
                        builder = builder.header("Content-Disposition",
                                "filename*=UTF-8''" + URLEncoder.encode(object.getObjectName(), "utf-8"));
                    }
                    builder.lastModified(lmd);
                } else if (object instanceof Collection) {
                    ContentRepresentation representation = new ContentRepresentation();
                    representation.setContext(
                            OrtolangConfig.getInstance().getProperty(OrtolangConfig.Property.API_CONTEXT));
                    representation.setBase("/content/key");
                    representation.setAlias("");
                    representation.setRoot("");
                    representation.setPath("/" + key);
                    representation.setParentPath("");
                    representation.setOrder(order);
                    representation.setLinkbykey(true);
                    representation.setElements(new ArrayList<>(((Collection) object).getElements()));
                    sort(representation, asc, order);
                    builder = Response.ok(
                            TemplateEngine.getInstance(TEMPLATE_ENGINE_CL).process("collection", representation));
                    builder.lastModified(lmd);
                } else if (object instanceof Link) {
                    return Response.seeOther(uriInfo.getBaseUriBuilder().path(ContentResource.class)
                            .path(((Link) object).getTarget()).build()).build();
                } else {
                    return Response.serverError().entity("object type not supported").build();
                }
            }
            builder.cacheControl(cc);
            return builder.build();
        } catch (AccessDeniedException e) {
            Map<String, Object> params = new HashMap<>();
            params.put("fd", download);
            params.put("O", asc);
            params.put("C", order);
            return redirectToAuth("/content/key/" + key, login, params, securityContext);
        }
    }

    @GET
    @Produces(MediaType.TEXT_HTML)
    public Response workspaces(@QueryParam("O") @DefaultValue("A") String asc,
            @QueryParam("l") @DefaultValue("true") boolean login, @Context SecurityContext securityContext)
            throws TemplateEngineException, CoreServiceException {
        LOGGER.log(Level.INFO, "GET /content");
        try {
            ContentRepresentation representation = new ContentRepresentation();
            representation
                    .setContext(OrtolangConfig.getInstance().getProperty(OrtolangConfig.Property.API_CONTEXT));
            representation.setBase("/content");
            representation.setPath("/");
            representation.setOrder("N");
            List<String> aliases = core.listAllWorkspaceAlias();
            List<CollectionElement> elements = new ArrayList<>(aliases.size());
            for (String alias : aliases) {
                elements.add(
                        new CollectionElement(Collection.OBJECT_TYPE, alias, -1, -1, "ortolang/workspace", ""));
            }
            sort(representation, elements, asc);
            representation.setElements(elements);
            return Response.ok(TemplateEngine.getInstance(TEMPLATE_ENGINE_CL).process("collection", representation))
                    .build();
        } catch (AccessDeniedException e) {
            Map<String, Object> params = new HashMap<>();
            params.put("O", asc);
            return redirectToAuth("/content", login, params, securityContext);
        }
    }

    @GET
    @Path("/{alias}")
    @Produces(MediaType.TEXT_HTML)
    public Response workspace(@PathParam("alias") String alias, @QueryParam("O") @DefaultValue("A") String asc,
            @QueryParam("l") @DefaultValue("true") boolean login, @Context SecurityContext securityContext,
            @Context Request request) throws TemplateEngineException, CoreServiceException, AliasNotFoundException,
            KeyNotFoundException, BrowserServiceException {
        LOGGER.log(Level.INFO, "GET /content/" + alias);
        ContentRepresentation representation = new ContentRepresentation();
        representation.setContext(OrtolangConfig.getInstance().getProperty(OrtolangConfig.Property.API_CONTEXT));
        representation.setBase("/content");
        representation.setAlias(alias);
        representation.setPath("/" + alias);
        representation.setParentPath("/");
        representation.setOrder("N");
        try {
            String wskey = core.resolveWorkspaceAlias(alias);
            OrtolangObjectState state = browser.getState(wskey);
            CacheControl cc = new CacheControl();
            cc.setPrivate(true);
            ApiHelper.setCacheControlFromState(state, cc);
            Date lmd = new Date(state.getLastRefresh() / 1000 * 1000);
            ResponseBuilder builder = null;
            if (System.currentTimeMillis() - state.getLastRefresh() > 1000) {
                builder = request.evaluatePreconditions(lmd);
            }
            if (builder == null) {
                Workspace workspace = core.readWorkspace(wskey);
                List<CollectionElement> elements = new ArrayList<>();
                String latest = core.findWorkspaceLatestPublishedSnapshot(wskey);
                if (latest != null && latest.length() > 0) {
                    elements.add(new CollectionElement(Collection.OBJECT_TYPE, Workspace.LATEST, -1, -1,
                            "ortolang/snapshot", latest));
                }
                elements.add(new CollectionElement(Collection.OBJECT_TYPE, Workspace.HEAD, -1, -1,
                        "ortolang/snapshot", workspace.getHead()));
                for (SnapshotElement snapshot : workspace.getSnapshots()) {
                    elements.add(new CollectionElement(Collection.OBJECT_TYPE, snapshot.getName(), -1, -1,
                            "ortolang/snapshot", snapshot.getKey()));
                }
                for (TagElement tag : workspace.getTags()) {
                    elements.add(new CollectionElement(Collection.OBJECT_TYPE, tag.getName(), -1, -1,
                            "ortolang/tag", workspace.findSnapshotByName(tag.getSnapshot()).getKey()));
                }
                sort(representation, elements, asc);
                representation.setElements(elements);
                builder = Response
                        .ok(TemplateEngine.getInstance(TEMPLATE_ENGINE_CL).process("collection", representation));
                builder.lastModified(lmd);
            }
            builder.cacheControl(cc);
            return builder.build();
        } catch (AccessDeniedException e) {
            Map<String, Object> params = new HashMap<>();
            params.put("O", asc);
            return redirectToAuth(representation.getBase() + representation.getPath(), login, params,
                    securityContext);
        }
    }

    @GET
    @Path("/{alias}/{root}")
    @Produces(MediaType.TEXT_HTML)
    public Response snapshot(@PathParam("alias") String alias, @PathParam("root") final String root,
            @QueryParam("O") @DefaultValue("A") String asc, @QueryParam("C") @DefaultValue("N") String order,
            @QueryParam("l") @DefaultValue("true") boolean login, @Context SecurityContext securityContext,
            @Context Request request) throws TemplateEngineException, CoreServiceException, AccessDeniedException,
            AliasNotFoundException, KeyNotFoundException, BrowserServiceException {
        LOGGER.log(Level.INFO, "GET /content/" + alias + "/" + root);
        ContentRepresentation representation = new ContentRepresentation();
        representation.setContext(OrtolangConfig.getInstance().getProperty(OrtolangConfig.Property.API_CONTEXT));
        representation.setBase("/content");
        representation.setAlias(alias);
        representation.setRoot(root);
        representation.setPath("/" + alias + "/" + root);
        representation.setParentPath("/" + alias);
        representation.setOrder(order);
        try {
            String wskey = core.resolveWorkspaceAlias(alias);
            Workspace workspace = core.readWorkspace(wskey);
            String rkey;
            switch (root) {
            case Workspace.LATEST:
                String sname = core.findWorkspaceLatestPublishedSnapshot(wskey);
                rkey = workspace.findSnapshotByName(sname).getKey();
                if (rkey == null) {
                    return Response.status(Status.NOT_FOUND)
                            .entity("No version of this workspace has been published").type("text/plain").build();
                }
                break;
            case Workspace.HEAD:
                rkey = workspace.getHead();
                break;
            default:
                String snapshot = root;
                TagElement telement = workspace.findTagByName(snapshot);
                if (telement != null) {
                    snapshot = telement.getSnapshot();
                }
                SnapshotElement selement = workspace.findSnapshotByName(snapshot);
                if (selement == null) {
                    return Response.status(Status.NOT_FOUND).entity(
                            "Unable to find a root tag or snapshot with name [" + root + "] in this workspace")
                            .type("text/plain").build();
                }
                rkey = selement.getKey();
                break;
            }

            OrtolangObjectState state = browser.getState(rkey);
            CacheControl cc = new CacheControl();
            cc.setPrivate(true);
            if (!root.equals(Workspace.LATEST) && state.isLocked()) {
                cc.setMaxAge(691200);
                cc.setMustRevalidate(false);
            } else {
                cc.setMaxAge(0);
                cc.setMustRevalidate(true);
            }
            Date lmd = new Date(state.getLastRefresh() / 1000 * 1000);
            ResponseBuilder builder = null;
            if (System.currentTimeMillis() - state.getLastRefresh() > 1000) {
                builder = request.evaluatePreconditions(lmd);
            }
            if (builder == null) {
                Collection collection = core.readCollection(rkey);
                representation.setElements(new ArrayList<>(collection.getElements()));
                sort(representation, asc, order);
                builder = Response
                        .ok(TemplateEngine.getInstance(TEMPLATE_ENGINE_CL).process("collection", representation));
                builder.lastModified(lmd);
            }
            builder.cacheControl(cc);
            return builder.build();
        } catch (AccessDeniedException e) {
            Map<String, Object> params = new HashMap<>();
            params.put("O", asc);
            params.put("C", order);
            return redirectToAuth(representation.getBase() + representation.getPath(), login, params,
                    securityContext);
        }
    }

    @GET
    @Path("/{alias}/{root}/{path: .*}")
    @Produces({ MediaType.TEXT_HTML, MediaType.WILDCARD })
    public Response path(@PathParam("alias") String alias, @PathParam("root") final String root,
            @PathParam("path") String path, @QueryParam("fd") boolean download,
            @QueryParam("O") @DefaultValue("A") String asc, @QueryParam("C") @DefaultValue("N") String order,
            @QueryParam("l") @DefaultValue("true") boolean login, @Context SecurityContext securityContext,
            @Context Request request) throws TemplateEngineException, CoreServiceException, KeyNotFoundException,
            AliasNotFoundException, InvalidPathException, OrtolangException, BinaryStoreServiceException,
            DataNotFoundException, URISyntaxException, BrowserServiceException, UnsupportedEncodingException,
            SecurityServiceException, PathNotFoundException {
        LOGGER.log(Level.INFO, "GET /content/" + alias + "/" + root + "/" + path);
        ContentRepresentation representation = new ContentRepresentation();
        representation.setContext(OrtolangConfig.getInstance().getProperty(OrtolangConfig.Property.API_CONTEXT));
        representation.setBase("/content");
        representation.setAlias(alias);
        representation.setRoot(root);
        representation.setPath("/" + alias + "/" + root + "/" + path);
        representation.setParentPath("/" + alias + "/" + root);
        representation.setOrder(order);
        try {
            String wskey = core.resolveWorkspaceAlias(alias);
            boolean cacheableRoot = false;
            String rroot = null;
            if (root.equals(Workspace.LATEST)) {
                rroot = core.findWorkspaceLatestPublishedSnapshot(wskey);
                if (rroot == null) {
                    return Response.status(Status.NOT_FOUND)
                            .entity("No version of this workspace has been published").type("text/plain").build();
                }
            } else if (!root.equals(Workspace.HEAD)) {
                Workspace workspace = core.readWorkspace(wskey);
                if (!workspace.containsTagName(root)) {
                    cacheableRoot = true;
                }
            }
            if (rroot == null) {
                rroot = root;
            }
            PathBuilder npath = PathBuilder.fromPath(path);
            String okey = core.resolveWorkspacePath(wskey, rroot, npath.build());
            OrtolangObjectState state = browser.getState(okey);

            CacheControl cc = new CacheControl();
            cc.setPrivate(true);
            if (cacheableRoot) {
                cc.setMaxAge(691200);
                cc.setMustRevalidate(false);
            } else {
                cc.setMaxAge(0);
                cc.setMustRevalidate(true);
            }
            Date lmd = new Date(state.getLastRefresh() / 1000 * 1000);
            ResponseBuilder builder = null;
            if (System.currentTimeMillis() - state.getLastRefresh() > 1000) {
                builder = request.evaluatePreconditions(lmd);
            }
            if (builder == null) {
                OrtolangObject object = browser.findObject(okey);
                if (object instanceof DataObject) {
                    security.checkPermission(okey, "download");
                    File content = store.getFile(((DataObject) object).getStream());
                    builder = Response.ok(content).header("Content-Type", ((DataObject) object).getMimeType())
                            .header("Content-Length", ((DataObject) object).getSize())
                            .header("Accept-Ranges", "bytes");
                    if (download) {
                        builder = builder.header("Content-Disposition", "attachment; filename*=UTF-8''"
                                + URLEncoder.encode(object.getObjectName(), "utf-8"));
                    } else {
                        builder = builder.header("Content-Disposition",
                                "filename*=UTF-8''" + URLEncoder.encode(object.getObjectName(), "utf-8"));
                    }
                    builder.lastModified(lmd);
                } else if (object instanceof Collection) {
                    representation.setElements(new ArrayList<>(((Collection) object).getElements()));
                    sort(representation, asc, order);
                    builder = Response.ok(
                            TemplateEngine.getInstance(TEMPLATE_ENGINE_CL).process("collection", representation));
                    builder.lastModified(lmd);
                } else if (object instanceof Link) {
                    return Response.seeOther(uriInfo.getBaseUriBuilder().path(ContentResource.class)
                            .path(((Link) object).getTarget()).build()).build();
                } else {
                    return Response.serverError().entity("object type not supported").build();
                }
            }
            builder.cacheControl(cc);
            return builder.build();
        } catch (AccessDeniedException e) {
            Map<String, Object> params = new HashMap<>();
            params.put("fd", download);
            params.put("O", asc);
            params.put("C", order);
            return redirectToAuth(representation.getBase() + representation.getPath(), login, params,
                    securityContext);
        }
    }

    private Response redirectToAuth(String path, boolean login, Map<String, Object> params,
            SecurityContext securityContext) {
        if (securityContext.getUserPrincipal() == null || securityContext.getUserPrincipal().getName()
                .equals(MembershipService.UNAUTHENTIFIED_IDENTIFIER)) {
            if (login) {
                UriBuilder builder = UriBuilder.fromPath(path);
                for (Map.Entry<String, Object> param : params.entrySet()) {
                    builder.queryParam(param.getKey(), param.getValue());
                }
                String redirect = builder.build().toString();
                String encodedRedirect = Base64.getUrlEncoder().encodeToString(redirect.getBytes());
                LOGGER.log(Level.FINE, "user is not authenticated, redirecting to authentication");
                NewCookie rcookie = new NewCookie(AuthResource.REDIRECT_PATH_PARAM_NAME, encodedRedirect,
                        OrtolangConfig.getInstance().getProperty(OrtolangConfig.Property.API_CONTEXT),
                        uriInfo.getBaseUri().getHost(), 1, "Redirect path after authentication", 300,
                        new Date(System.currentTimeMillis() + 300000), false, false);
                return Response
                        .seeOther(uriInfo.getBaseUriBuilder().path(AuthResource.class)
                                .queryParam(AuthResource.REDIRECT_PATH_PARAM_NAME, encodedRedirect).build())
                        .cookie(rcookie).build();
            } else {
                LOGGER.log(Level.FINE, "user is not authenticated, but login redirect disabled");
                return Response.status(Status.UNAUTHORIZED).entity("You are not authorized to access this content")
                        .build();
            }
        } else {
            LOGGER.log(Level.FINE, "user is already authenticated, access denied");
            return Response.status(Status.UNAUTHORIZED).entity("You are not authorized to access this content")
                    .build();
        }
    }

    private void sort(ContentRepresentation representation, String asc, String order) {
        if ("D".equals(asc)) {
            switch (order) {
            case "T":
                Collections.sort(representation.getElements(), CollectionElement.ElementTypeDescComparator);
                break;
            case "M":
                Collections.sort(representation.getElements(), CollectionElement.ElementDateDescComparator);
                break;
            case "S":
                Collections.sort(representation.getElements(), CollectionElement.ElementSizeDescComparator);
                break;
            default:
                Collections.sort(representation.getElements(), CollectionElement.ElementNameDescComparator);
                break;
            }
            representation.setAsc(false);
        } else {
            switch (order) {
            case "T":
                Collections.sort(representation.getElements(), CollectionElement.ElementTypeAscComparator);
                break;
            case "M":
                Collections.sort(representation.getElements(), CollectionElement.ElementDateAscComparator);
                break;
            case "S":
                Collections.sort(representation.getElements(), CollectionElement.ElementSizeAscComparator);
                break;
            default:
                Collections.sort(representation.getElements(), CollectionElement.ElementNameAscComparator);
                break;
            }
            representation.setAsc(true);
        }
    }

    private void sort(ContentRepresentation representation, List<CollectionElement> elements, String asc) {
        if ("D".equals(asc)) {
            Collections.sort(elements, CollectionElement.ElementNameDescComparator);
            representation.setAsc(false);
        } else {
            Collections.sort(elements, CollectionElement.ElementNameAscComparator);
            representation.setAsc(true);
        }
    }

    interface ArchiveEntryFactory {

        ArchiveEntry createArchiveEntry(String name, long modificationDate, long size);
    }

}