org.orderofthebee.addons.support.tools.share.ContentStreamer.java Source code

Java tutorial

Introduction

Here is the source code for org.orderofthebee.addons.support.tools.share.ContentStreamer.java

Source

/**
 * Copyright (C) 2017 Axel Faust / Markus Joos
 * Copyright (C) 2017 Order of the Bee
 *
 * This file is part of Community Support Tools
 *
 * Community Support Tools 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.
 *
 * Community Support Tools 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 Community Support Tools. If not, see
 * <http://www.gnu.org/licenses/>.
 */
package org.orderofthebee.addons.support.tools.share;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.SocketException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import java.util.TimeZone;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.io.FileUtils;
import org.apache.log4j.Logger;
import org.springframework.extensions.surf.util.URLEncoder;
import org.springframework.extensions.webscripts.Cache;
import org.springframework.extensions.webscripts.WebScriptRequest;
import org.springframework.extensions.webscripts.WebScriptResponse;

/**
 * @author Axel Faust, <a href="http://acosix.de">Acosix GmbH</a>
 */
public class ContentStreamer {

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

    // copied from Repository-tier contentStreamer
    private static final String HEADER_CONTENT_RANGE = "Content-Range";

    private static final String HEADER_CONTENT_LENGTH = "Content-Length";

    private static final String HEADER_ACCEPT_RANGES = "Accept-Ranges";

    private static final String HEADER_RANGE = "Range";

    private static final String HEADER_USER_AGENT = "User-Agent";

    // copied from Repository-tier HttpRangeProcessor
    private static final String HEADER_CONTENT_TYPE = "Content-Type";

    private static final String MULTIPART_BYTERANGES_BOUNDRY = "<ALF4558907921887235966L>";

    private static final String MULTIPART_BYTERANGES_BOUNDRY_SEP = "--" + MULTIPART_BYTERANGES_BOUNDRY;

    private static final int CHUNKSIZE = 64 * 1024;

    // copied from WebScriptServletResponseImpl
    protected static final String NO_CACHE = "no-cache";

    protected static ThreadLocal<SimpleDateFormat> s_dateFormat = new ThreadLocal<>();

    // Request/Response abstractions to simplify our code
    protected static interface Request {

        /**
         * Returns the value of the specified request header as a <code>String</code>. If the request did not include a header of the
         * specified name, this method returns <code>null</code>. If there are multiple headers with the same name, this method returns the
         * first head in the request. The header name is case insensitive. You can use this method with any request header.
         *
         * @param name
         *            a <code>String</code> specifying the
         *            header name
         *
         * @return a <code>String</code> containing the
         *         value of the requested
         *         header, or <code>null</code>
         *         if the request does not
         *         have a header of that name
         *
         * @see HttpServletRequest#getHeader(String)
         * @see WebScriptRequest#getHeader(String)
         *
         */
        String getHeader(String name);
    }

    protected static interface Response {

        /**
         * <p>
         * Sets the status code for this response. This method is used to set the return status code when there is no error (for example,
         * for the status codes SC_OK or SC_MOVED_TEMPORARILY). If there is an error, and the caller wishes to invoke an error-page defined
         * in the web application, the <code>sendError</code> method should be used instead.
         * </p>
         *
         * <p>
         * The container clears the buffer and sets the Location header, preserving cookies and other headers.
         * </p>
         *
         * @param status
         *            the status code
         *
         * @see HttpServletResponse#setStatus(int)
         * @see WebScriptResponse#setStatus(int)
         */
        void setStatus(int status);

        /**
         * Sets a response header with the given name and value.If the header had already been set, the new value overwrites the previous
         * one. The <code>containsHeader</code> method can be used to test for the presence of a header before setting its value.
         *
         * @param name
         *            the name of the header
         * @param value
         *            the header value If it contains octet string,
         *            it should be encoded according to RFC 2047
         *            (http://www.ietf.org/rfc/rfc2047.txt)
         *
         * @see HttpServletResponse#setHeader(String, String)
         * @see WebScriptResponse#setHeader(String, String)
         *
         */
        void setHeader(String name, String value);

