001    /**
002     * Copyright (c) 2000-2013 Liferay, Inc. All rights reserved.
003     *
004     * This library is free software; you can redistribute it and/or modify it under
005     * the terms of the GNU Lesser General Public License as published by the Free
006     * Software Foundation; either version 2.1 of the License, or (at your option)
007     * any later version.
008     *
009     * This library is distributed in the hope that it will be useful, but WITHOUT
010     * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
011     * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
012     * details.
013     */
014    
015    package com.liferay.portal.kernel.servlet;
016    
017    import com.liferay.portal.kernel.log.Log;
018    import com.liferay.portal.kernel.log.LogFactoryUtil;
019    import com.liferay.portal.kernel.nio.charset.CharsetEncoderUtil;
020    import com.liferay.portal.kernel.util.ArrayUtil;
021    import com.liferay.portal.kernel.util.FileUtil;
022    import com.liferay.portal.kernel.util.GetterUtil;
023    import com.liferay.portal.kernel.util.HttpUtil;
024    import com.liferay.portal.kernel.util.MimeTypesUtil;
025    import com.liferay.portal.kernel.util.PropsKeys;
026    import com.liferay.portal.kernel.util.PropsUtil;
027    import com.liferay.portal.kernel.util.RandomAccessInputStream;
028    import com.liferay.portal.kernel.util.ServerDetector;
029    import com.liferay.portal.kernel.util.StreamUtil;
030    import com.liferay.portal.kernel.util.StringBundler;
031    import com.liferay.portal.kernel.util.StringPool;
032    import com.liferay.portal.kernel.util.StringUtil;
033    import com.liferay.portal.kernel.util.Validator;
034    
035    import java.io.ByteArrayInputStream;
036    import java.io.File;
037    import java.io.FileInputStream;
038    import java.io.IOException;
039    import java.io.InputStream;
040    import java.io.OutputStream;
041    
042    import java.net.SocketException;
043    
044    import java.nio.ByteBuffer;
045    import java.nio.CharBuffer;
046    import java.nio.channels.Channels;
047    import java.nio.channels.FileChannel;
048    
049    import java.util.ArrayList;
050    import java.util.Collections;
051    import java.util.List;
052    
053    import javax.servlet.ServletOutputStream;
054    import javax.servlet.http.HttpServletRequest;
055    import javax.servlet.http.HttpServletResponse;
056    
057    /**
058     * @author Brian Wing Shun Chan
059     * @author Shuyang Zhou
060     */
061    public class ServletResponseUtil {
062    
063            public static List<Range> getRanges(
064                            HttpServletRequest request, HttpServletResponse response,
065                            long length)
066                    throws IOException {
067    
068                    String rangeString = request.getHeader(HttpHeaders.RANGE);
069    
070                    if (Validator.isNull(rangeString)) {
071                            return Collections.emptyList();
072                    }
073    
074                    if (!rangeString.matches(_RANGE_REGEX)) {
075                            throw new IOException(
076                                    "Range header does not match regular expression " +
077                                    rangeString);
078                    }
079    
080                    List<Range> ranges = new ArrayList<Range>();
081    
082                    String[] rangeFields = StringUtil.split(rangeString.substring(6));
083    
084                    if (rangeFields.length > _MAX_RANGE_FIELDS) {
085                            StringBundler sb = new StringBundler(8);
086    
087                            sb.append("Request range ");
088                            sb.append(rangeString);
089                            sb.append(" with ");
090                            sb.append(rangeFields.length);
091                            sb.append(" range fields has exceeded maximum allowance as ");
092                            sb.append("specified by the property \"");
093                            sb.append(PropsKeys.WEB_SERVER_SERVLET_MAX_RANGE_FIELDS);
094                            sb.append("\"");
095    
096                            throw new IOException(sb.toString());
097                    }
098    
099                    for (String rangeField : rangeFields) {
100                            int index = rangeField.indexOf(StringPool.DASH);
101    
102                            long start = GetterUtil.getLong(rangeField.substring(0, index), -1);
103                            long end = GetterUtil.getLong(
104                                    rangeField.substring(index + 1, rangeField.length()), -1);
105    
106                            if (start == -1) {
107                                    start = length - end;
108                                    end = length - 1;
109                            }
110                            else if ((end == -1) || (end > (length - 1))) {
111                                    end = length - 1;
112                            }
113    
114                            if (start > end) {
115                                    throw new IOException(
116                                            "Range start " + start + " is greater than end " + end);
117                            }
118    
119                            Range range = new Range(start, end, length);
120    
121                            ranges.add(range);
122                    }
123    
124                    return ranges;
125            }
126    
127            public static boolean isClientAbortException(IOException ioe) {
128                    Class<?> clazz = ioe.getClass();
129    
130                    String className = clazz.getName();
131    
132                    if (className.equals(_CLIENT_ABORT_EXCEPTION)) {
133                            return true;
134                    }
135                    else {
136                            return false;
137                    }
138            }
139    
140            public static void sendFile(
141                            HttpServletRequest request, HttpServletResponse response,
142                            String fileName, byte[] bytes)
143                    throws IOException {
144    
145                    sendFile(request, response, fileName, bytes, null);
146            }
147    
148            public static void sendFile(
149                            HttpServletRequest request, HttpServletResponse response,
150                            String fileName, byte[] bytes, String contentType)
151                    throws IOException {
152    
153                    sendFile(request, response, fileName, bytes, contentType, null);
154            }
155    
156            public static void sendFile(
157                            HttpServletRequest request, HttpServletResponse response,
158                            String fileName, byte[] bytes, String contentType,
159                            String contentDispositionType)
160                    throws IOException {
161    
162                    setHeaders(
163                            request, response, fileName, contentType, contentDispositionType);
164    
165                    write(response, bytes);
166            }
167    
168            public static void sendFile(
169                            HttpServletRequest request, HttpServletResponse response,
170                            String fileName, InputStream is)
171                    throws IOException {
172    
173                    sendFile(request, response, fileName, is, null);
174            }
175    
176            public static void sendFile(
177                            HttpServletRequest request, HttpServletResponse response,
178                            String fileName, InputStream is, long contentLength,
179                            String contentType)
180                    throws IOException {
181    
182                    sendFile(request, response, fileName, is, 0, contentType, null);
183            }
184    
185            public static void sendFile(
186                            HttpServletRequest request, HttpServletResponse response,
187                            String fileName, InputStream is, long contentLength,
188                            String contentType, String contentDispositionType)
189                    throws IOException {
190    
191                    setHeaders(
192                            request, response, fileName, contentType, contentDispositionType);
193    
194                    write(response, is, contentLength);
195            }
196    
197            public static void sendFile(
198                            HttpServletRequest request, HttpServletResponse response,
199                            String fileName, InputStream is, String contentType)
200                    throws IOException {
201    
202                    sendFile(request, response, fileName, is, 0, contentType);
203            }
204    
205            /**
206             * @deprecated As of 6.1.0
207             */
208            public static void sendFile(
209                            HttpServletResponse response, String fileName, byte[] bytes)
210                    throws IOException {
211    
212                    sendFile(null, response, fileName, bytes);
213            }
214    
215            /**
216             * @deprecated As of 6.1.0
217             */
218            public static void sendFile(
219                            HttpServletResponse response, String fileName, byte[] bytes,
220                            String contentType)
221                    throws IOException {
222    
223                    sendFile(null, response, fileName, bytes, contentType);
224            }
225    
226            /**
227             * @deprecated As of 6.1.0
228             */
229            public static void sendFile(
230                            HttpServletResponse response, String fileName, InputStream is)
231                    throws IOException {
232    
233                    sendFile(null, response, fileName, is);
234            }
235    
236            /**
237             * @deprecated As of 6.1.0
238             */
239            public static void sendFile(
240                            HttpServletResponse response, String fileName, InputStream is,
241                            int contentLength, String contentType)
242                    throws IOException {
243    
244                    sendFile(null, response, fileName, is, contentLength, contentType);
245            }
246    
247            /**
248             * @deprecated As of 6.1.0
249             */
250            public static void sendFile(
251                            HttpServletResponse response, String fileName, InputStream is,
252                            String contentType)
253                    throws IOException {
254    
255                    sendFile(null, response, fileName, is, contentType);
256            }
257    
258            public static void write(
259                            HttpServletRequest request, HttpServletResponse response,
260                            String fileName, List<Range> ranges, InputStream inputStream,
261                            long fullLength, String contentType)
262                    throws IOException {
263    
264                    OutputStream outputStream = null;
265    
266                    try {
267                            outputStream = response.getOutputStream();
268    
269                            Range fullRange = new Range(0, fullLength - 1, fullLength);
270    
271                            Range firstRange = null;
272    
273                            if (!ranges.isEmpty()) {
274                                    firstRange = ranges.get(0);
275                            }
276    
277                            if ((firstRange == null) || firstRange.equals(fullRange)) {
278                                    if (_log.isDebugEnabled()) {
279                                            _log.debug("Writing full range");
280                                    }
281    
282                                    response.setContentType(contentType);
283    
284                                    setHeaders(
285                                            request, response, fileName, contentType, null, fullRange);
286    
287                                    copyRange(
288                                            inputStream, outputStream, fullRange.getStart(),
289                                            fullRange.getLength());
290                            }
291                            else if (ranges.size() == 1) {
292                                    if (_log.isDebugEnabled()) {
293                                            _log.debug("Attempting to write a single range");
294                                    }
295    
296                                    Range range = ranges.get(0);
297    
298                                    response.setContentType(contentType);
299    
300                                    setHeaders(
301                                            request, response, fileName, contentType, null, range);
302    
303                                    response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
304    
305                                    copyRange(
306                                            inputStream, outputStream, range.getStart(),
307                                            range.getLength());
308                            }
309                            else if (ranges.size() > 1 ) {
310                                    if (_log.isDebugEnabled()) {
311                                            _log.debug("Attempting to write multiple ranges");
312                                    }
313    
314                                    ServletOutputStream servletOutputStream =
315                                            (ServletOutputStream)outputStream;
316    
317                                    String boundary =
318                                            "liferay-multipart-boundary-" + System.currentTimeMillis();
319    
320                                    String multipartContentType =
321                                            "multipart/byteranges; boundary=" + boundary;
322    
323                                    response.setContentType(multipartContentType);
324    
325                                    setHeaders(
326                                            request, response, fileName, multipartContentType, null);
327    
328                                    response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
329    
330                                    for (int i = 0; i < ranges.size(); i++) {
331                                            Range range = ranges.get(i);
332    
333                                            servletOutputStream.println();
334                                            servletOutputStream.println(
335                                                    StringPool.DOUBLE_DASH + boundary);
336                                            servletOutputStream.println(
337                                                    HttpHeaders.CONTENT_TYPE + ": " + contentType);
338                                            servletOutputStream.println(
339                                                    HttpHeaders.CONTENT_RANGE + ": " +
340                                                            range.getContentRange());
341                                            servletOutputStream.println();
342    
343                                            copyRange(
344                                                    inputStream, outputStream, range.getStart(),
345                                                    range.getLength());
346                                    }
347    
348                                    servletOutputStream.println();
349                                    servletOutputStream.println(
350                                            StringPool.DOUBLE_DASH + boundary + StringPool.DOUBLE_DASH);
351                            }
352                    }
353                    finally {
354                            try {
355                                    inputStream.close();
356                            }
357                            catch (IOException ioe) {
358                            }
359                    }
360            }
361    
362            public static void write(
363                            HttpServletResponse response,
364                            BufferCacheServletResponse bufferCacheServletResponse)
365                    throws IOException {
366    
367                    if (bufferCacheServletResponse.isByteMode()) {
368                            write(response, bufferCacheServletResponse.getByteBuffer());
369                    }
370                    else if (bufferCacheServletResponse.isCharMode()) {
371                            write(response, bufferCacheServletResponse.getCharBuffer());
372                    }
373            }
374    
375            public static void write(HttpServletResponse response, byte[] bytes)
376                    throws IOException {
377    
378                    write(response, bytes, 0, 0);
379            }
380    
381            public static void write(
382                            HttpServletResponse response, byte[] bytes, int offset,
383                            int contentLength)
384                    throws IOException {
385    
386                    try {
387    
388                            // LEP-3122
389    
390                            if (!response.isCommitted()) {
391    
392                                    // LEP-536
393    
394                                    if (contentLength == 0) {
395                                            contentLength = bytes.length;
396                                    }
397    
398                                    response.setContentLength(contentLength);
399    
400                                    response.flushBuffer();
401    
402                                    if (response instanceof BufferCacheServletResponse) {
403                                            BufferCacheServletResponse bufferCacheServletResponse =
404                                                    (BufferCacheServletResponse)response;
405    
406                                            bufferCacheServletResponse.setByteBuffer(
407                                                    ByteBuffer.wrap(bytes, offset, contentLength));
408                                    }
409                                    else {
410                                            ServletOutputStream servletOutputStream =
411                                                    response.getOutputStream();
412    
413                                            if ((contentLength == 0) && ServerDetector.isJetty()) {
414                                            }
415                                            else {
416                                                    servletOutputStream.write(bytes, offset, contentLength);
417                                            }
418                                    }
419                            }
420                    }
421                    catch (IOException ioe) {
422                            if ((ioe instanceof SocketException) ||
423                                    isClientAbortException(ioe)) {
424    
425                                    if (_log.isWarnEnabled()) {
426                                            _log.warn(ioe);
427                                    }
428                            }
429                            else {
430                                    throw ioe;
431                            }
432                    }
433            }
434    
435            public static void write(HttpServletResponse response, byte[][] bytesArray)
436                    throws IOException {
437    
438                    try {
439    
440                            // LEP-3122
441    
442                            if (!response.isCommitted()) {
443                                    int contentLength = 0;
444    
445                                    for (byte[] bytes : bytesArray) {
446                                            contentLength += bytes.length;
447                                    }
448    
449                                    response.setContentLength(contentLength);
450    
451                                    response.flushBuffer();
452    
453                                    ServletOutputStream servletOutputStream =
454                                            response.getOutputStream();
455    
456                                    for (byte[] bytes : bytesArray) {
457                                            servletOutputStream.write(bytes);
458                                    }
459                            }
460                    }
461                    catch (IOException ioe) {
462                            if ((ioe instanceof SocketException) ||
463                                    isClientAbortException(ioe)) {
464    
465                                    if (_log.isWarnEnabled()) {
466                                            _log.warn(ioe);
467                                    }
468                            }
469                            else {
470                                    throw ioe;
471                            }
472                    }
473            }
474    
475            public static void write(
476                            HttpServletResponse response, ByteBuffer byteBuffer)
477                    throws IOException {
478    
479                    if (response instanceof BufferCacheServletResponse) {
480                            BufferCacheServletResponse bufferCacheServletResponse =
481                                    (BufferCacheServletResponse)response;
482    
483                            bufferCacheServletResponse.setByteBuffer(byteBuffer);
484                    }
485                    else {
486                            write(
487                                    response, byteBuffer.array(), byteBuffer.position(),
488                                    byteBuffer.limit());
489                    }
490            }
491    
492            public static void write(
493                            HttpServletResponse response, CharBuffer charBuffer)
494                    throws IOException {
495    
496                    if (response instanceof BufferCacheServletResponse) {
497                            BufferCacheServletResponse bufferCacheServletResponse =
498                                    (BufferCacheServletResponse)response;
499    
500                            bufferCacheServletResponse.setCharBuffer(charBuffer);
501                    }
502                    else {
503                            ByteBuffer byteBuffer = CharsetEncoderUtil.encode(
504                                    StringPool.UTF8, charBuffer);
505    
506                            write(response, byteBuffer);
507                    }
508            }
509    
510            public static void write(HttpServletResponse response, File file)
511                    throws IOException {
512    
513                    if (response instanceof BufferCacheServletResponse) {
514                            BufferCacheServletResponse bufferCacheServletResponse =
515                                    (BufferCacheServletResponse)response;
516    
517                            ByteBuffer byteBuffer = ByteBuffer.wrap(FileUtil.getBytes(file));
518    
519                            bufferCacheServletResponse.setByteBuffer(byteBuffer);
520                    }
521                    else {
522                            FileInputStream fileInputStream = new FileInputStream(file);
523    
524                            FileChannel fileChannel = fileInputStream.getChannel();
525    
526                            try {
527                                    int contentLength = (int)fileChannel.size();
528    
529                                    response.setContentLength(contentLength);
530    
531                                    response.flushBuffer();
532    
533                                    fileChannel.transferTo(
534                                            0, contentLength,
535                                            Channels.newChannel(response.getOutputStream()));
536                            }
537                            finally {
538                                    fileChannel.close();
539                            }
540                    }
541            }
542    
543            public static void write(HttpServletResponse response, InputStream is)
544                    throws IOException {
545    
546                    write(response, is, 0);
547            }
548    
549            public static void write(
550                            HttpServletResponse response, InputStream is, long contentLength)
551                    throws IOException {
552    
553                    if (response.isCommitted()) {
554                            return;
555                    }
556    
557                    if (contentLength > 0) {
558                            response.setHeader(
559                                    HttpHeaders.CONTENT_LENGTH, String.valueOf(contentLength));
560                    }
561    
562                    response.flushBuffer();
563    
564                    StreamUtil.transfer(is, response.getOutputStream());
565            }
566    
567            public static void write(HttpServletResponse response, String s)
568                    throws IOException {
569    
570                    if (response instanceof BufferCacheServletResponse) {
571                            BufferCacheServletResponse bufferCacheServletResponse =
572                                    (BufferCacheServletResponse)response;
573    
574                            bufferCacheServletResponse.setString(s);
575                    }
576                    else {
577                            ByteBuffer byteBuffer = CharsetEncoderUtil.encode(
578                                    StringPool.UTF8, s);
579    
580                            write(response, byteBuffer);
581                    }
582            }
583    
584            protected static void copyRange(
585                            InputStream inputStream, OutputStream outputStream, long start,
586                            long length)
587                    throws IOException {
588    
589                    if (inputStream instanceof FileInputStream) {
590                            FileInputStream fileInputStream = (FileInputStream)inputStream;
591    
592                            FileChannel fileChannel = fileInputStream.getChannel();
593    
594                            fileChannel.transferTo(
595                                    start, length, Channels.newChannel(outputStream));
596                    }
597                    else if (inputStream instanceof ByteArrayInputStream) {
598                            ByteArrayInputStream byteArrayInputStream =
599                                    (ByteArrayInputStream)inputStream;
600    
601                            byteArrayInputStream.skip(start);
602    
603                            StreamUtil.transfer(byteArrayInputStream, outputStream, length);
604                    }
605                    else {
606                            RandomAccessInputStream randomAccessInputStream =
607                                    new RandomAccessInputStream(inputStream);
608    
609                            randomAccessInputStream.seek(start);
610    
611                            StreamUtil.transfer(randomAccessInputStream, outputStream, length);
612                    }
613            }
614    
615            protected static void setHeaders(
616                    HttpServletRequest request, HttpServletResponse response,
617                    String fileName, String contentType, String contentDispositionType) {
618    
619                    if (_log.isDebugEnabled()) {
620                            _log.debug("Sending file of type " + contentType);
621                    }
622    
623                    // LEP-2201
624    
625                    if (Validator.isNotNull(contentType)) {
626                            response.setContentType(contentType);
627                    }
628    
629                    if (!response.containsHeader(HttpHeaders.CACHE_CONTROL)) {
630                            response.setHeader(
631                                    HttpHeaders.CACHE_CONTROL,
632                                    HttpHeaders.CACHE_CONTROL_PRIVATE_VALUE);
633                    }
634    
635                    if (Validator.isNull(fileName)) {
636                            return;
637                    }
638    
639                    String contentDispositionFileName = "filename=\"" + fileName + "\"";
640    
641                    // If necessary for non-ASCII characters, encode based on RFC 2184.
642                    // However, not all browsers support RFC 2184. See LEP-3127.
643    
644                    boolean ascii = true;
645    
646                    for (int i = 0; i < fileName.length(); i++) {
647                            if (!Validator.isAscii(fileName.charAt(i))) {
648                                    ascii = false;
649    
650                                    break;
651                            }
652                    }
653    
654                    if (!ascii) {
655                            String encodedFileName = HttpUtil.encodeURL(fileName, true);
656    
657                            if (BrowserSnifferUtil.isIe(request)) {
658                                    contentDispositionFileName =
659                                            "filename=\"" + encodedFileName + "\"";
660                            }
661                            else {
662                                    contentDispositionFileName =
663                                            "filename*=UTF-8''" + encodedFileName;
664                            }
665                    }
666    
667                    if (Validator.isNull(contentDispositionType)) {
668                            String extension = GetterUtil.getString(
669                                    FileUtil.getExtension(fileName));
670    
671                            extension = StringUtil.toLowerCase(extension);
672    
673                            String[] mimeTypesContentDispositionInline = null;
674    
675                            try {
676                                    mimeTypesContentDispositionInline = PropsUtil.getArray(
677                                            PropsKeys.MIME_TYPES_CONTENT_DISPOSITION_INLINE);
678                            }
679                            catch (Exception e) {
680                                    mimeTypesContentDispositionInline = new String[0];
681                            }
682    
683                            if (ArrayUtil.contains(
684                                            mimeTypesContentDispositionInline, extension)) {
685    
686                                    contentDispositionType = HttpHeaders.CONTENT_DISPOSITION_INLINE;
687    
688                                    contentType = MimeTypesUtil.getContentType(fileName);
689    
690                                    response.setContentType(contentType);
691                            }
692                            else {
693                                    contentDispositionType =
694                                            HttpHeaders.CONTENT_DISPOSITION_ATTACHMENT;
695                            }
696                    }
697    
698                    StringBundler sb = new StringBundler(4);
699    
700                    sb.append(contentDispositionType);
701                    sb.append(StringPool.SEMICOLON);
702                    sb.append(StringPool.SPACE);
703                    sb.append(contentDispositionFileName);
704    
705                    if (_log.isDebugEnabled()) {
706                            _log.debug("Setting content disposition header " + sb.toString());
707                    }
708    
709                    response.setHeader(HttpHeaders.CONTENT_DISPOSITION, sb.toString());
710            }
711    
712            protected static void setHeaders(
713                    HttpServletRequest request, HttpServletResponse response,
714                    String fileName, String contentType, String contentDispositionType,
715                    Range range) {
716    
717                    setHeaders(
718                            request, response, fileName, contentType, contentDispositionType);
719    
720                    if (range != null) {
721                            response.setHeader(
722                                    HttpHeaders.CONTENT_RANGE, range.getContentRange());
723    
724                            response.setHeader(
725                                    HttpHeaders.CONTENT_LENGTH, String.valueOf(range.getLength()));
726                    }
727            }
728    
729            private static final String _CLIENT_ABORT_EXCEPTION =
730                    "org.apache.catalina.connector.ClientAbortException";
731    
732            private static final int _MAX_RANGE_FIELDS = GetterUtil.getInteger(
733                    PropsUtil.get(PropsKeys.WEB_SERVER_SERVLET_MAX_RANGE_FIELDS));
734    
735            private static final String _RANGE_REGEX =
736                    "^bytes=\\d*-\\d*(,\\s?\\d*-\\d*)*$";
737    
738            private static Log _log = LogFactoryUtil.getLog(ServletResponseUtil.class);
739    
740    }