/*
 * $Header: /home/cvspublic/jakarta-slide/proposals/tamino/src/store/org/apache/slide/store/tamino/common/XDescriptorsHandler.java,v 1.5 2005/02/23 16:36:54 pnever Exp $
 * $Revision: 1.5 $
 * $Date: 2005/02/23 16:36:54 $
 *
 * ====================================================================
 *
 * Copyright 1999-2004 The Apache Software Foundation.
 *
 * Licensed 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.slide.store.tamino.common;

import com.softwareag.common.instrumentation.logging.Level;
import com.softwareag.common.instrumentation.logging.Logger;
import com.softwareag.common.instrumentation.logging.LoggerFactory;
import com.softwareag.common.instrumentation.logging.LoggerUtil;
import org.apache.slide.store.tamino.datastore.IDbSession;
import org.apache.slide.store.tamino.datastore.XDbHandler;
import org.apache.slide.store.tamino.jdomobjects.XDescriptors;
import org.apache.slide.store.tamino.jdomobjects.XFactory;
import org.apache.slide.store.tamino.jdomobjects.XTLock;
import org.apache.slide.store.tamino.store.XChildStore;
import org.apache.slide.store.tamino.store.XDescriptorsStore;
import org.apache.slide.store.tamino.store.monitoring.AbstractMonitorable;
import org.apache.slide.store.tamino.store.monitoring.Monitor;
import org.apache.slide.util.ClassName;
import org.apache.slide.util.Configuration;
import org.apache.slide.util.XAssertionFailed;
import org.apache.slide.util.XException;


/**
 * Implementation for the Descriptors Handler.
 *
 * @author    peter.nevermann@softwareag.com
 *
 * @version   $Revision: 1.5 $
 *
 * @see       IDescriptorsHandler
 *
 */