        /**
         * Sets the Content Type
         *
         * @param contentType
         *            String
         *
         * @see HttpServletResponse#setContentType(String)
         * @see WebScriptResponse#setContentType(String)
         */
        void setContentType(String contentType);

        /**
         * Sets the Content Encoding
         *
         * @param contentEncoding
         *            String
         *
         * @see WebScriptResponse#setContentEncoding(String)
         */
        void setContentEncoding(String contentEncoding);

        /**
         * Sets the Cache control
         *
         * @param cache
         *            cache control
         *
         * @see WebScriptResponse#setCache(Cache)
         */
        void setCache(Cache cache);

        /**
         * Returns a {@link OutputStream} suitable for writing binary data in the response. The servlet container does not encode the binary
         * data.
         *
         * @return a {@link OutputStream} for writing binary data
         *
         * @exception IOException
         *                if an input or output exception occurred
         *
         * @see HttpServletResponse#getOutputStream()
         * @see WebScriptResponse#getOutputStream()
         */
        OutputStream getOutputStream() throws IOException;
    }

    protected static class WebScriptRequestWrapper implements Request {

        protected final WebScriptRequest req;

        protected WebScriptRequestWrapper(final WebScriptRequest req) {
            this.req = req;
        }

        /**
         *
         * {@inheritDoc}
         */
        @Override
        public String getHeader(final String name) {
            return this.req.getHeader(name);
        }
    }

    protected static class HttpServletRequestWrapper implements Request {

        protected final HttpServletRequest req;

        protected HttpServletRequestWrapper(final HttpServletRequest req) {
            this.req = req;
        }

        /**
         *
         * {@inheritDoc}
         */
        @Override
        public String getHeader(final String name) {
            return this.req.getHeader(name);
        }
    }

    protected static class WebScriptResponseWrapper implements Response {

        protected final WebScriptResponse res;

        protected WebScriptResponseWrapper(final WebScriptResponse res) {
            this.res = res;
        }

        /**
         *
         * {@inheritDoc}
         */
        @Override
        public void setStatus(final int status) {
            this.res.setStatus(status);
        }

        /**
         *
         * {@inheritDoc}
         */
        @Override
        public void setHeader(final String name, final String value) {
            this.res.setHeader(name, value);
        }

        /**
         *
         * {@inheritDoc}
         */
        @Override
        public void setContentType(final String contentType) {
            this.res.setContentType(contentType);
        }

        /**
         *
         * {@inheritDoc}
         */
        @Override
        public void setContentEncoding(final String contentEncoding) {
            this.res.setContentEncoding(contentEncoding);
        }

        /**
         *
         * {@inheritDoc}
         */
        @Override
        public void setCache(final Cache cache) {
            this.res.setCache(cache);
        }

        /**
         *
         * {@inheritDoc}
         */
        @Override
        public OutputStream getOutputStream() throws IOException {
            return this.res.getOutputStream();
        }

    }

    protected static class HttpServletResponseWrapper implements Response {

        protected final HttpServletResponse res;

        protected HttpServletResponseWrapper(final HttpServletResponse res) {
            this.res = res;
        }

        /**
         *
         * {@inheritDoc}
         */
        @Override
        public void setStatus(final int status) {
            this.res.setStatus(status);
        }

        /**
         *
         * {@inheritDoc}
         */
        @Override
        public void setHeader(final String name, final String value) {
            this.res.setHeader(name, value);
        }

        /**
         *
         * {@inheritDoc}
         */
        @Override
        public void setContentType(final String contentType) {
            this.res.setContentType(contentType);
        }

        /**
         *
         * {@inheritDoc}
         */
        @Override
        public void setContentEncoding(final String contentEncoding) {
            this.res.setCharacterEncoding(contentEncoding);
        }

