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.PropsKeys;
025    import com.liferay.portal.kernel.util.PropsUtil;
026    import com.liferay.portal.kernel.util.RandomAccessInputStream;
027    import com.liferay.portal.kernel.util.ServerDetector;
028    import com.liferay.portal.kernel.util.StreamUtil;
029    import com.liferay.portal.kernel.util.StringBundler;
030    import com.liferay.portal.kernel.util.StringPool;
031    import com.liferay.portal.kernel.util.StringUtil;
032    import com.liferay.portal.kernel.util.Validator;
033    
034    import java.io.ByteArrayInputStream;
035    import java.io.File;
036    import java.io.FileInputStream;
037    import java.io.IOException;
038    import java.io.InputStream;
039    import java.io.OutputStream;
040    
041    import java.net.SocketException;
042    
043    import java.nio.ByteBuffer;
044    import java.nio.CharBuffer;
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 As of 6.1.0
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 As of 6.1.0
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 As of 6.1.0
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 As of 6.1.0
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 As of 6.1.0
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(
349                            HttpServletResponse response,
350                            BufferCacheServletResponse bufferCacheServletResponse)
351                    throws IOException {
352    
353                    if (bufferCacheServletResponse.isByteMode()) {
354                            write(response, bufferCacheServletResponse.getByteBuffer());
355                    }
356                    else if (bufferCacheServletResponse.isCharMode()) {
357                            write(response, bufferCacheServletResponse.getCharBuffer());
358                    }
359            }
360    
361            public static void write(HttpServletResponse response, byte[] bytes)
362                    throws IOException {
363    
364                    write(response, bytes, 0, 0);
365            }
366    
367            public static void write(
368                            HttpServletResponse response, byte[] bytes, int offset,
369                            int contentLength)
370                    throws IOException {
371    
372                    try {
373    
374                            // LEP-3122
375    
376                            if (!response.isCommitted()) {
377    
378                                    // LEP-536
379    
380                                    if (contentLength == 0) {
381                                            contentLength = bytes.length;
382                                    }
383    
384                                    response.setContentLength(contentLength);
385    
386                                    response.flushBuffer();
387    
388                                    if (response instanceof BufferCacheServletResponse) {
389                                            BufferCacheServletResponse bufferCacheServletResponse =
390                                                    (BufferCacheServletResponse)response;
391    
392                                            bufferCacheServletResponse.setByteBuffer(
393                                                    ByteBuffer.wrap(bytes, offset, contentLength));
394                                    }
395                                    else {
396                                            ServletOutputStream servletOutputStream =
397                                                    response.getOutputStream();
398    
399                                            if ((contentLength == 0) && ServerDetector.isJetty()) {
400                                            }
401                                            else {
402                                                    servletOutputStream.write(bytes, offset, contentLength);
403                                            }
404                                    }
405                            }
406                    }
407                    catch (IOException ioe) {
408                            if ((ioe instanceof SocketException) ||
409                                    isClientAbortException(ioe)) {
410    
411                                    if (_log.isWarnEnabled()) {
412                                            _log.warn(ioe);
413                                    }
414                            }
415                            else {
416                                    throw ioe;
417                            }
418                    }
419            }
420    
421            public static void write(HttpServletResponse response, byte[][] bytesArray)
422                    throws IOException {
423    
424                    try {
425    
426                            // LEP-3122
427    
428                            if (!response.isCommitted()) {
429                                    int contentLength = 0;
430    
431                                    for (byte[] bytes : bytesArray) {
432                                            contentLength += bytes.length;
433                                    }
434    
435                                    response.setContentLength(contentLength);
436    
437                                    response.flushBuffer();
438    
439                                    ServletOutputStream servletOutputStream =
440                                            response.getOutputStream();
441    
442                                    for (byte[] bytes : bytesArray) {
443                                            servletOutputStream.write(bytes);
444                                    }
445                            }
446                    }
447                    catch (IOException ioe) {
448                            if ((ioe instanceof SocketException) ||
449                                    isClientAbortException(ioe)) {
450    
451                                    if (_log.isWarnEnabled()) {
452                                            _log.warn(ioe);
453                                    }
454                            }
455                            else {
456                                    throw ioe;
457                            }
458                    }
459            }
460    
461            public static void write(
462                            HttpServletResponse response, ByteBuffer byteBuffer)
463                    throws IOException {
464    
465                    if (response instanceof BufferCacheServletResponse) {
466                            BufferCacheServletResponse bufferCacheServletResponse =
467                                    (BufferCacheServletResponse)response;
468    
469                            bufferCacheServletResponse.setByteBuffer(byteBuffer);
470                    }
471                    else {
472                            write(
473                                    response, byteBuffer.array(), byteBuffer.position(),
474                                    byteBuffer.limit());
475                    }
476            }
477    
478            public static void write(
479                            HttpServletResponse response, CharBuffer charBuffer)
480                    throws IOException {
481    
482                    if (response instanceof BufferCacheServletResponse) {
483                            BufferCacheServletResponse bufferCacheServletResponse =
484                                    (BufferCacheServletResponse)response;
485    
486                            bufferCacheServletResponse.setCharBuffer(charBuffer);
487                    }
488                    else {
489                            ByteBuffer byteBuffer = CharsetEncoderUtil.encode(
490                                    StringPool.UTF8, charBuffer);
491    
492                            write(response, byteBuffer);
493                    }
494            }
495    
496            public static void write(HttpServletResponse response, File file)
497                    throws IOException {
498    
499                    if (response instanceof BufferCacheServletResponse) {
500                            BufferCacheServletResponse bufferCacheServletResponse =
501                                    (BufferCacheServletResponse)response;
502    
503                            ByteBuffer byteBuffer = ByteBuffer.wrap(FileUtil.getBytes(file));
504    
505                            bufferCacheServletResponse.setByteBuffer(byteBuffer);
506                    }
507                    else {
508                            FileInputStream fileInputStream = new FileInputStream(file);
509    
510                            FileChannel fileChannel = fileInputStream.getChannel();
511    
512                            try {
513                                    int contentLength = (int)fileChannel.size();
514    
515                                    response.setContentLength(contentLength);
516    
517                                    response.flushBuffer();
518    
519                                    fileChannel.transferTo(
520                                            0, contentLength,
521                                            Channels.newChannel(response.getOutputStream()));
522                            }
523                            finally {
524                                    fileChannel.close();
525                            }
526                    }
527            }
528    
529            public static void write(HttpServletResponse response, InputStream is)
530                    throws IOException {
531    
532                    write(response, is, 0);
533            }
534    
535            public static void write(
536                            HttpServletResponse response, InputStream is, long contentLength)
537                    throws IOException {
538    
539                    if (response.isCommitted()) {
540                            return;
541                    }
542    
543                    if (contentLength > 0) {
544                            response.setHeader(
545                                    HttpHeaders.CONTENT_LENGTH, String.valueOf(contentLength));
546                    }
547    
548                    response.flushBuffer();
549    
550                    StreamUtil.transfer(is, response.getOutputStream());
551            }
552    
553            public static void write(HttpServletResponse response, String s)
554                    throws IOException {
555    
556                    if (response instanceof BufferCacheServletResponse) {
557                            BufferCacheServletResponse bufferCacheServletResponse =
558                                    (BufferCacheServletResponse)response;
559    
560                            bufferCacheServletResponse.setString(s);
561                    }
562                    else {
563                            ByteBuffer byteBuffer = CharsetEncoderUtil.encode(
564                                    StringPool.UTF8, s);
565    
566                            write(response, byteBuffer);
567                    }
568            }
569    
570            protected static void copyRange(
571                            InputStream inputStream, OutputStream outputStream, long start,
572                            long length)
573                    throws IOException {
574    
575                    if (inputStream instanceof FileInputStream) {
576                            FileInputStream fileInputStream = (FileInputStream)inputStream;
577    
578                            FileChannel fileChannel = fileInputStream.getChannel();
579    
580                            fileChannel.transferTo(
581                                    start, length, Channels.newChannel(outputStream));
582                    }
583                    else if (inputStream instanceof ByteArrayInputStream) {
584                            ByteArrayInputStream byteArrayInputStream =
585                                    (ByteArrayInputStream)inputStream;
586    
587                            byteArrayInputStream.skip(start);
588    
589                            StreamUtil.transfer(byteArrayInputStream, outputStream, length);
590                    }
591                    else {
592                            RandomAccessInputStream randomAccessInputStream =
593                                    new RandomAccessInputStream(inputStream);
594    
595                            randomAccessInputStream.seek(start);
596    
597                            StreamUtil.transfer(randomAccessInputStream, outputStream, length);
598                    }
599            }
600    
601            protected static boolean isClientAbortException(IOException ioe) {
602                    Class<?> clazz = ioe.getClass();
603    
604                    String className = clazz.getName();
605    
606                    if (className.equals(_CLIENT_ABORT_EXCEPTION)) {
607                            return true;
608                    }
609                    else {
610                            return false;
611                    }
612            }
613    
614            protected static void setHeaders(
615                    HttpServletRequest request, HttpServletResponse response,
616                    String fileName, String contentType, String contentDispositionType) {
617    
618                    if (_log.isDebugEnabled()) {
619                            _log.debug("Sending file of type " + contentType);
620                    }
621    
622                    // LEP-2201
623    
624                    if (Validator.isNotNull(contentType)) {
625                            response.setContentType(contentType);
626                    }
627    
628                    if (!response.containsHeader(HttpHeaders.CACHE_CONTROL)) {
629                            response.setHeader(
630                                    HttpHeaders.CACHE_CONTROL,
631                                    HttpHeaders.CACHE_CONTROL_PRIVATE_VALUE);
632                    }
633    
634                    if (Validator.isNull(fileName)) {
635                            return;
636                    }
637    
638                    String contentDispositionFileName = "filename=\"" + fileName + "\"";
639    
640                    // If necessary for non-ASCII characters, encode based on RFC 2184.
641                    // However, not all browsers support RFC 2184. See LEP-3127.
642    
643                    boolean ascii = true;
644    
645                    for (int i = 0; i < fileName.length(); i++) {
646                            if (!Validator.isAscii(fileName.charAt(i))) {
647                                    ascii = false;
648    
649                                    break;
650                            }
651                    }
652    
653                    if (!ascii) {
654                            String encodedFileName = HttpUtil.encodeURL(fileName, true);
655    
656                            if (BrowserSnifferUtil.isIe(request)) {
657                                    contentDispositionFileName =
658                                            "filename=\"" + encodedFileName + "\"";
659                            }
660                            else {
661                                    contentDispositionFileName =
662                                            "filename*=UTF-8''" + encodedFileName;
663                            }
664                    }
665    
666                    if (Validator.isNull(contentDispositionType)) {
667                            String extension = GetterUtil.getString(
668                                    FileUtil.getExtension(fileName)).toLowerCase();
669    
670                            String[] mimeTypesContentDispositionInline = null;
671    
672                            try {
673                                    mimeTypesContentDispositionInline = PropsUtil.getArray(
674                                            PropsKeys.MIME_TYPES_CONTENT_DISPOSITION_INLINE);
675                            }
676                            catch (Exception e) {
677                                    mimeTypesContentDispositionInline = new String[0];
678                            }
679    
680                            if (ArrayUtil.contains(
681                                            mimeTypesContentDispositionInline, extension)) {
682    
683                                    contentDispositionType = HttpHeaders.CONTENT_DISPOSITION_INLINE;
684                            }
685                            else {
686                                    contentDispositionType =
687                                            HttpHeaders.CONTENT_DISPOSITION_ATTACHMENT;
688                            }
689                    }
690    
691                    StringBundler sb = new StringBundler(4);
692    
693                    sb.append(contentDispositionType);
694                    sb.append(StringPool.SEMICOLON);
695                    sb.append(StringPool.SPACE);
696                    sb.append(contentDispositionFileName);
697    
698                    if (_log.isDebugEnabled()) {
699                            _log.debug("Setting content disposition header " + sb.toString());
700                    }
701    
702                    response.setHeader(HttpHeaders.CONTENT_DISPOSITION, sb.toString());
703            }
704    
705            protected static void setHeaders(
706                    HttpServletRequest request, HttpServletResponse response,
707                    String fileName, String contentType, String contentDispositionType,
708                    Range range) {
709    
710                    setHeaders(
711                            request, response, fileName, contentType, contentDispositionType);
712    
713                    if (range != null) {
714                            response.setHeader(
715                                    HttpHeaders.CONTENT_RANGE, range.getContentRange());
716    
717                            response.setHeader(
718                                    HttpHeaders.CONTENT_LENGTH, String.valueOf(range.getLength()));
719                    }
720            }
721    
722            private static final String _CLIENT_ABORT_EXCEPTION =
723                    "org.apache.catalina.connector.ClientAbortException";
724    
725            private static final int _MAX_RANGE_FIELDS = GetterUtil.getInteger(
726                    PropsUtil.get(PropsKeys.WEB_SERVER_SERVLET_MAX_RANGE_FIELDS));
727    
728            private static final String _RANGE_REGEX =
729                    "^bytes=\\d*-\\d*(,\\s?\\d*-\\d*)*$";
730    
731            private static Log _log = LogFactoryUtil.getLog(ServletResponseUtil.class);
732    
733    }