Java tutorial
/** * Copyright (c) 2015 Bosch Software Innovations GmbH and others. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ package org.eclipse.hawkbit.rest.util; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.math.RoundingMode; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.eclipse.hawkbit.artifact.repository.model.DbArtifact; import org.eclipse.hawkbit.repository.ControllerManagement; import org.eclipse.hawkbit.repository.model.ActionStatus; import org.eclipse.hawkbit.repository.model.Artifact; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import com.google.common.base.Preconditions; import com.google.common.math.DoubleMath; import com.google.common.net.HttpHeaders; /** * Utility class for the Rest Source API. */ public final class RestResourceConversionHelper { private static final Logger LOG = LoggerFactory.getLogger(RestResourceConversionHelper.class); private static final int BUFFER_SIZE = 4096; private RestResourceConversionHelper() { } /** * Write response without target relation. * * @param artifact * the artifact * @param servletResponse * to be sent back to the requesting client * @param request * from the client * @param file * to be write to the client response * * @return http code */ public static ResponseEntity<InputStream> writeFileResponse(final Artifact artifact, final HttpServletResponse servletResponse, final HttpServletRequest request, final DbArtifact file) { return writeFileResponse(artifact, servletResponse, request, file, null, null); } /** * <p> * Write response with target relation and publishes events concerning the * download progress based on given update action status. * </p> * * <p> * The request supports RFC7233 range requests. * </p> * * @param artifact * the artifact * @param response * to be sent back to the requesting client * @param request * from the client * @param file * to be write to the client response * @param controllerManagement * to write progress updates to * @param statusId * of the {@link ActionStatus} * * @return http code * * @see <a href="https://tools.ietf.org/html/rfc7233">https://tools.ietf.org * /html/rfc7233</a> */ public static ResponseEntity<InputStream> writeFileResponse(final Artifact artifact, final HttpServletResponse response, final HttpServletRequest request, final DbArtifact file, final ControllerManagement controllerManagement, final Long statusId) { ResponseEntity<InputStream> result; final String etag = artifact.getSha1Hash(); final Long lastModified = artifact.getLastModifiedAt() != null ? artifact.getLastModifiedAt() : artifact.getCreatedAt(); final long length = file.getSize(); response.reset(); response.setBufferSize(BUFFER_SIZE); response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + artifact.getFilename()); response.setHeader(HttpHeaders.ETAG, etag); response.setHeader(HttpHeaders.ACCEPT_RANGES, "bytes"); response.setDateHeader(HttpHeaders.LAST_MODIFIED, lastModified); response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE); final ByteRange full = new ByteRange(0, length - 1, length); final List<ByteRange> ranges = new ArrayList<>(); // Validate and process Range and If-Range headers. final String range = request.getHeader("Range"); if (range != null) { LOG.debug("range header for filename ({}) is: {}", artifact.getFilename(), range); // Range header matches"bytes=n-n,n-n,n-n..." if (!range.matches("^bytes=\\d*-\\d*(,\\d*-\\d*)*$")) { response.setHeader(HttpHeaders.CONTENT_RANGE, "bytes */" + length); LOG.debug("range header for filename ({}) is not satisfiable: ", artifact.getFilename()); return new ResponseEntity<>(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE); } // RFC: if the representation is unchanged, send me the part(s) that // I am requesting in // Range; otherwise, send me the entire representation. checkForShortcut(request, etag, lastModified, full, ranges); // it seems there are valid ranges result = extractRange(response, length, ranges, range); // return if range extraction turned out to be invalid if (result != null) { return result; } } // full request - no range if (ranges.isEmpty() || ranges.get(0).equals(full)) { LOG.debug("filename ({}) results into a full request: ", artifact.getFilename()); handleFullFileRequest(artifact, response, file, controllerManagement, statusId, full); result = new ResponseEntity<>(HttpStatus.OK); } // standard range request else if (ranges.size() == 1) { LOG.debug("filename ({}) results into a standard range request: ", artifact.getFilename()); handleStandardRangeRequest(artifact, response, file, controllerManagement, statusId, ranges); result = new ResponseEntity<>(HttpStatus.PARTIAL_CONTENT); } // multipart range request else { LOG.debug("filename ({}) results into a multipart range request: ", artifact.getFilename()); handleMultipartRangeRequest(artifact, response, file, controllerManagement, statusId, ranges); result = new ResponseEntity<>(HttpStatus.PARTIAL_CONTENT); } return result; } private static void handleFullFileRequest(final Artifact artifact, final HttpServletResponse response, final DbArtifact file, final ControllerManagement controllerManagement, final Long statusId, final ByteRange full) { final ByteRange r = full; response.setHeader(HttpHeaders.CONTENT_RANGE, "bytes " + r.getStart() + "-" + r.getEnd() + "/" + r.getTotal()); response.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(r.getLength())); try (InputStream inputStream = file.getFileInputStream()) { copyStreams(inputStream, response.getOutputStream(), controllerManagement, statusId, r.getStart(), r.getLength()); } catch (final IOException e) { LOG.error("fullfileRequest of file ({}) failed!", artifact.getFilename(), e); throw new FileSteamingFailedException(artifact.getFilename()); } } private static ResponseEntity<InputStream> extractRange(final HttpServletResponse response, final long length, final List<ByteRange> ranges, final String range) { if (ranges.isEmpty()) { for (final String part : range.substring(6).split(",")) { long start = sublong(part, 0, part.indexOf('-')); long end = sublong(part, part.indexOf('-') + 1, part.length()); if (start == -1) { start = length - end; end = length - 1; } else if (end == -1 || end > length - 1) { end = length - 1; } // Check if Range is syntactically valid. If not, then return // 416. if (start > end) { response.setHeader(HttpHeaders.CONTENT_RANGE, "bytes */" + length); return new ResponseEntity<>(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE); } // Add range. ranges.add(new ByteRange(start, end, length)); } } return null; } private static long sublong(final String value, final int beginIndex, final int endIndex) { final String substring = value.substring(beginIndex, endIndex); return substring.length() > 0 ? Long.parseLong(substring) : -1; } private static void checkForShortcut(final HttpServletRequest request, final String etag, final long lastModified, final ByteRange full, final List<ByteRange> ranges) { final String ifRange = request.getHeader(HttpHeaders.IF_RANGE); if (ifRange != null && !ifRange.equals(etag)) { try { final long ifRangeTime = request.getDateHeader(HttpHeaders.IF_RANGE); if (ifRangeTime != -1 && ifRangeTime + 1000 < lastModified) { ranges.add(full); } } catch (final IllegalArgumentException ignore) { LOG.info("Invalid if-range header field", ignore); ranges.add(full); } } } private static void handleMultipartRangeRequest(final Artifact artifact, final HttpServletResponse response, final DbArtifact file, final ControllerManagement controllerManagement, final Long statusId, final List<ByteRange> ranges) { response.setContentType("multipart/byteranges; boundary=" + ByteRange.MULTIPART_BOUNDARY); response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); for (final ByteRange r : ranges) { try (InputStream inputStream = file.getFileInputStream()) { // Add multipart boundary and header fields for every range. response.getOutputStream().println(); response.getOutputStream().println("--" + ByteRange.MULTIPART_BOUNDARY); response.getOutputStream() .println("Content-Range: bytes " + r.getStart() + "-" + r.getEnd() + "/" + r.getTotal()); // Copy single part range of multi part range. copyStreams(inputStream, response.getOutputStream(), controllerManagement, statusId, r.getStart(), r.getLength()); } catch (final IOException e) { throwFileStreamingFailedException(artifact, e); } } try { // End with final multipart boundary. response.getOutputStream().println(); response.getOutputStream().print("--" + ByteRange.MULTIPART_BOUNDARY + "--"); } catch (final IOException e) { throwFileStreamingFailedException(artifact, e); } } private static void throwFileStreamingFailedException(final Artifact artifact, final IOException e) { LOG.error("multipartRangeRequest of file ({}) failed!", artifact.getFilename(), e); throw new FileSteamingFailedException(artifact.getFilename()); } private static void handleStandardRangeRequest(final Artifact artifact, final HttpServletResponse response, final DbArtifact file, final ControllerManagement controllerManagement, final Long statusId, final List<ByteRange> ranges) { final ByteRange r = ranges.get(0); response.setHeader(HttpHeaders.CONTENT_RANGE, "bytes " + r.getStart() + "-" + r.getEnd() + "/" + r.getTotal()); response.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(r.getLength())); response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); try (InputStream inputStream = file.getFileInputStream()) { copyStreams(inputStream, response.getOutputStream(), controllerManagement, statusId, r.getStart(), r.getLength()); } catch (final IOException e) { LOG.error("standardRangeRequest of file ({}) failed!", artifact.getFilename(), e); throw new FileSteamingFailedException(artifact.getFilename()); } } private static long copyStreams(final InputStream from, final OutputStream to, final ControllerManagement controllerManagement, final Long statusId, final long start, final long length) throws IOException { Preconditions.checkNotNull(from); Preconditions.checkNotNull(to); final byte[] buf = new byte[BUFFER_SIZE]; long total = 0; int progressPercent = 1; // skipp until start is reached long skipped = 0; do { skipped += from.skip(start); } while (skipped < start); long toRead = length; boolean toContinue = true; long shippedSinceLastEvent = 0; while (toContinue) { final int r = from.read(buf); if (r == -1) { break; } toRead -= r; if (toRead > 0) { to.write(buf, 0, r); total += r; shippedSinceLastEvent += r; } else { to.write(buf, 0, (int) toRead + r); total += toRead + r; shippedSinceLastEvent += toRead + r; toContinue = false; } if (controllerManagement != null) { final int newPercent = DoubleMath.roundToInt(total * 100.0 / length, RoundingMode.DOWN); // every 10 percent an event if (newPercent == 100 || newPercent > progressPercent + 10) { progressPercent = newPercent; controllerManagement.downloadProgress(statusId, length, shippedSinceLastEvent, total); shippedSinceLastEvent = 0; } } } return total; } /** * Checks given CSV string for defined match value or * wildcard. * * @param matchHeader * to search through * @param toMatch * to search for * * @return <code>true</code> if string matches. */ public static boolean matchesHttpHeader(final String matchHeader, final String toMatch) { final String[] matchValues = matchHeader.split("\\s*,\\s*"); Arrays.sort(matchValues); return Arrays.binarySearch(matchValues, toMatch) > -1 || Arrays.binarySearch(matchValues, "*") > -1; } }