/*
 * $Header: /home/cvspublic/jakarta-slide/proposals/tamino/src/store/org/apache/slide/store/tamino/tools/stores/XDomainFileHandler.java,v 1.3 2004/07/30 06:52:06 ozeigermann Exp $
 * $Revision: 1.3 $
 * $Date: 2004/07/30 06:52:06 $
 *
 * ====================================================================
 *
 * 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.stores;

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.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import org.apache.slide.store.tamino.common.XGlobals;
import org.apache.slide.store.tamino.tools.Env;
import org.apache.slide.store.tamino.tools.stores.XDomain;
import org.apache.slide.store.tamino.tools.stores.XDomainConfig;
import org.apache.slide.store.tamino.tools.stores.XDomainConstants;
import org.apache.slide.store.tamino.tools.stores.XNamespace;
import org.apache.slide.store.tamino.tools.stores.XStoreGroup;
import org.apache.slide.util.ClassName;
import org.apache.slide.util.JDom;
import org.apache.slide.util.PublicIdResolver;
import org.apache.slide.util.XAssertionFailed;
import org.apache.slide.util.XException;
import org.jdom.DocType;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.input.SAXBuilder;
import org.xml.sax.InputSource;


/**
 ** Utility to handle store-specific configuration information contained in the Domain.XML
 ** file. This includes security configuration.
 ** <p>
 ** Details:
 ** <li>Only tamino-enabled stores are handled (classname=org.apache.slide.store.tamino.store.XParentStore).
 **     In the following, "store" always stands for "tamino-enabled store".
 ** <li>Each store webdav-enables a particular Tamino collection.
 ** <li>Each store has a logical (LN) and a physical name (PN). The PN is the Tamino collection
 **     name. The LN is arbitrary but should be a valid file/folder name.
 **     <i>(Which platform?)</i>
 ** <li>In the configuration file Domain.xml, there are 3 related strings: the store name, the
 **     scope name and the Tamino collection name. These relate to LN/PN as follows:
 **     <pre>
 **     collection name = PN
 **     store name = LN
 **     scope = "/"+LN
 **     </pre>
 ** <li>A store can be configured by 2 means: single API calls (createStore(), setParameter(), ...)
 **     or, alternatively, by supplying a "short" configuration file.
 ** <li>A "short" configuration file is an XML file which conforms to the following DTD:
 **     <pre>
 **     &lt!ELEMENT configuration  (parameter+ )&gt
 **     &lt!ATTLIST configuration  namespace CDATA  #REQUIRED &gt
 **     &lt!ELEMENT parameter  (#PCDATA )&gt
 **     &lt!ATTLIST parameter  name CDATA  #REQUIRED &gt
 **     </pre>
 **
 ** @author    peter.nevermann@softwareag.com
 ** @version   $Revision: 1.3 $
 **/
public class XDomainFileHandler implements XDomainConstants {
    //--
    
    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 XDomainFileHandler singleton;
    
    /**
     ** Returns a singleton for read-only purposes. Callers of this method must not modify the
     ** domain file returned by this method. To modify a domain file, use one of the create
     ** methods to obtain your own instance of the domain file.
     **/
    public static synchronized XDomainFileHandler get() {
        if (singleton == null) {
            try {
                singleton = XDomainFileHandler.create(true);
            } catch (XException e) {
                throw new XAssertionFailed(e);
            }
        }
        return singleton;
    }
    
    /**
     ** Creates a new instance. Use this method if you want to modify the domain file; use
     ** the <code>get</code> method if you want read-only access.
     **
     ** @exception  XException if loading the XML file fails
     **/
    public static XDomainFileHandler create(boolean validating) throws XException {
        return new XDomainFileHandler(Env.get().home + File.separator + XGlobals.DOMAIN_XML, validating);
    }
    
    /**
     ** Creates a new instance. Use this method if you want to modify the domain file; use
     ** the <code>get</code> method if you want read-only access.
     **
     ** @exception  XException if loading the XML file fails
     **/
    public static XDomainFileHandler create(String domainXml) throws XException {
        return new XDomainFileHandler(domainXml, true);
    }
    
    //--
    
    /** The Domain.XML document used for toXml. Namespaces will be inserted, parameters will be appended. */
    private XDomain domain;
    
    /** JDom tree as loaded from the file **/
    private Element root;
    
    /** The Domain.XML file */
    private File domainXml;
    
    private final boolean validating;
    
    /**
     ** Constructs a XDomainFileHandler using path to domain file and dtd.
     ** Private, use get or create.
     **
     ** @param      domainXml the path to the Domain.XML file
     ** @exception  XException when loading the XML file fails
     **/
    private XDomainFileHandler(String domainXml, boolean validating) throws XException {
        if( logger.isLoggable(Level.FINE) )
            logger.entering( CLASSNAME, "<init>", new Object[] {domainXml}  );
        
        this.domainXml = new File( domainXml );
        this.validating = validating;
        load();
        if( logger.isLoggable(Level.FINE) )
            logger.exiting( CLASSNAME, "<init>" );
    }
    
    public XDomain getDomain() {
        return domain;
    }
    
    public XNamespace getNamespace(String name) throws XException {
        return domain.getNamespace(name);
    }
    