        @Override
        public void setCache(final Cache cache) {
            // copied from WebScriptServletResponseImpl and adapted to avoid +-concatenation
            // set Cache-Control
            final StringBuilder cacheControl = new StringBuilder(64);
            String pragma = "";
            if (cache.getIsPublic()) {
                cacheControl.append("public");
            }
            if (cache.getNeverCache()) {
                if (cacheControl.length() > 0) {
                    cacheControl.append(", ");
                }
                cacheControl.append(NO_CACHE);
                pragma = NO_CACHE;
            }
            if (cache.getMaxAge() != null && cache.getNeverCache() == false) {
                if (cacheControl.length() > 0) {
                    cacheControl.append(", ");
                }
                cacheControl.append("max-age=").append(cache.getMaxAge());
            }
            if (cache.getMustRevalidate() && cache.getNeverCache() == false) {
                if (cacheControl.length() > 0) {
                    cacheControl.append(", ");
                }
                cacheControl.append("must-revalidate");
            }
            if (cacheControl.length() > 0) {
                final String cacheControlValue = cacheControl.toString();
                this.res.setHeader("Cache-Control", cacheControlValue);
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("Cache - set response header Cache-Control: " + cacheControl);
                }
                // special case for IE Ajax request handling
                if (NO_CACHE.equals(cacheControlValue)) {
                    this.res.setHeader("Expires", "Thu, 01 Jan 1970 00:00:00 GMT");
                }
            }
            if (pragma.length() > 0) {
                this.res.setHeader("Pragma", pragma);
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("Cache - set response header Pragma: " + pragma);
                }
            }

