/*
 * 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.hadoop.fs.swift.http;

import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.fs.swift.exceptions.SwiftConnectionClosedException;
import org.apache.hadoop.fs.swift.util.SwiftUtils;

import java.io.ByteArrayInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;

/**
 * This replaces the input stream release class from JetS3t and AWS;
 * # Failures in the constructor are relayed up instead of simply logged.
 * # it is set up to be more robust at teardown
 * # release logic is thread safe
 * Note that the thread safety of the inner stream contains no thread
 * safety guarantees -this stream is not to be read across streams.
 * The thread safety logic here is to ensure that even if somebody ignores
 * that rule, the release code does not get entered twice -and that
 * any release in one thread is picked up by read operations in all others.
 */
public class HttpInputStreamWithRelease extends InputStream {

  private static final Log LOG =
    LogFactory.getLog(HttpInputStreamWithRelease.class);
  private final URI uri;
  private HttpMethod method;
  //flag to say the stream is released -volatile so that read operations
  //pick it up even while unsynchronized.
  private volatile boolean released;
  //volatile flag to verify that data is consumed.
  private volatile boolean dataConsumed;
  private InputStream inStream;
  /**
   * In debug builds, this is filled in with the construction-time
   * stack, which is then included in logs from the finalize(), method.
   */
  private final Exception constructionStack;

  /**
   * Why the stream is closed
   */
  private String reasonClosed = "unopened";

  public HttpInputStreamWithRelease(URI uri, HttpMethod method) throws
                                                                IOException {
    this.uri = uri;
    this.method = method;
    constructionStack = LOG.isDebugEnabled() ? new Exception("stack") : null;
    if (method == null) {
      throw new IllegalArgumentException("Null 'method' parameter ");
    }
    try {
      inStream = method.getResponseBodyAsStream();
    } catch (IOException e) {
      inStream = new ByteArrayInputStream(new byte[]{});
      throw releaseAndRethrow("getResponseBodyAsStream() in constructor -" + e, e);
    }
  }

  @Override
  public void close() throws IOException {
    release("close()", null);
  }

  /**
   * Release logic
   * @param reason reason for release (used in debug messages)
   * @param ex exception that is a cause -null for non-exceptional releases
   * @return true if the release took place here
   * @throws IOException if the abort or close operations failed.
   */
  private synchronized boolean release(String reason, Exception ex) throws
                                                                   IOException {
    if (!released) {
      reasonClosed = reason;
      try {
        if (LOG.isDebugEnabled()) {
          LOG.debug("Releasing connection to " + uri + ":  " + reason, ex);
        }
        if (method != null) {
          if (!dataConsumed) {
            method.abort();
          }
          method.releaseConnection();
        }
        if (inStream != null) {
          //this guard may seem un-needed, but a stack trace seen
          //on the JetS3t predecessor implied that it
          //is useful
          inStream.close();
        }
        return true;
      } finally {
        //if something went wrong here, we do not want the release() operation
        //to try and do anything in advance.
        released = true;
        dataConsumed = true;
      }
    } else {
      return false;
    }
  }

  /**
   * Release the method, using the exception as a cause
   * @param operation operation that failed
   * @param ex the exception which triggered it.
   * @return the exception to throw
   */
  private IOException releaseAndRethrow(String operation, IOException ex) {
    try {
      release(operation, ex);
    } catch (IOException ioe) {
      LOG.debug("Exception during release: " + operation + " - " + ioe, ioe);
      //make this the exception if there was none before
      if (ex == null) {
        ex = ioe;
      }
    }
    return ex;
  }

  /**
   * Assume that the connection is not released: throws an exception if it is
   * @throws SwiftConnectionClosedException
   */
  private synchronized void assumeNotReleased() throws SwiftConnectionClosedException {
    if (released || inStream == null) {
      throw new SwiftConnectionClosedException(reasonClosed);
    }
  }

  @Override
  public int available() throws IOException {
    assumeNotReleased();
    try {
      return inStream.available();
    } catch (IOException e) {
      throw releaseAndRethrow("available() failed -" + e, e);
    }
  }

  @Override
  public int read() throws IOException {
    assumeNotReleased();
    int read = 0;
    try {
      read = inStream.read();
    } catch (EOFException e) {
      if (LOG.isDebugEnabled()) {
        LOG.debug("EOF exception " + e, e);
      }
      read = -1;
    } catch (IOException e) {
      throw releaseAndRethrow("read()", e);
    }
    if (read < 0) {
      dataConsumed = true;
      release("read() -all data consumed", null);
    }
    return read;
  }

  @Override
  public int read(byte[] b, int off, int len) throws IOException {
    SwiftUtils.validateReadArgs(b, off, len);
    //if the stream is already closed, then report an exception.
    assumeNotReleased();
    //now read in a buffer, reacting differently to different operations
    int read;
    try {
      read = inStream.read(b, off, len);
    } catch (EOFException e) {
      if (LOG.isDebugEnabled()) {
        LOG.debug("EOF exception " + e, e);
      }
      read = -1;
    } catch (IOException e) {
      throw releaseAndRethrow("read(b, off, " + len + ")", e);
    }
    if (read < 0) {
      dataConsumed = true;
      release("read() -all data consumed", null);
    }
    return read;
  }

  /**
   * Finalizer does release the stream, but also logs at WARN level
   * including the URI at fault
   */
  @Override
  protected void finalize() {
    try {
      if (release("finalize()", constructionStack)) {
        LOG.warn("input stream of " + uri
                 + " not closed properly -cleaned up in finalize()");
      }
    } catch (Exception e) {
      //swallow anything that failed here
      LOG.warn("Exception while releasing " + uri + "in finalizer",
               e);
    }
  }

  @Override
  public String toString() {
    return "HttpInputStreamWithRelease working with " + uri
      +" released=" + released
      +" dataConsumed=" + dataConsumed;
  }
}
