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