/*
 * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
 *
 * The contents of this file are subject to the CCM Public
 * License (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.redhat.com/licenses/ccmpl.html.
 *
 * Software distributed under the License is distributed on an
 * "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either express
 * or implied. See the License for the specific language
 * governing rights and limitations under the License.
 *
 */
package  com.arsdigita.cms.publishToFile;

import com.arsdigita.cms.ContentSection;
import com.arsdigita.cms.Template;

import com.arsdigita.initializer.Configuration;
import com.arsdigita.initializer.InitializationException;

import com.arsdigita.domain.DomainObject;
import com.arsdigita.domain.DomainObjectInstantiator;
import com.arsdigita.domain.DomainObjectFactory;

import com.arsdigita.persistence.DataObject;
import com.arsdigita.persistence.SessionManager;
import com.arsdigita.persistence.TransactionContext;
import com.arsdigita.persistence.metadata.MetadataRoot;
import com.arsdigita.persistence.metadata.ObjectType;
import com.arsdigita.runtime.CCM;

import org.apache.log4j.Logger;

import com.arsdigita.web.URL;

import java.io.File;

import java.util.List;
import java.util.Iterator;

/**
 * Initializes the publish-to-file service. The configuration is described in
 * the {@link com.arsdigita.cms.publishToFile} page.
 *
 * @author Jeff Teeters (teeters@arsdigita.com)
 * @version $Revision: #23 $ $Date: 2004/04/07 $
 */
public class Initializer implements com.arsdigita.initializer.Initializer {

    private static Logger s_log = Logger.getLogger(Initializer.class);

    private final static String PUBLISH_DESTINATIONS = "destination";
    private final static String PUBLISH_TO_FILE_LISTENER = "publishListener";
    private final static String QUEUE_POLL_STARTUP_DELAY = "startupDelay";
    private final static String QUEUE_POLL_DELAY = "pollDelay";
    private final static String RETRY_DELAY = "retryDelay";
    private final static String REQUEST_TIMEOUT = "requestTimeout";
    private final static String BLOCK_SIZE = "blockSize";
    private final static String BLOCK_SELECT_METHOD = "blockSelectMethod";
    private final static String MAXIMUM_FAIL_COUNT = "maximumFailCount";

    private Configuration m_conf = new Configuration();

    public Initializer() throws InitializationException {
        m_conf.initParameter(
            PUBLISH_DESTINATIONS,
            "List of publish destinations for content types" +
            "Each element is a four-element list in the format " +
            "'{ \"content type\", \"root directory\", \"shared storage\", " +
            "\"url stub\" }'. " +
            "Content type is the object type of the content type." +
            "Root directory must be a path to a writable directory, relative" +
            "to the webapp root. Shared storage must be true if the root " +
            "directory is shared NFS storage, false otherwise. URL stub is " +
            "must be the path component of the URL from which the live server " +
            "will serve from this directory.",
            List.class);
        m_conf.initParameter(
            PUBLISH_TO_FILE_LISTENER,
            "Class which implements PublishToFileListener used to " +
            "perform additional actions when publishing or unpublishing " +
            "to the file system.",
            String.class);
        m_conf.initParameter(
            QUEUE_POLL_STARTUP_DELAY,
            "Time (in seconds) after system startup to wait before " +
            "starting to monitor publishToFile queue.  A value < 0 " +
            "disables processing of the queue on this server.",
            Integer.class, new Integer(30));
        m_conf.initParameter(
            QUEUE_POLL_DELAY,
            "Time (in seconds) between checking if there are entries " +
            "in the publishToFile queue. A value <= 0 disables processing " +
            "the queue on this server.",
            Integer.class, new Integer(1));
        m_conf.initParameter(
            RETRY_DELAY,
            "Time to wait (seconds) before retrying to process a " +
            "failed entry.",
            Integer.class, new Integer(120));
        m_conf.initParameter(
            REQUEST_TIMEOUT,
            "Time to wait (seconds) before aborting item request.",
            Integer.class, new Integer(PublishToFile.DEFAULT_TIMEOUT));
        m_conf.initParameter(
            BLOCK_SIZE, "number of queue entries to process in one txn."
            , Integer.class, new Integer(20));
        m_conf.initParameter(
            BLOCK_SELECT_METHOD,
            "Method used to select entries for processing.  " +
            "'QueuedOrder'-in  queued order. 'GroupByParent'-group " +
            "entries according to parent when selecting items " +
            "(allows optimizations if a listener task required for " +
            "all elements in a folder can be done only once for the " +
            "folder).",
            String.class, "QueuedOrder");
        m_conf.initParameter(
            MAXIMUM_FAIL_COUNT,
            "Maximum Fail Count for actions in Queue Manager. " +
            "If Fail Count in Database is more than Specified " +
            "Limit, Queue Manager will ignore the action. The " +
            "Default Value -1 will ignore this parameter.",
            Integer.class, new Integer(-1));
    }

