/* **************************************************************************

  $Archive: /njcl_v2/src/com/novell/service/file/nw/calls/EAAccessor.java $
  $Revision: 9 $
  $Modtime: 1/28/00 12:46p $
 
  Copyright (c) 1998 Novell, Inc.  All Rights Reserved.

  THIS WORK IS  SUBJECT  TO  U.S.  AND  INTERNATIONAL  COPYRIGHT  LAWS  AND
  TREATIES.   NO  PART  OF  THIS  WORK MAY BE  USED,  PRACTICED,  PERFORMED
  COPIED, DISTRIBUTED, REVISED, MODIFIED, TRANSLATED,  ABRIDGED, CONDENSED,
  EXPANDED,  COLLECTED,  COMPILED,  LINKED,  RECAST, TRANSFORMED OR ADAPTED
  WITHOUT THE PRIOR WRITTEN CONSENT OF NOVELL, INC. ANY USE OR EXPLOITATION
  OF THIS WORK WITHOUT AUTHORIZATION COULD SUBJECT THE PERPETRATOR TO
  CRIMINAL AND CIVIL LIABILITY.

****************************************************************************/

package com.novell.service.file.nw.calls;

import java.io.IOException;
import java.io.ByteArrayOutputStream;

import com.novell.service.jncp.NSIException;
import com.novell.java.io.spi.DataAccessor;

import com.novell.service.session.Session;
import com.novell.service.session.SessionException;
import com.novell.service.session.xplat.CallsService;

/** @internal
 * Allows native read or write access to an Extended Attribute on a
 * directory entry in the NetWare file system.
 *
 * <p>However, once any of the input methods are called, then it is in
 * input mode, and any output methods will throw exceptions.  A NetWare
 * EA cannot be both read and written at the same time.  The EAHandle
 * must be closed and then reopened to allow a different type of access.
 * Thus, this data accessor is not suitable for a random-access file.
 *
 * <p>The native EA handle is actually a structure that must be 'malloc()'ed
 * before passing it into the constructor of this class.  The 'finalize()'
 * method does a 'free()' on the EA handle, to prevent memory leaks.
 * The pointer to the malloc'ed EA handle struct is stored as an 'int'
 * in Java, and passed back out to all the natives as a "C" long, and is
 * then casted to be of type (EAHANDLE *).
 * </p>
 */
public class EAAccessor implements DataAccessor
{
   private static final String serviceKey    = CallsService.KEY;

   public static final int EA_INIT = 0;   // progressive states
   public static final int EA_READ = 1;   //   INIT -> READ -> CLOSED
   public static final int EA_WRITE = 2;  //   INIT -> WRITE -> CLOSED
   public static final int EA_CLOSED = 3; // invalid: INIT->READ->WRITE, etc.

   // size of buffer required for native reads and writes of EA data
   private static final int EA_BUF_SIZE = 512;

   CallsService service;

   int EAHandle;        // pointer to a malloc'ed EAHANDLE struct
   int state = EA_INIT; // start out in INIT state
   
   boolean savedByteValid = false;
   byte save = 0;       // saved byte -- see length()
   byte[] readBuf;      // EA_BUF_SIZE * 2 bytes long
   int startBufPos = 0; // markers for readBuf
   int endBufPos = 0;
   int totalReadSize = 0;  // set on any read() to total EA size
   boolean doneNativeReads = false; // set true when native EA is fully read

   // used as buffer to hold bytes written to EA, until the EAAccessor
   //   is closed, then all bytes are written out to the native EA
   // This is necessary because the NCP to write an EA requires the total
   //   bytes that will be written as one of its parameters, so we have
   //   to know exactly how many bytes will be written...
   ByteArrayOutputStream writeStream;

   /**
    * Constructs an EAAccessor with a EA handle.
    *
    * <p>The EA is already open, and so it can be read from or written
    * to directly after instantiation.
    * </p>
    *
    * @param      fileHandle     The "C" pointer to an allocated EAHANDLE
    *                            struct.
    */
   public EAAccessor (int EAHandle, Session session)
      throws IOException
   {
      try
      {
         this.EAHandle = EAHandle;
         this.service = (CallsService) session.getService (serviceKey);
      }
      catch (SessionException e)
      {
         throw (new IOException (e.getMessage ()));
      }

   }