    /**
     ** Commit changes to the Domain.xml file.
     **
     ** @pre        true
     ** @post       true
     **
     ** @exception  XException
     **/
    public boolean save() throws XException {
        Element current;
        Element normalized;
        
        current = domain.toXml();
        
        // from/to xml might change xml physically (but not logically)
        normalized = XDomain.fromXml(root).toXml();
        if (JDom.equals(current, normalized)) {
            return false;
        } else {
            try {
                OutputStreamWriter out = new OutputStreamWriter(new FileOutputStream(domainXml), JDom.UTF_8);
                JDom.outputter().output( current, out );
                out.close();
            }
            catch( IOException x ) {
                throw new XException("commit failed", x );
            }
            if( logger.isLoggable(Level.INFO) )
                logger.info( CLASSNAME, "commit", "Saved file '"+domainXml+"' successfully" );
            
            return true;
        }
    }
    
    /**
     ** Rollback changes to the Domain.xml file.
     **
     ** @pre        true
     ** @post       true
     **
     ** @exception  XException
     **/
    public void rollback() throws XException {
        if( logger.isLoggable(Level.FINE) )
            logger.entering( CLASSNAME, "rollback" );
        
        load();
        if( logger.isLoggable(Level.INFO) )
            logger.info( CLASSNAME, "rollback", "Changes to file '"+
                            domainXml+"' rolled-back successfully" );
        
        if( logger.isLoggable(Level.FINE) )
            logger.exiting( CLASSNAME, "rollback" );
    }
    
    private void load() throws XException {
        if( logger.isLoggable(Level.FINE) )
            logger.entering( CLASSNAME, "load" );
        
        try {
            if( !domainXml.exists() ) {
                throw new XException("file not found: " + domainXml);
            }
            try {
                root = new SAXBuilder().build( domainXml ).getRootElement();
            }
            catch( IOException x ) {
                throw new XException("load failed: " + x.getMessage(), x );
            }
            catch( JDOMException x ) {
                throw new XException("load failed: " + x.getMessage(), x );
            }
            domain = XDomain.fromXml(root);
        } catch (XException e) {
            throw new XException(domainXml + ": " + e.getMessage(), e);
        }
        
        if( logger.isLoggable(Level.FINE) )
            logger.exiting( CLASSNAME, "load" );
    }
    
    private Element parse(InputSource src) throws XException {
        SAXBuilder builder;
        
        builder = new SAXBuilder();
        try {
            builder.setValidation( validating );
            if( validating ) {
                PublicIdResolver er = new PublicIdResolver( false );
                er.map( PublicIdResolver.CONFIG_DTD_PUBLIC_ID, Env.get().home+File.separator+XDomainConstants.ETC+File.separator+PublicIdResolver.CONFIG_DTD_FILENAME );
                builder.setEntityResolver( er );
            }
            return builder.build( src ).getRootElement();
        }
        catch( IOException x ) {
            throw new XException( "Parsing of config XML string failed", x );
        }
        catch( JDOMException x ) {
            throw new XException( "Parsing of config XML string failed", x );
        }
    }
    
    //-- config file stuff
    
    public Document getGlobalConfig() {
        Document doc;
        Element config;
        
        config = domain.getConfig().toConfigXml();
        config.getContent().addAll(0, domain.getParameterComments());
        if ( validating ) {
            doc = new Document(config, new DocType(CONFIGURATION, CONFIG_DTD2));
        } else {
            doc = new Document(config);
        }
        return doc;
    }
    
    public void setGlobalConfig(InputSource src) throws XException {
        domain.setConfig(XDomainConfig.fromXml(parse(src)));
    }
    
    public void clearGlobalConfig() throws XException {
        domain.setConfig(XDomainConfig.createEmpty());
    }
    
    public Document getStoreConfig(String namespace, String storeGroup) throws XException {
        return getStoreConfig(namespace, domain.getNamespace(namespace).getStoreGroup(storeGroup));
    }
    
    public Document getStoreConfig(String namespace, XStoreGroup store) {
        if( logger.isLoggable(Level.FINE) )
            logger.entering( CLASSNAME, "getStoreConfig", new Object[]{namespace,store}  );
        
        Element result;
        
        result = store.toConfigXml(namespace);
        if( logger.isLoggable(Level.FINE) )
            logger.exiting( CLASSNAME, "getStoreConfig" );
        if( validating ) {
            return new Document(result, new DocType(XDomainConstants.CONFIGURATION, XDomainConstants.CONFIG_DTD) );
        } else {
            return new Document(result);
        }
    }
    
    public void setStoreConfig( String defaultNsName, String storeName, InputSource configXml ) throws XException {
        setStoreConfig( defaultNsName, storeName, parse(configXml) );
    }

    public void setStoreConfig( String defaultNsName, String storeName, Element root ) throws XException {
        String nsName;
        XNamespace ns;
        XStoreGroup old;
        XStoreGroup new_;
        
        nsName = root.getAttributeValue(XDomainConstants.NAMESPACE);
        if (nsName == null) {
            nsName = defaultNsName;
        }
        ns = domain.getNamespace(nsName);
        old = ns.lookupStoreGroup(storeName);
        new_ = XStoreGroup.fromConfigXml(domain.getConfig().createTypes(), storeName, root);
        if (old == null) {
            // TODO: ugly trick to validate arguments
            new_.checkCompatible(new_);
        } else {
            old.checkCompatible(new_);
            ns.removeStoreGroup(storeName);
        }
        ns.addStoreGroup(new_);
    }
}

