/*
 * Copyright 2008 Sun Microsystems, Inc.  All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Sun designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Sun in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
 * CA 95054 USA or visit www.sun.com if you need additional information or
 * have any questions.
 */

package com.sun.servicetag;

import java.io.*;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;

import static com.sun.servicetag.Util.*;
import static com.sun.servicetag.RegistrationDocument.*;

/**
 * A service tag registry is a XML-based registry containing
 * the list of {@link ServiceTag service tags} installed in the system.
 * The {@code Registry} class provides interfaces
 * to add, remove, update, and get a service tag from a service tag
 * registry.
 * This {@code Registry} class may not be supported
 * on all systems. The {@link #isSupported} method
 * can be called to determine if it is supported.
 * <p>
 * A registry may implement restrictions to only allow certain users
 * to {@link #updateServiceTag update} and
 * to {@link #removeServiceTag remove} a service tag record. Typically,
 * only the owner of the service tag, the owner of the registry
 * and superuser are authorized to update or remove a service tag in
 * the registry.
 *
 * @see <a href="https://sn-tools.central.sun.com/twiki/bin/view/ServiceTags/ServiceTagDevGuideHelper">
 * Service Tag User Guide</a>
 */
public class Registry {

    private static final String STCLIENT_SOLARIS = "/usr/bin/stclient";
    private static final String STCLIENT_LINUX = "/opt/sun/servicetag/bin/stclient";
    // stclient exit value (see sthelper.h)
    private static final int ST_ERR_NOT_AUTH = 245;
    private static final int ST_ERR_REC_NOT_FOUND = 225;

    // The stclient output has to be an exported interface
    private static final String INSTANCE_URN_DESC = "Product instance URN=";
    private static boolean initialized = false;
    private static boolean supportsHelperClass = true; // default
    private static File stclient = null;
    private static String stclientPath = null;
    private static Registry registry = new Registry();

    // System properties for testing
    private static String SVCTAG_STCLIENT_CMD = "servicetag.stclient.cmd";
    private static String SVCTAG_STHELPER_SUPPORTED = "servicetag.sthelper.supported";

    private Registry() {
    }

    private synchronized static String getSTclient() {
        if (!initialized) {
            // the system property always overrides the default setting
            if (System.getProperty(SVCTAG_STHELPER_SUPPORTED) != null) {
                supportsHelperClass = Boolean.getBoolean(SVCTAG_STHELPER_SUPPORTED);
            }

            // This is only used for testing
            stclientPath = System.getProperty(SVCTAG_STCLIENT_CMD);
            if (stclientPath != null) {
                return stclientPath;
            }

            // Initialization to determine the platform's stclient pathname
            String os = System.getProperty("os.name");
            if (os.equals("SunOS")) {
                stclient = new File(STCLIENT_SOLARIS);
            } else if (os.equals("Linux")) {
                stclient = new File(STCLIENT_LINUX);
            } else if (os.startsWith("Windows")) {
                stclient = getWindowsStClientFile();
            } else {
                if (isVerbose()) {
                    System.out.println("Running on non-Sun JDK");
                }
            }
            initialized = true;
        }

        // com.sun.servicetag package has to be compiled with JDK 5 as well
        // JDK 5 doesn't support the File.canExecute() method.
        // Risk not checking isExecute() for the stclient command is very low.

        if (stclientPath == null && stclient != null && stclient.exists()) {
            stclientPath = stclient.getAbsolutePath();
        }
        return stclientPath;
    }

    /**
     * Returns the system service tag registry. The {@code Registry} class
     * may not be supported on some platforms; use the {@link #isSupported}
     * method to determine if it is supported.
     *
     * @return the {@code Registry} object for the system service tag registry.
     *
     * @throws UnsupportedOperationException if the {@code Registry} class is
     * not supported.
     */
    public static Registry getSystemRegistry() {
        if (isSupported()) {
            return registry;
        } else {
            throw new UnsupportedOperationException("Registry class is not supported");
        }
    }

    /**
     * Returns {@code true} if the {@code Registry} class is supported on this system.
     *
     * @return {@code true} if the {@code Registry} class is supported;
     * otherwise, return {@code false}.
     */
    public static boolean isSupported() {
        return (getSTclient() != null && supportsHelperClass);
    }