   /**
    * If the stream is not closed, it will be closed now.
    *
    * @exception  IOException    When an I/O error occurs closing the stream.
    */
   public void finalize ()
      throws IOException
   {
      if (this.state < EA_CLOSED)
         close ();
   }

   /**
    * Returns bytes available from current stream position without blocking.
    *
    * @return                    Number of bytes available to read (without
    *                            blocking) from the current position in the
    *                            stream.
    * @exception  IOException    When the stream is in write mode.
    */
   public int available ()
      throws IOException
   {
      checkStatus (EA_READ);

      return (endBufPos - startBufPos);
   }

   /**
    * Closes stream.
    *
    * <p>If the EA was written to, then the entire data is buffered
    * internally in a ByteArrayOutputStream.  In this case, the entire data
    * is then flushed to the EA and the EAHandle is closed.
    *
    * <p>If the EA was read from, it is imperative that the rest of the EA
    * data be read.  The EA NCP's say that if there is any data that is
    * left unread before closing the EA handle, it is subject to
    * corruption.  Thus, we have to read the entire EA before closing
    * to prevent the documented erratic behavior.
    * </p>
    *
    * @exception  IOException    When there is an error flushing data or
    *                            closing the native EA handle.  After a
    *                            close fails, the 
    */
   public synchronized void close ()
      throws IOException
   {
      // don't allow a close except for INIT, READ, and WRITE states
      if (this.state < EA_INIT || this.state >= EA_CLOSED)
         throw new IOException ();

      try
      {
         // bytes have been written to the EA, now it's time to actually
         //   put them in the native EA
         if (this.state == EA_WRITE)
         {
            // get the byte array
            byte[] bytesToWrite = writeStream.toByteArray ();
            int[] actualBytesWritten = new int[1];
            int count = 0;
            int ccode = 0;

            // write all bytes in 'bytesToWrite' -- on the last successful
            //   request, ccode will be 1, so then the loop exits
            while (ccode == 0)
            {
               try
               {
               service.writeEA (
                     EAHandle,
                     bytesToWrite.length,
                     bytesToWrite,
                     count,    // count offset
                     EA_BUF_SIZE,
                     actualBytesWritten);

               }
               catch (NSIException e)
               {
               ccode = e.getCCode ();
               if (ccode != 0 && ccode != 1)
               {
                  // actually free the memory allocated for the EAHandle
                  service.freePtr (EAHandle);

                  throw new IOException (Integer.toString (ccode));
               }
               }

               count += actualBytesWritten[0];
            }
         }
         // bytes have been read, but we have to ensure that all bytes in
         //   the EA are read before closing the EA handle -- NCP doc
         //   says so
         else if (this.state == EA_READ)
         {
            int n = 0;

            while (n != -1)
               n = read (readBuf);
         }

         // now close the native EA handle
         service.closeEA (EAHandle);

      }
      catch (SessionException e)
      {
         throw (new IOException (e.getMessage ()));
      }
      finally
      {
         try
         {
         // actually free the memory allocated for the EAHandle
         service.freePtr (EAHandle);
         }
         catch (SessionException e)
         {
            throw (new IOException (e.getMessage ()));
         }

         this.state = EA_CLOSED;
      }
   }

   /**
    * Flush cannot be implemented for EAs.
    *
    * <p>All output data is held in the ByteArrayOutputStream until
    * the stream is closed, so no data is written to the EA until the
    * stream is closed, so flush doesn't really do anything.
    * </p>
    *
    * @exception  IOException    When the stream is in read mode.
    */
   public void flush ()
      throws IOException
   {
      checkStatus (EA_WRITE);

      // no code here...
   }

