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