            // set ETag
            if (cache.getETag() != null) {
                final String eTag = "\"" + cache.getETag() + "\"";
                this.res.setHeader("ETag", eTag);
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("Cache - set response header ETag: " + eTag);
                }
            }

            // set Last Modified
            if (cache.getLastModified() != null) {
                this.res.setDateHeader("Last-Modified", cache.getLastModified().getTime());
                if (LOGGER.isDebugEnabled()) {
                    final SimpleDateFormat formatter = getHTTPDateFormat();
                    final String lastModified = formatter.format(cache.getLastModified());
                    LOGGER.debug("Cache - set response header Last-Modified: " + lastModified);
                }
            }
        }

        /**
         *
         * {@inheritDoc}
         */
        @Override
        public OutputStream getOutputStream() throws IOException {
            return this.res.getOutputStream();
        }

    }

    // provide method variants for specific request / response types
    protected void streamContent(final WebScriptRequest req, final WebScriptResponse res, final File file,
            final Long modifiedTime, final boolean attach, final String attachFileName,
            final Map<String, Object> model, final String mimetype) throws IOException {
        final WebScriptRequestWrapper reqWrapper = new WebScriptRequestWrapper(req);
        final WebScriptResponseWrapper resWrapper = new WebScriptResponseWrapper(res);
        this.streamContent(reqWrapper, resWrapper, file, modifiedTime, attach, attachFileName, model, mimetype);
    }

    protected void streamContent(final HttpServletRequest req, final HttpServletResponse res, final File file,
            final Long modifiedTime, final boolean attach, final String attachFileName,
            final Map<String, Object> model, final String mimetype) throws IOException {
        final HttpServletRequestWrapper reqWrapper = new HttpServletRequestWrapper(req);
        final HttpServletResponseWrapper resWrapper = new HttpServletResponseWrapper(res);
        this.streamContent(reqWrapper, resWrapper, file, modifiedTime, attach, attachFileName, model, mimetype);
    }

    // copied from Repo-tier ContentStreamer and base classes since they aren't available in Share
    protected void streamContent(final Request req, final Response res, final File file, final Long modifiedTime,
            final boolean attach, final String attachFileName, final Map<String, Object> model,
            final String mimetype) throws IOException {
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Retrieving content from file " + file.getAbsolutePath() + " (attach: " + attach + ")");
        }

        // determine mimetype from file extension
        final long lastModified = modifiedTime == null ? file.lastModified() : modifiedTime;
        final Date lastModifiedDate = new Date(lastModified);

        this.streamContentImpl(req, res, file, attach, lastModifiedDate, String.valueOf(lastModifiedDate.getTime()),
                attachFileName, model, mimetype);
    }

    protected void streamContentImpl(final Request req, final Response res, final File file, final boolean attach,
            final Date modified, final String eTag, final String attachFileName, final Map<String, Object> model,
            final String mimetype) throws IOException {
        this.setAttachment(null, res, attach, attachFileName);

        res.setHeader(HEADER_ACCEPT_RANGES, "bytes");
        try {
            boolean processedRange = false;
            String range = req.getHeader(HEADER_CONTENT_RANGE);
            final long size = file.length();
            // the default encoding is Java's default encoding
            final String encoding = "UTF-8";

            if (range == null) {
                range = req.getHeader(HEADER_RANGE);
            }
            if (range != null) {
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("Found content range header: " + range);
                }

                // ensure the range header is starts with "bytes=" and process the range(s)
                if (range.length() > 6) {
                    if (range.indexOf(',') != -1) {
                        LOGGER.warn("Multi-range not supported");
                    } else {
                        processedRange = this.processRange(res, file, range.substring(6), mimetype,
                                req.getHeader(HEADER_USER_AGENT));
                    }
                }
            }
            if (processedRange == false) {
                LOGGER.debug("Sending complete file content...");

                // set mimetype for the content and the character encoding for the stream
                res.setContentType(mimetype);
                res.setContentEncoding(encoding);

                // return the complete entity range
                res.setHeader(HEADER_CONTENT_RANGE,
                        "bytes 0-" + Long.toString(size - 1L) + "/" + Long.toString(size));
                res.setHeader(HEADER_CONTENT_LENGTH, Long.toString(size));

                // set caching
                this.setResponseCache(res, modified, eTag, model);

                // get the content and stream directly to the response output stream
                // assuming the repository is capable of streaming in chunks, this should allow large files
                // to be streamed directly to the browser response stream.
                FileUtils.copyFile(file, res.getOutputStream());
            }
        } catch (final SocketException e1) {
            // the client cut the connection - our mission was accomplished apart from a little error message
            if (LOGGER.isInfoEnabled()) {
                LOGGER.info("Client aborted stream read:\n\tfile: " + file);
            }
        }
    }

    protected void setAttachment(final Request req, final Response res, final boolean attach,
            final String attachFileName) {
        if (attach == true) {
            String headerValue = "attachment";
            if (attachFileName != null && attachFileName.length() > 0) {
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("Attaching content using filename: " + attachFileName);
                }

                if (req == null) {
                    headerValue += "; filename*=UTF-8''" + encodeURL(attachFileName) + "; filename=\""
                            + attachFileName + "\"";
                } else {
                    final String userAgent = req.getHeader(HEADER_USER_AGENT);
                    final boolean isLegacy = (null != userAgent)
                            && (userAgent.contains("MSIE 8") || userAgent.contains("MSIE 7"));
                    if (isLegacy) {
                        headerValue += "; filename=\"" + encodeURL(attachFileName);
                    } else {
                        headerValue += "; filename=\"" + attachFileName + "\"; filename*=UTF-8''"
                                + encodeURL(attachFileName);
                    }
                }
            }

            // set header based on filename - will force a Save As from the browse if it doesn't recognize it
            // this is better than the default response of the browser trying to display the contents
            res.setHeader("Content-Disposition", headerValue);
        }
    }

    protected void setResponseCache(final Response res, final Date modified, final String eTag,
            final Map<String, Object> model) {
        final Cache cache = new Cache();
        if (model == null || model.get("allowBrowserToCache") == null
                || ((String) model.get("allowBrowserToCache")).equals("false")) {
            cache.setNeverCache(false);
            cache.setMustRevalidate(true);
            cache.setMaxAge(0L);
            cache.setLastModified(modified);
            cache.setETag(eTag);
        } else {
            cache.setNeverCache(false);
            cache.setMustRevalidate(false);
            cache.setMaxAge(new Long(31536000));
            cache.setLastModified(modified);
            cache.setETag(eTag);
            res.setCache(cache);
        }
        res.setCache(cache);
    }

    // copied from Repo-tier HttpRangeProcessor
    protected boolean processRange(final Response res, final File file, final String range, final String mimetype,
            final String userAgent) throws IOException {
        // test for multiple byte ranges present in header
        return this.processSingleRange(res, file, range, mimetype);
    }

    protected boolean processSingleRange(final Response res, final File file, final String range,
            final String mimetype) throws IOException {
        // return the specific set of bytes as requested in the content-range header

        /*
         * Examples of byte-content-range-spec values, assuming that the entity contains total of 1234 bytes:
         * The first 500 bytes:
         * bytes 0-499/1234
         * The second 500 bytes:
         * bytes 500-999/1234
         * All except for the first 500 bytes:
         * bytes 500-1233/1234
         */
        /*
         * 'Range' header example:
         * bytes=10485760-20971519
         */

        boolean processedRange = false;
        Range r = null;
        try {
            r = Range.constructRange(range, mimetype, file.length());
        } catch (final IllegalArgumentException err) {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("Failed to parse range header - returning 416 status code: " + err.getMessage());
            }

            res.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
            res.setHeader(HEADER_CONTENT_RANGE, "\"*\"");
            res.getOutputStream().close();
            return true;
        }

        // set Partial Content status and range headers
        final String contentRange = "bytes " + Long.toString(r.start) + "-" + Long.toString(r.end) + "/"
                + Long.toString(file.length());

        res.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
        res.setContentType(mimetype);
        res.setHeader(HEADER_CONTENT_RANGE, contentRange);
        res.setHeader(HEADER_CONTENT_LENGTH, Long.toString((r.end - r.start) + 1L));

        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Processing: Content-Range: " + contentRange);
        }

        InputStream is = null;
        try {
            // output the binary data for the range
            OutputStream os = null;
            os = res.getOutputStream();
            is = new FileInputStream(file);

            this.streamRangeBytes(r, is, os, 0L);

            os.close();
            processedRange = true;
        } catch (final IOException err) {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("Unable to process single range due to IO Exception: " + err.getMessage());
            }
            throw err;
        } finally {
            if (is != null) {
                is.close();
            }
        }

        return processedRange;
    }

    protected void streamRangeBytes(final Range r, final InputStream is, final OutputStream os, final long offset)
            throws IOException {
        if (r.start != 0L && r.start > offset) {
            final long skipped = offset + is.skip(r.start - offset);
            if (skipped < r.start) {
                // Nothing left to download!
                return;
            }
        }
        final long span = (r.end - r.start) + 1L;
        long bytesLeft = span;
        int read = 0;

        // Check that bytesLeft isn't greater than int can hold
        int bufSize;
        if (bytesLeft >= Integer.MAX_VALUE - 8) {
            bufSize = CHUNKSIZE;
        } else {
            bufSize = ((int) bytesLeft) < CHUNKSIZE ? (int) bytesLeft : CHUNKSIZE;
        }
        byte[] buf = new byte[bufSize];

        while ((read = is.read(buf)) > 0 && bytesLeft != 0L) {
            os.write(buf, 0, read);

            bytesLeft -= read;

            if (bytesLeft != 0L) {
                int resize;
                if (bytesLeft >= Integer.MAX_VALUE - 8) {
                    resize = CHUNKSIZE;
                } else {
                    resize = ((int) bytesLeft) < CHUNKSIZE ? (int) bytesLeft : CHUNKSIZE;
                }
                if (resize != buf.length) {
                    buf = new byte[resize];
                }
            }
            if (LOGGER.isTraceEnabled()) {
                LOGGER.trace("...wrote " + read + " bytes, with " + bytesLeft + " to go...");
            }
        }
    }

    /**
     * Representation of a single byte range.
     */
    protected static class Range implements Comparable<Range> {

        private final long start;

        private final long end;

        private final long entityLength;

        private final String contentType;

        private String contentRange;

        /**
         * Constructor
         *
         * @param contentType
         *            Mimetype of the range content
         * @param start
         *            Start position in the parent entity
         * @param end
         *            End position in the parent entity
         * @param entityLength
         *            Length of the parent entity
         */
        Range(final String contentType, final long start, final long end, final long entityLength) {
            this.contentType = HEADER_CONTENT_TYPE + ": " + contentType;
            this.start = start;
            this.end = end;
            this.entityLength = entityLength;
        }

        /**
         * Factory method to construct a byte range from a range header value.
         *
         * @param range
         *            Range header value
         * @param contentType
         *            Mimetype of the range
         * @param entityLength
         *            Length of the parent entity
         *
         * @return Range
         *
         * @throws IllegalArgumentException
         *             for an invalid range
         */
        static Range constructRange(String range, final String contentType, final long entityLength) {
            if (range == null) {
                throw new IllegalArgumentException("Range argument is mandatory");
            }

            // strip total if present - it does not give us anything useful
            if (range.indexOf('/') != -1) {
                range = range.substring(0, range.indexOf('/'));
            }

            // find the separator
            final int separator = range.indexOf('-');
            if (separator == -1) {
                throw new IllegalArgumentException("Invalid range: " + range);
            }

            try {
                // split range and parse values
                long start = 0L;
                if (separator != 0) {
                    start = Long.parseLong(range.substring(0, separator));
                }
                long end = entityLength - 1L;
                if (separator != range.length() - 1) {
                    end = Long.parseLong(range.substring(separator + 1));
                }

                if (start > end) {
                    throw new IllegalArgumentException("Range start can not be less than range end: " + range);
                }
                // return object to represent the byte-range
                return new Range(contentType, start, end, entityLength);
            } catch (final NumberFormatException err) {
                throw new IllegalArgumentException("Unable to parse range value: " + range);
            }
        }

        /**
         * Output the header bytes for a multi-part byte range header
         */
        void outputHeader(final ServletOutputStream os) throws IOException {
            // output multi-part boundry separator
            os.println(MULTIPART_BYTERANGES_BOUNDRY_SEP);
            // output content type and range size sub-header for this part
            os.println(this.contentType);
            os.println(this.getContentRange());
            os.println();
        }

        /**
         * @return the length in bytes of the byte range content including the header bytes
         */
        int getLength() {
            // length in bytes of range plus it's header plus section marker and line feed bytes
            return MULTIPART_BYTERANGES_BOUNDRY_SEP.length() + 2 + this.contentType.length() + 2
                    + this.getContentRange().length() + 4 + (int) (this.end - this.start + 1L) + 2;
        }

        /**
         * @return the Content-Range header string value for this byte range
         */
        private String getContentRange() {
            if (this.contentRange == null) {
                this.contentRange = "Content-Range: bytes " + Long.toString(this.start) + "-"
                        + Long.toString(this.end) + "/" + Long.toString(this.entityLength);
            }
            return this.contentRange;
        }

        @Override
        public String toString() {
            return this.start + "-" + this.end;
        }

        /**
         * @see java.lang.Comparable#compareTo(java.lang.Object)
         */
        @Override
        public int compareTo(final Range o) {
            return this.start > o.start ? 1 : -1;
        }
    }

    // copied from Repo-tier WebDAVHelper
    protected final static String encodeURL(final String s) {
        return encodeURL(s, null);
    }

    protected final static String encodeURL(final String s, final String userAgent) {
        return URLEncoder.encode(s);
    }

    // copied from WebScriptServletResponseImpl
    protected static SimpleDateFormat getHTTPDateFormat() {
        if (s_dateFormat.get() != null) {
            return s_dateFormat.get();
        }

        final SimpleDateFormat formatter = new SimpleDateFormat("EEE, dd MMM yyyy kk:mm:ss zzz");
        formatter.setLenient(false);
        formatter.setTimeZone(TimeZone.getTimeZone("GMT"));
        s_dateFormat.set(formatter);
        return s_dateFormat.get();
    }
}