/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 * 
 * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
 * 
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License. You can obtain
 * a copy of the License at https://glassfish.dev.java.net/public/CDDL+GPL.html
 * or glassfish/bootstrap/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 * 
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at glassfish/bootstrap/legal/LICENSE.txt.
 * Sun designates this particular file as subject to the "Classpath" exception
 * as provided by Sun in the GPL Version 2 section of the License file that
 * accompanied this code.  If applicable, add the following below the License
 * Header, with the fields enclosed by brackets [] replaced by your own
 * identifying information: "Portions Copyrighted [year]
 * [name of copyright owner]"
 * 
 * Contributor(s):
 * 
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */
package com.sun.enterprise.registration;

import com.sun.enterprise.registration.RegistrationService.RegistrationStatus;
import com.sun.enterprise.registration.RegistrationService.RegistrationReminder;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.logging.Logger;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import java.util.ArrayList;
import java.util.List;

import java.io.File;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;

/**
 *
 * @author msiraj
 * @author tjquinn
 */

/** Manages a local repository for service tags and registration status.
 * <p>
 * The client code specifies (in the constructor) the file path or the File 
 * which is to hold the persisted registration repository.  Note that any public 
 * method that modifies any part of the DOM for the repository also 
 * flushes the repository to the file before returning.
 * <p>
 * If the specified file exists, the manager loads its contents as the initial
 * value of the document.  If the file does not exist, the manager creates a
 * new in-memory document containing the top-level registry element and the
 * default registrationstatus element but does NOT write out this initial data.
 * Writes to the file occur only in response to the client code invoking one of
 * the methods that modifies the DOM.
 */

public class RepositoryManager {
    
    private Logger logger = Logger.getLogger(getClass().getPackage().getName());

    /**
     * Creates a new manager which persists the registration data in the specified File.
     * @throws RegistrationException for any errors creating the XML parser or
     * transformer used for reading and writing the registration data or during
     * the initial load of the repository from the local file
     */
    public RepositoryManager(File registrationFile) throws RegistrationException {
        this.registrationFile = registrationFile;
        try {
            logger.fine("RepositoryManager created for file " + registrationFile.getCanonicalPath());
            documentBuilder = documentBuilderFactory.newDocumentBuilder();
            transformer = transformerFactory.newTransformer();
            transformer.setOutputProperty(OutputKeys.INDENT, "yes");
            transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount","4");
            loadOrCreateDocument();
        } catch (Exception e) {
            throw new RegistrationException(e);
        }
    }

    /**
     * Returns a list of all service tag objects represented in the repository.
     * @return List<ServiceTag>
     * the in-memory cache (if it is not already there)
     */
    public List<ServiceTagImpl> getServiceTags() {
        NodeList nodes = document.getElementsByTagName(ServiceTagImpl.SERVICE_TAG);
        List<ServiceTagImpl> serviceTags = new ArrayList<ServiceTagImpl>();
        
        /* nodes is guaranteed to be non-null */
        for(int i = 0 ; i < nodes.getLength();i++) {
            Element elem = (Element)nodes.item(i);
            serviceTags.add(new ServiceTagImpl(elem));
        }
        return serviceTags;
    }
    
    /**
     * Returns the current registration status value.
     * Returns RegistrationService.REGISTERED if any one service tag is registered.
     * @return RegistrationService.RegistrationStatus enum for the current registration status
     */
    public RegistrationStatus getRegistrationStatus() {
        NodeList nodes = document.getElementsByTagName(ServiceTagImpl.SERVICE_TAG);
        for(int i = 0 ; i < nodes.getLength();i++) {
            Element elem = (Element)nodes.item(i);
            RegistrationStatus rs = 
                    RegistrationStatus.valueOf(getSubElementValue(elem, ServiceTagImpl.REGISTRATION_STATUS));
            if (rs.equals(RegistrationStatus.REGISTERED))
                return rs;
        }
        return RegistrationStatus.NOT_REGISTERED;
    }
    

    /**
     * Sets the registration status value for all service tags and persists the change 
     * to the on-disk repository.
     * @param status the new RegistrationStatus value
     * @throws RegistrationException for errors writing the registration data
     * to the local repository
     */
    public void setRegistrationStatus(RegistrationStatus status) throws RegistrationException {
        NodeList nodes = document.getElementsByTagName(ServiceTagImpl.SERVICE_TAG);
        for(int i = 0 ; i < nodes.getLength();i++) {
            Element elem = (Element)nodes.item(i);
            setSubElementValue(elem, ServiceTagImpl.REGISTRATION_STATUS, status.toString());
        }
        writeToFile();
    }

