001
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
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<>();
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
210 @Deprecated
211 public static void sendFile(
212 HttpServletResponse response, String fileName, byte[] bytes)
213 throws IOException {
214
215 sendFile(null, response, fileName, bytes);
216 }
217
218
221 @Deprecated
222 public static void sendFile(
223 HttpServletResponse response, String fileName, byte[] bytes,
224 String contentType)
225 throws IOException {
226
227 sendFile(null, response, fileName, bytes, contentType);
228 }
229
230
233 @Deprecated
234 public static void sendFile(
235 HttpServletResponse response, String fileName,
236 InputStream inputStream)
237 throws IOException {
238
239 sendFile(null, response, fileName, inputStream);
240 }
241
242
245 @Deprecated
246 public static void sendFile(
247 HttpServletResponse response, String fileName,
248 InputStream inputStream, int contentLength, String contentType)
249 throws IOException {
250
251 sendFile(
252 null, response, fileName, inputStream, contentLength, contentType);
253 }
254
255
258 @Deprecated
259 public static void sendFile(
260 HttpServletResponse response, String fileName,
261 InputStream inputStream, String contentType)
262 throws IOException {
263
264 sendFile(null, response, fileName, inputStream, contentType);
265 }
266
267 public static void sendFileWithRangeHeader(
268 HttpServletRequest request, HttpServletResponse response,
269 String fileName, InputStream inputStream, long contentLength,
270 String contentType)
271 throws IOException {
272
273 if (_log.isDebugEnabled()) {
274 _log.debug("Accepting ranges for the file " + fileName);
275 }
276
277 response.setHeader(
278 HttpHeaders.ACCEPT_RANGES, HttpHeaders.ACCEPT_RANGES_BYTES_VALUE);
279
280 List<Range> ranges = null;
281
282 try {
283 ranges = getRanges(request, response, contentLength);
284 }
285 catch (IOException ioe) {
286 if (_log.isErrorEnabled()) {
287 _log.error(ioe);
288 }
289
290 response.setHeader(
291 HttpHeaders.CONTENT_RANGE, "bytes */" + contentLength);
292
293 response.sendError(
294 HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
295
296 return;
297 }
298
299 if ((ranges == null) || ranges.isEmpty()) {
300 sendFile(
301 request, response, fileName, inputStream, contentLength,
302 contentType);
303 }
304 else {
305 if (_log.isDebugEnabled()) {
306 _log.debug(
307 "Request has range header " +
308 request.getHeader(HttpHeaders.RANGE));
309 }
310
311 write(
312 request, response, fileName, ranges, inputStream, contentLength,
313 contentType);
314 }
315 }
316
317 public static void write(
318 HttpServletRequest request, HttpServletResponse response,
319 String fileName, List<Range> ranges, InputStream inputStream,
320 long fullLength, String contentType)
321 throws IOException {
322
323 OutputStream outputStream = null;
324
325 try {
326 outputStream = response.getOutputStream();
327
328 Range fullRange = new Range(0, fullLength - 1, fullLength);
329
330 Range firstRange = null;
331
332 if (!ranges.isEmpty()) {
333 firstRange = ranges.get(0);
334 }
335
336 if ((firstRange == null) || firstRange.equals(fullRange)) {
337 if (_log.isDebugEnabled()) {
338 _log.debug("Writing full range");
339 }
340
341 response.setContentType(contentType);
342
343 setHeaders(
344 request, response, fileName, contentType, null, fullRange);
345
346 copyRange(
347 inputStream, outputStream, fullRange.getStart(),
348 fullRange.getLength());
349 }
350 else if (ranges.size() == 1) {
351 if (_log.isDebugEnabled()) {
352 _log.debug("Attempting to write a single range");
353 }
354
355 Range range = ranges.get(0);
356
357 response.setContentType(contentType);
358
359 setHeaders(
360 request, response, fileName, contentType, null, range);
361
362 response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
363
364 copyRange(
365 inputStream, outputStream, range.getStart(),
366 range.getLength());
367 }
368 else if (ranges.size() > 1 ) {
369 if (_log.isDebugEnabled()) {
370 _log.debug("Attempting to write multiple ranges");
371 }
372
373 ServletOutputStream servletOutputStream =
374 (ServletOutputStream)outputStream;
375
376 String boundary =
377 "liferay-multipart-boundary-" + System.currentTimeMillis();
378
379 String multipartContentType =
380 "multipart/byteranges; boundary=" + boundary;
381
382 response.setContentType(multipartContentType);
383
384 setHeaders(
385 request, response, fileName, multipartContentType, null);
386
387 response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
388
389 for (int i = 0; i < ranges.size(); i++) {
390 Range range = ranges.get(i);
391
392 servletOutputStream.println();
393 servletOutputStream.println(
394 StringPool.DOUBLE_DASH + boundary);
395 servletOutputStream.println(
396 HttpHeaders.CONTENT_TYPE + ": " + contentType);
397 servletOutputStream.println(
398 HttpHeaders.CONTENT_RANGE + ": " +
399 range.getContentRange());
400 servletOutputStream.println();
401
402 inputStream = copyRange(
403 inputStream, outputStream, range.getStart(),
404 range.getLength());
405 }
406
407 servletOutputStream.println();
408 servletOutputStream.println(
409 StringPool.DOUBLE_DASH + boundary + StringPool.DOUBLE_DASH);
410 }
411 }
412 finally {
413 try {
414 inputStream.close();
415 }
416 catch (IOException ioe) {
417 }
418 }
419 }
420
421 public static void write(
422 HttpServletResponse response,
423 BufferCacheServletResponse bufferCacheServletResponse)
424 throws IOException {
425
426 if (bufferCacheServletResponse.isByteMode()) {
427 write(response, bufferCacheServletResponse.getByteBuffer());
428 }
429 else if (bufferCacheServletResponse.isCharMode()) {
430 write(response, bufferCacheServletResponse.getCharBuffer());
431 }
432 }
433
434 public static void write(HttpServletResponse response, byte[] bytes)
435 throws IOException {
436
437 write(response, bytes, 0, 0);
438 }
439
440 public static void write(
441 HttpServletResponse response, byte[] bytes, int offset,
442 int contentLength)
443 throws IOException {
444
445 try {
446
447
448
449 if (!response.isCommitted()) {
450
451
452
453 if (contentLength == 0) {
454 contentLength = bytes.length;
455 }
456
457 response.setContentLength(contentLength);
458
459 response.flushBuffer();
460
461 if (response instanceof BufferCacheServletResponse) {
462 BufferCacheServletResponse bufferCacheServletResponse =
463 (BufferCacheServletResponse)response;
464
465 bufferCacheServletResponse.setByteBuffer(
466 ByteBuffer.wrap(bytes, offset, contentLength));
467 }
468 else {
469 ServletOutputStream servletOutputStream =
470 response.getOutputStream();
471
472 if ((contentLength == 0) && ServerDetector.isJetty()) {
473 }
474 else {
475 servletOutputStream.write(bytes, offset, contentLength);
476 }
477 }
478 }
479 }
480 catch (IOException ioe) {
481 if ((ioe instanceof SocketException) ||
482 isClientAbortException(ioe)) {
483
484 if (_log.isWarnEnabled()) {
485 _log.warn(ioe);
486 }
487 }
488 else {
489 throw ioe;
490 }
491 }
492 }
493
494 public static void write(HttpServletResponse response, byte[][] bytesArray)
495 throws IOException {
496
497 try {
498
499
500
501 if (!response.isCommitted()) {
502 long contentLength = 0;
503
504 for (byte[] bytes : bytesArray) {
505 contentLength += bytes.length;
506 }
507
508 setContentLength(response, contentLength);
509
510 response.flushBuffer();
511
512 ServletOutputStream servletOutputStream =
513 response.getOutputStream();
514
515 for (byte[] bytes : bytesArray) {
516 servletOutputStream.write(bytes);
517 }
518 }
519 }
520 catch (IOException ioe) {
521 if ((ioe instanceof SocketException) ||
522 isClientAbortException(ioe)) {
523
524 if (_log.isWarnEnabled()) {
525 _log.warn(ioe);
526 }
527 }
528 else {
529 throw ioe;
530 }
531 }
532 }
533
534 public static void write(
535 HttpServletResponse response, ByteBuffer byteBuffer)
536 throws IOException {
537
538 if (response instanceof BufferCacheServletResponse) {
539 BufferCacheServletResponse bufferCacheServletResponse =
540 (BufferCacheServletResponse)response;
541
542 bufferCacheServletResponse.setByteBuffer(byteBuffer);
543 }
544 else {
545 write(
546 response, byteBuffer.array(),
547 byteBuffer.arrayOffset() + byteBuffer.position(),
548 byteBuffer.arrayOffset() + byteBuffer.limit());
549 }
550 }
551
552 public static void write(
553 HttpServletResponse response, CharBuffer charBuffer)
554 throws IOException {
555
556 if (response instanceof BufferCacheServletResponse) {
557 BufferCacheServletResponse bufferCacheServletResponse =
558 (BufferCacheServletResponse)response;
559
560 bufferCacheServletResponse.setCharBuffer(charBuffer);
561 }
562 else {
563 ByteBuffer byteBuffer = CharsetEncoderUtil.encode(
564 StringPool.UTF8, charBuffer);
565
566 write(response, byteBuffer);
567 }
568 }
569
570 public static void write(HttpServletResponse response, File file)
571 throws IOException {
572
573 if (response instanceof BufferCacheServletResponse) {
574 BufferCacheServletResponse bufferCacheServletResponse =
575 (BufferCacheServletResponse)response;
576
577 ByteBuffer byteBuffer = ByteBuffer.wrap(FileUtil.getBytes(file));
578
579 bufferCacheServletResponse.setByteBuffer(byteBuffer);
580 }
581 else {
582 FileInputStream fileInputStream = new FileInputStream(file);
583
584 try (FileChannel fileChannel = fileInputStream.getChannel()) {
585 long contentLength = fileChannel.size();
586
587 setContentLength(response, contentLength);
588
589 response.flushBuffer();
590
591 fileChannel.transferTo(
592 0, contentLength,
593 Channels.newChannel(response.getOutputStream()));
594 }
595 }
596 }
597
598 public static void write(
599 HttpServletResponse response, InputStream inputStream)
600 throws IOException {
601
602 write(response, inputStream, 0);
603 }
604
605 public static void write(
606 HttpServletResponse response, InputStream inputStream,
607 long contentLength)
608 throws IOException {
609
610 if (response.isCommitted()) {
611 StreamUtil.cleanUp(inputStream);
612
613 return;
614 }
615
616 if (contentLength > 0) {
617 response.setHeader(
618 HttpHeaders.CONTENT_LENGTH, String.valueOf(contentLength));
619 }
620
621 response.flushBuffer();
622
623 StreamUtil.transfer(inputStream, response.getOutputStream());
624 }
625
626 public static void write(HttpServletResponse response, String s)
627 throws IOException {
628
629 if (response instanceof BufferCacheServletResponse) {
630 BufferCacheServletResponse bufferCacheServletResponse =
631 (BufferCacheServletResponse)response;
632
633 bufferCacheServletResponse.setString(s);
634 }
635 else {
636 ByteBuffer byteBuffer = CharsetEncoderUtil.encode(
637 StringPool.UTF8, s);
638
639 write(response, byteBuffer);
640 }
641 }
642
643 protected static InputStream copyRange(
644 InputStream inputStream, OutputStream outputStream, long start,
645 long length)
646 throws IOException {
647
648 if (inputStream instanceof FileInputStream) {
649 FileInputStream fileInputStream = (FileInputStream)inputStream;
650
651 FileChannel fileChannel = fileInputStream.getChannel();
652
653 fileChannel.transferTo(
654 start, length, Channels.newChannel(outputStream));
655
656 return fileInputStream;
657 }
658 else if (inputStream instanceof ByteArrayInputStream) {
659 ByteArrayInputStream byteArrayInputStream =
660 (ByteArrayInputStream)inputStream;
661
662 byteArrayInputStream.reset();
663
664 byteArrayInputStream.skip(start);
665
666 StreamUtil.transfer(byteArrayInputStream, outputStream, length);
667
668 return byteArrayInputStream;
669 }
670 else if (inputStream instanceof RandomAccessInputStream) {
671 RandomAccessInputStream randomAccessInputStream =
672 (RandomAccessInputStream)inputStream;
673
674 randomAccessInputStream.seek(start);
675
676 StreamUtil.transfer(
677 randomAccessInputStream, outputStream, StreamUtil.BUFFER_SIZE,
678 false, length);
679
680 return randomAccessInputStream;
681 }
682
683 return copyRange(
684 new RandomAccessInputStream(inputStream), outputStream, start,
685 length);
686 }
687
688 protected static void setContentLength(
689 HttpServletResponse response, long contentLength) {
690
691 response.setHeader(
692 HttpHeaders.CONTENT_LENGTH, String.valueOf(contentLength));
693 }
694
695 protected static void setHeaders(
696 HttpServletRequest request, HttpServletResponse response,
697 String fileName, String contentType, String contentDispositionType) {
698
699 if (_log.isDebugEnabled()) {
700 _log.debug("Sending file of type " + contentType);
701 }
702
703
704
705 if (Validator.isNotNull(contentType)) {
706 response.setContentType(contentType);
707 }
708
709 if (!response.containsHeader(HttpHeaders.CACHE_CONTROL)) {
710 response.setHeader(
711 HttpHeaders.CACHE_CONTROL,
712 HttpHeaders.CACHE_CONTROL_PRIVATE_VALUE);
713 }
714
715 if (Validator.isNull(fileName)) {
716 return;
717 }
718
719 String contentDispositionFileName = "filename=\"" + fileName + "\"";
720
721
722
723
724 boolean ascii = true;
725
726 for (int i = 0; i < fileName.length(); i++) {
727 if (!Validator.isAscii(fileName.charAt(i))) {
728 ascii = false;
729
730 break;
731 }
732 }
733
734 if (!ascii) {
735 String encodedFileName = HttpUtil.encodeURL(fileName, true);
736
737 if (BrowserSnifferUtil.isIe(request)) {
738 contentDispositionFileName =
739 "filename=\"" + encodedFileName + "\"";
740 }
741 else {
742 contentDispositionFileName =
743 "filename*=UTF-8''" + encodedFileName;
744 }
745 }
746
747 if (Validator.isNull(contentDispositionType)) {
748 String extension = GetterUtil.getString(
749 FileUtil.getExtension(fileName));
750
751 extension = StringUtil.toLowerCase(extension);
752
753 String[] mimeTypesContentDispositionInline = null;
754
755 try {
756 mimeTypesContentDispositionInline = PropsUtil.getArray(
757 PropsKeys.MIME_TYPES_CONTENT_DISPOSITION_INLINE);
758 }
759 catch (Exception e) {
760 mimeTypesContentDispositionInline = new String[0];
761 }
762
763 if (ArrayUtil.contains(
764 mimeTypesContentDispositionInline, extension)) {
765
766 contentDispositionType = HttpHeaders.CONTENT_DISPOSITION_INLINE;
767
768 contentType = MimeTypesUtil.getContentType(fileName);
769
770 response.setContentType(contentType);
771 }
772 else {
773 contentDispositionType =
774 HttpHeaders.CONTENT_DISPOSITION_ATTACHMENT;
775 }
776 }
777
778 StringBundler sb = new StringBundler(4);
779
780 sb.append(contentDispositionType);
781 sb.append(StringPool.SEMICOLON);
782 sb.append(StringPool.SPACE);
783 sb.append(contentDispositionFileName);
784
785 if (_log.isDebugEnabled()) {
786 _log.debug("Setting content disposition header " + sb.toString());
787 }
788
789 response.setHeader(HttpHeaders.CONTENT_DISPOSITION, sb.toString());
790 }
791
792 protected static void setHeaders(
793 HttpServletRequest request, HttpServletResponse response,
794 String fileName, String contentType, String contentDispositionType,
795 Range range) {
796
797 setHeaders(
798 request, response, fileName, contentType, contentDispositionType);
799
800 if (range != null) {
801 response.setHeader(
802 HttpHeaders.CONTENT_RANGE, range.getContentRange());
803
804 response.setHeader(
805 HttpHeaders.CONTENT_LENGTH, String.valueOf(range.getLength()));
806 }
807 }
808
809 private static final String _CLIENT_ABORT_EXCEPTION =
810 "org.apache.catalina.connector.ClientAbortException";
811
812 private static final int _MAX_RANGE_FIELDS = GetterUtil.getInteger(
813 PropsUtil.get(PropsKeys.WEB_SERVER_SERVLET_MAX_RANGE_FIELDS));
814
815 private static final String _RANGE_REGEX =
816 "^bytes=\\d*-\\d*(,\\s?\\d*-\\d*)*$";
817
818 private static final Log _log = LogFactoryUtil.getLog(
819 ServletResponseUtil.class);
820
821 }