/*
 ************************************************************************
 *
 * Copyright (c) 2003-2004, C&C Research Laboratories, NEC Europe Ltd.
 *
 * Copyright in this software belongs to C&C Research Laboratories,
 * Rathausallee 10, 53757 Sankt Augustin, Germany.
 *
 * This software may not be used, sold, licensed, transferred, copied
 * or reproduced in whole or in part in any manner or form or in or
 * on any media by any person other than in accordance with the terms
 * of the Licence Agreement supplied with the software, or otherwise
 * without the prior written consent of the copyright owners.
 *
 * This software is distributed WITHOUT ANY WARRANTY, without even the
 * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
 * PURPOSE, except where stated in the Licence Agreement supplied with
 * the software.
 *
 *  Created By :           G.A. Kohring
 *  Created for Project :  GEMSS (IST-2001-37153)
 *
 ************************************************************************
 */

package de.nece.ccrle.organizer;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FilenameFilter;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamClass;
import java.io.OptionalDataException;
import java.io.OutputStream;
import java.io.Serializable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;

/**
 * A repository for session objects implemented a collection of flat files
 * stored in a directory on a local file system.
 *
 * @author Greg Kohring
 */
public class SessionLDR extends LocalDiskRepository implements SessionRepository {
    private static Logger logger =
                       Logger.getLogger( SessionLDR.class.getName() );

    /**
     * The suffix attached to all session files.
     */
    public static final String SUFFIX = ".sobj";

    // Buffer size for reading files
    private static final int BUF_SIZE = 2048;

    // Byte marker which separates objects in the session file
    private static final int MARKER = 0xbb;

    // The loader repository
    private LoaderRepository loaderRepository = null;

    /** @link dependency */
    /*# SessionImpl lnkSessionImpl; */

    /**
     * Constructs a new session repository using the specified directory name.
     * If the directory pointed to by the specified directory name does 
     * not exist it will be created.
     * 
     * @param directoryName a <code>String</code> specifying the name of a
     *      directory to be used for the repository.
     */
    public SessionLDR( String directoryName ) throws FileNotFoundException {
        this( new File( directoryName ) );
    }

    /**
     * Constructs a new session repository using the specified directory.
     * If the specified directory does not exist it will be created.
     * 
     * @param directory a <code>File</code> specifying the
     *      directory to be used for the repository.
     * @throws IOException if an I/O error occurs while opening or
     *      creating the repository.
     */
    public SessionLDR( File directory ) throws FileNotFoundException {
        super( directory, false );
    }

    /**
     * Retrieves a list of the sessions contained in this repository.
     *
     * @return an array of <code>String</code>s containing the names of
     *      known sessions.
     */
    public String[] sessions() {

        // Strip off the suffixes to form the session keys

        File[] files = listing();
        String sessionKeys[] = new String[ files.length ];
        for ( int s = 0; s < files.length; s++){
            String name = files[s].getName();
            sessionKeys[s] = name.substring( 0, name.lastIndexOf( SUFFIX ) );
        }

        return sessionKeys;
    }

    public Object[] getKeys() {
        return sessions();
    }

    public Object getItem( Object key ){
        try{
            return getSession( (String) key );
        } catch ( IOException ioe ){
            //We really should be logging these errors somewhere
            logger.info( ioe.getMessage() );
            return null;
        } catch ( ClassCastException cce ){
            //We really should be logging these errors somewhere
            logger.info( cce.getMessage() );
            return null;
        }
    }

    /**
     * Retrieves the session associated with the specified id.
     *
     * @param sessionID a <code>String</code> identifying the session to
     *      be retrieved.
     * @return the <code>Session</code> object associated with the 
     *      specified id.
     * @throws IOException if an I/O error occurs while retrieving the session
     *      from storage.
     */
    public Session getSession( String sessionID ) throws IOException {

        // Get the file holding the session data

        String sessionName = sessionID + SUFFIX;
        File sessionFile = new File( getDirectory(), sessionName );

        if ( sessionFile.exists() ) {

        // Load the session

            Session session = new SessionImpl( sessionID,
                                               sessionFile.lastModified() );

            load( session, sessionFile );

        // Return the session object

            return session;

        } else {

        // If the session does not exist, throw an exception

            throw new IOException( "session not found: " + sessionID );
        }
    }

    public Session createSession( String sessionID ) throws IOException {
        return new SessionImpl( sessionID, 0 );
    }


    /**
     * Associates the specified item with the specified key.
     * Overrides the {@link Repository#add(Object,Object)} from
     * {@link Repository} to force <code>key</code> to be a
     * <code>String</code> object, and <code>item</code> to be a
     * <code>Session</code> object.
     *
     * @param key the key to be associated with the specified item.
     * @param item the item to be added to the repository.
     * @return <code>true</code> if the repository was changed as a result
     *      of this operation.
     * @throws ClassCastException if either of the input parameters are of
     *      the wrong type.
     */
    public boolean add( Object key, Object item ) {
        return addSession( (String) key, (Session) item );
    }