    private static List<String> getCommandList() {
        // Set up the arguments to call stclient
        List<String> command = new ArrayList<String>();
        if (System.getProperty(SVCTAG_STCLIENT_CMD) != null) {
            // This is for jtreg testing use. This will be set to something
            // like:
            // $JAVA_HOME/bin/java -cp $TEST_DIR \
            //    -Dstclient.registry.path=$TEST_DIR/registry.xml \
            //    SvcTagClient
            //
            // On Windows, the JAVA_HOME and TEST_DIR path could contain
            // space e.g. c:\Program Files\Java\jdk1.6.0_05\bin\java.
            // The SVCTAG_STCLIENT_CMD must be set with a list of
            // space-separated parameters.  If a parameter contains spaces,
            // it must be quoted with '"'.

            String cmd = getSTclient();
            int len = cmd.length();
            int i = 0;
            while (i < len) {
                char separator = ' ';
                if (cmd.charAt(i) == '"') {
                    separator = '"';
                    i++;
                }
                // look for the separator or matched the closing '"'
                int j;
                for (j = i+1; j < len; j++) {
                    if (cmd.charAt(j) == separator) {
                        break;
                    }
                }

                if (i == j-1) {
                    // add an empty parameter
                    command.add("\"\"");
                } else {
                    // double quotes and space are not included
                    command.add(cmd.substring(i,j));
                }

                // skip spaces
                for (i = j+1; i < len; i++) {
                    if (!Character.isSpaceChar(cmd.charAt(i))) {
                        break;
                    }
                }
            }
            if (isVerbose()) {
                System.out.println("Command list:");
                for (String s : command) {
                    System.out.println(s);
                }
            }
        } else {
            command.add(getSTclient());
        }
        return command;
    }

    // Returns null if the service tag record not found;
    // or throw UnauthorizedAccessException or IOException
    // based on the exitValue.
    private static ServiceTag checkReturnError(int exitValue,
                                               String output,
                                               ServiceTag st) throws IOException {
        switch (exitValue) {
            case ST_ERR_REC_NOT_FOUND:
                return null;
            case ST_ERR_NOT_AUTH:
                if (st != null) {
                    throw new UnauthorizedAccessException(
                        "Not authorized to access " + st.getInstanceURN() +
                        " installer_uid=" + st.getInstallerUID());
                } else  {
                    throw new UnauthorizedAccessException(
                        "Not authorized:" + output);
                }
            default:
                throw new IOException("stclient exits with error" +
                     " (" + exitValue + ")\n" + output);
        }
    }

    /**
     * Adds a service tag to this registry.
     * If the given service tag has an empty <tt>instance_urn</tt>,
     * this helper class will generate a URN and place it in the
     * copy of the service tag in this registry.
     * This method will return the {@code ServiceTag} representing
     * the service tag entry to this registry.
     *
     * @param st {@code ServiceTag} object
     * @return a {@code ServiceTag} object representing the service tag
     *         entry to this registry.
     *
     * @throws IllegalArgumentException if a service tag of the same
     * <tt>instance_urn</tt> already exists in this registry.
     *
     * @throws java.io.IOException if an I/O error occurs in this operation.
     */
    public ServiceTag addServiceTag(ServiceTag st) throws IOException {
        List<String> command = getCommandList();
        command.add("-a");
        if (st.getInstanceURN().length() > 0) {
            ServiceTag sysSvcTag = getServiceTag(st.getInstanceURN());
            if (sysSvcTag != null) {
                throw new IllegalArgumentException("Instance_urn = " +
                    st.getInstanceURN() + " already exists");
            }
            command.add("-i");
            command.add(st.getInstanceURN());
        }
        command.add("-p");
        command.add(st.getProductName());
        command.add("-e");
        command.add(st.getProductVersion());
        command.add("-t");
        command.add(st.getProductURN());
        if (st.getProductParentURN().length() > 0) {
            command.add("-F");
            command.add(st.getProductParentURN());
        }
        command.add("-P");
        command.add(st.getProductParent());
        if (st.getProductDefinedInstanceID().length() > 0) {
            command.add("-I");
            command.add(st.getProductDefinedInstanceID());
        }
        command.add("-m");
        command.add(st.getProductVendor());
        command.add("-A");
        command.add(st.getPlatformArch());
        command.add("-z");
        command.add(st.getContainer());
        command.add("-S");
        command.add(st.getSource());

        BufferedReader in = null;
        try {
            ProcessBuilder pb = new ProcessBuilder(command);
            Process p = pb.start();
            String output = commandOutput(p);
            if (isVerbose()) {
                System.out.println("Output from stclient -a command:");
                System.out.println(output);
            }
            String urn = "";
            if (p.exitValue() == 0) {
                // Obtain the instance urn from the stclient output
                in = new BufferedReader(new StringReader(output));
                String line = null;
                while ((line = in.readLine()) != null) {
                    line = line.trim();
                    if (line.startsWith(INSTANCE_URN_DESC)) {
                        urn = line.substring(INSTANCE_URN_DESC.length());
                        break;
                    }
                }
                if (urn.length() == 0) {
                    throw new IOException("Error in creating service tag:\n" +
                        output);
                }
                return getServiceTag(urn);
            } else {
                return checkReturnError(p.exitValue(), output, st);
            }
        } finally {
            if (in != null) {
                in.close();
            }
        }
    }