    public Configuration getConfiguration() {
        return m_conf;
    }


    public void startup() {
        TransactionContext txn =
            SessionManager.getSession().getTransactionContext();
        txn.beginTxn();
        setupPublishToFileSystem();

        QueueManager.requeueMissingFiles();

        txn.commitTxn();
    }

    public void shutdown() {
        QueueManager.stopWatchingQueue();
    }


    /**
     * initialize source and destinations for publishing to the file system
     */
    private void setupPublishToFileSystem() throws InitializationException {
        DomainObjectInstantiator inst = new DomainObjectInstantiator() {
                public DomainObject doNewInstance(DataObject dobj) {
                    return new PublishedFile(dobj);
                }
            };
        DomainObjectFactory.registerInstantiator(
            PublishedFile.BASE_DATA_OBJECT_TYPE,
            inst);


        processDestination((List) m_conf.getParameter(PUBLISH_DESTINATIONS));

        PublishToFile.setRequestTimeout(getInteger(REQUEST_TIMEOUT).intValue());

        QueueManager.setRetryDelay(getInteger(RETRY_DELAY));
        QueueManager.setBlockSize(getInteger(BLOCK_SIZE));
        QueueManager.setBlockSelectMethod(getString(BLOCK_SELECT_METHOD));
        QueueManager.setMaximumFailCount(getInteger(MAXIMUM_FAIL_COUNT));


        // setup listener if specified
        String listenerName = getString( PUBLISH_TO_FILE_LISTENER );
        if (listenerName != null) {
            PublishToFileListener listener = null;
            Class listenerClass = null;
            try {
                listenerClass = Class.forName(listenerName);
            } catch (ClassNotFoundException ex) {
                invalidParam(PUBLISH_TO_FILE_LISTENER,
                             "could not find listener class " + listenerClass);
            }

            try {
                listener = (PublishToFileListener)listenerClass.newInstance();
            } catch (InstantiationException ex) {
                invalidParam(PUBLISH_TO_FILE_LISTENER,
                             "could not find instantiate listener class " +
                             listenerClass + " (" + ex.getMessage() + ")");
            } catch (IllegalAccessException ex) {
                invalidParam(PUBLISH_TO_FILE_LISTENER,
                             "could not find instantiate listener class " +
                             listenerClass + " (" + ex.getMessage() + ")");
            }
            QueueManager.setListener(listener);
        }

        // start thread for monitoring queue
        int startupDelay = getInteger(QUEUE_POLL_STARTUP_DELAY).intValue();
        int pollDelay = getInteger(QUEUE_POLL_DELAY).intValue();
        QueueManager.startWatchingQueue(startupDelay, pollDelay);
    }

    //
    // Process publishing destinations
    //