    /**
     * Returns the current registration status value.
     * @return RegistrationService.RegistrationReminder enum for the current registration status
     */
    public RegistrationReminder getRegistrationReminder() {
        Element regElement = findRegistrationReminderElement();
        return RegistrationReminder.valueOf(regElement.getTextContent());
    }

    /**
     * Sets the registration status value and persists the change to the on-disk
     * repository.
     * @param reminder the new RegistrationReminder value
     * @throws RegistrationException for errors writing the registration data
     * to the local repository
     */
    public void setRegistrationReminder(RegistrationReminder reminder) throws RegistrationException {
        findRegistrationReminderElement().setTextContent(reminder.toString());
        writeToFile();
    }


    /**
     * Adds an element for the specified ServiceTag object (and writes the revised
     * DOM to the file).
     * @param serviceTag the ServiceTag for which to add an XML element
     * @throws RegistrationException for errors writing the registration data
     * to the local repository
     */
    public void add(ServiceTagImpl serviceTag) throws RegistrationException {
        Element st = findServiceTag(serviceTag);
        if (st != null) {
            throw new RegistrationException(StringManager.getString("rpmgr.svrTagExists"));
            //throw new RegistrationException("rpmgr.svrTagExists");
        }
        /*
         * Create a new Element in the active document using the specified 
         * service tag information.
         */
        st = serviceTag.getElement(document);
        Element registrationReminderElement = findRegistrationReminderElement();
        /*
         * Insert the new service tag just before the registration status element, if it
         * is found. Otherwise insert it under registryElement
         */
        if (registrationReminderElement != null) {
            registryElement.insertBefore(st, registrationReminderElement);
        }
        else {
            registryElement.appendChild(st);
        }

        writeToFile();
    }

    /**
     * Removes the XML element corresponding to the specified service tag (and
     * persists the change to the file).
     * @param serviceTag the ServiceTag for which the XML element should be removed
     * @throws RegistrationException for errors writing the registration data
     * to the local repository
     */
    public void remove(ServiceTagImpl serviceTag) throws RegistrationException {
        Element st = findServiceTag(serviceTag);
        if (st == null) {
         throw new RegistrationException(StringManager.getString("rpmgr.noSuchSrvTag"));
         //throw new RegistrationException("rpmgr.noSuchSrvTag");
        }
        registryElement.removeChild(st);
        writeToFile();
    }
    
    /**
     * Sets the status for the specified service tag in the repository (and 
     * persists the change to the file).
     * @param serviceTag the service tag of interest
     * @param the status to be assigned
     * @throws RegistrationException for errors writing the registration data
     * to the local repository
     */
    public void setRegistrationStatus(ServiceTagImpl serviceTag, ServiceTagImpl.RegistrationStatus status) throws RegistrationException {
        Element st = findServiceTag(serviceTag);
        if (st == null) {
            throw new RegistrationException(StringManager.getString("rpmgr.noSuchSrvTag"));
        }
        setSubElementValue(st, ServiceTagImpl.REGISTRATION_STATUS, status.toString());
        writeToFile();
    }

    public ServiceTagImpl.RegistrationStatus getRegistrationStatus(ServiceTagImpl serviceTag) throws RegistrationException {
        Element st = findServiceTag(serviceTag);
        if (st == null) {
            throw new RegistrationException(StringManager.getString("rpmgr.noSuchSrvTag"));
        }
        return ServiceTagImpl.RegistrationStatus.valueOf(getSubElementValue(st, ServiceTagImpl.REGISTRATION_STATUS));
    }

    public void setStatus(ServiceTagImpl serviceTag, ServiceTagImpl.Status status) throws RegistrationException {
        Element st = findServiceTag(serviceTag);
        if (st == null) {
            throw new RegistrationException(StringManager.getString("rpmgr.noSuchSrvTag"));
        }
        setSubElementValue(st, ServiceTagImpl.STATUS, status.toString());
        writeToFile();
    }

    public ServiceTagImpl.Status getStatus(ServiceTagImpl serviceTag) throws RegistrationException {
        Element st = findServiceTag(serviceTag);
        if (st == null) {
            throw new RegistrationException(StringManager.getString("rpmgr.noSuchSrvTag"));
        }
        return ServiceTagImpl.Status.valueOf(getSubElementValue(st, ServiceTagImpl.STATUS));
    }
    
