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 boolean isClientAbortException(IOException ioe) {
127                    Class<?> clazz = ioe.getClass();
128    
129                    String className = clazz.getName();
130    
131                    if (className.equals(_CLIENT_ABORT_EXCEPTION)) {
132                            return true;
133                    }
134                    else {
135                            return false;
136                    }
137            }
138    
139            public static void sendFile(
140                            HttpServletRequest request, HttpServletResponse response,
141                            String fileName, byte[] bytes)
142                    throws IOException {
143    
144                    sendFile(request, response, fileName, bytes, null);
145            }
146    
147            public static void sendFile(
148                            HttpServletRequest request, HttpServletResponse response,
149                            String fileName, byte[] bytes, String contentType)
150                    throws IOException {
151    
152                    sendFile(request, response, fileName, bytes, contentType, null);
153            }
154    
155            public static void sendFile(
156                            HttpServletRequest request, HttpServletResponse response,
157                            String fileName, byte[] bytes, String contentType,
158                            String contentDispositionType)
159                    throws IOException {
160    
161                    setHeaders(
162                            request, response, fileName, contentType, contentDispositionType);
163    
164                    write(response, bytes);
165            }
166    
167            public static void sendFile(
168                            HttpServletRequest request, HttpServletResponse response,
169                            String fileName, InputStream is)
170                    throws IOException {
171    
172                    sendFile(request, response, fileName, is, null);
173            }
174    
175            public static void sendFile(
176                            HttpServletRequest request, HttpServletResponse response,
177                            String fileName, InputStream is, long contentLength,
178                            String contentType)
179                    throws IOException {
180    
181                    sendFile(request, response, fileName, is, 0, contentType, null);
182            }
183    
184            public static void sendFile(
185                            HttpServletRequest request, HttpServletResponse response,
186                            String fileName, InputStream is, long contentLength,
187                            String contentType, String contentDispositionType)
188                    throws IOException {
189    
190                    setHeaders(
191                            request, response, fileName, contentType, contentDispositionType);
192    
193                    write(response, is, contentLength);
194            }
195    
196            public static void sendFile(
197                            HttpServletRequest request, HttpServletResponse response,
198                            String fileName, InputStream is, String contentType)
199                    throws IOException {
200    
201                    sendFile(request, response, fileName, is, 0, contentType);
202            }
203    
204            /**
205             * @deprecated As of 6.1.0
206             */
207            public static void sendFile(
208                            HttpServletResponse response, String fileName, byte[] bytes)
209                    throws IOException {
210    
211                    sendFile(null, response, fileName, bytes);
212            }
213    
214            /**
215             * @deprecated As of 6.1.0
216             */
217            public static void sendFile(
218                            HttpServletResponse response, String fileName, byte[] bytes,
219                            String contentType)
220                    throws IOException {
221    
222                    sendFile(null, response, fileName, bytes, contentType);
223            }
224    
225            /**
226             * @deprecated As of 6.1.0
227             */
228            public static void sendFile(
229                            HttpServletResponse response, String fileName, InputStream is)
230                    throws IOException {
231    
232                    sendFile(null, response, fileName, is);
233            }
234    
235            /**
236             * @deprecated As of 6.1.0
237             */
238            public static void sendFile(
239                            HttpServletResponse response, String fileName, InputStream is,
240                            int contentLength, String contentType)
241                    throws IOException {
242    
243                    sendFile(null, response, fileName, is, contentLength, contentType);
244            }
245    
246            /**
247             * @deprecated As of 6.1.0
248             */
249            public static void sendFile(
250                            HttpServletResponse response, String fileName, InputStream is,
251                            String contentType)
252                    throws IOException {
253    
254                    sendFile(null, response, fileName, is, contentType);
255            }
256    
257            public static void write(
258                            HttpServletRequest request, HttpServletResponse response,
259                            String fileName, List<Range> ranges, InputStream inputStream,
260                            long fullLength, String contentType)
261                    throws IOException {
262    
263                    OutputStream outputStream = null;
264    
265                    try {
266                            outputStream = response.getOutputStream();
267    
268                            Range fullRange = new Range(0, fullLength - 1, fullLength);
269    
270                            Range firstRange = null;
271    
272                            if (!ranges.isEmpty()) {
273                                    firstRange = ranges.get(0);
274                            }
275    
276                            if ((firstRange == null) || firstRange.equals(fullRange)) {
277                                    if (_log.isDebugEnabled()) {
278                                            _log.debug("Writing full range");
279                                    }
280    
281                                    response.setContentType(contentType);
282    
283                                    setHeaders(
284                                            request, response, fileName, contentType, null, fullRange);
285    
286                                    copyRange(
287                                            inputStream, outputStream, fullRange.getStart(),
288                                            fullRange.getLength());
289                            }
290                            else if (ranges.size() == 1) {
291                                    if (_log.isDebugEnabled()) {
292                                            _log.debug("Attempting to write a single range");
293                                    }
294    
295                                    Range range = ranges.get(0);
296    
297                                    response.setContentType(contentType);
298    
299                                    setHeaders(
300                                            request, response, fileName, contentType, null, range);
301    
302                                    response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
303    
304                                    copyRange(
305                                            inputStream, outputStream, range.getStart(),
306                                            range.getLength());
307                            }
308                            else if (ranges.size() > 1 ) {
309                                    if (_log.isDebugEnabled()) {
310                                            _log.debug("Attempting to write multiple ranges");
311                                    }
312    
313                                    ServletOutputStream servletOutputStream =
314                                            (ServletOutputStream)outputStream;
315    
316                                    String boundary =
317                                            "liferay-multipart-boundary-" + System.currentTimeMillis();
318    
319                                    String multipartContentType =
320                                            "multipart/byteranges; boundary=" + boundary;
321    
322                                    response.setContentType(multipartContentType);
323    
324                                    setHeaders(
325                                            request, response, fileName, multipartContentType, null);
326    
327                                    response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
328    
329                                    for (int i = 0; i < ranges.size(); i++) {
330                                            Range range = ranges.get(i);
331    
332                                            servletOutputStream.println();
333                                            servletOutputStream.println(
334                                                    StringPool.DOUBLE_DASH + boundary);
335                                            servletOutputStream.println(
336                                                    HttpHeaders.CONTENT_TYPE + ": " + contentType);
337                                            servletOutputStream.println(
338                                                    HttpHeaders.CONTENT_RANGE + ": " +
339                                                            range.getContentRange());
340                                            servletOutputStream.println();
341    
342                                            copyRange(
343                                                    inputStream, outputStream, range.getStart(),
344                                                    range.getLength());
345                                    }
346    
347                                    servletOutputStream.println();
348                                    servletOutputStream.println(
349                                            StringPool.DOUBLE_DASH + boundary + StringPool.DOUBLE_DASH);
350                            }
351                    }
352                    finally {
353                            try {
354                                    inputStream.close();
355                            }
356                            catch (IOException ioe) {
357                            }
358                    }
359            }
360    
361            public static void write(
362                            HttpServletResponse response,
363                            BufferCacheServletResponse bufferCacheServletResponse)
364                    throws IOException {
365    
366                    if (bufferCacheServletResponse.isByteMode()) {
367                            write(response, bufferCacheServletResponse.getByteBuffer());
368                    }
369                    else if (bufferCacheServletResponse.isCharMode()) {
370                            write(response, bufferCacheServletResponse.getCharBuffer());
371                    }
372            }
373    
374            public static void write(HttpServletResponse response, byte[] bytes)
375                    throws IOException {
376    
377                    write(response, bytes, 0, 0);
378            }
379    
380            public static void write(
381                            HttpServletResponse response, byte[] bytes, int offset,
382                            int contentLength)
383                    throws IOException {
384    
385                    try {
386    
387                            // LEP-3122
388    
389                            if (!response.isCommitted()) {
390    
391                                    // LEP-536
392    
393                                    if (contentLength == 0) {
394                                            contentLength = bytes.length;
395                                    }
396    
397                                    response.setContentLength(contentLength);
398    
399                                    response.flushBuffer();
400    
401                                    if (response instanceof BufferCacheServletResponse) {
402                                            BufferCacheServletResponse bufferCacheServletResponse =
403                                                    (BufferCacheServletResponse)response;
404    
405                                            bufferCacheServletResponse.setByteBuffer(
406                                                    ByteBuffer.wrap(bytes, offset, contentLength));
407                                    }
408                                    else {
409                                            ServletOutputStream servletOutputStream =
410                                                    response.getOutputStream();
411    
412                                            if ((contentLength == 0) && ServerDetector.isJetty()) {
413                                            }
414                                            else {
415                                                    servletOutputStream.write(bytes, offset, contentLength);
416                                            }
417                                    }
418                            }
419                    }
420                    catch (IOException ioe) {
421                            if ((ioe instanceof SocketException) ||
422                                    isClientAbortException(ioe)) {
423    
424                                    if (_log.isWarnEnabled()) {
425                                            _log.warn(ioe);
426                                    }
427                            }
428                            else {
429                                    throw ioe;
430                            }
431                    }
432            }
433    
434            public static void write(HttpServletResponse response, byte[][] bytesArray)
435                    throws IOException {
436    
437                    try {
438    
439                            // LEP-3122
440    
441                            if (!response.isCommitted()) {
442                                    int contentLength = 0;
443    
444                                    for (byte[] bytes : bytesArray) {
445                                            contentLength += bytes.length;
446                                    }
447    
448                                    response.setContentLength(contentLength);
449    
450                                    response.flushBuffer();
451    
452                                    ServletOutputStream servletOutputStream =
453                                            response.getOutputStream();
454    
455                                    for (byte[] bytes : bytesArray) {
456                                            servletOutputStream.write(bytes);
457                                    }
458                            }
459                    }
460                    catch (IOException ioe) {
461                            if ((ioe instanceof SocketException) ||
462                                    isClientAbortException(ioe)) {
463    
464                                    if (_log.isWarnEnabled()) {
465                                            _log.warn(ioe);
466                                    }
467                            }
468                            else {
469                                    throw ioe;
470                            }
471                    }
472            }
473    
474            public static void write(
475                            HttpServletResponse response, ByteBuffer byteBuffer)
476                    throws IOException {
477    
478                    if (response instanceof BufferCacheServletResponse) {
479                            BufferCacheServletResponse bufferCacheServletResponse =
480                                    (BufferCacheServletResponse)response;
481    
482                            bufferCacheServletResponse.setByteBuffer(byteBuffer);
483                    }
484                    else {
485                            write(
486                                    response, byteBuffer.array(), byteBuffer.position(),
487                                    byteBuffer.limit());
488                    }
489            }
490    
491            public static void write(
492                            HttpServletResponse response, CharBuffer charBuffer)
493                    throws IOException {
494    
495                    if (response instanceof BufferCacheServletResponse) {
496                            BufferCacheServletResponse bufferCacheServletResponse =
497                                    (BufferCacheServletResponse)response;
498    
499                            bufferCacheServletResponse.setCharBuffer(charBuffer);
500                    }
501                    else {
502                            ByteBuffer byteBuffer = CharsetEncoderUtil.encode(
503                                    StringPool.UTF8, charBuffer);
504    
505                            write(response, byteBuffer);
506                    }
507            }
508    
509            public static void write(HttpServletResponse response, File file)
510                    throws IOException {
511    
512                    if (response instanceof BufferCacheServletResponse) {
513                            BufferCacheServletResponse bufferCacheServletResponse =
514                                    (BufferCacheServletResponse)response;
515    
516                            ByteBuffer byteBuffer = ByteBuffer.wrap(FileUtil.getBytes(file));
517    
518                            bufferCacheServletResponse.setByteBuffer(byteBuffer);
519                    }
520                    else {
521                            FileInputStream fileInputStream = new FileInputStream(file);
522    
523                            FileChannel fileChannel = fileInputStream.getChannel();
524    
525                            try {
526                                    int contentLength = (int)fileChannel.size();
527    
528                                    response.setContentLength(contentLength);
529    
530                                    response.flushBuffer();
531    
532                                    fileChannel.transferTo(
533                                            0, contentLength,
534                                            Channels.newChannel(response.getOutputStream()));
535                            }
536                            finally {
537                                    fileChannel.close();
538                            }
539                    }
540            }
541    
542            public static void write(HttpServletResponse response, InputStream is)
543                    throws IOException {
544    
545                    write(response, is, 0);
546            }
547    
548            public static void write(
549                            HttpServletResponse response, InputStream is, long contentLength)
550                    throws IOException {
551    
552                    if (response.isCommitted()) {
553                            return;
554                    }
555    
556                    if (contentLength > 0) {
557                            response.setHeader(
558                                    HttpHeaders.CONTENT_LENGTH, String.valueOf(contentLength));
559                    }
560    
561                    response.flushBuffer();
562    
563                    StreamUtil.transfer(is, response.getOutputStream());
564            }
565    
566            public static void write(HttpServletResponse response, String s)
567                    throws IOException {
568    
569                    if (response instanceof BufferCacheServletResponse) {
570                            BufferCacheServletResponse bufferCacheServletResponse =
571                                    (BufferCacheServletResponse)response;
572    
573                            bufferCacheServletResponse.setString(s);
574                    }
575                    else {
576                            ByteBuffer byteBuffer = CharsetEncoderUtil.encode(
577                                    StringPool.UTF8, s);
578    
579                            write(response, byteBuffer);
580                    }
581            }
582    
583            protected static void copyRange(
584                            InputStream inputStream, OutputStream outputStream, long start,
585                            long length)
586                    throws IOException {
587    
588                    if (inputStream instanceof FileInputStream) {
589                            FileInputStream fileInputStream = (FileInputStream)inputStream;
590    
591                            FileChannel fileChannel = fileInputStream.getChannel();
592    
593                            fileChannel.transferTo(
594                                    start, length, Channels.newChannel(outputStream));
595                    }
596                    else if (inputStream instanceof ByteArrayInputStream) {
597                            ByteArrayInputStream byteArrayInputStream =
598                                    (ByteArrayInputStream)inputStream;
599    
600                            byteArrayInputStream.skip(start);
601    
602                            StreamUtil.transfer(byteArrayInputStream, outputStream, length);
603                    }
604                    else {
605                            RandomAccessInputStream randomAccessInputStream =
606                                    new RandomAccessInputStream(inputStream);
607    
608                            randomAccessInputStream.seek(start);
609    
610                            StreamUtil.transfer(randomAccessInputStream, outputStream, length);
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));
669    
670                            extension = StringUtil.toLowerCase(extension);
671    
672                            String[] mimeTypesContentDispositionInline = null;
673    
674                            try {
675                                    mimeTypesContentDispositionInline = PropsUtil.getArray(
676                                            PropsKeys.MIME_TYPES_CONTENT_DISPOSITION_INLINE);
677                            }
678                            catch (Exception e) {
679                                    mimeTypesContentDispositionInline = new String[0];
680                            }
681    
682                            if (ArrayUtil.contains(
683                                            mimeTypesContentDispositionInline, extension)) {
684    
685                                    contentDispositionType = HttpHeaders.CONTENT_DISPOSITION_INLINE;
686                            }
687                            else {
688                                    contentDispositionType =
689                                            HttpHeaders.CONTENT_DISPOSITION_ATTACHMENT;
690                            }
691                    }
692    
693                    StringBundler sb = new StringBundler(4);
694    
695                    sb.append(contentDispositionType);
696                    sb.append(StringPool.SEMICOLON);
697                    sb.append(StringPool.SPACE);
698                    sb.append(contentDispositionFileName);
699    
700                    if (_log.isDebugEnabled()) {
701                            _log.debug("Setting content disposition header " + sb.toString());
702                    }
703    
704                    response.setHeader(HttpHeaders.CONTENT_DISPOSITION, sb.toString());
705            }
706    
707            protected static void setHeaders(
708                    HttpServletRequest request, HttpServletResponse response,
709                    String fileName, String contentType, String contentDispositionType,
710                    Range range) {
711    
712                    setHeaders(
713                            request, response, fileName, contentType, contentDispositionType);
714    
715                    if (range != null) {
716                            response.setHeader(
717                                    HttpHeaders.CONTENT_RANGE, range.getContentRange());
718    
719                            response.setHeader(
720                                    HttpHeaders.CONTENT_LENGTH, String.valueOf(range.getLength()));
721                    }
722            }
723    
724            private static final String _CLIENT_ABORT_EXCEPTION =
725                    "org.apache.catalina.connector.ClientAbortException";
726    
727            private static final int _MAX_RANGE_FIELDS = GetterUtil.getInteger(
728                    PropsUtil.get(PropsKeys.WEB_SERVER_SERVLET_MAX_RANGE_FIELDS));
729    
730            private static final String _RANGE_REGEX =
731                    "^bytes=\\d*-\\d*(,\\s?\\d*-\\d*)*$";
732    
733            private static Log _log = LogFactoryUtil.getLog(ServletResponseUtil.class);
734    
735    }