   /**
    * Returns true if mark/reset methods are supported, false if they are
    * not.
    *
    * <p>If this method returns false, reset will throw an exception if
    * called.
    * </p>
    * 
    * @return                    true if mark/reset methods are supported,
    *                            false if they are not.
    */
   public boolean markSupported ()
   {
      return (false);
   }

   /**
    * Mark/reset are currently not supported on EA streams.
    *
    * @param      limit          The number of bytes can be read before
    *                            the mark is invalidated.
    */
   public void mark (int limit)
   {
      // no code here...
   }

   /**
    * Reads and returns next byte on stream.
    *
    * @return                    The next byte on the stream.
    *                            If the end-of-file was reached, this
    *                            method returns -1.
    * @exception  IOException    When an error occurs reading the next
    *                            byte, or when stream is in write mode.
    */
   public int read ()
      throws IOException
   {
      byte[] b = new byte[1];
      int ccode = readBytes (b, 0, 1);

      if (ccode == -1)
         return (-1);

      byte signed = b[0];
      int unsigned;

      if (signed < 0)
         unsigned = signed & 0x7f + 0x80;
      else
         unsigned = signed;

      return (unsigned);
   }

   /**
    * Reads bytes from stream and fills <i>b</i> with the data read.
    *
    * @param      b              The array of bytes to store bytes in.
    * @return                    number of bytes actually read -- if less
    *                            than 'b.length' then end-of-file
    *                            was reached.  This method returns -1 when
    *                            end-of-file was reached and no bytes were
    *                            read.
    * @exception  IOException    when an error occurs reading bytes from
    *                            the stream, or when stream is in write mode.
    */
   public int read (byte[] b)
      throws IOException
   {
      return (readBytes (b, 0, b.length));
   }

   /**
    * Reads bytes from stream and writes bytes to an offset within the
    * specified byte array.
    *
    * @param      b              The array of bytes to store bytes in.
    * @param      off            The offset into 'b' at which to store the
    *                            bytes.
    * @param      len            The number of bytes to read.
    * @return                    The number of bytes actually read -- if less
    *                            than 'len', then end-of-file was reached.
    *                            This method returns -1 when end-of-file
    *                            was reached and no bytes were read.
    * @exception  IOException    When an error occurs reading 'len'
    *                            bytes from stream, or when the file is
    *                            in write mode.
    */
   public int read (byte[] b, int off, int len)
      throws IOException
   {
      return (readBytes (b, off, len));
   }

