| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.sling.servlets.get.impl.helpers; |
| |
| import static javax.servlet.http.HttpServletResponse.SC_NOT_MODIFIED; |
| import static org.apache.sling.api.servlets.HttpConstants.HEADER_IF_MODIFIED_SINCE; |
| import static org.apache.sling.api.servlets.HttpConstants.HEADER_LAST_MODIFIED; |
| |
| import java.io.BufferedInputStream; |
| import java.io.Closeable; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.io.PrintWriter; |
| import java.util.ArrayList; |
| import java.util.Date; |
| import java.util.Iterator; |
| import java.util.StringTokenizer; |
| |
| import javax.servlet.RequestDispatcher; |
| import javax.servlet.ServletException; |
| import javax.servlet.ServletOutputStream; |
| import javax.servlet.http.HttpServletRequest; |
| import javax.servlet.http.HttpServletResponse; |
| |
| import org.apache.jackrabbit.JcrConstants; |
| import org.apache.sling.api.SlingConstants; |
| import org.apache.sling.api.SlingHttpServletRequest; |
| import org.apache.sling.api.SlingHttpServletResponse; |
| import org.apache.sling.api.request.RequestDispatcherOptions; |
| import org.apache.sling.api.resource.Resource; |
| import org.apache.sling.api.resource.ResourceMetadata; |
| import org.apache.sling.api.resource.ResourceNotFoundException; |
| import org.apache.sling.api.resource.ResourceResolver; |
| import org.apache.sling.api.resource.ResourceUtil; |
| import org.apache.sling.api.resource.ValueMap; |
| import org.apache.sling.api.resource.external.ExternalizableInputStream; |
| import org.apache.sling.api.servlets.HttpConstants; |
| import org.apache.sling.api.servlets.SlingSafeMethodsServlet; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| /** |
| * The <code>StreamRendererServlet</code> streams the current resource to the |
| * client on behalf of the |
| * {@link org.apache.sling.servlets.get.impl.DefaultGetServlet}. If the current |
| * resource cannot be streamed it is rendered using the |
| * {@link PlainTextRendererServlet}. |
| */ |
| public class StreamRendererServlet extends SlingSafeMethodsServlet { |
| |
| public static final String EXT_RES = "res"; |
| |
| private static final long serialVersionUID = -1L; |
| |
| /** |
| * MIME multipart separation string |
| */ |
| private static final String mimeSeparation = "SLING_MIME_BOUNDARY"; |
| |
| // size threshold for sending an Accept-Ranges header back in the response (100KB) |
| private static final int ACCEPT_RANGES_THRESHOLD = 100 * 1024; |
| |
| // Accept-Ranges header name |
| private static final String ACCEPT_RANGES_HEADER = "Accept-Ranges"; |
| |
| // Accept-Ranges header value |
| private static final String ACCEPT_RANGES_BYTES = "bytes"; |
| |
| /** |
| * Full range marker. |
| */ |
| private static ArrayList<Range> FULL = new ArrayList<>(0); |
| |
| static final int IO_BUFFER_SIZE = 2048; |
| |
| /** default log */ |
| private final Logger log = LoggerFactory.getLogger(getClass()); |
| |
| private boolean index; |
| |
| private String[] indexFiles; |
| |
| public StreamRendererServlet(boolean index, String[] indexFiles) { |
| this.index = index; |
| this.indexFiles = indexFiles; |
| } |
| |
| @Override |
| protected void doGet(SlingHttpServletRequest request, |
| SlingHttpServletResponse response) throws ServletException, |
| IOException { |
| |
| processRequest(request, response); |
| |
| } |
| |
| @Override |
| protected void doHead(SlingHttpServletRequest request, |
| SlingHttpServletResponse response) throws ServletException, |
| IOException { |
| processRequest(request, response); |
| } |
| |
| private void processRequest(SlingHttpServletRequest request, |
| SlingHttpServletResponse response) throws ServletException, |
| IOException { |
| |
| // whether this servlet is called as of a request include |
| final boolean included = request.getAttribute(SlingConstants.ATTR_REQUEST_SERVLET) != null; |
| |
| // ensure no extension or "res" |
| String ext = request.getRequestPathInfo().getExtension(); |
| if (ext != null && !ext.equals(EXT_RES)) { |
| request.getRequestProgressTracker().log( |
| "StreamRendererServlet does not support for extension " + ext); |
| if (included || response.isCommitted()) { |
| log.error( |
| "StreamRendererServlet does not support extension {}", |
| ext); |
| } else { |
| response.sendError(HttpServletResponse.SC_NOT_FOUND); |
| } |
| return; |
| } |
| |
| Resource resource = request.getResource(); |
| |
| if (ResourceUtil.isNonExistingResource(resource)) { |
| throw new ResourceNotFoundException("No data to render."); |
| } |
| // trailing slash on url means directory listing |
| |
| if ("/".equals(request.getRequestPathInfo().getSuffix())) { |
| renderDirectory(request, response, included); |
| return; |
| } |
| |
| // check the last modification time and If-Modified-Since header |
| if (!included) { |
| ResourceMetadata meta = resource.getResourceMetadata(); |
| long modifTime = meta.getModificationTime(); |
| if (unmodified(request, modifTime)) { |
| response.setStatus(SC_NOT_MODIFIED); |
| return; |
| } |
| } |
| |
| // fall back to plain text rendering if the resource has no stream |
| if (resource.getResourceType().equals(JcrConstants.NT_LINKEDFILE)) { |
| final ValueMap vm = resource.adaptTo(ValueMap.class); |
| final String actualResourcePath = vm.get(JcrConstants.JCR_CONTENT, String.class); |
| resource = request.getResourceResolver().getResource(actualResourcePath); |
| } |
| InputStream stream = resource.adaptTo(InputStream.class); |
| if (stream != null) { |
| if ( stream instanceof ExternalizableInputStream) { |
| response.sendRedirect(((ExternalizableInputStream)stream).getURI().toString()); |
| return; |
| } |
| if (isHeadRequest(request)) { |
| setContentLength(response, resource.getResourceMetadata().getContentLength()); |
| setHeaders(resource, response); |
| return; |
| } |
| |
| streamResource(resource, stream, included, request, response); |
| |
| } else { |
| |
| // the resource is the root, do not redirect, immediately index |
| if (isRootResourceRequest(resource)) { |
| |
| renderDirectory(request, response, included); |
| |
| } else if (included || response.isCommitted() ) { |
| |
| // request is included or committed, not redirecting |
| request.getRequestProgressTracker().log( |
| "StreamRendererServlet: Not redirecting with trailing slash, response is committed or request included"); |
| log.warn("StreamRendererServlet: Not redirecting with trailing slash, response is committed or request included"); |
| |
| } else { |
| |
| // redirect to this with trailing slash to render the index |
| String url = request.getResourceResolver().map(request, |
| resource.getPath()) |
| + "/"; |
| response.sendRedirect(url); |
| |
| } |
| } |
| } |
| |
| private boolean isRootResourceRequest(Resource resource) { |
| return ("/".equals(resource.getPath())) || |
| ("/".equals(resource.getResourceResolver().map(resource.getPath()))); |
| } |
| |
| private boolean isHeadRequest(HttpServletRequest request) { |
| return HttpConstants.METHOD_HEAD.equals(request.getMethod()); |
| } |
| |
| /** |
| * Returns <code>true</code> if the request has a |
| * <code>If-Modified-Since</code> header whose date value is later than the |
| * last modification time given as <code>modifTime</code>. |
| * |
| * @param request The <code>ComponentRequest</code> checked for the |
| * <code>If-Modified-Since</code> header. |
| * @param modifTime The last modification time to compare the header to. |
| * @return <code>true</code> if the <code>modifTime</code> is less than or |
| * equal to the time of the <code>If-Modified-Since</code> header. |
| */ |
| private boolean unmodified(HttpServletRequest request, long modifTime) { |
| if (modifTime > 0) { |
| long modTime = modifTime / 1000; // seconds |
| long ims = request.getDateHeader(HEADER_IF_MODIFIED_SINCE) / 1000; |
| return modTime <= ims; |
| } |
| |
| // we have no modification time value, assume modified |
| return false; |
| } |
| |
| private void streamResource(final Resource resource, |
| final InputStream stream, final boolean included, |
| final SlingHttpServletRequest request, |
| final SlingHttpServletResponse response) throws IOException { |
| // finally stream the resource |
| try { |
| |
| final ArrayList<Range> ranges; |
| if (included) { |
| |
| // no range support on included requests |
| ranges = FULL; |
| |
| } else { |
| |
| // parse optional ranges |
| ranges = parseRange(request, response, |
| resource.getResourceMetadata()); |
| if (ranges == null) { |
| // there was something wrong, the parseRange has sent a |
| // response and we are done |
| return; |
| } |
| |
| // set various response headers, unless the request is included |
| setHeaders(resource, response); |
| } |
| |
| ServletOutputStream out = response.getOutputStream(); |
| |
| if (ranges == FULL) { |
| |
| // return full resource |
| setContentLength(response, |
| resource.getResourceMetadata().getContentLength()); |
| byte[] buf = new byte[IO_BUFFER_SIZE]; |
| int rd; |
| while ((rd = stream.read(buf)) >= 0) { |
| out.write(buf, 0, rd); |
| } |
| |
| } else { |
| |
| // return ranges of the resource |
| response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); |
| |
| if (ranges.size() == 1) { |
| |
| Range range = ranges.get(0); |
| response.addHeader("Content-Range", "bytes " + range.start |
| + "-" + range.end + "/" + range.length); |
| setContentLength(response, range.end - range.start + 1); |
| |
| copy(stream, out, range); |
| |
| } else { |
| |
| response.setContentType("multipart/byteranges; boundary=" |
| + mimeSeparation); |
| |
| copy(resource, out, ranges.iterator()); |
| } |
| |
| } |
| |
| } finally { |
| closeSilently(stream); |
| } |
| } |
| |
| private void renderDirectory(final SlingHttpServletRequest request, |
| final SlingHttpServletResponse response, final boolean included) |
| throws ServletException, IOException { |
| |
| // request is included or committed, not rendering index |
| if (included || response.isCommitted()) { |
| request.getRequestProgressTracker().log( |
| "StreamRendererServlet: Not rendering index, response is committed or request included"); |
| log.warn("StreamRendererServlet: Not rendering index, response is committed or request included"); |
| return; |
| } |
| |
| Resource resource = request.getResource(); |
| ResourceResolver resolver = request.getResourceResolver(); |
| |
| // check for an index file |
| for (String index : indexFiles) { |
| Resource fileRes = resolver.getResource(resource, index); |
| if (fileRes != null && !ResourceUtil.isSyntheticResource(fileRes)) { |
| |
| setHeaders(fileRes, response); |
| |
| if (isHeadRequest(request)) { |
| return; |
| } |
| |
| // include the index resource with no suffix and selectors ! |
| RequestDispatcherOptions rdo = new RequestDispatcherOptions(); |
| rdo.setReplaceSuffix(""); |
| rdo.setReplaceSelectors(""); |
| |
| RequestDispatcher dispatcher; |
| if (index.indexOf('.') < 0) { |
| String filePath = fileRes.getPath() + ".html"; |
| dispatcher = request.getRequestDispatcher(filePath, rdo); |
| } else { |
| dispatcher = request.getRequestDispatcher(fileRes, rdo); |
| } |
| |
| dispatcher.include(request, response); |
| return; |
| } |
| } |
| |
| if (index) { |
| if (isHeadRequest(request)) { |
| setHeaders(resource, response); |
| return; |
| } |
| renderIndex(resource, response); |
| } else { |
| response.sendError(HttpServletResponse.SC_FORBIDDEN); |
| } |
| |
| } |
| |
| /** |
| * @param resource |
| * @param response |
| */ |
| private void setHeaders(Resource resource, |
| SlingHttpServletResponse response) { |
| |
| final ResourceMetadata meta = resource.getResourceMetadata(); |
| final long modifTime = meta.getModificationTime(); |
| if (modifTime > 0) { |
| response.setDateHeader(HEADER_LAST_MODIFIED, modifTime); |
| } |
| |
| final String defaultContentType = "application/octet-stream"; |
| String contentType = meta.getContentType(); |
| if (contentType == null || defaultContentType.equals(contentType)) { |
| // if repository doesn't provide a content-type, or |
| // provides the |
| // default one, |
| // try to do better using our servlet context |
| final String ct = getServletContext().getMimeType( |
| resource.getPath()); |
| if (ct != null) { |
| contentType = ct; |
| } |
| } |
| |
| if (contentType == null) { |
| contentType = defaultContentType; |
| } |
| |
| response.setContentType(contentType); |
| |
| String encoding = meta.getCharacterEncoding(); |
| if (encoding != null) { |
| response.setCharacterEncoding(encoding); |
| } |
| |
| // announce support for ranges if we know the size to be larger than 100KB |
| if (meta.getContentLength() > ACCEPT_RANGES_THRESHOLD) { |
| response.setHeader(ACCEPT_RANGES_HEADER, ACCEPT_RANGES_BYTES); |
| } |
| } |
| |
| /** |
| * Set the <code>Content-Length</code> header to the give value. If the |
| * length is larger than <code>Integer.MAX_VALUE</code> it is converted to a |
| * string and the <code>setHeader(String, String)</code> method is called |
| * instead of the <code>setContentLength(int)</code> method. |
| * |
| * @param response The response on which to set the |
| * <code>Content-Length</code> header. |
| * @param length The content length to be set. If this value is equal to or |
| * less than zero, the header is not set. |
| */ |
| private void setContentLength(final HttpServletResponse response, final long length) { |
| if (length > 0) { |
| if (length < Integer.MAX_VALUE) { |
| response.setContentLength((int) length); |
| } else { |
| response.setHeader("Content-Length", String.valueOf(length)); |
| } |
| } |
| } |
| |
| private void renderIndex(Resource resource, |
| SlingHttpServletResponse response) throws IOException { |
| |
| response.setContentType("text/html"); |
| response.setCharacterEncoding("UTF-8"); |
| |
| String path = resource.getPath(); |
| |
| PrintWriter pw = response.getWriter(); |
| pw.println("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 3.2 Final//EN\">"); |
| pw.println("<html>"); |
| pw.println("<head>"); |
| pw.println("<title>Index of " + path + "</title>"); |
| pw.println("</head>"); |
| |
| pw.println("<body>"); |
| pw.println("<h1>Index of " + path + "</h1>"); |
| |
| pw.println("<pre>"); |
| pw.println("Name Last modified Size Description"); |
| pw.println("<hr>"); |
| |
| if (!"/".equals(path)) { |
| pw.println("<a href='../'>../</a> - Parent"); |
| } |
| |
| // render the children |
| Iterator<Resource> children = ResourceUtil.listChildren(resource); |
| while (children.hasNext()) { |
| renderChild(pw, children.next()); |
| } |
| |
| pw.println("</pre>"); |
| pw.println("</body>"); |
| pw.println("</html>"); |
| |
| } |
| |
| private void renderChild(PrintWriter pw, Resource resource) { |
| |
| String name = ResourceUtil.getName(resource.getPath()); |
| |
| InputStream ins = resource.adaptTo(InputStream.class); |
| if (ins == null) { |
| name += "/"; |
| } else { |
| closeSilently(ins); |
| } |
| |
| String displayName = name; |
| String suffix; |
| if (displayName.length() >= 32) { |
| displayName = displayName.substring(0, 29) + "..."; |
| suffix = ""; |
| } else { |
| suffix = " ".substring( |
| 0, 32 - displayName.length()); |
| } |
| pw.printf("<a href='%s'>%s</a>%s", name, displayName, suffix); |
| |
| ResourceMetadata meta = resource.getResourceMetadata(); |
| long lastModified = meta.getModificationTime(); |
| pw.print(" " + new Date(lastModified) + " "); |
| |
| long length = meta.getContentLength(); |
| if (length > 0) { |
| pw.print(length); |
| } else { |
| pw.print('-'); |
| } |
| |
| pw.println(); |
| } |
| |
| //---------- Range header support |
| // The following code is copy-derived from the Tomcate DefaultServlet |
| // http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/catalina/servlets/DefaultServlet.java?view=markup |
| |
| /** |
| * Copies a number of ranges from the given resource to the output stream. |
| * Copy the contents of the specified input stream to the specified output |
| * stream, and ensure that both streams are closed before returning (even in |
| * the face of an exception). |
| * |
| * @param resource The resource from which to send ranges |
| * @param ostream The output stream to write to |
| * @param ranges Iterator of the ranges the client wanted to retrieve |
| * @exception IOException if an input/output error occurs |
| */ |
| private void copy(Resource resource, ServletOutputStream ostream, |
| Iterator<Range> ranges) throws IOException { |
| |
| String contentType = resource.getResourceMetadata().getContentType(); |
| IOException exception = null; |
| |
| while ((exception == null) && (ranges.hasNext())) { |
| |
| InputStream resourceInputStream = resource.adaptTo(InputStream.class); |
| InputStream istream = new BufferedInputStream(resourceInputStream, |
| IO_BUFFER_SIZE); |
| |
| try { |
| Range currentRange = ranges.next(); |
| |
| // Writing MIME header. |
| ostream.println(); |
| ostream.println("--" + mimeSeparation); |
| if (contentType != null) { |
| ostream.println("Content-Type: " + contentType); |
| } |
| ostream.println("Content-Range: bytes " + currentRange.start + "-" |
| + currentRange.end + "/" + currentRange.length); |
| ostream.println(); |
| |
| // Copy content |
| try { |
| copy(istream, ostream, currentRange); |
| } catch(IOException e) { |
| exception = e; |
| } |
| } finally { |
| closeSilently(istream); |
| } |
| |
| } |
| |
| ostream.println(); |
| ostream.print("--" + mimeSeparation + "--"); |
| |
| if(exception != null) { |
| throw exception; |
| } |
| } |
| |
| /** |
| * Copy the contents of the specified input stream to the specified |
| * output stream. |
| * |
| * @param istream The input stream to read from |
| * @param ostream The output stream to write to |
| * @param range Range the client wanted to retrieve |
| * @exception IOException if an input/output error occurs |
| */ |
| private void copy(InputStream istream, OutputStream ostream, |
| Range range) throws IOException { |
| // HTTP Range 0-9 means "byte 9 included" |
| final long endIndex = range.end + 1; |
| log.debug("copy: Serving bytes: {}-{}", range.start, endIndex); |
| staticCopyRange(istream, ostream, range.start, endIndex); |
| } |
| |
| // static, package-private method to make unit testing easier |
| static void staticCopyRange(InputStream istream, |
| OutputStream ostream, long start, long end) throws IOException { |
| long position = 0; |
| byte buffer[] = new byte[IO_BUFFER_SIZE]; |
| |
| while (position < start) { |
| long skipped = istream.skip(start - position); |
| if (skipped == 0) { |
| // skip() may return zero if for whatever reason it wasn't |
| // able to advance the stream. In such cases we need to |
| // fall back to read() to force the skipping of bytes. |
| int len = (int) Math.min(start - position, buffer.length); |
| skipped = istream.read(buffer, 0, len); |
| if (skipped == -1) { |
| throw new IOException("Failed to skip " + start |
| + " bytes; only skipped " + position + " bytes"); |
| } |
| } |
| position += skipped; |
| } |
| |
| while (position < end) { |
| int len = (int) Math.min(end - position, buffer.length); |
| int read = istream.read(buffer, 0, len); |
| if (read != -1) { |
| position += read; |
| ostream.write(buffer, 0, read); |
| } else { |
| break; |
| } |
| } |
| } |
| |
| /** |
| * Parse the range header. |
| * |
| * @param request The servlet request we are processing |
| * @param response The servlet response we are creating |
| * @return ArrayList of ranges parsed from the Range header or {@link #FULL} |
| * if the full resource should be returned or <code>null</code> if |
| * an error occurred parsing the header and the request has been |
| * finished sending an error status. |
| */ |
| private ArrayList<Range> parseRange(HttpServletRequest request, |
| HttpServletResponse response, ResourceMetadata metadata) |
| throws IOException { |
| |
| // Checking If-Range |
| String headerValue = request.getHeader("If-Range"); |
| if (headerValue != null) { |
| |
| long headerValueTime = (-1L); |
| try { |
| headerValueTime = request.getDateHeader("If-Range"); |
| } catch (IllegalArgumentException e) { |
| // Ignore |
| } |
| |
| if (headerValueTime == (-1L)) { |
| |
| // If the ETag the client gave does not match the entity |
| // etag, then the entire entity is returned. |
| // Sling: no etag support yet, return full range |
| return FULL; |
| |
| } else if (metadata.getModificationTime() > (headerValueTime + 1000)) { |
| |
| // If the timestamp of the entity the client got is older than |
| // the last modification date of the entity, the entire entity |
| // is returned. |
| return FULL; |
| |
| } |
| |
| } |
| |
| long fileLength = metadata.getContentLength(); |
| if (fileLength == 0) { |
| return FULL; |
| } |
| |
| // Retrieving the range header (if any is specified) |
| String rangeHeader = request.getHeader("Range"); |
| if (rangeHeader == null) { |
| return FULL; |
| } |
| |
| // bytes is the only range unit supported (and I don't see the point |
| // of adding new ones). |
| if (!rangeHeader.startsWith("bytes")) { |
| failParseRange(response, fileLength, rangeHeader); |
| return null; |
| } |
| |
| rangeHeader = rangeHeader.substring(6); |
| |
| // Vector which will contain all the ranges which are successfully |
| // parsed. |
| ArrayList<Range> result = new ArrayList<>(); |
| StringTokenizer commaTokenizer = new StringTokenizer(rangeHeader, ","); |
| |
| // Parsing the range list |
| while (commaTokenizer.hasMoreTokens()) { |
| String rangeDefinition = commaTokenizer.nextToken().trim(); |
| |
| Range currentRange = new Range(); |
| currentRange.length = fileLength; |
| |
| int dashPos = rangeDefinition.indexOf('-'); |
| |
| if (dashPos == -1) { |
| failParseRange(response, fileLength, rangeHeader); |
| return null; |
| } |
| |
| if (dashPos == 0) { |
| |
| try { |
| long offset = Long.parseLong(rangeDefinition); |
| currentRange.start = fileLength + offset; |
| currentRange.end = fileLength - 1; |
| } catch (NumberFormatException e) { |
| failParseRange(response, fileLength, rangeHeader); |
| return null; |
| } |
| |
| } else { |
| |
| try { |
| currentRange.start = Long.parseLong(rangeDefinition.substring( |
| 0, dashPos)); |
| if (dashPos < rangeDefinition.length() - 1) |
| currentRange.end = Long.parseLong(rangeDefinition.substring( |
| dashPos + 1, rangeDefinition.length())); |
| else |
| currentRange.end = fileLength - 1; |
| } catch (NumberFormatException e) { |
| failParseRange(response, fileLength, rangeHeader); |
| return null; |
| } |
| |
| } |
| |
| if (!currentRange.validate()) { |
| failParseRange(response, fileLength, rangeHeader); |
| return null; |
| } |
| |
| result.add(currentRange); |
| } |
| |
| return result; |
| } |
| |
| /** |
| * Sends a 416 error response to the client if the Range header is |
| * not acceptable |
| */ |
| private void failParseRange(final HttpServletResponse response, |
| final long fileLength, final String rangeHeader) throws IOException { |
| log.error("parseRange: Cannot support range {}; sending 416", |
| rangeHeader); |
| response.addHeader("Content-Range", "bytes */" + fileLength); |
| response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); |
| } |
| |
| private void closeSilently(final Closeable closeable) { |
| if (closeable != null) { |
| try { |
| closeable.close(); |
| } catch (IOException ignore) { |
| } |
| } |
| } |
| |
| // --------- Range Inner Class |
| |
| protected class Range { |
| |
| public long start; |
| |
| public long end; |
| |
| public long length; |
| |
| /** |
| * Validate range. |
| * |
| * @return {@code true} if the range is valid, {@code false} otherwise |
| */ |
| public boolean validate() { |
| if (end >= length) end = length - 1; |
| return ((start >= 0) && (end >= 0) && (start <= end) && (length > 0)); |
| } |
| |
| } |
| } |