    /**
     * Removes a service tag of the given <tt>instance_urn</tt> from this
     * registry.
     *
     * @param instanceURN the <tt>instance_urn</tt> of the service tag
     *        to be removed.
     *
     * @return the {@code ServiceTag} object removed from this registry;
     * or {@code null} if the service tag does not exist in this registry.
     *
     * @throws UnauthorizedAccessException if the user is not authorized to
     * remove the service tag of the given <tt>instance_urn</tt>
     * from this registry.
     *
     * @throws java.io.IOException if an I/O error occurs in this operation.
     */
    public ServiceTag removeServiceTag(String instanceURN) throws IOException {
        ServiceTag st = getServiceTag(instanceURN);
        if (st == null) {
            return null;
        }

        List<String> command = getCommandList();
        command.add("-d");
        command.add("-i");
        command.add(instanceURN);

        ProcessBuilder pb = new ProcessBuilder(command);
        Process p = pb.start();
        String output = commandOutput(p);
        if (isVerbose()) {
            System.out.println("Output from stclient -d command:");
            System.out.println(output);
        }
        if (p.exitValue() == 0) {
            return st;
        } else {
            return checkReturnError(p.exitValue(), output, st);
        }
    }

    /**
     * Updates the <tt>product_defined_instance_id</tt> in the service tag
     * of the specified <tt>instance_urn</tt> in this registry.
     *
     * @param instanceURN the <tt>instance_urn</tt> of the service tag to be updated.
     * @param productDefinedInstanceID the value of the
     * <tt>product_defined_instance_id</tt> to be set.
     *
     * @return the updated {@code ServiceTag} object;
     * or {@code null} if the service tag does not exist in this
     * registry.
     *
     * @throws UnauthorizedAccessException if the user is not authorized to
     * update the service tag from this registry.
     *
     * @throws IOException if an I/O error occurs in this operation.
     */
    public ServiceTag updateServiceTag(String instanceURN,
                                       String productDefinedInstanceID)
            throws IOException {
        ServiceTag svcTag = getServiceTag(instanceURN);
        if (svcTag == null) {
            return null;
        }

        List<String> command = getCommandList();
        command.add("-u");
        command.add("-i");
        command.add(instanceURN);
        command.add("-I");
        if (productDefinedInstanceID.length() > 0) {
            command.add(productDefinedInstanceID);
        } else {
            command.add("\"\"");
        }

        ProcessBuilder pb = new ProcessBuilder(command);
        Process p = pb.start();
        String output = commandOutput(p);
        if (isVerbose()) {
            System.out.println("Output from stclient -u command:");
            System.out.println(output);
        }

        if (p.exitValue() == 0) {
            return getServiceTag(instanceURN);
        } else {
            return checkReturnError(p.exitValue(), output, svcTag);
        }
    }

