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