hu.api.SivaPlayerVideoServlet.java Source code

Java tutorial

Introduction

Here is the source code for hu.api.SivaPlayerVideoServlet.java

Source

/*
 * net/balusc/webapp/FileServlet.java
 *
 * Copyright (C) 2009 BalusC
 *
 * 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 library 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
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along with this library.
 * If not, see <http://www.gnu.org/licenses/>.
 */

package hu.api;

import hu.model.EParticipationRestriction;
import hu.model.Video;
import hu.model.api.CollaborationMedia;
import hu.model.api.CollaborationPost;
import hu.model.api.CollaborationThread;
import hu.model.api.ECollaborationThreadVisibility;
import hu.model.api.SivaPlayerLogEntry;
import hu.model.api.SivaPlayerSession;
import hu.model.users.EUserType;
import hu.model.users.User;
import hu.persistence.IApiStore;
import hu.persistence.IPersistenceProvider;
import hu.persistence.IUserStore;
import hu.persistence.IVideoStore;
import hu.persistence.InconsistencyException;
import hu.util.BrandingConfiguration;
import hu.util.CommonUtils;
import hu.util.MailService;
import hu.util.SecurityUtils;

import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.RandomAccessFile;
import java.net.URISyntaxException;
import java.net.URLDecoder;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.zip.GZIPOutputStream;

import javax.faces.FactoryFinder;
import javax.faces.application.Application;
import javax.faces.component.UIViewRoot;
import javax.faces.context.FacesContext;
import javax.faces.context.FacesContextFactory;
import javax.faces.lifecycle.Lifecycle;
import javax.faces.lifecycle.LifecycleFactory;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.annotation.MultipartConfig;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.Part;

import org.apache.commons.io.IOUtils;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

/**
 * A file servlet supporting resume of downloads and client-side caching and
 * GZIP of text content. This servlet can also be used for images, client-side
 * caching would become more efficient. This servlet can also be used for text
 * files, GZIP would decrease network bandwidth.
 * 
 * @author BalusC
 * @link 
 *       http://balusc.blogspot.com/2009/02/fileservlet-supporting-resume-and.html
 */
@MultipartConfig(fileSizeThreshold = 1024 * 1024 * 10, maxFileSize = 1024 * 1024 * 12, maxRequestSize = 1024 * 1024
        * 101)
public class SivaPlayerVideoServlet extends AbstractServlet {

    private static final long serialVersionUID = 1L;
    private static final int DEFAULT_BUFFER_SIZE = 10240; // 10KB.
    private static final long DEFAULT_EXPIRE_TIME = 86400000L; // ..ms = 1 week.
    private static final String MULTIPART_BOUNDARY = "MULTIPART_BYTERANGES";
    private static final ArrayList<String> ALLOWED_FILE_TYPES = new ArrayList<String>() {

        private static final long serialVersionUID = 1L;

        {
            add("jpg");
            add("jpeg");
            add("png");
            add("gif");
            add("pdf");
        }
    };

    private boolean isAJAXRequest = false;
    private String videoPath;
    private SivaPlayerSession session = null;
    private IPersistenceProvider persistenceProvider;
    private MailService mailService;
    private BrandingConfiguration brandingConfiguration;
    private User currentUser = null;

    public void init() throws ServletException {

        // Define base path somehow. You can define it as init-param of the
        // servlet.
        this.videoPath = System.getProperty("user.home") + "/.sivaServer/videos";
    }

