/*
 * Copyright (C) 2005-2011 Alfresco Software Limited.
 *
 * This file is part of Alfresco
 *
 * Alfresco is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Alfresco is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Alfresco. If not, see 
* Only the instance need be constructed. The required mimetype, encoding, etc * will be copied across by this class. * * @return Returns a reader onto the location referenced by this instance. * The instance must always be a new instance. * @throws ContentIOException */ protected abstract ContentReader createReader() throws ContentIOException; /** * Performs checks and copies required reader attributes */ public final ContentReader getReader() throws ContentIOException { ContentReader reader = createReader(); if (reader == null) { throw new AlfrescoRuntimeException("ContentReader failed to create new reader: \n" + " reader: " + this); } else if (reader.getContentUrl() == null || !reader.getContentUrl().equals(getContentUrl())) { throw new AlfrescoRuntimeException("ContentReader has different URL: \n" + " reader: " + this + "\n" + " new reader: " + reader); } // copy across common attributes reader.setMimetype(this.getMimetype()); reader.setEncoding(this.getEncoding()); reader.setLocale(this.getLocale()); // done if (logger.isDebugEnabled()) { logger.debug("Reader spawned new reader: \n" + " reader: " + this + "\n" + " new reader: " + reader); } return reader; } /** * An automatically created listener sets the flag */ public synchronized final boolean isClosed() { if (channel != null) { return !channel.isOpen(); } else { return false; } } public synchronized boolean isChannelOpen() { if (channel != null) { return channel.isOpen(); } else { return false; } } /** * Provides low-level access to read content from the repository. *
     * This is the only of the content reading methods that needs to be implemented
     * by derived classes.  All other content access methods make use of this in their
     * underlying implementations.
     * 
     * @return Returns a channel from which content can be read
     * @throws ContentIOException if the channel could not be opened or the underlying content
     *      has disappeared
     */
    protected abstract ReadableByteChannel getDirectReadableChannel() throws ContentIOException;
    /**
     * Create a channel that performs callbacks to the given listeners.
     *  
     * @param directChannel the result of {@link #getDirectReadableChannel()}
     * @param listeners the listeners to call
     * @return Returns a channel
     * @throws ContentIOException
     */
    private ReadableByteChannel getCallbackReadableChannel(
            ReadableByteChannel directChannel,
            List 
     * All the content is streamed into memory.  So, like the interface said,
     * be careful with this method.
     * 
     * @see ContentAccessor#getEncoding()
     */
    public final String getContentString() throws ContentIOException
    {
        try
        {
            // read from the stream into a byte[]
            InputStream is = getContentInputStream();
            ByteArrayOutputStream os = new ByteArrayOutputStream();
            FileCopyUtils.copy(is, os);  // both streams are closed
            byte[] bytes = os.toByteArray();
            // get the encoding for the string
            String encoding = getEncoding();
            // create the string from the byte[] using encoding if necessary
            String content = (encoding == null) ? new String(bytes) : new String(bytes, encoding);
            // done
            return content;
        }
        catch (IOException e)
        {
            throw new ContentIOException("Failed to copy content to string: \n" +
                    "   accessor: " + this,
                    e);
        }
    }
    
    /**
     * Does a comparison of the binaries associated with two readers.  Several shortcuts are assumed to be valid:OutputStream
     */
    public final void getContent(OutputStream os) throws ContentIOException
    {
        try
        {
            InputStream is = getContentInputStream();
            FileCopyUtils.copy(is, os);  // both streams are closed
            // done
        }
        catch (IOException e)
        {
            throw new ContentIOException("Failed to copy content to output stream: \n" +
                    "   accessor: " + this,
                    e);
        }
    }
    public final void getContent(File file) throws ContentIOException
    {
        try
        {
            InputStream is = getContentInputStream();
            FileOutputStream os = new FileOutputStream(file);
            FileCopyUtils.copy(is, os);  // both streams are closed
            // done
        }
        catch (IOException e)
        {
            throw new ContentIOException("Failed to copy content to file: \n" +
                    "   accessor: " + this + "\n" +
                    "   file: " + file,
                    e);
        }
    }
    
    public final String getContentString(int length) throws ContentIOException
    {
        if (length < 0 || length > Integer.MAX_VALUE)
        {
            throw new IllegalArgumentException("Character count must be positive and within range");
        }
        Reader reader = null;
        try
        {
            // just create buffer of the required size
            char[] buffer = new char[length];
            
            String encoding = getEncoding();
            // create a reader from the input stream
            if (encoding == null)
            {
                reader = new InputStreamReader(getContentInputStream());
            }
            else
            {
                reader = new InputStreamReader(getContentInputStream(), encoding);
            }
            // read it all, if possible
            int count = reader.read(buffer, 0, length);
            
            // there may have been fewer characters - create a new string as the result
            return (count != -1 ? new String(buffer, 0, count) : "");
        }
        catch (IOException e)
        {
            throw new ContentIOException("Failed to copy content to string: \n" +
                    "   accessor: " + this + "\n" +
                    "   length: " + length,
                    e);
        }
        finally
        {
            if (reader != null)
            {
                try { reader.close(); } catch (Throwable e) { logger.error(e); }
            }
        }
    }
    /**
     * Makes use of the encoding, if available, to convert bytes to a string.
     * 
     *  - if the readers are the same instance, then the binaries are the same
     *  - if the size field is different, then the binaries are different
     * Otherwise the binaries are {@link EqualsHelper#binaryStreamEquals(InputStream, InputStream) compared}.
     * 
     * @return          Returns true if the underlying binaries are the same
     * @throws ContentIOException
     */
    public static boolean compareContentReaders(ContentReader left, ContentReader right) throws ContentIOException
    {
        if (left == right)
        {
            return true;
        }
        else if (left == null || right == null)
        {
            return false;
        }
        else if (left.getSize() != right.getSize())
        {
            return false;
        }
        InputStream leftIs = left.getContentInputStream();
        InputStream rightIs = right.getContentInputStream();
        try
        {
            return EqualsHelper.binaryStreamEquals(leftIs, rightIs);
        }
        catch (IOException e)
        {
            throw new ContentIOException(
                    "Failed to compare content reader streams: \n" +
                    "   Left:  " + left + "\n" +
                    "   right: " + right);
        }
    }
    
    /**
     * InputStream that wraps another InputStream to terminate early after a timeout
     * or after reading a number of bytes. It terminates by either returning end of file
     * (-1) or throwing an IOException.
     * @author Alan Davis
     */
    private class TimeSizeRestrictedInputStream extends InputStream
    {
        private final AtomicBoolean timeoutFlag = new AtomicBoolean(false);
        
        private final InputStream is;
        private final long timeoutMs;
        private final long readLimitBytes;
        private final Action timeoutAction;
        private final Action readLimitAction;
        private final TransformerDebug transformerDebug;
        
        private long readCount = 0;
        public TimeSizeRestrictedInputStream(InputStream is,
                long timeoutMs, Action timeoutAction,
                long readLimitBytes, Action readLimitAction,
                TransformerDebug transformerDebug)
        {
            this.is = useBufferedInputStream ? new BufferedInputStream(is) : is;
            this.timeoutMs = timeoutMs;
            this.timeoutAction = timeoutAction;
            this.readLimitBytes = readLimitBytes;
            this.readLimitAction = readLimitAction;
            this.transformerDebug = transformerDebug;
            
            if (timeoutMs > 0)
            {
                timer.schedule(new TimerTask()
                {
                    @Override
                    public void run()
                    {
                        timeoutFlag.set(true);
                    }
                
                }, timeoutMs);
            }
        }
        @Override
        public int read() throws IOException
        {
            // Throws exception or return true to indicate EOF
            if (hitTimeout() || hitReadLimit())
            {
                return -1;
            }
            
            int n = is.read();
            if (n > 0)
            {
                readCount++;
            }
            return n;
        }
        private boolean hitTimeout() throws IOException
        {
            if (timeoutMs > 0 && timeoutFlag.get())
            {
                timeoutAction.throwIOExceptionIfRequired(
                    "Transformation has taken too long ("+
                    (timeoutMs/1000)+" seconds)", transformerDebug);
                return true;
            }
            return false;
        }
        private boolean hitReadLimit() throws IOException
        {
            if (readLimitBytes > 0 && readCount >= readLimitBytes)
            {
                readLimitAction.throwIOExceptionIfRequired(
                    "Transformation has read too many bytes ("+
                    (readLimitBytes/1024)+"K)", transformerDebug);
                return true;
            }
            return false;
        }
        @Override
        public void close() throws IOException
        {
            try
            {
                is.close();
            }
            finally
            {
                super.close();
            }
        }
    };
}