public class XDescriptorsHandler extends AbstractMonitorable
    implements IDescriptorsHandler, XGlobals {
    
    private static final String LOGNAME = LoggerUtil.getThisClassName();
    private static final String CLASSNAME = new ClassName(LOGNAME).getPlainName();
    private static Logger logger = LoggerFactory.getLogger(LOGNAME);
    private static Logger getLogger = LoggerFactory.getLogger(LOGNAME+".get");
    
    /** The global cache. */
    private XDescriptorsCache globalCache;
    
    /** An instance of XDescriptorsStore for calling back. */
    private XDescriptorsStore childStore;
    
    /** Synchronization point for the ON_OPEN_TA_WAIT handling. */
    protected final Object onOpenTaSynchPoint = new Object();
    
    /**
     * Default constructor.
     *
     * @param      childStore   the Slide accessor (XDescriptorsStore)
     *
     * @pre        childStore != null
     * @post       globalCache != null
     * @post       deltas != null
     */
    public XDescriptorsHandler(XDescriptorsStore childStore, XTLockSettings tlockSettings) {
        if( logger.isLoggable(Level.FINE) )
            logger.entering( CLASSNAME, "<init>", new Object[] {childStore, tlockSettings} );
        
        globalCache = new XDescriptorsCache(this, tlockSettings, onOpenTaSynchPoint);
        if( logger.isLoggable(Level.FINE) )
            logger.fine(CLASSNAME, "<init>", "Created global cache");
        
        this.childStore = childStore;
        
        if( logger.isLoggable(Level.FINE) )
            logger.exiting( CLASSNAME, "<init>" );
    }
    
    public boolean useBinding() {
        return Configuration.useBinding(childStore.getParentStore());
    }
    
    /**
     * Initializes this component.
     */
    public void initialize() {
        if( logger.isLoggable(Level.FINE) )
            logger.entering( CLASSNAME, "initialize" );
        
        // monitoring varialbles
        this.monName = "DescriptorsHandler";
        this.monParent = childStore;
        this.monChildren.add( globalCache );
        ((AbstractMonitorable)globalCache).setParent(this);
        this.monitor = Monitor.getMonitor( this ); // this comes last!!
        
        // init the children
        globalCache.initialize();
        
        if( logger.isLoggable(Level.FINE) )
            logger.exiting( CLASSNAME, "initialize" );
    }
    
    
    //-- lookup functionality
    
    /**
     * <p>
     * Get descriptors object for the requested unique URI.
     * The access sequence for get is specified as follows:
     * <li> search in delta cache for the current thread; if not found...</li>
     * <li> search in global cache; if not found...</li>
     * <li> retrieve from database; if not found...</li>
     * <li> create a new descriptors object if the create argument is true</li></p>
     * <p>
     * Moreover, the following holds:
     * If (readOnly == true) and the descritors object was...
     * <li> found in delta cache: return it! (Note, the returned object is modifiable!)
     * <li> found in global cache: return it!
     * <li> retrieved from database: add it to the global cache, mark it read-only and
     *      return it!
     * <li> created: add it to the global cache, mark it read-only and
     *      return it! (<b>this is to fulfill "never returns null" - makes sense?</b>)</p>
     * <p>
     * Else If (readOnly == false) and the descritors object was...
     * <li> found in delta cache: return it!
     * <li> found in global cache: clone it, mark clone modifiable, add clone to delta
     *      cache and return the clone!
     * <li> retrieved from database: add it to global cache, clone it, mark clone as
     *      modifiable, add clone to delta cache and return the clone!
     * <li> created: add it to global cache, clone it, mark clone as
     *      modifiable, add clone to delta cache and return the clone!</p>
     *
     * <p><b>NOTE</b>:
     * This method must be <b>synchronized</b> to avoid being pre-empted after
     * the searching phase and before modifying the caches.
     *
     * @pre        uri != null
     * @post
     *
     * @param      uri       the requested URI - used for logging purpose only;
     *                       needed for lookup in uri cache, may be null if readOnly == true
     * @param      uuri      unique uri which is used to actually used for lookup
     *
     * @return     the requested or new descriptors object; null if not found and create was false
     *
     * @exception  XException   nothing will be locked in this case
     *
     * @return null if not found
     */
    public synchronized IDescriptors lookup(String uri, String uuri, int lockType)
        throws XException {
        IDescriptors result;
        
        if (lockType != XTLock.NO_LOCK) {
            if (uri == null) {
                throw new XAssertionFailed();
            }
            prepareForModify();
        }
        result = globalCache.checkout(lockType, uri, uuri);
        if( result != null ) {
            if( logger.isLoggable(Level.FINE) ) log(uuri, "found in global cache");
            return result;
        }
        result = getDbSession().readDescriptor( uuri );
        if (result != null) {
            if (logger.isLoggable(Level.FINE)) log(uuri, "found in database");
            return globalCache.add( lockType, uri, result );
        }
        if (logger.isLoggable(Level.FINE)) log(uuri, "not found");
        return null;
    }
    
    public synchronized IDescriptors lookupUriCache(int lockType, String uri) throws XTLockedException {
        return globalCache.checkoutUriCache(lockType, uri);
    }
    
    /** helper for lookup methods */
    private void log(String uuri, String event) {
        logger.fine(CLASSNAME, "lookup", "uuri=" + uuri + ": " + event);
    }
    
    //-- more methods used by the resolver
    
    public synchronized IDescriptors create(String uri) throws XConflictException {
        IDescriptors d;
        
        prepareForModify();
        d = XFactory.createUriDescriptor( useBinding(), childStore.getParameter(TAMINO_COLLECTION), uri );
        return globalCache.add( XTLock.WRITE_LOCK, uri, d );
    }
    
    public synchronized XDescriptors cacheQueryResult(String uri, XDescriptors d) {
        XDescriptors result = null;
        try {
            result = (XDescriptors)globalCache.checkout(XTLock.NO_LOCK, null, d.getUuri());
        }
        catch (XTLockedException e) {}
        if (result == null) {
            result = (XDescriptors)globalCache.add(XTLock.NO_LOCK, uri, d);
        }
        return result;
    }
    
    //-- transaction functionality
    
    /**
     * <p>Prepare for a transaction commit of the transaction associated to the current
     * thread.
     * <p>Flush uncommitted descriptors objects to the database.
     *
     * @exception  XException
     */
    public synchronized void prepare() throws XException {
        if( logger.isLoggable(Level.FINE) )
            logger.entering( CLASSNAME, "prepare" );
        
        getDbSession().processDescriptors( globalCache.getModifiedDescriptors() );
        if( logger.isLoggable(Level.FINE) )
            logger.fine(CLASSNAME, "prepare", "Flushed descriptors to the database");
        
        if( logger.isLoggable(Level.FINE) )
            logger.exiting( CLASSNAME, "prepare" );
    }
    
    /**
     * <p>Commit the global transaction associated to the current thread.
     * <p>If database session is still available: commit database and clean-up session.
     * <p>Clean-up delta cache: remove entries marked deleted; call commitEvent for
     * non-deleted entries and move them to the global cache; remove delta cache.
     *
     * @param      onePhase   If true, a one-phase commit protocol should be used to commit
     *                        the work done within the current thread.
     *
     * @exception  XException
     */
    public synchronized void commit( boolean onePhase ) throws XException {
        if( logger.isLoggable(Level.FINE) )
            logger.entering( CLASSNAME, "commit", new Object[] {new Boolean(onePhase)} );
        
        IDbSession dbSession = null;
        try {
            dbSession = getDbHandler().getDbSession();
            if( dbSession != null ) {
                // WAM if onePhase, the descriptors are not yet processed (no prepare)
                if (onePhase) {
                    getDbSession().processDescriptors( globalCache.getModifiedDescriptors() );
                }
                
                // commit and remove db session
                dbSession.commit(onePhase);
            }
        } finally {
            globalCache.checkinAll(true);
            if( dbSession != null) {
                getDbHandler().removeDbSession();
            }
        }
        
        if( logger.isLoggable(Level.FINE) )
            logger.exiting( CLASSNAME, "commit" );
    }
    
    /**
     * <p>Roll back work done on behalf of a transaction branch.
     * <p>If database session is still available: rollback database and clean-up session.
     * <p>Clean-up delta cache: remove all entries; remove delta cache.
     *
     * @exception  XException
     */
    public synchronized void rollback() throws XException {
        if( logger.isLoggable(Level.FINE) )
            logger.entering( CLASSNAME, "rollback" );
        
        IDbSession dbSession = null;
        try {
            dbSession = getDbHandler().getDbSession();
            if( dbSession != null ) {
                dbSession.rollback();
            }
        }
        finally {
            globalCache.checkinAll(false);
            if( dbSession != null ) {
                getDbHandler().removeDbSession();
            }
            
            if( logger.isLoggable(Level.FINE) )
                logger.fine(CLASSNAME, "rollback", "Removed delta cache");
        }
        
        if( logger.isLoggable(Level.FINE) )
            logger.exiting( CLASSNAME, "rollback" );
    }
    
    /**
     ** Returns true if this handler is in repair mode (i.e. read_only).
     ** @return     true if this handler is in repair mode
     **/
    public synchronized boolean isInRepairMode() {
        if( logger.isLoggable(Level.FINE) )
            logger.entering( CLASSNAME, "isInRepairMode" );
        
        boolean result = childStore.isInRepairMode();
        
        if( logger.isLoggable(Level.FINE) )
            logger.exiting( CLASSNAME, "isInRepairMode", new Boolean(result) );
        return result;
    }
    
    /**
     ** Set repair mode for this handler.
     ** @param     onOpenTA 0=WAIT, 1=ERROR, 2=ROLLBACK
     ** @param     waitTimeout the timeout for onOpenTA=0 (WAIT)
     ** @exception  XException
     **/
    public synchronized void setRepairMode( int onOpenTA, long waitTimeout ) throws XException {
        if( logger.isLoggable(Level.FINE) )
            logger.entering( CLASSNAME, "setRepairMode", new Object[] {new Integer(onOpenTA), new Long(waitTimeout)}  );
        
        if (onOpenTA == ON_OPEN_TA_ROLLBACK) {
            logger.warning( "Not yet implemented ON_OPEN_TA_ROLLBACK, using default ON_OPEN_TA_WAIT" );
            onOpenTA = ON_OPEN_TA_WAIT;
        }
        switch( onOpenTA ) {
            case ON_OPEN_TA_WAIT:
                if( globalCache.isModified()) {
                    try {
                        logger.warning( "Setting repair mode for store "+
                                           childStore.getParentStore().getName()+
                                           " has to wait for open transactions [timeout: "+(waitTimeout/1000)+" sec]" );
                        long start = System.currentTimeMillis();
                        
                        synchronized( onOpenTaSynchPoint ) {
                            onOpenTaSynchPoint.wait( waitTimeout );
                        }
                        
                        long end = System.currentTimeMillis();
                        if( (end-start) >= waitTimeout && globalCache.isModified() ) {
                            String lrr = globalCache.reportLockedResources();
                            if( logger.isLoggable(Level.WARNING) )
                                logger.warning( "Setting repair mode for store "+
                                                   childStore.getParentStore().getName()+
                                                   " timed-out. There are still open transactions:\n"+lrr );
                            throw new XConflictException( "Setting repair mode for store "+
                                                             childStore.getParentStore().getName()+
                                                             " timed-out. There are still open transactions:\n"+lrr );
                        }
                    }
                    catch( InterruptedException x ) {
                        if( logger.isLoggable(Level.WARNING) ) x.getMessage();
                        if( globalCache.isModified() ) {
                            String lrr = globalCache.reportLockedResources();
                            if( logger.isLoggable(Level.WARNING) )
                                logger.warning( "Setting repair mode for store "+
                                                   childStore.getParentStore().getName()+
                                                   " interrupted. There are open transactions:\n"+lrr );
                            throw new XConflictException( "Setting repair mode for store "+
                                                             childStore.getParentStore().getName()+
                                                             " interrupted. There are open transactions:\n"+lrr );
                        }
                    }
                }
                break;
            case ON_OPEN_TA_ERROR:
                if( globalCache.isModified()) {
                    String lrr = globalCache.reportLockedResources();
                    if( logger.isLoggable(Level.WARNING) )
                        logger.warning( "Setting repair mode for store "+
                                           childStore.getParentStore().getName()+
                                           " failed. There are open transactions:\n"+lrr );
                    throw new XConflictException( "Setting repair mode for store "+
                                                     childStore.getParentStore().getName()+
                                                     " failed. There are open transactions:\n"+lrr );
                }
                break;
            default:
        }
        
        if( logger.isLoggable(Level.FINE) )
            logger.exiting( CLASSNAME, "setRepairMode" );
    }
    
    /**
     ** Release repair mode for this handler .
     ** @exception  XException
     **/
    public synchronized void releaseRepairMode() throws XException {
        if( logger.isLoggable(Level.FINE) )
            logger.entering( CLASSNAME, "releaseRepairMode" );
        
        if( isInRepairMode() ) {
            globalCache.clear();
        }
        
        if( logger.isLoggable(Level.FINE) )
            logger.exiting( CLASSNAME, "releaseRepairMode" );
    }
    
    /**
     * Make sure there's no repair request.
     */
    private void prepareForModify() throws XConflictException {
        if( !globalCache.isModifying()) {
            if( isInRepairMode() )
                throw new XConflictException( "New transaction refused - store for scope "+
                                                 ((XChildStore)childStore).getScope()+
                                                 " is in repair mode" );
        }
    }
    
    /**
     ** Get the associated DB handler.
     **
     ** @return     return the DB handler
     **/
    private XDbHandler getDbHandler() {
        if( logger.isLoggable(Level.FINE) )
            logger.entering( CLASSNAME, "getDbHandler" );
        
        XDbHandler dbHandler = childStore.getDbHandler();
        
        if( logger.isLoggable(Level.FINE) )
            logger.exiting( CLASSNAME, "getDbHandler", dbHandler );
        return dbHandler;
    }
    
    /**
     * Get the database session.
     */
    private IDbSession getDbSession() throws XException {
        IDbSession dbSession = getDbHandler().getDbSession();
        if( dbSession == null ) {
            dbSession = getDbHandler().createDbSession ();
            if( logger.isLoggable(Level.FINE) )
                logger.fine(CLASSNAME, "dbSession", "Created DB session for "+childStore);
        }
        return dbSession;
    }
}

