All Downloads are FREE. Search and download functionalities are using the official Maven repository.

ucar.unidata.io.http.HTTPRandomAccessFile Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 1998-2018 University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */

package ucar.unidata.io.http;

import org.apache.http.Header;
import ucar.httpservices.HTTPFactory;
import ucar.httpservices.HTTPMethod;
import ucar.httpservices.HTTPSession;
import ucar.unidata.util.Urlencoded;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.channels.WritableByteChannel;

/**
 * Gives access to files over HTTP, using "Accept-Ranges" HTTP header to do random access.
 * This version uses a single instance of HttpClient, following performance guidelines at
 * http://jakarta.apache.org/commons/httpclient/performance.html
 * Plus other improvements.
 *
 * @author John Caron, based on work by Donald Denbo
 */

public class HTTPRandomAccessFile extends ucar.unidata.io.RandomAccessFile {
  static public final int defaultHTTPBufferSize = 20 * 1000;       // 20K
  static public final int maxHTTPBufferSize = 10 * 1000 * 1000;     // 10 M
  static private final boolean debug = false, debugDetails = false;

  ///////////////////////////////////////////////////////////////////////////////////

  private String url;
  private HTTPSession session = null;
  private long total_length = 0;

  public HTTPRandomAccessFile(String url) throws IOException {
    this(url, defaultHTTPBufferSize);
    location = url;
  }

  @Urlencoded
  public HTTPRandomAccessFile(String url, int bufferSize) throws IOException {
    super(bufferSize);
    file = null;
    this.url = url;
    location = url;
    if (debugLeaks)
      allFiles.add(location);

    session = HTTPFactory.newSession(url);

    boolean needtest = true;

    try (
      HTTPMethod method = HTTPFactory.Head(session,url)) {

      doConnect(method);

      Header head = method.getResponseHeader("Accept-Ranges");
      if (head == null) {
        needtest = true; // header is optional - need more testing

      } else if (head.getValue().equalsIgnoreCase("bytes")) {
        needtest = false;

      } else if (head.getValue().equalsIgnoreCase("none")) {
        throw new IOException("Server does not support byte Ranges");
      }

      head = method.getResponseHeader("Content-Length");
      if (head == null) {
        throw new IOException("Server does not support Content-Length");
      }

      try {
        total_length = Long.parseLong(head.getValue());
        /* Some HTTP server report 0 bytes length. 
         * Do the Range bytes test if the server is reporting 0 bytes length*/
        if (total_length==0) needtest = true;
      } catch (NumberFormatException e) {
        throw new IOException("Server has malformed Content-Length header");
      }

    }

    if (needtest && !rangeOk(url))
      throw new IOException("Server does not support byte Ranges");

    if (total_length > 0) {
      // this means that we will read the file in one gulp then deal with it in memory
      int useBuffer = (int) Math.min(total_length, maxHTTPBufferSize); // entire file size if possible
      useBuffer = Math.max(useBuffer, defaultHTTPBufferSize); // minimum buffer
      setBufferSize(useBuffer);
    }

    if (debugLeaks) openFiles.add(location);
  }

  public void close() throws IOException {
    if (debugLeaks)
      openFiles.remove(location);

    if (session != null) {
      session.close();
      session = null;
    }
  }

  private boolean rangeOk(String url)
  {
    try {
      try (HTTPMethod method = HTTPFactory.Get(session, url)) {
        method.setRange(0,0);
        doConnect(method);

        int code = method.getStatusCode();
        if(code != 206)
          throw new IOException("Server does not support Range requests, code= " + code);
        Header head = method.getResponseHeader("Content-Range");
        total_length = Long.parseLong(head.getValue().substring(head.getValue().lastIndexOf("/") + 1));
        // clear stream
        method.close();
        return true;
      }
    } catch (IOException e) {
      return false;
    }
  }

  private void doConnect(HTTPMethod method) throws IOException {

    // Execute the method.
    int statusCode = method.execute();

    if (statusCode == 404)
      throw new FileNotFoundException(url + " " + method.getStatusLine());

    if (statusCode >= 300)
      throw new IOException(url + " " + method.getStatusLine());

    if (debugDetails) {
      // request headers dont seem to be available until after execute()
      printHeaders("Request: " + method.getURI().toString(), method.getRequestHeaders());
      printHeaders("Response: " + method.getStatusCode(), method.getResponseHeaders());
    }
  }

  private void printHeaders(String title, Header[] heads) {
    System.out.println(title);
    for (Header head : heads) {
      System.out.print("  " + head.toString());
    }
    System.out.println();
  }

  /**
   * Read directly from file, without going through the buffer.
   * All reading goes through here or readToByteChannel;
   *
   * @param pos    start here in the file
   * @param buff   put data into this buffer
   * @param offset buffer offset
   * @param len    this number of bytes
   * @return actual number of bytes read
   * @throws IOException on io error
   */
  @Override
  protected int read_(long pos, byte[] buff, int offset, int len) throws IOException {
    long end = pos + len - 1;
    if (end >= total_length)
      end = total_length - 1;

    if (debug) System.out.println(" HTTPRandomAccessFile bytes=" + pos + "-" + end + ": ");

    try (HTTPMethod method = HTTPFactory.Get(session,url)) {
      method.setFollowRedirects(true);
      method.setRange(pos,end);
      doConnect(method);

      int code = method.getStatusCode();
      if (code != 206)
        throw new IOException("Server does not support Range requests, code= " + code);

      String s = method.getResponseHeader("Content-Length").getValue();
      if (s == null)
        throw new IOException("Server does not send Content-Length header");

      int readLen = Integer.parseInt(s);
      readLen = Math.min(len, readLen);

      InputStream is = method.getResponseAsStream();
      readLen = copy(is, buff, offset, readLen);
      return readLen;

    }
  }

  private int copy(InputStream in, byte[] buff, int offset, int want) throws IOException {
    int done = 0;
    while (want > 0) {
      int bytesRead = in.read(buff, offset + done, want);
      if (bytesRead == -1) break;
      done += bytesRead;
      want -= bytesRead;
    }
    return done;
  }

  @Override
  public long readToByteChannel(WritableByteChannel dest, long offset, long nbytes) throws IOException {
    int n = (int) nbytes;
    byte[] buff = new byte[n];
    int done = read_(offset, buff, 0, n);
    dest.write(ByteBuffer.wrap(buff));
    return done;
  }

  // override selected RandomAccessFile public methods

  @Override
  public long length() throws IOException {
    long fileLength = total_length;
    if (fileLength < dataEnd)
      return dataEnd;
    else
      return fileLength;
  }

  /**
   * Always returns {@code 0L}, as we cannot easily determine the last time that a remote file was modified.
   *
   * @return  {@code 0L}, always.
   */
  // LOOK: An idea of how we might implement this: https://github.com/Unidata/thredds/pull/479#issuecomment-194562614
  @Override
  public long getLastModified() {
    return 0;
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy