/*
 * $Header: /home/cvspublic/jakarta-slide/proposals/tamino/src/store/org/apache/slide/store/tamino/tools/repairer/RepairHandler.java,v 1.4 2004/12/15 10:38:27 pnever Exp $
 * $Revision: 1.4 $
 * $Date: 2004/12/15 10:38:27 $
 *
 * ====================================================================
 *
 * 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.tools.repairer;

import java.util.*;

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 java.io.IOException;
import java.net.ConnectException;
import java.net.MalformedURLException;
import java.net.URL;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.methods.DeleteMethod;
import org.apache.commons.httpclient.methods.HeadMethod;
import org.apache.commons.httpclient.methods.PutMethod;
import org.apache.slide.store.tamino.common.XGlobals;
import org.apache.slide.store.tamino.jdomobjects.XUuri;
import org.apache.slide.store.tamino.security.XURMAccessor;
import org.apache.slide.store.tamino.tools.Tws;
import org.apache.slide.store.tamino.tools.repairer.XRepairModeToken;
import org.apache.slide.store.tamino.tools.repairer.enabler.DefaultNaming;
import org.apache.slide.store.tamino.tools.repairer.enabler.Naming;
import org.apache.slide.store.tamino.tools.stores.XDomain;
import org.apache.slide.store.tamino.tools.stores.XDomainFileHandler;
import org.apache.slide.store.tamino.tools.stores.XNamespace;
import org.apache.slide.store.tamino.tools.stores.XStore;
import org.apache.slide.store.tamino.tools.stores.XStoreGroup;
import org.apache.slide.util.Configuration;
import org.apache.slide.util.XAssertionFailed;
import org.apache.slide.util.XException;
import org.apache.slide.util.XUri;
import org.apache.slide.util.os.Catalina;


/**
 * This class handles all WebDAV meta data consistency tests for stores
 * given by DomainFileHandler
 *
 * <P>
 * The following table lists all checks which will be performed:
 * <P>
 *
 * <table border=1 cellpadding=5>
 * <tr>
 * <th>Check</th>
 * <th>Action to repair this problem</th>
 * </tr>
 *
 * <tr>
 *   <td>Find all documents without metadata.</td>
 *   <td>Add the missing metadata structure</td>
 * </tr>
 * <tr>
 *   <td>Find all metadata without a document.</td>
 *   <td>Remove this metadata.</td>
 * </tr>
 * <tr>
 *   <td>Check correctness of stored metadata DTD.</td>
 *   <td>Create a new correct DTD and store it into Tamino.</td>
 * </tr>
 * <tr>
 *   <td>Check correctness of metadata concerning the DTD.</td>
 *   <td>?</td>
 * </tr>
 * <tr>
 *   <td>Check for unique URL's in metadata.</td>
 *   <td>Delete the doubles (?).</td>
 * </tr>
 * <tr>
 *   <td>Check for the correct content length.</td>
 *   <td>Correct the content length.</td>
 * </tr>
 * <tr>
 *   <td>Check the consistency of the directory information:
 *       a directory has more or less entries as real documents.</td>
 *   <td>Correct the directory entry.</td>
 * </tr>
 * <tr>
 *   <td>Check the consistency of the directory information:
 *       a document has directory information but the directory doesn't know this document.</td>
 *   <td>Add this document to the directory.</td>
 * </tr>
 *
 * </table>
 *
 * @author    Hardy.Kiefer@softwareag.com
 * @author    Peter.Nevermann@softwareag.com
 *
 * @version $Revision: 1.4 $
 */
public class RepairHandler implements XGlobals, RepairConstants {
    public static final String DEFAULT_NAMING_CLASS = DefaultNaming.class.getName();
    
    /**
     * Logging
     */
    private static final String LOGNAME = LoggerUtil.getThisClassName();
    private static Logger logger = LoggerFactory.getLogger(LOGNAME);
    
    /** Checks */
    private static final int REPAIR = 1;
    private static final int ENABLE = 2;
    private static final int MIGRATE = 3;
    
    public  static final String ALL_STORES = "*";
    
    private final String namespace;
    private final Catalina catalina;
    // Server URL & authentication
    private boolean serverIsRunning;
    private final Set lockedStores;
    private final String host;
    private int port;
    private String user;
    private String pwd;
    
    private List patches;
    private final String namingClass;
    
    // Helpers
    private final XDomain domain;
    private XRepairLogWriter logWriter = null;

    private final XURMAccessor urmAccessor;
    
    /**
     * Creates a new Repair instance which performs only a check or
     * try to repair the inconsistency.
     *
     * @pre   url != null
     * @param url url of Tamino WebDAV Server
     * @param user user for autentication
     * @param pwd password for autentication
     * @param console generate console output
     */
    public RepairHandler( String namespace, Catalina catalina, String  url, String user, String pwd, boolean console, String namingClass, XURMAccessor urmAccessor)
        throws XException
    {
        if( logger.isLoggable(Level.FINE) )
            logger.entering( LOGNAME, "<init>", new Object[] { url }  );
        if (namingClass == null) {
            throw new XAssertionFailed();
        }
        
        this.namespace = namespace;
        this.catalina = catalina;
        this.patches = new ArrayList();
        this.namingClass = namingClass;
        this.urmAccessor = urmAccessor;
        try {
            URL urlObj = new URL(url);
            this.host = urlObj.getHost();
            this.port = urlObj.getPort();
            this.user = user;
            this.pwd  = pwd;
            String userInfo = urlObj.getUserInfo();
            if( userInfo != null && userInfo.length() > 0 ) {
                StringTokenizer t = new StringTokenizer( userInfo, ":" );
                this.user = t.nextToken();
                if( t.hasMoreTokens() )
                    this.pwd = t.nextToken();
            }
            
            if (port == -1) {
                port = 80;
            }
        }
        catch ( MalformedURLException e ) {
            throw new XException(e);
        }
        
        String uri = XUri.SEP+catalina.getContext()+REPAIRER_URI+XUri.SEP;
        
        HeadMethod hm = new HeadMethod( uri );
        
        XHttpClient client = new XHttpClient(host, port, user, pwd);
        
        try {
            client.executeMethod( hm );
            serverIsRunning = true;
        }
        catch( IOException he ) {
            serverIsRunning = false;
        }
        
        logWriter = new XRepairLogWriter( catalina.getContext(), host, port, user, pwd, console, serverIsRunning );
        domain = XDomainFileHandler.get().getDomain();
        lockedStores = new HashSet();

        if (XUuri.getScopes() == null) {
            XUuri.initStoreScopes(Tws.readNamespace());
        }
        
        if( logger.isLoggable(Level.FINE) )
            logger.exiting( LOGNAME, "<init>" );
    }
    
    public static boolean useBinding(String uri) throws XException {
        if (!Configuration.useGlobalBinding()) {
            return false;
        }
        XUri scope = XUuri.extractStoreUri(new XUri(uri));
        return XDomainFileHandler.get().getDomain().getStoreByScope(scope).useBinding();
    }
    
    //-- main methods
    
    public void repair(String store, OnOpenTaToken onOpenTaToken, boolean checkOnly ) {
        repair( REPAIR, store, onOpenTaToken, checkOnly);
    }
    
    public void migrate(String store, boolean checkOnly) {
        repair( MIGRATE, store, OnOpenTaToken.DEFAULT, checkOnly);
    }
    
    public void enable(String store, boolean checkOnly ) {
        repair( ENABLE, store, OnOpenTaToken.DEFAULT, checkOnly);
    }
    
    //-- worker methods
    
    /**
     * Method repair
     *
     * @param    checks              the list of checks to perform
     * @param    storeToCheck        the store to check
     * @param    token               an OnOpenTaToken
     * @param    checkOnly           if true, no repair is performed
     * @param    parameters          additional parameters
     *
     */
    private void repair(int mode, String storeToCheck, OnOpenTaToken token, boolean checkOnly) {
        if( logger.isLoggable(Level.FINE) )
            logger.entering( LOGNAME, "check", new Object[] {storeToCheck,token,new Boolean(checkOnly)} );
        
        if (checkOnly)
            logWriter.println( TC_CONSOLE_ONLY, MSG_START_CHECK );
        else
            logWriter.println( TC_CONSOLE_ONLY, MSG_START_REPAIR );
        
        long startTime = System.currentTimeMillis();
        List groups = new ArrayList();
        try {
            groups = getGroups( storeToCheck );
            if( groups.isEmpty() ){
                // No store found at all
                if (storeToCheck.equals(ALL_STORES)) {
                    throw new XException("Could not find any store configuration");
                } else {
                    // the single indicated store not found:
                    throw new XException("Store configuration \"" + storeToCheck + "\" not found");
                }
            }
            // iterate over repair units
            Iterator iter = groups.iterator();
            while( iter.hasNext() ) {
                logWriter.println( TC_CONSOLE_ONLY, "--" );
                XStoreGroup group = (XStoreGroup)iter.next();
                logWriter.openLogFile( group, checkOnly );
                
                doRepair( mode, group, token, checkOnly);
                logWriter.println( TC_CONSOLE_ONLY, "--" );
                
                // total statistic:
                logWriter.println(TC_TOTAL_COUNT, MSG_INCONS_FOUND, patches.size());
                logWriter.println(TC_TOTAL_COUNT, MSG_INCONS_REPAIRED, checkOnly? 0 : patches.size());
                logWriter.println(TC_TOTAL_TIME, MSG_MS, (System.currentTimeMillis() - startTime));
                logWriter.println(TC_TOTAL_RESULT, MSG_SUCCESS);
                
                logWriter.closeLogFile();
                logWriter.initialize();
            }
        } catch ( XException e ) {
            e.printStackTrace();
            logWriter.println( TC_ERROR, e.getMessage() );
            logWriter.println(TC_TOTAL_RESULT, MSG_UNABLE_TO_REPAIR);
        } finally {
            // unlock store:
            Iterator storesIt = lockedStores.iterator();
            while ( storesIt.hasNext() ) {
                XStore qs = (XStore)storesIt.next();
                unLockStore( qs.getName() );
            }
            logWriter.println( TC_CONSOLE_ONLY, "--" );
            if (checkOnly)
                logWriter.println( TC_CONSOLE_ONLY, MSG_END_CHECK );
            else
                logWriter.println( TC_CONSOLE_ONLY, MSG_END_REPAIR );
            
            if( logWriter.isLogFileOpen() )
                logWriter.closeLogFile();
            
            logWriter.initialize();
        } // end-finally
        
        if( logger.isLoggable(Level.FINE) )
            logger.exiting( LOGNAME, "check" );
    }
    
    /**
     * Method getInconsistenciesRepaired
     *
     * @return   a long
     *
     */
    public List getPatches() {
        return patches;
    }
    
    /**
     * Repair worker method
     */
    private void doRepair(int mode, XStoreGroup storeGroup, OnOpenTaToken token, boolean checkOnly) throws XException {
        logWriter.println( TC_CONSOLE_ONLY, storeGroup.toString() );
        logWriter.println( TC_CONSOLE_ONLY, "--" );
        
        XTaminoClient taminoClient = XTaminoClient.create(storeGroup.getMainStore(), user, pwd);
        XTaminoClient taminoClient4History = XTaminoClient.create(storeGroup.getByType("history"), user, pwd);
        
        // get exclusive access to the store
        if( serverIsRunning ) {
            lockStore( storeGroup.getMainStore().getName(), token );
            lockedStores.add( storeGroup.getMainStore() );
        }
        
        // perform the checks
        patches = new ArrayList();
        Iterator checksIt = createChecks(mode, taminoClient, taminoClient4History, storeGroup).iterator();
        try {
            while (checksIt.hasNext()) {
                // run the check
                long checkStartTime = System.currentTimeMillis();
                AbstractCheck check = (AbstractCheck)checksIt.next();
                
                if( logger.isLoggable(Level.FINE) )
                    logger.entering( LOGNAME, check.getClass().getName() + ".repair",
                                    new Object[]{new Boolean(checkOnly)});
                
                check.logStart(logWriter);
                check.repair(logWriter, checkOnly);
                
                if( logger.isLoggable(Level.FINE) )
                    logger.exiting( LOGNAME, check.getClass().getName() + ".repair" );
                
                List localPatches = check.getPatches();
                int fixed;
                
                fixed = checkOnly? 0 : localPatches.size();
                // log and statistics
                logWriter.println(TC_CHECK_TIME, MSG_MS, (System.currentTimeMillis() - checkStartTime));
                logWriter.println(TC_CHECK_COUNT, MSG_INCONS_FOUND, localPatches.size());
                logWriter.println(TC_CHECK_COUNT, MSG_INCONS_REPAIRED, fixed);
                patches.addAll(localPatches);
                // commit here to avoid:
                // Tamino access failure (INOXYE9291: Transaction aborted because it has taken too long
                taminoClient.commit();
                if (checkOnly && localPatches.size() > 0) {
                    // stop here because the following rely on previous checks to have succedded.
                    // E.g. the NamespaceConsistencyCheck needs an ObjectNode - which is checked
                    // by the ValidDescriptorsCheck
                    break;
                }
            }
        }
        catch( XException x ) {
            taminoClient.rollback();
            throw x;
        }
    }
    
    /**
     * This method locks the given store by giving a number of ms to wait if
     * a transaction is still open.
     * The repairer get exclusive access to the given store
     *
     * @param      store  store to be locked
     * @param      token  how to handle in case of open transaction
     *
     * @exception  XException when session conflicts with running transactions
     * @return     true  - if store could be locked
     *             false - if store not available
     */
    public boolean lockStore( String store, OnOpenTaToken token ) throws XException {
        
        if( logger.isLoggable(Level.FINE) )
            logger.entering( LOGNAME, "lockStore", new Object[] {store, token} );
        boolean result = true;
        XRepairModeToken xmlToken = new XRepairModeToken( token );
        // get target path:
        String targetPath = XUri.SEP + catalina.getContext() + REPAIRER_URI + XUri.SEP + store + XUri.SEP + XRepairModeToken.REPAIR_MODE_TOKEN;
        if( logger.isLoggable(Level.FINE) )
            logger.fine(LOGNAME, "lockStore", "targetPath = " + targetPath );
        
        // send it via Http client:
        PutMethod putMethod = new PutMethod(targetPath);
        try {
            putMethod.setRequestBody( xmlToken.outputDocument() );
            putMethod.addRequestHeader( "Content-Type", "text/xml; charset=\"utf-8\"" );
            
            XHttpClient client = new XHttpClient(host, port, user, pwd);
            client.executeMethod(putMethod);
            
        } catch( Exception e ) {
            result = false;
            e.printStackTrace();
            throw new XException( "Could not lock store "+store, e );
        }
        
        int rc = putMethod.getStatusCode();
        if( logger.isLoggable(Level.FINE) ) {
            logger.fine(LOGNAME, "lockStore", "PutMethod status code = " + rc );
            logger.fine(LOGNAME, "lockStore", "PutMethod status text = " + putMethod.getStatusText() );
        }
        
        if( rc >= 400 ) {
            // could not lock the store ... stopping
            throw new XException("Could not lock store "+store+" (RC="+rc+" "+HttpStatus.getStatusText(rc)+")");
        }
        
        if( logger.isLoggable(Level.FINE) )
            logger.exiting( LOGNAME, "lockStore", new Boolean(result));
        return result;
    }
    
    /**
     * This method unlocks the given store.
     *
     * @param      store  store to be unlocked
     *
     */
    public void unLockStore( String store ) {
        if( logger.isLoggable(Level.FINE) )
            logger.entering( LOGNAME, "unLockStore", new Object[] {store} );
        // get target path:
        String targetPath = XUri.SEP + catalina.getContext() + REPAIRER_URI + XUri.SEP + store + XUri.SEP + XRepairModeToken.REPAIR_MODE_TOKEN;
        if( logger.isLoggable(Level.FINE) )
            logger.fine(LOGNAME, "unLockStore", "targetPath = " + targetPath );
        // send it via Http client:
        DeleteMethod delMethod = new DeleteMethod(targetPath);
        XHttpClient client = new XHttpClient(host, port, user, pwd);
        
        try {
            client.executeMethod(delMethod);
            int rc = delMethod.getStatusCode();
            if( logger.isLoggable(Level.FINE) ) {
                logger.fine(LOGNAME, "unLockStore", "PutMethod status code = " + rc );
                logger.fine(LOGNAME, "unLockStore", "PutMethod status text = " + delMethod.getStatusText() );
            }
        } catch (ConnectException he) {
            // Server not found: Repairer will run without conflicts
        } catch( Exception e ) {
            e.printStackTrace();
            logWriter.println( TC_ERROR, "Could not unlock store "+store );
        }
        
        if( logger.isLoggable(Level.FINE) )
            logger.exiting( LOGNAME, "unLockStore");
    }
    
    
    /**
     * Get the list of checks to perform.
     * This methods combines a list of essential tests in depending of the test mask.
     */
    private List createChecks(int mode, XTaminoClient taminoClient, XTaminoClient taminoClient4History, XStoreGroup storeGroup) throws XException {
        XNamespace ns;
        XStore main;
        List checks;
        String tmp;
        XUri rootUri;
        String rootUuri;
        
        main = storeGroup.getMainStore();
        tmp = storeGroup.getMainStore().getScope().toString();
        rootUri = main.getScope();
        rootUuri = XUuri.getStoreUuri(main.useBinding(), new XUri(tmp));
        checks = new ArrayList();  // required to keep the order of the checks :-)
        ns = XDomainFileHandler.get().getNamespace(namespace);
        switch (mode) {
            case ENABLE:
                checks.add(new MissingMetadataCheck(taminoClient, rootUri, rootUuri, createNaming()));
                break;
            case MIGRATE:
                checks.add(new MetadataVersionCheck(ns, taminoClient, urmAccessor));
                checks.add(new ContentSchemaCheck(taminoClient));
                break;
            case REPAIR:
                // CAUTION: ordering is important here - earlier checks check preconditions for later checks
//                checks.add(new MetadataVersionCheck(ns, taminoClient, urmAccessor));
//                checks.add(new ContentSchemaCheck(taminoClient));
                
                checks.add(new UniqueDescriptorsIdCheck(taminoClient));
                checks.add(new ValidDescriptorsCheck(taminoClient));
                checks.add(new ContentCheck(storeGroup, taminoClient, taminoClient4History));
                checks.add(new UniqueContentIdCheck(storeGroup, taminoClient));
                checks.add(new MissingMetadataCheck(taminoClient, rootUri, rootUuri, createNaming()));
                checks.add(new NamespaceConsistencyCheck(taminoClient));
//                checks.add(new GarbageCheck(main, taminoClient));
                break;
            default:
                throw new XAssertionFailed("unkown mode: " + mode);
        }
        
        return checks;
    }
    
    private Naming createNaming() throws XException {
        Class cl;
        Object obj;
        
        try {
            try {
                cl = Class.forName(namingClass);
            } catch (ClassNotFoundException e) {
                throw new XException("class not found");
            }
            try {
                obj = cl.newInstance();
            } catch (InstantiationException e) {
                throw new XException("constructor throws exception: " + e.getMessage());
            } catch (IllegalAccessException e) {
                throw new XException("instantiation not allowed: " + e.getMessage());
            }
            try {
                return (Naming) obj;
            } catch (ClassCastException e) {
                throw new XException("class does not implement the naming interface");
            }
        } catch (XException e) {
            throw new XException("naming class '" + namingClass + "': " + e.getMessage());
        }
    }
    
    /**
     * Get the list of logical stores to check. Associated DeltaV stores are excluded.
     */
    private List getGroups( String storeToCheck ) throws XException {
        boolean all = ALL_STORES.equals(storeToCheck);
        List result = new ArrayList();
        Iterator namespaces = domain.getNamespaces().iterator();
        while (namespaces.hasNext()) {
            XNamespace ns = (XNamespace) namespaces.next();
            Iterator groups = ns.getPublicStoreGroups().iterator();
            while( groups.hasNext() ) {
                XStoreGroup group = (XStoreGroup)groups.next();
                if( all || group.name.equals(storeToCheck) ) {
                    result.add( group );
                }
            }
        }
        
        return result;
    }
}