    /**
     * Goes through all service tags in the repository, and updates runtime
     * values if necessary. 
     * @throws RegistrationException in case of errors writing the output
     */
    public void updateRuntimeValues() throws RegistrationException {
        NodeList nodes = document.getElementsByTagName(ServiceTagImpl.SERVICE_TAG);
        
        /* nodes is guaranteed to be non-null */
        for(int i = 0 ; i < nodes.getLength();i++) {
            Element elem = (Element)nodes.item(i);
            /* generate value for instanceURN if necessary */
            String instanceURN = getSubElementValue(elem, ServiceTagImpl.INSTANCE_URN);
            if (instanceURN == null || instanceURN.length() == 0) {
                instanceURN = ServiceTagImpl.getNewInstanceURN();
                setSubElementValue(elem, ServiceTagImpl.INSTANCE_URN, instanceURN, true);
            }

            /* generate value for platformArchif necessary */
            String platformArch = getSubElementValue(elem, ServiceTagImpl.PLATFORM_ARCH);
            if (platformArch == null || platformArch.length() == 0) {
                platformArch = System.getProperty("os.arch");
                setSubElementValue(elem, ServiceTagImpl.PLATFORM_ARCH, platformArch, true);
            }
            
            /* generate value for product_defined_inst_id if necessary */
            String product_defined_inst_id = getSubElementValue(elem, ServiceTagImpl.PRODUCT_DEFINED_INST_ID);
            if (product_defined_inst_id == null || product_defined_inst_id.length() == 0) {
                product_defined_inst_id = ServiceTagImpl.getDefaultProductDefinedInstID();
                setSubElementValue(elem, ServiceTagImpl.PRODUCT_DEFINED_INST_ID, product_defined_inst_id, true);
            }            
        }
        writeToFile();        
    }

    
    /**
     * Writes the contents of the specified Node to the OutputStream provided.
     * <p>
     * Typically used for testing and debugging.  Not intended as part of the
     * normally-used public interface.
     * @param node the root of the subtree to write out
     * @param stream the OutputStream to which to write the XML
     * @throws RegistrationException in case of errors writing the output
     */
    void write(Node node, OutputStream stream) throws RegistrationException {
        try {
            transformer.transform(new DOMSource(node), new StreamResult(stream));
        } catch (Exception e) {
            throw new RegistrationException(e);
        }
    }
    
    /**
     * Writes the cached document to the provided output stream
     * <p>
     * Typically used for testing and debugging.  Not intended as part of the
     * normally-used public interface.
     * @param stream the OutputStream to which to write the document
     * @throws RegistrationException in case of errors writing the output
     */
    void write(OutputStream stream) throws RegistrationException {
        write(document, stream);
    }

    /**
     * Locates the registration status element from the document.
     * <p>
     * If absent, adds the element to the end of the document and initializes it to
     * the default value.
     * @return Element for the registration status 
     */
    private Element findRegistrationReminderElement() {
        Element result = null;
        NodeList nodes = document.getElementsByTagName(REGISTRATION_REMINDER_TAG);
        if (nodes.getLength() > 0) {
            /*
             * There should be at most one, but take the first if 
             * there is at least one.
             */
            result = (Element)nodes.item(0);
        }
        return result;
    }

    /**
     * Locates the servicetag element corresponding to the specified ServiceTag
     * object.
     * @param serviceTag the ServiceTag for which to find the corresponding element
     * @return the Element for that service tag; null if not found
     */
    private Element findServiceTag(ServiceTagImpl serviceTag) {
        Element result = null;
        NodeList nodes = document.getElementsByTagName(ServiceTagImpl.SERVICE_TAG);
        for (int i = 0; i < nodes.getLength(); i++) {
            if (isSameServiceTag(serviceTag, nodes.item(i))) {
                result = (Element) nodes.item(i);
                break;
            }
        }
        return result;
    }
    
    /**
     * Indicates whether the specified ServiceTag object and the specified Node
     * represent the same service tag, based on the unique identifying information.
     * @param serviceTag the ServiceTag of interest
     * @param candidateNode the node that may represent the ServiceTag
     * @return boolean if the node represents the specified service tag
     */
    private boolean isSameServiceTag(ServiceTagImpl serviceTag, Node candidateNode) {
        boolean result = false;
        if (candidateNode instanceof Element) {
            Element candidateElement = (Element) candidateNode;
            String productURN = getSubElementValue(candidateElement, ServiceTagImpl.PRODUCT_URN);
            String instanceURN = getSubElementValue(candidateElement, ServiceTagImpl.INSTANCE_URN);
            if (productURN != null && instanceURN != null ) {
                result = productURN.equals(serviceTag.getSvcTag().getProductURN())
                      && instanceURN.equals(serviceTag.getSvcTag().getInstanceURN());
            }
        }
        return result;
    }
    
    
    private String getSubElementValue(Element rootElement, String subElementName ) {
        NodeList nodes = rootElement.getElementsByTagName(subElementName);
        if (nodes.getLength() > 0) {
            return ((Element)nodes.item(0)).getTextContent();
        }
        return null;        
    }