    private static void processDestination(List dest)
        throws InitializationException {
        if (dest == null) {
            invalidParam(PUBLISH_DESTINATIONS,
                         "publish destinations must not be null");
        }
        if (dest.size() < 1) {
            invalidParam(PUBLISH_DESTINATIONS,
                         "publish destinations must contain at " +
                         "least one entry");
        }

        Iterator entries = dest.iterator();
        while (entries.hasNext()) {
            processDestinationEntry((List)entries.next());
        }
    }

    private static void processDestinationEntry(List entry)
        throws InitializationException {

        if ( entry.size() != 4 ) {
            invalidParam(PUBLISH_DESTINATIONS,
                         "publish destinations entry must contain " +
                         "four elements: '{ \n" +
                         "  \"content type\",\n" +
                         "  \"root directory\", \n" +
                         "  \"is shared\", \n" +
                         "  \"url stub\" \n" +
                         "};\n");
        }

        String contentType = (String)entry.get(0);
        String destRoot = (String) entry.get(1);
        Boolean sharedRoot = (Boolean) entry.get(2);
        String destURL = (String) entry.get(3);

        if ( contentType == null || contentType.trim().length() == 0) {
            invalidParam(PUBLISH_DESTINATIONS,
                         "the destination content type must not be null");
        }

        ObjectType type = MetadataRoot.getMetadataRoot().getObjectType(contentType);
        if (type == null) {
            invalidParam(PUBLISH_DESTINATIONS,
                         "the destination content type cannot be found");
        }

        if (destRoot == null || destRoot.trim().length() == 0) {
            invalidParam(PUBLISH_DESTINATIONS,
                         "the destination root must not be null");
        }
        if (destRoot.endsWith("/")) {
            invalidParam(PUBLISH_DESTINATIONS,
                         "the destination root '" + destRoot +
                         "' must not end with a '/'");
        }

        destRoot = new File(System.getProperty("ccm.home"),
                            destRoot).getPath();


        if (sharedRoot == null) {
            invalidParam(PUBLISH_DESTINATIONS,
                         "the destination shared flag must not be null");
        }


        if (destURL == null || "".equals(destURL.trim())) {
            invalidParam(PUBLISH_DESTINATIONS,
                         "the destination URL must not be null");
        }
        if (!destURL.startsWith("/")) {
            invalidParam(PUBLISH_DESTINATIONS,
                         "the destination URL '" + destURL +
                         "' must start with a '/'");
        }
        if (destURL.endsWith("/")) {
            invalidParam(PUBLISH_DESTINATIONS,
                         "the destination URL '" + destURL +
                         "' must not end with a '/'");
        }

        DestinationStub dest = new DestinationStub(destRoot,
                                                   sharedRoot.booleanValue(),
                                                   destURL);

        File file = dest.getFile();
        if (!file.exists()) {
            file.mkdirs();
        }
        boolean writable = false;
        try {
            writable = file.canWrite() && file.isDirectory();
        } catch ( SecurityException ex ) {
            // Will be reported as an initalization error
        }
        if ( ! writable ) {
            // HACK: Let's see if we can write to the config directory.  If we can,
            // then we're running as ccmadmin inside of ccm load, and there is no
            // need to thrown an exception.
            File conf = CCM.getConfigDirectory();
            if (conf.isDirectory() && conf.canWrite()) {
                // we're ok
            } else {
                invalidParam(PUBLISH_DESTINATIONS,  " the document root '" + file
                             +"' must be a writable directory");
            }
        }

        if (Template.BASE_DATA_OBJECT_TYPE.equals(contentType) || !ContentSection.getConfig().getDisableItemPfs()) {
            PublishToFile.addDestination(contentType,
                                         dest);
        }
    }

    private Integer getInteger(String paramName) {
        return (Integer) m_conf.getParameter(paramName);
    }

    private String getString(String paramName) {
        return (String) m_conf.getParameter(paramName);
    }

    private static void invalidParam(String param, String msg)
        throws InitializationException {

        throw new InitializationException(
            "publishToFile: parameter " + param +": " + msg);
    }
}