    protected void doHead(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        this.doAction(request, response, "HEAD");
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        this.doAction(request, response, "GET");
    }

    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        this.doAction(request, response, "POST");
    }

    private void doAction(HttpServletRequest request, HttpServletResponse response, String requestType)
            throws ServletException, IOException {

        // Check if it's an AJAX request
        this.isAJAXRequest = (request.getParameter("ajax") != null && request.getParameter("ajax").equals("true"));

        // Allow Cross-Origin-Requests
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Access-Control-Allow-Methods", "GET, PUT, POST, DELETE, OPTIONS");
        response.setHeader("Access-Control-Max-Age", "1000");
        response.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With");

        // URL pattern: /videoId
        // Get request parameter from URL and check if it has been set.
        // Show 400 if less or more parameters than allowed.
        String requestedVideo = request.getPathInfo();
        if (requestedVideo == null || requestedVideo.split("/").length < 2
                || requestedVideo.split("/")[1].equals("")) {
            this.sendError(response, HttpServletResponse.SC_BAD_REQUEST,
                    "The video folder has to be specified for using this web service.");
            return;
        }

        this.persistenceProvider = (IPersistenceProvider) getServletContext().getAttribute("PersistenceProvider");
        this.mailService = (MailService) getServletContext().getAttribute("mailService");
        this.brandingConfiguration = (BrandingConfiguration) getServletContext()
                .getAttribute("brandingConfiguration");

        // Check if it's a watching request
        if (request.getPathInfo().endsWith("/watch.html")) {
            this.providePlayer(request, response);
            return;
        }

        // Check if it's a log request and perform logging if so
        if (request.getPathInfo().endsWith("/log") && requestType.equals("POST")) {
            this.doLogging(request, response);
            return;
        }

        // Check if it's a checkSession request and provide session status if so
        if (requestedVideo.endsWith("/getStats.js")) {
            this.getStats(request, response);
            return;
        }

        // Check if user requests user secret and perform login
        if (request.getPathInfo().endsWith("/getSecret.js") && requestType.equals("POST")) {
            this.provideUserSecret(request, response, requestType);
            return;
        }

        // Check if current session exists and if it is allowed to access this
        // video, stop further execution, if so.
        boolean result = handleAccess(request, response, requestType);
        if (!result) {
            return;
        }

        // Check if it's collaboration request and provide data
        if (request.getPathInfo().endsWith("/getCollaboration.js")) {
            this.provideCollaboration(request, response);
            return;
        }

        // Check if it's a thread creation request
        if (request.getPathInfo().endsWith("/createCollaborationThread.js")) {
            this.createCollaborationThread(request, response);
            return;
        }

        // Check if it's a post creation request
        if (request.getPathInfo().endsWith("/createCollaborationPost.js")) {
            this.createCollaborationPost(request, response);
            return;
        }

        // Check if it's a post activation request
        if (request.getPathInfo().endsWith("/activateCollaborationPost.js")) {
            this.activateCollaborationPost(request, response);
            return;
        }

        // Check if it's a post creation request
        if (request.getPathInfo().endsWith("/deleteCollaborationThread.js")) {
            this.deleteCollaborationThread(request, response);
            return;
        }

        // Check if it's a post creation request
        if (request.getPathInfo().endsWith("/deleteCollaborationPost.js")) {
            this.deleteCollaborationPost(request, response);
            return;
        }

        // Check if it's a checkSession request and provide session status if so
        if (requestedVideo.endsWith("/checkSession.js")) {
            this.provideSessionStatus(request, response);
            return;
        }

        // Decode the file name from the URL and check if file actually exists
        // in
        // file system, send 404 if not
        File file = new File(videoPath, URLDecoder.decode(requestedVideo, "UTF-8"));
        if (!file.exists()) {
            this.sendError(response, HttpServletResponse.SC_NOT_FOUND, "File not found");
            return;
        }

        // Create log entry for file request
        this.logFileRequest(requestedVideo);

        // Check if configuration is requested and do needed preparing and
        // stop standard file preparation
        if (file.getName().equals("export.js")) {
            this.provideConfigFile(request, response, file);
            return;
        }

        // Prepare some variables. The ETag is an unique identifier of the file.
        String fileName = file.getName();
        long length = file.length();
        long lastModified = file.lastModified();
        String eTag = fileName + "_" + length + "_" + lastModified;
        long expires = System.currentTimeMillis() + DEFAULT_EXPIRE_TIME;

        // Validate request headers for caching
        // ---------------------------------------------------

        // If-None-Match header should contain "*" or ETag. If so, then return
        // 304.
        String ifNoneMatch = request.getHeader("If-None-Match");
        if (ifNoneMatch != null && matches(ifNoneMatch, eTag)) {
            response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
            response.setHeader("ETag", eTag); // Required in 304.
            response.setDateHeader("Expires", expires); // Postpone cache with 1
            // week.
            return;
        }

        // If-Modified-Since header should be greater than LastModified. If so,
        // then return 304.
        // This header is ignored if any If-None-Match header is specified.
        long ifModifiedSince = request.getDateHeader("If-Modified-Since");
        if (ifNoneMatch == null && ifModifiedSince != -1 && ifModifiedSince + 1000 > lastModified) {
            response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
            response.setHeader("ETag", eTag); // Required in 304.
            response.setDateHeader("Expires", expires); // Postpone cache with 1
            // week.
            return;
        }

        // Validate request headers for resume
        // ----------------------------------------------------

        // If-Match header should contain "*" or ETag. If not, then return 412.
        String ifMatch = request.getHeader("If-Match");
        if (ifMatch != null && !matches(ifMatch, eTag)) {
            response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
            return;
        }

        // If-Unmodified-Since header should be greater than LastModified. If
        // not, then return 412.
        long ifUnmodifiedSince = request.getDateHeader("If-Unmodified-Since");
        if (ifUnmodifiedSince != -1 && ifUnmodifiedSince + 1000 <= lastModified) {
            response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
            return;
        }

        // Validate and process range
        // -------------------------------------------------------------

        // Prepare some variables. The full Range represents the complete file.
        Range full = new Range(0, length - 1, length);
        List<Range> ranges = new ArrayList<Range>();

        // Validate and process Range and If-Range headers.
        String range = request.getHeader("Range");
        if (range != null) {

            // Range header should match format "bytes=n-n,n-n,n-n...". If not,
            // then return 416.
            if (!range.matches("^bytes=\\d*-\\d*(,\\d*-\\d*)*$")) {
                response.setHeader("Content-Range", "bytes */" + length); // Required
                // in
                // 416.
                response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
                return;
            }

            // If-Range header should either match ETag or be greater then
            // LastModified. If not,
            // then return full file.
            String ifRange = request.getHeader("If-Range");
            if (ifRange != null && !ifRange.equals(eTag)) {
                try {
                    long ifRangeTime = request.getDateHeader("If-Range"); // Throws
                    // IAE
                    // if
                    // invalid.
                    if (ifRangeTime != -1 && ifRangeTime + 1000 < lastModified) {
                        ranges.add(full);
                    }
                } catch (IllegalArgumentException ignore) {
                    ranges.add(full);
                }
            }

            // If any valid If-Range header, then process each part of byte
            // range.
            if (ranges.isEmpty()) {
                for (String part : range.substring(6).split(",")) {
                    // Assuming a file with length of 100, the following
                    // examples returns bytes at:
                    // 50-80 (50 to 80), 40- (40 to length=100), -20
                    // (length-20=80 to length=100).
                    long start = sublong(part, 0, part.indexOf("-"));
                    long end = sublong(part, part.indexOf("-") + 1, part.length());

                    if (start == -1) {
                        start = length - end;
                        end = length - 1;
                    } else if (end == -1 || end > length - 1) {
                        end = length - 1;
                    }

                    // Check if Range is syntactically valid. If not, then
                    // return 416.
                    if (start > end) {
                        response.setHeader("Content-Range", "bytes */" + length); // Required
                        // in
                        // 416.
                        response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
                        return;
                    }

                    // Add range.
                    ranges.add(new Range(start, end, length));
                }
            }
        }

        // Prepare and initialize response
        // --------------------------------------------------------

        // Get content type by file name and set default GZIP support and
        // content disposition.
        String contentType = getServletContext().getMimeType(fileName);
        boolean acceptsGzip = false;
        String disposition = "inline";

        // If content type is unknown, then set the default value.
        // For all content types, see:
        // http://www.w3schools.com/media/media_mimeref.asp
        // To add new content types, add new mime-mapping entry in web.xml.
        if (contentType == null) {
            contentType = "application/octet-stream";
        }

        // If content type is text, then determine whether GZIP content encoding
        // is supported by
        // the browser and expand content type with the one and right character
        // encoding.
        if (contentType.startsWith("text")) {
            String acceptEncoding = request.getHeader("Accept-Encoding");
            acceptsGzip = acceptEncoding != null && accepts(acceptEncoding, "gzip");
            contentType += ";charset=UTF-8";
        }

        // Else, expect for images, determine content disposition. If content
        // type is supported by
        // the browser, then set to inline, else attachment which will pop a
        // 'save as' dialogue.
        else if (!contentType.startsWith("image")) {
            String accept = request.getHeader("Accept");
            disposition = accept != null && accepts(accept, contentType) ? "inline" : "attachment";
        }

        // Initialize response.
        response.reset();
        response.setBufferSize(DEFAULT_BUFFER_SIZE);
        response.setHeader("Content-Disposition", disposition + ";filename=\"" + fileName + "\"");
        response.setHeader("Accept-Ranges", "bytes");
        response.setHeader("ETag", eTag);
        response.setDateHeader("Last-Modified", lastModified);
        response.setDateHeader("Expires", expires);

        // Send requested file (part(s)) to client
        // ------------------------------------------------

        // Prepare streams.
        RandomAccessFile input = null;
        OutputStream output = null;

        try {
            // Open streams.
            input = new RandomAccessFile(file, "r");
            output = response.getOutputStream();

            if (ranges.isEmpty() || ranges.get(0) == full) {

                // Return full file.
                Range r = full;
                response.setContentType(contentType);
                response.setHeader("Content-Range", "bytes " + r.start + "-" + r.end + "/" + r.total);

                if (requestType.equals("GET")) {
                    if (acceptsGzip) {
                        // The browser accepts GZIP, so GZIP the content.
                        response.setHeader("Content-Encoding", "gzip");
                        output = new GZIPOutputStream(output, DEFAULT_BUFFER_SIZE);
                    } else {
                        // Content length is not directly predictable in case of
                        // GZIP.
                        // So only add it if there is no means of GZIP, else
                        // browser will hang.
                        response.setHeader("Content-Length", String.valueOf(r.length));
                    }

                    // Copy full range.
                    copy(input, output, r.start, r.length);
                }

            } else if (ranges.size() == 1) {

                // Return single part of file.
                Range r = ranges.get(0);
                response.setContentType(contentType);
                response.setHeader("Content-Range", "bytes " + r.start + "-" + r.end + "/" + r.total);
                response.setHeader("Content-Length", String.valueOf(r.length));
                response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); // 206.

                if (requestType.equals("GET")) {
                    // Copy single part range.
                    copy(input, output, r.start, r.length);
                }

            } else {

                // Return multiple parts of file.
                response.setContentType("multipart/byteranges; boundary=" + MULTIPART_BOUNDARY);
                response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); // 206.

                if (requestType.equals("GET")) {
                    // Cast back to ServletOutputStream to get the easy println
                    // methods.
                    ServletOutputStream sos = (ServletOutputStream) output;

                    // Copy multi part range.
                    for (Range r : ranges) {
                        // Add multipart boundary and header fields for every
                        // range.
                        sos.println();
                        sos.println("--" + MULTIPART_BOUNDARY);
                        sos.println("Content-Type: " + contentType);
                        sos.println("Content-Range: bytes " + r.start + "-" + r.end + "/" + r.total);

                        // Copy single part range of multi part range.
                        copy(input, output, r.start, r.length);
                    }

                    // End with multipart boundary.
                    sos.println();
                    sos.println("--" + MULTIPART_BOUNDARY + "--");
                }
            }
        } finally {
            // Gently close streams.
            close(output);
            close(input);
        }
    }

    /**
     * Write prepared configuration file to response.
     * 
     * @param request
     *            servlet.
     * @param response
     *            servlet.
     * @param file
     *            containing the configuration.
     * @throws IOException
     */
    private void provideConfigFile(HttpServletRequest request, HttpServletResponse response, File file)
            throws IOException {
        // Check if it's an AJAX request or a normal JavaScript file request
        // and provide therefore prepared configuration.
        FileInputStream in = new FileInputStream(file);
        if (this.isAJAXRequest) {
            this.writeJSON(response, "{\"accessRestriction\":{\"accessToken\":\"" + this.session.getSessionToken()
                    + "\",\"expiresAt\":\"" + this.session.getExpireDate().toString() + "\",\"passed\":true},"
                    + IOUtils.toString(in, Charset.defaultCharset()).split("\\.src,")[1].replaceFirst("\\);$", ""));
        } else {
            String[] config = IOUtils.toString(in, Charset.defaultCharset()).split("\\.push\\(\\{");
            this.isAJAXRequest = true;
            this.writeJSON(response,
                    config[0] + ".push({\"accessRestriction\":{\"accessToken\":\"" + this.session.getSessionToken()
                            + "\",\"expiresAt\":\"" + this.session.getExpireDate().toString()
                            + "\",\"passed\":true}," + config[1]);
        }
    }

    /**
     * Write current file as loadFile entry to log
     * 
     * @param requestedVideo
     *            file to log.
     */
    private void logFileRequest(String requestedVideo) {
        ArrayList<SivaPlayerLogEntry> logEntries = new ArrayList<SivaPlayerLogEntry>();
        SivaPlayerLogEntry logEntry = new SivaPlayerLogEntry();
        logEntry.setSessionId(this.session.getId());
        logEntry.setType("loadFile");
        logEntry.setElement(requestedVideo);
        logEntry.setTime(new Date());
        logEntries.add(logEntry);
        try {
            this.persistenceProvider.getApiStore().createSivaPlayerLogEntries(logEntries);
        } catch (InconsistencyException ignore) {
            // ignore
        }
    }

    /**
     * Perform login and return user secret to response.
     * 
     * @param request
     *            servlet.
     * @param response
     *            servlet.
     * @param requestType
     *            of this request.
     * @throws IOException
     */
    private void provideUserSecret(HttpServletRequest request, HttpServletResponse response, String requestType)
            throws IOException {

        // Check if it is a GET request and provide information about
        // the access
        // restriction form if so
        if (!requestType.equals("POST")) {
            this.sendError(response, HttpServletResponse.SC_UNAUTHORIZED, "loginRequiredError", "login");
            return;
        }

        IVideoStore videoStore = this.persistenceProvider.getVideoStore();
        IUserStore userStore = this.persistenceProvider.getUserStore();

        // Get video from database and show 404 if video does not exist
        String videoDirectory = request.getPathInfo().split("/")[1];
        Video video = videoStore.findByDirectory(videoDirectory);
        if (video == null) {
            this.sendError(response, HttpServletResponse.SC_NOT_FOUND, "videoNotExistingError");
            return;
        }

        // Check if credentials where transmitted via HTTP POST and show
        // 400 if not
        String userName = request.getParameter("username");
        String userPassword = request.getParameter("password");
        if (userName == null || userPassword == null) {
            this.sendError(response, HttpServletResponse.SC_UNAUTHORIZED, "userNotFoundError");
            return;
        }

        // Check if a user with these credentials exists and if it is
        // not banned, show 401 if so
        User user = userStore.findByEmail(userName);
        String passwordHash = SecurityUtils.hash(userPassword);
        if (user == null || !passwordHash.equals(user.getPasswordHash())) {
            this.sendError(response, HttpServletResponse.SC_UNAUTHORIZED, "userNotFoundError");
            return;
        } else if (user.isBanned()) {
            this.sendError(response, HttpServletResponse.SC_UNAUTHORIZED, "userBannedError");
            return;
        }

        // Check if video is active or user is administrator or owner of the
        // video
        Date currentDate = new Date();
        if ((user == null || (!user.getUserType().equals(EUserType.Administrator)
                && !userStore.isUserOwnerOfVideo(user.getId(), video.getId())))
                && (video.getStart() == null || video.getStart().compareTo(currentDate) > 0
                        || (video.getStop() != null && video.getStop().compareTo(currentDate) < 0))) {
            this.sendError(response, HttpServletResponse.SC_NOT_FOUND, "videoNotActiveError");
            return;
        }

        // Check if user is a group attendant if the video is group
        // restricted
        if (video.getParticipationRestriction() == EParticipationRestriction.GroupAttendants) {
            if (!userStore.isUserAttendantOfGroup(user.getId(), video.getId())
                    && !userStore.isUserOwnerOfVideo(user.getId(), video.getId())
                    && user.getUserType() != EUserType.Administrator) {
                this.sendError(response, HttpServletResponse.SC_UNAUTHORIZED, "noAttendantError");
                return;
            }
        }

        // Send always positive session status as earlier session checks
        // allowed to continue processing.
        Map<String, String> jsonMap = new HashMap<String, String>();
        jsonMap.put("userEmail", user.getEmail());
        jsonMap.put("userSecret", user.getSecretKey());
        this.isAJAXRequest = true;
        response.setStatus(HttpServletResponse.SC_OK);
        this.writeJSON(response, (new JSONObject(jsonMap)).toString());
    }

    /**
     * Write collaboration information for the given video and scene as JSON.
     * 
     * @param request
     *            servlet.
     * @param response
     *            servlet.
     * @throws IOException
     */
    private void provideCollaboration(HttpServletRequest request, HttpServletResponse response) throws IOException {

        IVideoStore videoStore = this.persistenceProvider.getVideoStore();
        IApiStore apiStore = this.persistenceProvider.getApiStore();
        IUserStore userStore = this.persistenceProvider.getUserStore();

        // Get video from database and show 404 if video does not exist
        String videoDirectory = request.getPathInfo().split("/")[1];
        Video video = videoStore.findByDirectory(videoDirectory);
        if (video == null) {
            this.sendError(response, HttpServletResponse.SC_NOT_FOUND, "videoNotExistingError");
            return;
        }

        // Get scene and show 401 if no scene is given
        String scene = request.getParameter("scene");
        if (scene == null || scene.equals("")) {
            this.sendError(response, HttpServletResponse.SC_BAD_REQUEST, "malformedDataError");
            return;
        }

        // Get threads, posts and media from database and write it to JSON
        List<CollaborationThread> threads = apiStore.listCollaborationThreads(video.getId(), scene);
        JSONArray json = new JSONArray();
        CollaborationThreadLoop: for (CollaborationThread thread : threads) {
            JSONObject jsonThread = new JSONObject();
            try {
                jsonThread.put("threadId", thread.getId());
                jsonThread.put("title", thread.getTitle());
                JSONArray jsonPosts = new JSONArray();
                int i = 0;
                List<CollaborationPost> posts = apiStore.listCollaborationPosts(thread.getId());
                for (CollaborationPost post : posts) {
                    boolean isOwner = userStore.isUserOwnerOfVideo(this.currentUser.getId(), video.getId());

                    // Skip thread if visibility settings are not met
                    if (i == 0 && !(thread.getVisibility() == ECollaborationThreadVisibility.All
                            || post.getUserId() == this.currentUser.getId()
                            || (thread.getVisibility() == ECollaborationThreadVisibility.Administrator
                                    && (isOwner || this.currentUser.getUserType() == EUserType.Administrator)))) {
                        continue CollaborationThreadLoop;
                    }

                    // Skip post if not active and not post owner, video owner
                    // or administrator
                    if (!post.isActive() && !isOwner && this.currentUser.getUserType() != EUserType.Administrator
                            && post.getUserId() != this.currentUser.getId()) {

                        // Skip whole thread if first post is not active yet
                        if (i == 0) {
                            continue CollaborationThreadLoop;
                        }
                        continue;
                    }

                    JSONObject jsonPost = new JSONObject();
                    jsonPost.put("id", post.getId());
                    User user = this.persistenceProvider.getUserStore().findById(post.getUserId());
                    if (user != null) {
                        jsonPost.put("user", ((user.getTitle() != null) ? user.getTitle() + " " : "")
                                + user.getFirstName() + " " + user.getLastName());
                    } else {
                        jsonPost.put("user", "Anonymous");
                    }
                    jsonPost.put("date", post.getDate().getTime());
                    jsonPost.put("post", post.getPost());
                    jsonPost.put("active", (post.isActive()
                            || (this.currentUser.getUserType() != EUserType.Administrator && !isOwner)));
                    jsonPost.put("manageable", ((user != null && this.currentUser.getId() == user.getId())
                            || this.currentUser.getUserType() == EUserType.Administrator || isOwner));
                    JSONArray jsonMedia = new JSONArray();
                    List<CollaborationMedia> media = apiStore.listCollaborationMedia(post.getId());
                    for (CollaborationMedia mediaFile : media) {
                        JSONObject jsonMediaFile = new JSONObject();
                        jsonMediaFile.put("id", mediaFile.getId());
                        jsonMediaFile.put("filename",
                                "collaboration/" + mediaFile.getId() + "-" + mediaFile.getFilename());
                        jsonMedia.put(jsonMediaFile);
                    }
                    jsonPost.put("media", jsonMedia);
                    jsonPosts.put(jsonPost);
                    i++;
                }
                if (i == 0) {
                    continue;
                }
                jsonThread.put("posts", jsonPosts);
                jsonThread.put("start", thread.getDurationFrom());
                jsonThread.put("end", thread.getDurationTo());
            } catch (JSONException e) {
                this.sendError(response, HttpServletResponse.SC_BAD_REQUEST, "malformedDataError");
            }
            json.put(jsonThread);
        }

        this.isAJAXRequest = true;
        response.setStatus(HttpServletResponse.SC_OK);
        this.writeJSON(response, json.toString());
    }

    /**
     * Create a new {@link CollaborationThread} based on the user input.
     * 
     * @param request
     *            servlet.
     * @param response
     *            servlet.
     * @throws IOException
     * @throws ServletException
     * @throws IllegalStateException
     */
    private void createCollaborationThread(HttpServletRequest request, HttpServletResponse response)
            throws IOException, IllegalStateException, ServletException {

        IVideoStore videoStore = this.persistenceProvider.getVideoStore();
        IApiStore apiStore = this.persistenceProvider.getApiStore();

        // Get video from database and show 404 if video does not exist
        String videoDirectory = request.getPathInfo().split("/")[1];
        Video video = videoStore.findByDirectory(videoDirectory);
        if (video == null) {
            this.sendError(response, HttpServletResponse.SC_NOT_FOUND, "videoNotExistingError");
            return;
        }

        // Get scene and show 401 if no scene is given
        String scene = request.getParameter("scene");
        if (scene == null || scene.equals("")) {
            this.sendError(response, HttpServletResponse.SC_BAD_REQUEST, "malformedDataError");
            return;
        }

        // Create and save thread based on input
        CollaborationThread thread = new CollaborationThread(null);
        thread.setVideoId(video.getId());
        thread.setScene(request.getParameter("scene"));
        thread.setTitle(new String(request.getParameter("title").getBytes("iso-8859-1"), "UTF-8"));
        String[] durationFrom = request.getParameter("durationFrom").split(":");
        thread.setDurationFrom((Integer.parseInt(durationFrom[0]) * 60 + Integer.parseInt(durationFrom[1])));
        String[] durationTo = request.getParameter("durationTo").split(":");
        thread.setDurationTo((Integer.parseInt(durationTo[0]) * 60 + Integer.parseInt(durationTo[01])));
        thread.setVisibility(ECollaborationThreadVisibility.All);
        for (ECollaborationThreadVisibility visibility : ECollaborationThreadVisibility.values()) {
            if (request.getParameter("visibility").equals(visibility.toString())) {
                thread.setVisibility(visibility);
            }
        }

        try {
            thread = apiStore.createCollaborationThread(thread);
        } catch (InconsistencyException e) {
            this.sendError(response, HttpServletResponse.SC_BAD_REQUEST, "malformedDataError");
            return;
        }

        // Create post and media and delete thread if not successful
        if (!this.savePost(thread, video, request, response)) {
            try {
                apiStore.deleteCollaborationThread(thread.getId());
            } catch (InconsistencyException e) {
            }
        }
    }

    /**
     * Create a new {@link CollaborationThread} based on the user input.
     * 
     * @param request
     *            servlet.
     * @param response
     *            servlet.
     * @throws IOException
     * @throws ServletException
     * @throws IllegalStateException
     */
    private void createCollaborationPost(HttpServletRequest request, HttpServletResponse response)
            throws IOException, IllegalStateException, ServletException {

        IVideoStore videoStore = this.persistenceProvider.getVideoStore();
        IApiStore apiStore = this.persistenceProvider.getApiStore();

        // Get video from database and show 404 if video does not exist
        String videoDirectory = request.getPathInfo().split("/")[1];
        Video video = videoStore.findByDirectory(videoDirectory);
        if (video == null) {
            this.sendError(response, HttpServletResponse.SC_NOT_FOUND, "videoNotExistingError");
            return;
        }

        // Get scene and show 401 if no scene is given
        String scene = request.getParameter("scene");
        if (scene == null || scene.equals("")) {
            this.sendError(response, HttpServletResponse.SC_BAD_REQUEST, "malformedDataError");
            return;
        }

        // Get thread from database
        if (request.getParameter("thread") == null) {
            this.sendError(response, HttpServletResponse.SC_BAD_REQUEST, "malformedDataError");
            return;
        }
        CollaborationThread thread = apiStore
                .findCollaborationThreadById(Integer.parseInt(request.getParameter("thread")));
        if (thread == null) {
            this.sendError(response, HttpServletResponse.SC_BAD_REQUEST, "threadNotExisting");
            return;
        }

        // Create post and media
        this.savePost(thread, video, request, response);
    }

    /**
     * Write current session status to response.
     * 
     * @param request
     *            servlet.
     * @param response
     *            servlet.
     * @throws IOException
     * @throws ServletException
     * @throws IllegalStateException
     */
    private boolean savePost(CollaborationThread thread, Video video, HttpServletRequest request,
            HttpServletResponse response) throws IOException, IllegalStateException, ServletException {
        IApiStore apiStore = this.persistenceProvider.getApiStore();
        IUserStore userStore = this.persistenceProvider.getUserStore();

        // Create and save post based on input
        CollaborationPost post = new CollaborationPost(null);
        post.setThreadId(thread.getId());
        post.setUserId(this.currentUser.getId());
        post.setPost(new String(request.getParameter("post").getBytes("iso-8859-1"), "UTF-8"));
        post.setActive(userStore.isUserOwnerOfVideo(this.currentUser.getId(), video.getId())
                || this.currentUser.getUserType() == EUserType.Administrator
                || thread.getVisibility() == ECollaborationThreadVisibility.Me);

        try {
            post = apiStore.createCollaborationPost(post);
        } catch (InconsistencyException e) {
            this.sendError(response, HttpServletResponse.SC_BAD_REQUEST, "malformedDataError");
            return false;
        }

        // Create and save media based on input
        for (Part part : request.getParts()) {
            if (part.getName().equals("media[]")) {
                String filename = this.getFileName(part).replaceAll("[^a-zA-Z0-9.-]", "");
                String[] extension = filename.split("\\.");
                if (filename.equals("") || extension.length <= 1
                        || !ALLOWED_FILE_TYPES.contains(extension[extension.length - 1])) {
                    continue;
                }
                CollaborationMedia media = new CollaborationMedia(null);
                media.setPostId(post.getId());
                media.setFilename(filename);
                try {
                    media = apiStore.createCollaborationMedia(media);
                } catch (InconsistencyException e) {
                    try {
                        apiStore.deleteCollaborationPost(post.getId());
                    } catch (InconsistencyException e1) {
                    }
                    this.sendError(response, HttpServletResponse.SC_BAD_REQUEST, "malformedDataError");
                    return false;
                }
                File directory = new File(this.videoPath + "/" + video.getDirectory() + "/collaboration");
                if (!directory.exists()) {
                    directory.mkdir();
                }
                part.write(this.videoPath + "/" + video.getDirectory() + "/collaboration/" + media.getId() + "-"
                        + media.getFilename());
            }
        }

        // Write positive result and information about publishing state
        Map<String, String> jsonMap = new HashMap<String, String>();
        jsonMap.put("saved", "true");
        if (userStore.isUserOwnerOfVideo(this.currentUser.getId(), video.getId())
                || this.currentUser.getUserType() == EUserType.Administrator
                || thread.getVisibility() == ECollaborationThreadVisibility.Me
                || (thread.getVisibility() == ECollaborationThreadVisibility.Administrator
                        && this.currentUser.getUserType() == EUserType.Administrator)) {
            jsonMap.put("message", "collaborationPublished");
        } else {
            jsonMap.put("message", "collaborationActivationNeeded");
            int[] videoIds = { video.getId() };

            // Initialize JSF to get property files
            this.getFacesContext(request, response);

            Map<Integer, List<User>> owners = userStore.getUsersOwningGroupsOfVideos(videoIds);
            for (User owner : owners.get(video.getId())) {
                try {
                    this.mailService.sendMail(owner.getEmail(),
                            String.format(this.getCommonMessage("send_mail_new_collaboration_subject"),
                                    video.getTitle()),
                            String.format(this.getCommonMessage("send_mail_new_collaboration"), thread.getTitle(),
                                    post.getPost(),
                                    CommonUtils.buildContextPath(
                                            "/sivaPlayerVideos/" + video.getDirectory() + "/watch.html", null)
                                            + "#0=" + thread.getScene() + "%2C" + thread.getDurationFrom(),
                                    this.brandingConfiguration.getBrandingText("project_name")));
                } catch (URISyntaxException e) {
                } catch (IllegalArgumentException e) {
                }
            }
        }
        this.isAJAXRequest = true;
        response.setStatus(HttpServletResponse.SC_OK);
        this.writeJSON(response, (new JSONObject(jsonMap)).toString());
        return true;
    }

    /**
     * Activate the specified {@link CollaborationPost}.
     * 
     * @param request
     *            servlet.
     * @param response
     *            servlet.
     * @throws IOException
     * @throws ServletException
     * @throws IllegalStateException
     */
    private void activateCollaborationPost(HttpServletRequest request, HttpServletResponse response)
            throws IOException, IllegalStateException, ServletException {

        IVideoStore videoStore = this.persistenceProvider.getVideoStore();
        IApiStore apiStore = this.persistenceProvider.getApiStore();
        IUserStore userStore = this.persistenceProvider.getUserStore();

        // Get video from database and show 404 if video does not exist
        String videoDirectory = request.getPathInfo().split("/")[1];
        Video video = videoStore.findByDirectory(videoDirectory);
        if (video == null) {
            this.sendError(response, HttpServletResponse.SC_NOT_FOUND, "videoNotExistingError");
            return;
        }

        // Get postId and show 401 if no scene is given
        if (request.getParameter("postId") == null || request.getParameter("postId").equals("")) {
            this.sendError(response, HttpServletResponse.SC_BAD_REQUEST, "malformedDataError");
            return;
        }

        int postId = Integer.parseInt(request.getParameter("postId"));
        CollaborationPost post = apiStore.findCollaborationPostById(postId);

        if (post != null) {
            post.setActive(true);
            if (this.currentUser.getUserType() == EUserType.Administrator
                    || userStore.isUserOwnerOfVideo(this.currentUser.getId(), video.getId())) {
                try {
                    apiStore.saveCollaborationPost(post);
                } catch (InconsistencyException e) {
                    this.sendError(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "databaseError");
                    return;
                }
            }
        }

        Map<String, String> jsonMap = new HashMap<String, String>();
        jsonMap.put("saved", "true");
        this.isAJAXRequest = true;
        response.setStatus(HttpServletResponse.SC_OK);
        this.writeJSON(response, (new JSONObject(jsonMap)).toString());
    }

    /**
     * Delete the specified {@link CollaborationThread}.
     * 
     * @param request
     *            servlet.
     * @param response
     *            servlet.
     * @throws IOException
     * @throws ServletException
     * @throws IllegalStateException
     */
    private void deleteCollaborationThread(HttpServletRequest request, HttpServletResponse response)
            throws IOException, IllegalStateException, ServletException {

        IVideoStore videoStore = this.persistenceProvider.getVideoStore();
        IApiStore apiStore = this.persistenceProvider.getApiStore();
        IUserStore userStore = this.persistenceProvider.getUserStore();

        // Get video from database and show 404 if video does not exist
        String videoDirectory = request.getPathInfo().split("/")[1];
        Video video = videoStore.findByDirectory(videoDirectory);
        if (video == null) {
            this.sendError(response, HttpServletResponse.SC_NOT_FOUND, "videoNotExistingError");
            return;
        }

        // Get threadId and show 401 if no scene is given
        if (request.getParameter("threadId") == null || request.getParameter("threadId").equals("")) {
            this.sendError(response, HttpServletResponse.SC_BAD_REQUEST, "malformedDataError");
            return;
        }

        int threadId = Integer.parseInt(request.getParameter("threadId"));
        List<CollaborationPost> posts = apiStore.listCollaborationPosts(threadId);

        if (posts.size() > 0) {
            Integer threadPosterId = posts.get(0).getUserId();
            if ((threadPosterId != null && this.currentUser.getId() == threadPosterId)
                    || this.currentUser.getUserType() == EUserType.Administrator
                    || userStore.isUserOwnerOfVideo(this.currentUser.getId(), video.getId())) {
                try {
                    apiStore.deleteCollaborationThread(threadId);
                } catch (InconsistencyException e) {
                    this.sendError(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "databaseError");
                    return;
                }
            }
        }

        Map<String, String> jsonMap = new HashMap<String, String>();
        jsonMap.put("saved", "true");
        this.isAJAXRequest = true;
        response.setStatus(HttpServletResponse.SC_OK);
        this.writeJSON(response, (new JSONObject(jsonMap)).toString());
    }

    /**
     * Delete the specified {@link CollaborationPost}.
     * 
     * @param request
     *            servlet.
     * @param response
     *            servlet.
     * @throws IOException
     * @throws ServletException
     * @throws IllegalStateException
     */
    private void deleteCollaborationPost(HttpServletRequest request, HttpServletResponse response)
            throws IOException, IllegalStateException, ServletException {

        IVideoStore videoStore = this.persistenceProvider.getVideoStore();
        IApiStore apiStore = this.persistenceProvider.getApiStore();
        IUserStore userStore = this.persistenceProvider.getUserStore();

        // Get video from database and show 404 if video does not exist
        String videoDirectory = request.getPathInfo().split("/")[1];
        Video video = videoStore.findByDirectory(videoDirectory);
        if (video == null) {
            this.sendError(response, HttpServletResponse.SC_NOT_FOUND, "videoNotExistingError");
            return;
        }

        // Get postId and show 401 if no scene is given
        if (request.getParameter("postId") == null || request.getParameter("postId").equals("")) {
            this.sendError(response, HttpServletResponse.SC_BAD_REQUEST, "malformedDataError");
            return;
        }

        int postId = Integer.parseInt(request.getParameter("postId"));
        CollaborationPost post = apiStore.findCollaborationPostById(postId);

        if (post != null) {
            Integer posterId = post.getUserId();
            if ((posterId != null && this.currentUser.getId() == posterId)
                    || this.currentUser.getUserType() == EUserType.Administrator
                    || userStore.isUserOwnerOfVideo(this.currentUser.getId(), video.getId())) {
                try {
                    apiStore.deleteCollaborationPost(postId);
                } catch (InconsistencyException e) {
                    this.sendError(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "databaseError");
                    return;
                }
            }
        }

        Map<String, String> jsonMap = new HashMap<String, String>();
        jsonMap.put("saved", "true");
        this.isAJAXRequest = true;
        response.setStatus(HttpServletResponse.SC_OK);
        this.writeJSON(response, (new JSONObject(jsonMap)).toString());
    }

    /**
     * Extracts the filename from the {@link Part} header.
     * 
     * @param part
     *            to parse
     * @return the filename
     */
    private String getFileName(Part part) {
        String contentDisp = part.getHeader("content-disposition");
        String[] tokens = contentDisp.split(";");
        for (String token : tokens) {
            if (token.trim().startsWith("filename")) {
                return token.substring(token.indexOf("=") + 2, token.length() - 1);
            }
        }
        return "";
    }

    /**
     * Write current session status to response.
     * 
     * @param request
     *            servlet.
     * @param response
     *            servlet.
     * @throws IOException
     */
    private void provideSessionStatus(HttpServletRequest request, HttpServletResponse response) throws IOException {

        // Send always positive session status as earlier session checks
        // allowed to continue processing.
        Map<String, String> jsonMap = new HashMap<String, String>();
        jsonMap.put("isStillActive", "true");
        this.isAJAXRequest = true;
        response.setStatus(HttpServletResponse.SC_OK);
        this.writeJSON(response, (new JSONObject(jsonMap)).toString());
    }

    /**
     * Check if the video exists, the user is allowed to access it and create a
     * new session if provided authentication information is sufficient.
     * 
     * @param request
     *            servlet.
     * @param response
     *            servlet.
     * @param requestType
     *            of this request.
     * @return true if all restrictions are passed, false if checks failed.
     * @throws IOException
     */
    private boolean handleAccess(HttpServletRequest request, HttpServletResponse response, String requestType)
            throws IOException {

        IVideoStore videoStore = this.persistenceProvider.getVideoStore();
        IUserStore userStore = this.persistenceProvider.getUserStore();

        // Get video from database and show 404 if video does not exist
        String videoDirectory = request.getPathInfo().split("/")[1];
        Video video = videoStore.findByDirectory(videoDirectory);
        if (video == null) {
            this.sendError(response, HttpServletResponse.SC_NOT_FOUND, "videoNotExistingError");
            return false;
        }

        // Check if there already is a session and whether the associated user
        // is an administrator
        // or owner of the video
        IApiStore apiStore = this.persistenceProvider.getApiStore();
        this.session = ((request.getParameter("token") == null
                || request.getParameter("token").split("-").length != 2)
                        ? null
                        : apiStore.findSivaPlayerSessionByToken(
                                Integer.parseInt(request.getParameter("token").split("-")[0]),
                                request.getParameter("token").split("-")[1], false));
        if (this.session != null && this.session.getId() != null) {
            this.currentUser = userStore.findById(this.session.getUserId());
        }

        // Check if video is active or user is administrator or owner of the
        // video
        Date currentDate = new Date();
        if ((this.currentUser == null || (!this.currentUser.getUserType().equals(EUserType.Administrator)
                && !userStore.isUserOwnerOfVideo(this.currentUser.getId(), video.getId())))
                && (video.getStart() == null || video.getStart().compareTo(currentDate) > 0
                        || (video.getStop() != null && video.getStop().compareTo(currentDate) < 0))) {
            this.sendError(response, HttpServletResponse.SC_NOT_FOUND, "videoNotActiveError");
            return false;
        }

        // Check if there has been a SivaPlayer session token submitted and if
        // it hasn't been expired, generate a new token otherwise
        if (this.session == null || this.session.getId() == null) {

            // Check if user has sufficient rights for video and show 401 if not
            if (video.getParticipationRestriction() == EParticipationRestriction.GroupAttendants
                    || video.getParticipationRestriction() == EParticipationRestriction.Registered) {

                // Check if notice for session expiration is needed
                String sessionNotice = "loginRequiredError";
                if (request.getParameter("token") != null) {
                    sessionNotice = "sessionExpiredLoginError";
                }

                // Check if it is a GET request and provide information about
                // the access
                // restriction form if so
                if (!requestType.equals("POST")) {
                    this.sendError(response, HttpServletResponse.SC_UNAUTHORIZED, sessionNotice, "login");
                    return false;
                }

                // Check if credentials where transmitted via HTTP POST and show
                // 400 if not
                String userName = request.getParameter("username");
                String userPassword = request.getParameter("password");
                String userSecret = request.getParameter("secret");
                if (userName == null || (userPassword == null && userSecret == null)) {
                    this.sendError(response, HttpServletResponse.SC_UNAUTHORIZED, "userNotFoundError");
                    return false;
                }

                // Check if a user with these credentials exists
                this.currentUser = userStore.findByEmail(userName);
                String passwordHash = ((userPassword != null) ? SecurityUtils.hash(userPassword) : "");
                if (this.currentUser == null || (!passwordHash.equals(this.currentUser.getPasswordHash())
                        && !userSecret.equals(this.currentUser.getSecretKey()))) {
                    this.sendError(response, HttpServletResponse.SC_UNAUTHORIZED, "userNotFoundError");
                    return false;
                }

                // Check if user is banned and show 401 if so
                if (this.currentUser.isBanned()) {
                    this.sendError(response, HttpServletResponse.SC_UNAUTHORIZED, "userBannedError");
                    return false;
                }

                // Check if user is a group attendant if the video is group
                // restricted
                if (video.getParticipationRestriction() == EParticipationRestriction.GroupAttendants) {
                    if (!userStore.isUserAttendantOfGroup(this.currentUser.getId(), video.getId())
                            && !userStore.isUserOwnerOfVideo(this.currentUser.getId(), video.getId())
                            && this.currentUser.getUserType() != EUserType.Administrator) {
                        this.sendError(response, HttpServletResponse.SC_UNAUTHORIZED, "noAttendantError");
                        return false;
                    }
                }
            } else if (video.getParticipationRestriction() == EParticipationRestriction.Password) {

                // Check if notice for session expiration is needed
                String sessionNotice = "passwordRequiredError";
                if (request.getParameter("token") != null) {
                    sessionNotice = "sessionExpiredPasswordError";
                }

                // Check if it is a GET request and provide information about
                // the access
                // restriction form if so
                if (!requestType.equals("POST")) {
                    this.sendError(response, HttpServletResponse.SC_UNAUTHORIZED, sessionNotice, "password");
                    return false;
                }

                // Check if encrypted video password is equal to the password in
                // the URL
                String password = request.getParameter("password");
                if (password == null || !video.getPassword().equals(password)) {
                    this.sendError(response, HttpServletResponse.SC_UNAUTHORIZED, "wrongPasswordError");
                    return false;
                }
            } else if (video.getParticipationRestriction() == EParticipationRestriction.Token) {

                // Check if notice for session expiration is needed
                String sessionNotice = "tokenRequiredError";
                if (request.getParameter("token") != null) {
                    sessionNotice = "sessionExpiredTokenError";
                }

                // Check if it is a GET request and provide information about
                // the access
                // restriction form if so
                if (!requestType.equals("POST")) {
                    this.sendError(response, HttpServletResponse.SC_UNAUTHORIZED, sessionNotice, "token");
                    return false;
                }

                // Check if a token was provided and if it's related to the
                // video
                String token = request.getParameter("token");
                if (token == null) {
                    this.sendError(response, HttpServletResponse.SC_UNAUTHORIZED, "unknownTokenError");
                    return false;
                }

                Video tokenVideo = videoStore.findByToken(token);
                if (tokenVideo == null || !video.getId().equals(tokenVideo.getId())) {
                    this.sendError(response, HttpServletResponse.SC_UNAUTHORIZED, "unknownTokenError");
                    return false;
                }
            }

            // Create new session and write it to database
            if (!this.createSession(request, response, video, this.currentUser)) {
                return false;
            }

            // Return session information as JSON output
            Map<String, String> jsonMap = new HashMap<String, String>();
            jsonMap.put("accessToken", session.getSessionToken());
            jsonMap.put("expiresAt", session.getExpireDate().toString());
            response.setStatus(HttpServletResponse.SC_OK);
            this.writeJSON(response, (new JSONObject(jsonMap)).toString());
            return false;
        }
        return true;
    }

    /**
     * Creates a new {@link SivaPlayerSession}.
     * 
     * @param video
     *            of the session.
     * @param user
     *            of the session.
     * @return true if session could be created, false otherwise.
     * @throws IOException
     */
    private boolean createSession(HttpServletRequest request, HttpServletResponse response, Video video, User user)
            throws IOException {
        IApiStore apiStore = this.persistenceProvider.getApiStore();
        try {
            while (this.session == null || this.session.getId() == null) {
                this.session = new SivaPlayerSession(null, SecurityUtils.randomString(70));
                if (request.getParameter("token2") != null && request.getParameter("token2").length() == 40) {
                    String ip = request.getRemoteAddr().replaceAll("\\D+", "");
                    if (request.getHeader("X-Forwarded-For") != null) {
                        ip = request.getHeader("X-Forwarded-For").replaceAll("\\D+", "");
                    }
                    this.session.setSecondaryToken(ip + "-" + request.getParameter("token2"));
                }
                if (user != null) {
                    this.session.setUserId(user.getId());
                }
                this.session.setVideoId(video.getId());
                this.session.setVideoVersion(video.getVersion());
                this.session = apiStore.createSivaPlayerSession(this.session);
            }
        } catch (InconsistencyException e) {
            this.sendError(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "sessionCreationFailedError");
            return false;
        }

        // Log Client IP address
        /*
         * ArrayList<SivaPlayerLogEntry> entries = new
         * ArrayList<SivaPlayerLogEntry>(); SivaPlayerLogEntry entry = new
         * SivaPlayerLogEntry(); entry.setSessionId(this.session.getId());
         * entry.setType("getClientInformation"); entry.setElement("ip");
         * entry.setAdditionalInformation(request.getRemoteAddr());
         * entry.setTime(new Date()); entries.add(entry); try {
         * apiStore.createSivaPlayerLogEntries(entries); } catch
         * (InconsistencyException ignore) { this.sendError(response,
         * HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "savingError"); return
         * false; }
         */
        return true;
    }

    /**
     * Provide a player for playing the current video.
     * 
     * @param request
     *            servlet.
     * @param response
     *            servlet.
     * @throws IOException
     */
    private void providePlayer(HttpServletRequest request, HttpServletResponse response) throws IOException {

        IVideoStore videoStore = this.persistenceProvider.getVideoStore();
        IUserStore userStore = this.persistenceProvider.getUserStore();

        // Get video from database and show 404 if video does not exist
        String videoDirectory = request.getPathInfo().split("/")[1];
        Video video = videoStore.findByDirectory(videoDirectory);
        if (video == null) {
            this.sendError(response, HttpServletResponse.SC_NOT_FOUND, "videoNotExistingError");
            return;
        }

        String token = "";
        String externUserId = request.getParameter("externUserId");
        String code = request.getParameter("code");
        String firstName = request.getParameter("firstName");
        String lastName = request.getParameter("lastName");
        if (externUserId != null && !externUserId.equals("") && code != null) {
            String secret = SecurityUtils
                    .hash(video.getParticipationRestriction().toString().toLowerCase() + video.getDirectory())
                    .substring(10, 40);
            if (code.equals(SecurityUtils.hash(secret + externUserId))) {
                User user = userStore.findByExternUserId(externUserId);
                if (user == null) {
                    user = new User(null);
                    user.setFirstName(((firstName != null) ? firstName : "External User"));
                    user.setLastName(((lastName != null) ? lastName : "Anoymous"));
                    user.setExternUserId(externUserId);
                    user.setUserType(EUserType.Participant);
                    user.setPassword(SecurityUtils.randomString(20));
                    user.setEmail(secret + externUserId + "@anonymous.org");
                    try {
                        user = userStore.create(user);
                    } catch (InconsistencyException e) {
                        user = null;
                    }
                }
                if (user != null) {

                    // generate new SivaPlayerSession and provide session token for direct
                    // video access
                    token = SecurityUtils.randomString(70);
                    SivaPlayerSession session = new SivaPlayerSession(null, token);
                    session.setUserId(user.getId());
                    session.setVideoId(video.getId());
                    session.setVideoVersion(video.getVersion());
                    try {
                        session = this.persistenceProvider.getApiStore().createSivaPlayerSession(session);
                        token = session.getSessionToken();
                    } catch (InconsistencyException e) {
                        token = null;
                    }

                    // Log Client IP address
                    ArrayList<SivaPlayerLogEntry> entries = new ArrayList<SivaPlayerLogEntry>();
                    SivaPlayerLogEntry entry = new SivaPlayerLogEntry();
                    entry.setSessionId(session.getId());
                    entry.setType("getClientInformation");
                    entry.setElement("embeddedAccess");
                    /*
                     * entry.setElement("ip");
                     * entry.setAdditionalInformation(((HttpServletRequest
                     * )this.getCurrentFcInstance
                     * ().getExternalContext().getRequest()).getRemoteAddr());
                     */
                    entry.setTime(new Date());
                    entries.add(entry);
                    try {
                        this.persistenceProvider.getApiStore().createSivaPlayerLogEntries(entries);
                    } catch (InconsistencyException ignore) {
                    }
                }
            }
        }

        PrintWriter out = response.getWriter();
        out.print(
                "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">"
                        + "<html xmlns=\"http://www.w3.org/1999/xhtml\" xml:lang=\"de\">" + "<head>" + "<title>"
                        + video.getTitle() + "</title>" + "<meta name=\"HandheldFriendly\" content=\"True\" />"
                        + "<meta name=\"MobileOptimized\" content=\"400\" />"
                        + "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no\" />"
                        + "<meta http-equiv=\"Content-Type\" content=\"text/html;charset=utf-8\" />"
                        + "<style>body,html{height:100%;margin:0;padding:0;}</style>" + "</head>" + "<body>"
                        + "<div class=\"sivaPlayer\" style=\"display:none;\">"
                        + "<div class=\"sivaPlayer_configuration\">" + "<span class=\"common_log\">true</span>"
                        + "<span class=\"common_useSecretLogin\">false</span>" + "</div>" + "</div>"
                        + "<script src=\"./XML/export.js?token=" + token + "\" type=\"text/javascript\"></script>"
                        + "<script src=\"../../resources/SivaPlayer/js/initSivaPlayer.js?lang=en,de\" type=\"text/javascript\"></script>"
                        + "</body>" + "</html>");
    }

    /**
     * Save logged player data to database.
     * 
     * @param request
     *            servlet.
     * @param response
     *            servlet.
     * @throws IOException
     */
    private void doLogging(HttpServletRequest request, HttpServletResponse response) throws IOException {

        // Check if there is a set of logged data and try to parse it as JSON
        // Array,
        // display error if something goes wrong
        if (request.getParameter("data") == null || request.getParameter("data").equals("")) {
            this.sendError(response, HttpServletResponse.SC_BAD_REQUEST, "nothingToLogError");
            return;
        }

        JSONArray json;
        try {
            json = new JSONArray(request.getParameter("data"));
        } catch (JSONException e) {
            this.sendError(response, HttpServletResponse.SC_BAD_REQUEST, "malformedDataError");
            return;
        }

        String videoDirectory = request.getPathInfo().split("/")[1];
        IVideoStore videoStore = this.persistenceProvider.getVideoStore();
        Video video = videoStore.findByDirectory(videoDirectory);
        if (video == null) {
            this.sendError(response, HttpServletResponse.SC_NOT_FOUND, "videoNotExistingError");
            return;
        }

        // Create new session if no one exists get related session otherwise
        IApiStore apiStore = this.persistenceProvider.getApiStore();
        if (request.getParameter("token") == null || request.getParameter("token").equals("")
                || request.getParameter("token").equals("undefined")) {

            // Check if there is a user specified for logging
            User user = null;
            if (request.getParameter("email") != null && !request.getParameter("email").equals("")) {
                user = this.persistenceProvider.getUserStore().findByEmail(request.getParameter("email"));
                if (user == null || request.getParameter("secret") == null
                        || !user.getSecretKey().equals(request.getParameter("secret"))) {
                    this.sendError(response, HttpServletResponse.SC_FORBIDDEN, "unknownUserCredentials");
                    return;
                }
            }

            // Try to find an existing session for the provided secondary token
            // to add the logged data to this session, create a new session
            // otherwise
            String ip = request.getRemoteAddr().replaceAll("\\D+", "");
            if (request.getHeader("X-Forwarded-For") != null) {
                ip = request.getHeader("X-Forwarded-For").replaceAll("\\D+", "");
            }
            this.session = apiStore
                    .findSivaPlayerSessionBySecondaryToken(ip + "-" + request.getParameter("token2"));
            if (this.session == null || this.session.getId() == null) {

                // Create new session and write it to database
                if (!this.createSession(request, response, video, user)) {
                    return;
                }
            }
        } else {

            // Get related session and show error if session does not exist
            this.session = ((request.getParameter("token").split("-").length != 2) ? null
                    : apiStore.findSivaPlayerSessionByToken(
                            Integer.parseInt(request.getParameter("token").split("-")[0]),
                            request.getParameter("token").split("-")[1], true));
            if (this.session == null || this.session.getId() == null) {
                this.sendError(response, HttpServletResponse.SC_UNAUTHORIZED, "tokenError");
                return;
            }
        }

        // Write each log entry to database if it has not already been written
        ArrayList<SivaPlayerLogEntry> entries = new ArrayList<SivaPlayerLogEntry>();
        try {
            for (int i = 0; i < json.length(); i++) {
                JSONObject logEntry = json.getJSONObject(i);
                SivaPlayerLogEntry entry = new SivaPlayerLogEntry();
                entry.setSessionId(this.session.getId());
                entry.setPlayerSequenceId(logEntry.getInt("id"));
                entry.setSceneTimeOffset((float) logEntry.getDouble("sceneOffset"));
                entry.setTime(new Date(this.session.getStart().getTime() + logEntry.getLong("timeOffset")));
                entry.setType(logEntry.getString("type"));
                entry.setElement(logEntry.getString("element"));
                entry.setAdditionalInformation(logEntry.getString("extraInfo"));
                entry.setClientTime(logEntry.getLong("clientTime"));
                entries.add(entry);
            }
        } catch (JSONException e) {
            this.sendError(response, HttpServletResponse.SC_BAD_REQUEST, "malformedDataError");
            return;
        } catch (NullPointerException e) {
            e.printStackTrace();
            this.sendError(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "savingError");
            return;
        }

        try {
            apiStore.createSivaPlayerLogEntries(entries);
        } catch (InconsistencyException e) {
            this.sendError(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "savingError");
            return;
        }

        // Return positive JSON output as result of the logging
        response.setStatus(HttpServletResponse.SC_OK);
        response.setContentType("application/json");
        response.setCharacterEncoding("utf-8");

        Map<String, String> jsonMap = new HashMap<String, String>();
        jsonMap.put("logged", "true");
        // if(video.getId() == 1 || video.getId() == 2){
        // jsonMap.put("disabledTitle",
        // "Deaktivierung der aktuellen Beckenbodentrainerversion");
        // jsonMap.put("disabledText",
        // "Es ist eine neue Version des Beckenbodentrainers verfgbar. Diese Testversion wurde aus diesem Grund deaktiviert. Bitte laden Sie die neue Version hier herunter: <br /><a href=\"http://www.klinik-prof-schedel.de/bb\">http://www.klinik-prof-schedel.de/bb</a><br /><br /><b>Bitte beachten Sie,</b> dass Sie fr die Nutzung der neuen Version einen Lizenschlssel bentigen. Diesen erhalten Sie auf Anfrage von der Klinik Prof. Schedel.");
        // }
        this.isAJAXRequest = true;
        this.writeJSON(response, (new JSONObject(jsonMap)).toString());
    }

    /**
     * Return available stats for user.
     * 
     * @param request
     *            servlet.
     * @param response
     *            servlet.
     * @throws IOException
     */
    private void getStats(HttpServletRequest request, HttpServletResponse response) throws IOException {

        // Check if there is a user specified for getting the stats and abort if
        // not
        User user = null;
        IApiStore apiStore = this.persistenceProvider.getApiStore();
        if (request.getParameter("token") != null && !request.getParameter("token").equals("")
                && !request.getParameter("token").equals("undefined")) {
            SivaPlayerSession session = apiStore.findSivaPlayerSessionByToken(
                    Integer.parseInt(request.getParameter("token").split("-")[0]),
                    request.getParameter("token").split("-")[1], true);
            if (session.getUserId() == null) {
                this.sendError(response, HttpServletResponse.SC_UNAUTHORIZED, "loginRequiredTitle");
                return;
            }
            user = new User(session.getUserId());
        } else if (request.getParameter("email") == null && !request.getParameter("email").equals("")) {
            this.sendError(response, HttpServletResponse.SC_UNAUTHORIZED, "loginRequiredTitle");
            return;
        } else {
            user = this.persistenceProvider.getUserStore().findByEmail(request.getParameter("email"));
            if (user == null || request.getParameter("secret") == null
                    || !user.getSecretKey().equals(request.getParameter("secret"))) {
                this.sendError(response, HttpServletResponse.SC_FORBIDDEN, "unknownUserCredentials");
                return;
            }
        }

        // Get stats and write them to HTTP response
        HashMap<String, Integer> stats = apiStore.getSivaPlayerSessionDurationByDay(user.getId());
        if (stats.isEmpty()) {
            this.sendError(response, HttpServletResponse.SC_BAD_REQUEST, "notSufficentData");
        } else {
            this.isAJAXRequest = true;
            this.writeJSON(response, (new JSONObject(stats).toString()));
        }
    }

    /**
     * Returns true if the given accept header accepts the given value.
     * 
     * @param acceptHeader
     *            The accept header.
     * @param toAccept
     *            The value to be accepted.
     * @return True if the given accept header accepts the given value.
     */
    private static boolean accepts(String acceptHeader, String toAccept) {
        String[] acceptValues = acceptHeader.split("\\s*(,|;)\\s*");
        Arrays.sort(acceptValues);
        return Arrays.binarySearch(acceptValues, toAccept) > -1
                || Arrays.binarySearch(acceptValues, toAccept.replaceAll("/.*$", "/*")) > -1
                || Arrays.binarySearch(acceptValues, "*/*") > -1;
    }

    /**
     * Returns true if the given match header matches the given value.
     * 
     * @param matchHeader
     *            The match header.
     * @param toMatch
     *            The value to be matched.
     * @return True if the given match header matches the given value.
     */
    private static boolean matches(String matchHeader, String toMatch) {
        String[] matchValues = matchHeader.split("\\s*,\\s*");
        Arrays.sort(matchValues);
        return Arrays.binarySearch(matchValues, toMatch) > -1 || Arrays.binarySearch(matchValues, "*") > -1;
    }

    /**
     * Returns a substring of the given string value from the given begin index
     * to the given end index as a long. If the substring is empty, then -1 will
     * be returned
     * 
     * @param value
     *            The string value to return a substring as long for.
     * @param beginIndex
     *            The begin index of the substring to be returned as long.
     * @param endIndex
     *            The end index of the substring to be returned as long.
     * @return A substring of the given string value as long or -1 if substring
     *         is empty.
     */
    private static long sublong(String value, int beginIndex, int endIndex) {
        String substring = value.substring(beginIndex, endIndex);
        return (substring.length() > 0) ? Long.parseLong(substring) : -1;
    }

    /**
     * Copy the given byte range of the given input to the given output.
     * 
     * @param input
     *            The input to copy the given range to the given output for.
     * @param output
     *            The output to copy the given range from the given input for.
     * @param start
     *            Start of the byte range.
     * @param length
     *            Length of the byte range.
     * @throws IOException
     *             If something fails at I/O level.
     */
    private static void copy(RandomAccessFile input, OutputStream output, long start, long length)
            throws IOException {
        byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
        int read;

        if (input.length() == length) {
            // Write full range.
            while ((read = input.read(buffer)) > 0) {
                output.write(buffer, 0, read);
            }
        } else {
            // Write partial range.
            input.seek(start);
            long toRead = length;

            while ((read = input.read(buffer)) > 0) {
                if ((toRead -= read) > 0) {
                    output.write(buffer, 0, read);
                } else {
                    output.write(buffer, 0, (int) toRead + read);
                    break;
                }
            }
        }
    }

    /**
     * Get a message from the language file by using the key of the pair of
     * values.
     * 
     * @param key
     *            to which the corresponding message should be fetched.
     * @return the message that is defined in the language file for a certain
     *         key.
     */
    private String getCommonMessage(String key) {
        ResourceBundle bundle = ResourceBundle.getBundle("hu.configuration.CommonMessages",
                FacesContext.getCurrentInstance().getViewRoot().getLocale());

        return bundle.getString(key);
    }

    /**
     * Close closable resource.
     * 
     * @param resource
     *            to close.
     */
    private static void close(Closeable resource) {

        // Check if specified file is not null and try to close it if so
        if (resource != null) {
            try {
                resource.close();
            } catch (IOException ignore) {
                // ignore
            }
        }
    }

    /**
     * Write JSON output to HTTP response.
     * 
     * @param response
     *            servlet.
     * @param output
     *            to write to HTTP response.
     * @throws IOException
     */
    private void writeJSON(HttpServletResponse response, String output) throws IOException {
        response.setContentType("application/json");
        response.setCharacterEncoding("utf-8");
        if (!this.isAJAXRequest) {
            response.getWriter().write(
                    "if(!sivaVideoConfiguration){var sivaVideoConfiguration=[];};sivaVideoConfiguration.push({\"configPath\": document.getElementsByTagName('script')[document.getElementsByTagName('script').length - 1].src, \"accessRestriction\": ");
        }
        response.getWriter().write(output);
        if (!this.isAJAXRequest) {
            response.getWriter().write("});");
        }
        response.getWriter().flush();
        response.getWriter().close();
    }

    @Override
    protected void sendError(HttpServletResponse response, Integer errorCode, String errorDescription)
            throws IOException {
        this.sendError(response, errorCode, errorDescription, null);
    }

    @Override
    protected void sendError(HttpServletResponse response, Integer errorCode, String errorDescription,
            String additionalInformation) throws IOException {
        String message;
        if (errorCode == HttpServletResponse.SC_BAD_REQUEST) {
            message = "Bad request";
        } else if (errorCode == HttpServletResponse.SC_UNAUTHORIZED) {
            message = "Unauthorized";
        } else if (errorCode == HttpServletResponse.SC_FORBIDDEN) {
            message = "Forbidden";
        } else if (errorCode == HttpServletResponse.SC_NOT_FOUND) {
            message = "Not Found";
        } else {
            message = "Uknown Error";
        }

        // Create log entry if a session exists
        if (this.session != null && this.session.getId() != null) {
            ArrayList<SivaPlayerLogEntry> logEntries = new ArrayList<SivaPlayerLogEntry>();
            SivaPlayerLogEntry logEntry = new SivaPlayerLogEntry();
            logEntry.setSessionId(session.getId());
            logEntry.setType("HTTPError");
            logEntry.setElement(message);
            logEntry.setAdditionalInformation(errorDescription);
            logEntry.setTime(new Date());
            logEntries.add(logEntry);
            try {
                this.persistenceProvider.getApiStore().createSivaPlayerLogEntries(logEntries);
            } catch (InconsistencyException ignore) {
                // ignore
            }
        }

        Map<String, String> jsonMap = new HashMap<String, String>();
        jsonMap.put("code", errorCode.toString());
        jsonMap.put("message", message);
        jsonMap.put("description", errorDescription);
        if (additionalInformation != null) {
            jsonMap.put("additionalInformation", additionalInformation);
        }

        this.writeJSON(response, (new JSONObject(jsonMap)).toString());
    }

    /**
     * This class represents a byte range.
     */
    protected class Range {
        long start;
        long end;
        long length;
        long total;

        /**
         * Construct a byte range.
         * 
         * @param start
         *            Start of the byte range.
         * @param end
         *            End of the byte range.
         * @param total
         *            Total length of the byte source.
         */
        public Range(long start, long end, long total) {
            this.start = start;
            this.end = end;
            this.length = end - start + 1;
            this.total = total;
        }
    }

    protected FacesContext getFacesContext(HttpServletRequest request, HttpServletResponse response) {
        FacesContext facesContext = FacesContext.getCurrentInstance();
        if (facesContext == null) {

            FacesContextFactory contextFactory = (FacesContextFactory) FactoryFinder
                    .getFactory(FactoryFinder.FACES_CONTEXT_FACTORY);
            LifecycleFactory lifecycleFactory = (LifecycleFactory) FactoryFinder
                    .getFactory(FactoryFinder.LIFECYCLE_FACTORY);
            Lifecycle lifecycle = lifecycleFactory.getLifecycle(LifecycleFactory.DEFAULT_LIFECYCLE);

            facesContext = contextFactory.getFacesContext(request.getSession().getServletContext(), request,
                    response, lifecycle);

            // Set using our inner class
            InnerFacesContext.setFacesContextAsCurrentInstance(facesContext);

            // set a new viewRoot, otherwise context.getViewRoot returns null
            UIViewRoot view = facesContext.getApplication().getViewHandler().createView(facesContext, "");
            facesContext.setViewRoot(view);
        }
        return facesContext;
    }

    public void removeFacesContext() {
        InnerFacesContext.setFacesContextAsCurrentInstance(null);
    }

    protected Application getApplication(FacesContext facesContext) {
        return facesContext.getApplication();
    }

    @SuppressWarnings("deprecation")
    protected Object getManagedBean(String beanName, FacesContext facesContext) {
        return getApplication(facesContext).getVariableResolver().resolveVariable(facesContext, beanName);
    }

    private abstract static class InnerFacesContext extends FacesContext {
        protected static void setFacesContextAsCurrentInstance(FacesContext facesContext) {
            FacesContext.setCurrentInstance(facesContext);
        }
    }
}