    /**
     * Returns a {@code ServiceTag} object of the given  <tt>instance_urn</tt>
     * in this registry.
     *
     * @param instanceURN the  <tt>instance_urn</tt> of the service tag
     * @return a {@code ServiceTag} object of the given <tt>instance_urn</tt>
     * in this registry; or {@code null} if not found.
     *
     * @throws java.io.IOException if an I/O error occurs in this operation.
     */
    public ServiceTag getServiceTag(String instanceURN) throws IOException {
        if (instanceURN == null) {
            throw new NullPointerException("instanceURN is null");
        }

        List<String> command = getCommandList();
        command.add("-g");
        command.add("-i");
        command.add(instanceURN);

        ProcessBuilder pb = new ProcessBuilder(command);
        Process p = pb.start();
        String output = commandOutput(p);
        if (isVerbose()) {
            System.out.println("Output from stclient -g command:");
            System.out.println(output);
        }
        if (p.exitValue() == 0) {
            return parseServiceTag(output);
        } else {
            return checkReturnError(p.exitValue(), output, null);
        }
    }

    private ServiceTag parseServiceTag(String output) throws IOException {
        BufferedReader in = null;
        try {
            Properties props = new Properties();
            // parse the service tag output from stclient
            in = new BufferedReader(new StringReader(output));
            String line = null;
            while ((line = in.readLine()) != null) {
                if ((line = line.trim()).length() > 0) {
                    String[] ss = line.trim().split("=", 2);
                    if (ss.length == 2) {
                        props.setProperty(ss[0].trim(), ss[1].trim());
                    } else {
                        props.setProperty(ss[0].trim(), "");
                    }
                }
            }

            String urn = props.getProperty(ST_NODE_INSTANCE_URN);
            String productName = props.getProperty(ST_NODE_PRODUCT_NAME);
            String productVersion = props.getProperty(ST_NODE_PRODUCT_VERSION);
            String productURN = props.getProperty(ST_NODE_PRODUCT_URN);
            String productParent = props.getProperty(ST_NODE_PRODUCT_PARENT);
            String productParentURN = props.getProperty(ST_NODE_PRODUCT_PARENT_URN);
            String productDefinedInstanceID =
                props.getProperty(ST_NODE_PRODUCT_DEFINED_INST_ID);
            String productVendor = props.getProperty(ST_NODE_PRODUCT_VENDOR);
            String platformArch = props.getProperty(ST_NODE_PLATFORM_ARCH);
            String container = props.getProperty(ST_NODE_CONTAINER);
            String source = props.getProperty(ST_NODE_SOURCE);
            int installerUID =
                Util.getIntValue(props.getProperty(ST_NODE_INSTALLER_UID));
            Date timestamp =
                Util.parseTimestamp(props.getProperty(ST_NODE_TIMESTAMP));

            return new ServiceTag(urn,
                                  productName,
                                  productVersion,
                                  productURN,
                                  productParent,
                                  productParentURN,
                                  productDefinedInstanceID,
                                  productVendor,
                                  platformArch,
                                  container,
                                  source,
                                  installerUID,
                                  timestamp);
        } finally {
            if (in != null) {
                in.close();
            }
        }

    }

    /**
     * Returns the service tags of the specified
     * <tt>product_urn</tt> in this registry.
     *
     * @param productURN the  <tt>product_urn</tt> to look up
     * @return a {@code Set} of {@code ServiceTag} objects
     * of the specified <tt>product_urn</tt> in this registry.
     *
     * @throws java.io.IOException if an I/O error occurs in this operation.
     */
    public Set<ServiceTag> findServiceTags(String productURN) throws IOException {
        if (productURN == null) {
            throw new NullPointerException("productURN is null");
        }

        List<String> command = getCommandList();
        command.add("-f");
        command.add("-t");
        command.add(productURN);

        BufferedReader in = null;
        try {
            ProcessBuilder pb = new ProcessBuilder(command);
            Process p = pb.start();
            String output = commandOutput(p);

            Set<ServiceTag> instances = new HashSet<ServiceTag>();
            if (p.exitValue() == 0) {
                // parse the service tag output from stclient
                in = new BufferedReader(new StringReader(output));
                String line = null;
                while ((line = in.readLine()) != null) {
                    String s = line.trim();
                    if (s.startsWith("urn:st:")) {
                        instances.add(getServiceTag(s));
                    }
                }
            } else {
                checkReturnError(p.exitValue(), output, null);
            }
            return instances;
        } finally {
            if (in != null) {
                in.close();
            }
        }
    }
}