   /**
    * Reads bytes from an internal buffer, and if needed, makes an additional
    * native request to get the bytes from the EA.
    *
    * @param      b              The array of bytes to store bytes in.
    * @param      off            The offset into 'b' at which to store the
    *                            bytes.
    * @param      len            The number of bytes to read.
    * @return                    The number of bytes actually read -- if less
    *                            than 'len', then end-of-file was reached.
    *                            This method returns -1 when end-of-file
    *                            was reached and no bytes were read.
    * @exception  IOException    When an error occurs reading 'len'
    *                            bytes from stream, or when the file is
    *                            in write mode.
    */
   private synchronized int readBytes (byte[] b, int off, int len)
      throws IOException
   {
      // look to see if we're in read mode
      checkStatus (EA_READ);

      // EOF was reached
      if (this.state == EA_CLOSED ||
          doneNativeReads && startBufPos == endBufPos)
      {
         return (-1);
      }

      if (len == 0)
         return (0);

      // counter of how many bytes have been copied into 'b'
      int bytesCopied = 0;
      boolean finished = false;

      // if there is a saved byte -- see length()
      if (savedByteValid && len > 0)
      {
         // copy the saved byte into the array, and then make the arguments
         //   look just like the request was to copy one less byte than
         //   was actually requested
         b[off++] = save;
         len--;
         bytesCopied++;

         savedByteValid = false;

         // if the full request was satisfied by one byte, then shortcut exit
         if (len == 0)
            return (bytesCopied);
      }

      int ccode = 0;
      int bufSize;
      int bytesToCopy;
      int bufBytesCopied = 0;

      // 'finished' will be true when all bytes that have been
      //   requested are copied into 'b' or when EOF is reached
      while (!finished)
      {
         // number of bytes that should be copied
         bytesToCopy = len - bufBytesCopied;
         bufSize = endBufPos - startBufPos;

         // shave down the number of bytes to copy on this pass through
         //   if there aren't that many bytes in the 'readBuf' buffer
         if (bytesToCopy > bufSize)
            bytesToCopy = bufSize;

         if (bytesToCopy > 0)
         {
            // copy in appropriate number of available bytes from 'readBuf'
            System.arraycopy (readBuf, startBufPos, b,
                  off + bufBytesCopied, bytesToCopy);

            // increment counter by number of bytes copied
            bufBytesCopied += bytesToCopy;

            // move the buffer pointer along by as many bytes as are copied
            startBufPos += bytesToCopy;

            // if the buffer is empty, move both pointers back to zero
            if (startBufPos == endBufPos)
               startBufPos = endBufPos = 0;
            // otherwise, if the start pointer is over half the size
            //   of 'readBuf', copy all bytes to the start of 'readBuf'
            //   to make sure that there are at least EA_BUF_SIZE bytes
            //   empty to read from the native EA data
            else if (startBufPos >= readBuf.length / 2)
            {
               int limit = endBufPos - startBufPos;

               for (int n = 0; n < limit; n++)
                  readBuf[n] = readBuf[startBufPos + n];

               startBufPos = 0;
               endBufPos = limit;
            }

            // if not done and there are still bytes in the buffer, then
            //   continue copying until finished or no bytes in the buffer
            if (bufBytesCopied != len && endBufPos - startBufPos > 0)
               continue;
         }

         // if all required bytes have been copied, or all possible bytes
         //   have been copied, then exit now
         if (bufBytesCopied == len || doneNativeReads)
         {
            finished = true;
            continue;
         }

         try
         {
         // if the NCP reads of the native EA are not yet fully completed
         if (!doneNativeReads)
         {
            int[] totalEASize = new int[1];
            int[] bytesRead = new int[1];

            try
            {
            service.readEA (
                  EAHandle,
                  readBuf,
                  endBufPos,
                  EA_BUF_SIZE,
                  totalEASize,
                  bytesRead);

            }
            catch (NSIException e)
            {
               ccode = e.getCCode ();
            // on any error, and on EA_EOF (1), there is no point in
            //   continuing to try to read the native EA handle
            if (ccode != 0)
               doneNativeReads = true;

            // 0 and 1 signify successful completion
            if (ccode != 0 && ccode != 1)
               throw new IOException (Integer.toString (ccode));
            }
            // update the total EA size -- see length ()
            if (totalReadSize == 0)
               totalReadSize = totalEASize[0];

            // add the appropriate number of bytes to the end pointer
            endBufPos += bytesRead[0];
         }
         }
         catch (SessionException e)
         {
            throw (new IOException (e.getMessage ()));
         }
      }

      bytesCopied += bufBytesCopied;

      return bytesCopied == 0 ? -1 : bytesCopied;
   }

   /**
    * Writes one byte to stream.
    *
    * @param      b              The byte to write.
    * @exception  IOException    when an error occurs writing the specified
    *                            byte, or when the file is closed.
    */
   public void write (int b)
      throws IOException
   {
      checkStatus (EA_WRITE);

      writeStream.write (b);
   }

   /**
    * Writes an array of bytes to stream.
    *
    * @param      b              The array of bytes to write.  Exactly
    *                            'b.length' bytes will be written.
    * @exception  IOException    When an error occurs writing specified
    *                            bytes, or when the file is closed.
    */
   public void write (byte[] b)
      throws IOException
   {
      checkStatus (EA_WRITE);

      writeStream.write (b);
   }