    /**
     * Adds the specified session under the specified session id to the
     * repository.
     * <p> 
     * A session is saved a byte stream having the following format:
     * <pre>
     *      sessionID:string
     * </pre>
     * followed by a series of the following:
     * <pre>
     *      maker:byte
     *      key:String
     *      component: Object
     * </pre>
     * The  marker is a sinlge byte used to separate the components.
     * It has the value <code>0xbb</code>
     * Each class is annotated with the following information:
     * <pre>
     *      class loader id: String
     * </pre>
     * A class loader id equal to "default" implies the default class loader.
     *
     * @param sessionID the sessionID to be associated with the specified
     *      session.
     * @param session the session to be added to the repository.
     * @return <code>true</code> if the repository was changed as a result
     *      of this operation.
     */
    public boolean addSession(String sessionID, Session session) {
        try{
            String sessionName = sessionID + SUFFIX;
            File sessionFile = new File( getDirectory(), sessionName );

            save( session, sessionFile );

            // After adding a session, we need to re-initialize the 
            // repositories listing.

            initialize();

            return true;

        } catch ( IOException ioe ){
            logger.info( ioe.getMessage() );
            return false;
        }
    }


    protected File[] listing() {
        // Find all the session files

        File dir = getDirectory();
        File[] sessionFiles = dir.listFiles( new FilenameFilter() {
            public boolean accept(File directory, String name){
                if ( name.endsWith( SUFFIX ) ){
                    return true;
                } else {
                    return false;
                }
            }
        } );

        return sessionFiles;
    }

    public boolean remove( Object key ) throws IOException {
        if ( key instanceof String ){
            String fileName = ((String) key) + SUFFIX;
            return super.remove( fileName );
        } else {
            return false;
        }
    }

    public boolean removeSession( String sessionID ) throws IOException {
            String fileName = sessionID + SUFFIX;
            return super.remove( fileName );
    }

    /**
     * Specifies the class loader repository to be used for saving or
     * loading this session.
     *
     * @param lr a {@link LoaderRepository} object for use while saving
     *      or loading the current session.
     */
    public void useLoaders( LoaderRepository lr ){
        this.loaderRepository = lr;
    }

    // loads the session from the specified file
    private void load( Session session, File sessionFile ) 
            throws IOException{

        ObjectInputStream ois = null;

        try {
            ois = new ResolveClassOOStream( 
                        new BufferedInputStream( 
                                new FileInputStream( sessionFile ) ) );

            // Does the session ID match the session ID from the file?

            String oisSessionID = (String) ois.readObject();

            if ( !oisSessionID.equals( session.getSessionID() ) ){
                throw new IOException( " session ID does not match! " );
            }

            // Read in the session

            while( true ){
                byte marker = ois.readByte();
                String objID = (String) ois.readObject();
                Object obj = ois.readObject();
                session.put( objID, obj );
            }

        } catch ( EOFException eofe ){
            // ignore -- we are finished.
            ois.close();
        } catch ( OptionalDataException ode ){
            ois.close();
            if ( !ode.eof ){
                throw new IOException( ode.getMessage() );
            }
        } catch ( ClassNotFoundException cnfe ){
            ois.close();
            throw new IOException( cnfe.getMessage() );
        }

        ois.close();
    }

    // Saves the specified session to the specified file
    private void save( Session session, File sessionFile ) throws IOException{

        AnnotatedOOStream aoos = new AnnotatedOOStream(
                                   new BufferedOutputStream( 
                                     new FileOutputStream( sessionFile ) ) );

        aoos.flush();

        aoos.writeObject( session.getSessionID() );

        //Loop over all items saving those which are serializable

        for ( Iterator ci = session.entrySet().iterator(); ci.hasNext(); ){

            Map.Entry item = (Map.Entry) ci.next();

            if ( item.getValue() instanceof Serializable ){

                aoos.writeByte( MARKER );
                aoos.writeObject( item.getKey() );
                aoos.writeObject( item.getValue() );
            }
        }


        aoos.close();
    }

    protected void changed( List files ) {
        throw new UnsupportedOperationException();
    }

    protected void added( List files ) {
        throw new UnsupportedOperationException();
    }

    protected void deleted( List files ) {
        throw new UnsupportedOperationException();
    }

    public void addListener( RepositoryChangeListener fcl ) {
        throw new UnsupportedOperationException();
    }

    public void removeListener( RepositoryChangeListener fcl ) {
        throw new UnsupportedOperationException();
    }

    /**
     * A class for resolving objects on the input stream
     */
    private class ResolveClassOOStream extends ObjectInputStream {

        public ResolveClassOOStream( InputStream is ) throws IOException{
            super( is );
        }

        protected Class resolveClass( ObjectStreamClass desc )
                      throws IOException, ClassNotFoundException {
            String loaderID = readUTF();
            ClassLoader cl = loaderRepository.getClassLoader( loaderID );
            if ( cl == null ){
                ClassLoader tcl =
                        Thread.currentThread().getContextClassLoader();
                return tcl.loadClass( desc.getName() );
            } else {
                return cl.loadClass( desc.getName() );
            }
        }
    }


    /**
     * An object output steam for annotating classes
     */
    private class AnnotatedOOStream extends ObjectOutputStream {
                                                                                
        public AnnotatedOOStream( OutputStream os ) throws IOException{
            super( os );
        }
                                                                                
        protected void annotateClass( Class c ) throws IOException {
            if ( loaderRepository != null ){
                ClassLoader cl = c.getClassLoader();
                String clID = loaderRepository.getLoaderID( cl );
                if ( clID != null ){
                    writeUTF( clID );
                    return;
                }
            }
            writeUTF( "default" );
        }
                                                                                
    }

}