    private void setSubElementValue(Element rootElement, String subElementName, String value) 
            throws RegistrationException {
        setSubElementValue(rootElement, subElementName, value, false);
/*
        NodeList nodes = rootElement.getElementsByTagName(subElementName);
        if (nodes.getLength() > 0) {
            Element subElement = ((Element)nodes.item(0));
            subElement.setTextContent(value);
        }
        else {
            throw new RegistrationException(StringManager.getString("rpmgr.noSuchElement"));
        }
 */
    }
    
    private void setSubElementValue(Element rootElement, String subElementName, String value, boolean force) 
            throws RegistrationException {
        NodeList nodes = rootElement.getElementsByTagName(subElementName);
        
        if (nodes.getLength() > 0) {
            Element subElement = ((Element)nodes.item(0));
            subElement.setTextContent(value);
        }
        else {
            if (force) {
                Element element = document.createElement(subElementName);
                element.setTextContent(value);
                rootElement.appendChild(element);        
            }
            else {            
                throw new RegistrationException(StringManager.getString("rpmgr.noSuchElement"));
            }
        }
    }

    /**
     * Initializes the cached Document for the repository.
     * <p>
     * If the specified file exists, reads the document from that file.  If the 
     * file does not exist, creates a new document and populates it with the 
     * top-level registry element and the default registrationstatus element.
     * @throws RegistrationException for errors reading the registration into
     * the in-memory cache
     */
    private synchronized void loadOrCreateDocument() throws RegistrationException  {
        if (document == null) {
            if (registrationFile.exists()) {
                try {
                    document = documentBuilder.parse(registrationFile);
                    registryElement = findRegistryElement();
                } catch (Exception e) {
                    throw new RegistrationException(e);
                }
            }
            else {
                document = documentBuilder.newDocument();
                registryElement = document.createElement(REGISTRY_TAG);
                document.appendChild(registryElement);
                
                Element registrationStatusElement = document.createElement(REGISTRATION_REMINDER_TAG);
                registrationStatusElement.setTextContent(REGISTRATION_REMINDER_DEFAULT_VALUE.toString());
                registryElement.appendChild(registrationStatusElement);
                
            }
        }
    }
    
    /**
     * Writes the cached document to the on-disk file.
     * @throws RegistrationException for errors writing the cache into the file
     */
    private void writeToFile() throws RegistrationException {
        OutputStream os = null;
        try {
            os = new FileOutputStream(registrationFile);
            write(os);
        } catch (Exception e) {
            throw new RegistrationException(e);
        } finally {
            if (os != null) {
                try {
                    os.close();
                } catch (IOException ex) {
                    throw new RegistrationException(StringManager.getString("rpmgr.errClosingRepos"), ex);
                }
            }
        }
    }
    
    /**
     * Locates the top-level element, the registry element.
     * @return the top-level registry element
     */
    private Element findRegistryElement() {
        Element result = null;
        NodeList nodes = document.getElementsByTagName(REGISTRY_TAG);
        if (nodes.getLength() > 0) {
            result = (Element) nodes.item(0);
        }
        return result;
    }
    
    /** element name for the registrationstatus element */
    private static final String REGISTRATION_REMINDER_TAG = "registration_reminder";
    
    /** element name for the top-level registry element */
    private static final String REGISTRY_TAG = "registry";
    
    /** default value for the registration status element */
    private static final RegistrationReminder REGISTRATION_REMINDER_DEFAULT_VALUE = 
            RegistrationReminder.ASK_FOR_REGISTRATION;
    /*
     * Doc builder factory, doc builder, transformer factory, and transformer
     * are all reusable so get them once.
     */
    private final DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
    private final DocumentBuilder documentBuilder; 
    
    private final TransformerFactory transformerFactory = TransformerFactory.newInstance();
    private final Transformer transformer;
    
    /** the file to write the registration data to and read it from */
    private File registrationFile = null;
    
    /** the cached in-memory data */
    private Document document = null;
    
    /** the cached top-level registry element */
    private Element registryElement = null;
    
    
}