   /**
    * Writes specified number of bytes to stream from an offset into the
    * specified array.
    *
    * @param      b              The array to write.
    * @param      off            The offset into the array from which to
    *                            start writing.
    * @param      len            The number of bytes to write.
    * @exception  IOException    When an error occurs writing specified
    *                            bytes, or when the file is closed.
    */
   public void write (byte[] b, int off, int len)
      throws IOException
   {
      checkStatus (EA_WRITE);

      writeStream.write (b, off, len);
   }

   /**
    * Resets stream position to location saved by mark().
    *
    * @exception  IOException    If mark/reset methods are supported on the
    *                            file, or when an error occurs moving the
    *                            file pointer to the saved location, or when
    *                            the file is closed.  If mark/reset methods
    *                            are not supported by the file an
    *                            exception is always thrown.
    */
   public void reset ()
      throws IOException
   {
      throw new IOException ();
   }

   /**
    * Moves the file pointer forward a specified number of bytes.
    *
    * @param      n              The number of bytes to skip.
    * @return                    The number of bytes skipped.
    * @exception  IOException    When an error occurs getting or setting the
    *                            file pointer, or when the file is closed.
    */
   public long skip (long n)
      throws IOException
   {
      // ensure that the number is a positive int
      byte data[] = new byte[(int) (n & 0xEFFFFFFF)];

      return read (data);
   }

   /**
    * Returns total number of bytes in the stream.
    *
    * @return                    The total number of bytes contained in
    *                            the file.
    * @exception  IOException    When an error occurs getting the stream
    *                            length, or when the file is closed.
    */
   public long length ()
      throws IOException
   {
      checkStatus (EA_READ);

      // check the read size to see if it's been set yet
      if (totalReadSize == 0)
      {
         byte[] b = new byte[1];
         int bytesRead;
         
         // a side effect of read is that the 'totalReadSize' is set
         //   to the total EA size
         bytesRead = this.read (b);

         // if a byte was read, then stuff it back into save, so
         //   that it is not lost
         if (bytesRead != -1)
         {
            save = b[0];
            savedByteValid = true;
         }
      }

      return (totalReadSize);
   }

   /**
    * Moves file pointer to an absolute position in stream.
    *
    * <p>For EAs, this method is not implemented.
    * </p>
    *
    * @param      pos            The absolute position to move to.
    * @return                    The position moved to.
    * @exception  IOException    Always throws this exception.
    */
   public long seek (long pos)
      throws IOException
   {
      throw new IOException ();
   }

   /**
    * Changes stream length.
    *
    * <p>If the stream is smaller than the desired length, the stream is
    * extended to the desired length, using a byte value 0.
    *
    * <p>If the stream is larger than the desired length, the stream is
    * truncated at the desired length.
    * </p>
    *
    * @param      len            The desired absolute length of the stream.
    * @exception  IOException    When an error occurs setting the length of
    *                            the stream.
    */
   public void setLength(long len)
      throws IOException
   {
      throw new IOException();
   }

   /**
    * Ensures that the stream has a certain status.
    *
    * <p>Other methods call this method to make sure that it is valid
    * to perform their operation.  For example: it is invalid to call
    * read() and then write() -- EAs only allow you to read fully or
    * write fully.
    *
    * <p>If the stream has been initialized, but no requests have been
    * made, then required setup is performed, and then the state of the
    * stream is changed to the required state.
    * </p>
    *
    * @param      requiredState  The state that is required to perform
    *                            a certain operation.
    * @exception  IOException    When the current state of the stream is
    *                            incompatible with the required state.
    */
   private synchronized void checkStatus (int requiredState)
      throws IOException
   {
      // if the state has already progressed past INIT, then enforce
      //   the 'requiredState'
      if (this.state > EA_INIT)
      {
         if (this.state != requiredState)
            throw new IOException ();
      }
      // otherwise, modify the state depending on the 'requiredState'
      else
      {
         // if we're reading, set up the read buffer
         if (requiredState == EA_READ)
            readBuf = new byte[EA_BUF_SIZE * 2];
         // if we're writing make the output buffer
         else if (requiredState == EA_WRITE)
            writeStream = new ByteArrayOutputStream ();

         // set the required state
         this.state = requiredState;
      }
   }
}
