
package org.jgroups.conf;

/**
 * Uses XML to configure a protocol stack
 * @author Vladimir Blagojevic
 */

import org.jgroups.Global;
import org.jgroups.logging.Log;
import org.jgroups.logging.LogFactory;
import org.w3c.dom.*;
import org.xml.sax.EntityResolver;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.jgroups.stack.Configurator;
import org.jgroups.util.Util;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;

public class XmlConfigurator implements ProtocolStackConfigurator {
    private static final String               JAXP_SCHEMA_LANGUAGE = "http://java.sun.com/xml/jaxp/properties/schemaLanguage";
    private static final String               W3C_XML_SCHEMA = "http://www.w3.org/2001/XMLSchema";
    private final List<ProtocolConfiguration> configuration=new ArrayList<ProtocolConfiguration>();
    protected static final Log                log=LogFactory.getLog(XmlConfigurator.class);

    
    protected XmlConfigurator(List<ProtocolConfiguration> protocols) {
        configuration.addAll(protocols);
    }

    public static XmlConfigurator getInstance(URL url) throws java.io.IOException {
        return getInstance(url, null);
    }

    public static XmlConfigurator getInstance(InputStream stream) throws java.io.IOException {
        return getInstance(stream, null);
    }

    public static XmlConfigurator getInstance(Element el) throws java.io.IOException {
        return parse(el);
    }

    public static XmlConfigurator getInstance(URL url, Boolean validate) throws java.io.IOException {
        InputStream is = url.openStream();
        try {
            return getInstance(is, validate);
        } finally {
            try {
                is.close();
            } catch (IOException e) {
                log.warn("Failed to close InputStream", e);
            }
        }
    }

    public static XmlConfigurator getInstance(InputStream stream, Boolean validate) throws java.io.IOException {
        return parse(stream, validate);
    }

    /**
     * 
     * @param convert If false: print old plain output, else print new XML format
     * @return String with protocol stack in specified format
     */
    public String getProtocolStackString(boolean convert) {
        StringBuilder buf=new StringBuilder();
        Iterator<ProtocolConfiguration> it=configuration.iterator();
        if(convert)
            buf.append("<config>\n");
        while(it.hasNext()) {
            ProtocolConfiguration d=it.next();
            if(convert)
                buf.append("    <");
            buf.append(d.getProtocolString(convert));
            if(convert)
                buf.append("/>");
            if(it.hasNext()) {
                if(convert)
                    buf.append('\n');
                else
                    buf.append(':');
            }
        }
        if(convert)
            buf.append("\n</config>");
        return buf.toString();
    }

    public String getProtocolStackString() {
        return getProtocolStackString(false);
    }

    public List<ProtocolConfiguration> getProtocolStack() {
        return configuration;
    }



    protected static XmlConfigurator parse(InputStream stream, Boolean validate) throws java.io.IOException {
        /**
         * CAUTION: crappy code ahead ! I (bela) am not an XML expert, so the code below is pretty
         * amateurish... But it seems to work, and it is executed only on startup, so no perf loss
         * on the critical path. If somebody wants to improve this, please be my guest.
         */
        try {
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            boolean validation = false;
            String tmp = Util.getProperty(new String[] { Global.XML_VALIDATION }, null, null, false, null);
            if (tmp != null) {
                validation = Boolean.valueOf(tmp).booleanValue();
            } else if (validate != null) {
                validation = validate.booleanValue();
            }
            factory.setValidating(validation);
            factory.setNamespaceAware(validation);
            if (validation) {
                factory.setAttribute(JAXP_SCHEMA_LANGUAGE, W3C_XML_SCHEMA);
            }
            
            DocumentBuilder builder = factory.newDocumentBuilder();
            builder.setEntityResolver(new EntityResolver() {

                public InputSource resolveEntity(String publicId, String systemId) throws IOException {
                    if (systemId != null && systemId.startsWith("http://www.jgroups.org/schema/JGroups-")) {
                        String schemaName = systemId.substring("http://www.jgroups.org/".length());                        
                        InputStream schemaIs = getAsInputStreamFromClassLoader(schemaName);
                        if (schemaIs == null) {
                            throw new IOException("Schema not found from classloader: " + schemaName);
                        }
                        InputSource source = new InputSource(schemaIs);
                        source.setPublicId(publicId);
                        source.setSystemId(systemId);
                        return source;
                    }
                    return null;
                }
            });
            // Use AtomicReference to allow make variable final, not for atomicity
            // We store only last exception
            final AtomicReference<SAXParseException> exceptionRef = new AtomicReference<SAXParseException>();
            builder.setErrorHandler(new ErrorHandler() {

                public void warning(SAXParseException exception) throws SAXException {
                    log.warn("Warning during parse", exception);
                }

                public void fatalError(SAXParseException exception) throws SAXException {
                    log.fatal("Error during parse", exception);
                    exceptionRef.set(exception);
                }

                public void error(SAXParseException exception) throws SAXException {
                    log.error("Error during parse", exception);
                    exceptionRef.set(exception);
                }
            });
            Document document = builder.parse(stream);
            if (exceptionRef.get() != null) {
                throw exceptionRef.get();
            }

            // The root element of the document should be the "config" element,
            // but the parser(Element) method checks this so a check is not
            // needed here.
            Element configElement = document.getDocumentElement();
            return parse(configElement);
        } catch (Exception x) {
            if (x instanceof java.io.IOException)
                throw (java.io.IOException) x;
            else {
                IOException tmp = new IOException();
                tmp.initCause(x);
                throw tmp;
            }
        }
    }

    private static InputStream getAsInputStreamFromClassLoader(String filename) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        InputStream is = cl == null ? null : cl.getResourceAsStream(filename);
        if (is == null) {
            // check system class loader
            is = XmlConfigurator.class.getClassLoader().getResourceAsStream(filename);
        }
        return is;
    }
    
    protected static XmlConfigurator parse(Element root_element) throws java.io.IOException {
        XmlConfigurator configurator=null;
        final LinkedList<ProtocolConfiguration> prot_data=new LinkedList<ProtocolConfiguration>();

        /**
         * CAUTION: crappy code ahead ! I (bela) am not an XML expert, so the code below is pretty amateurish...
         * But it seems to work, and it is executed only on startup, so no perf loss on the critical path. If
         * somebody wants to improve this, please be my guest.
         */
        try {
            String root_name=root_element.getNodeName();
            if(!"config".equals(root_name.trim().toLowerCase())) {
                log.fatal("XML protocol stack configuration does not start with a '<config>' element; " + "maybe the XML configuration needs to be converted to the new format ?\n"
                          + "use 'java org.jgroups.conf.XmlConfigurator <old XML file> -new_format' to do so");
                throw new IOException("invalid XML configuration");
            }

            NodeList prots=root_element.getChildNodes();
            for(int i=0;i < prots.getLength();i++) {
                Node node=prots.item(i);
                if(node.getNodeType() != Node.ELEMENT_NODE)
                    continue;

                Element tag=(Element)node;
                String  protocol=tag.getTagName();
                Map<String,String> params=new HashMap<String,String>();

                NamedNodeMap attrs=tag.getAttributes();
                int attrLength=attrs.getLength();
                for(int a=0;a < attrLength;a++) {
                    Attr attr=(Attr)attrs.item(a);
                    String name=attr.getName();
                    String value=attr.getValue();
                    params.put(name, value);
                }
                ProtocolConfiguration cfg=new ProtocolConfiguration(protocol, params);
                prot_data.add(cfg);
            }
            configurator=new XmlConfigurator(prot_data);
        }
        catch(Exception x) {
            if(x instanceof java.io.IOException)
                throw (java.io.IOException)x;
            else {
                IOException tmp=new IOException();
                tmp.initCause(x);
                throw tmp;
            }
        }
        return configurator;
    }



    public static void main(String[] args) throws Exception {
        String input_file=null;
        XmlConfigurator conf;
        boolean old_format=false;

        for(int i=0;i < args.length;i++) {
            if(args[i].equals("-old")) {
                old_format=true;
                continue;
            }
            if(args[i].equals("-file")) {
                input_file=args[++i];
                continue;
            }
            help();
            return;
        }

        if(input_file != null) {
            InputStream input=null;

            try {
                input=new FileInputStream(new File(input_file));
            }
            catch(Throwable t) {
            }
            if(input == null) {
                try {
                    input=new URL(input_file).openStream();
                }
                catch(Throwable t) {
                }
            }

            if(input == null)
                input=Thread.currentThread().getContextClassLoader().getResourceAsStream(input_file);

            if(old_format) {
                String cfg=inputAsString(input);
                List<ProtocolConfiguration> tmp=Configurator.parseConfigurations(cfg);
                System.out.println(dump(tmp));
            }
            else {
                conf=XmlConfigurator.getInstance(input);
                String tmp=conf.getProtocolStackString();
                System.out.println("\n" + tmp);
            }
        }
        else {
            log.error("no input file given");
        }
    }

    
    private static String dump(Collection<ProtocolConfiguration> configs) {
        StringBuilder sb=new StringBuilder();
        String indent="  ";
        sb.append("<config>\n");

        for(ProtocolConfiguration cfg: configs) {
            sb.append(indent).append("<").append(cfg.getProtocolName());
            Map<String,String> props=cfg.getProperties();
            if(props.isEmpty()) {
                sb.append(" />\n");
            }
            else {
                sb.append("\n").append(indent).append(indent);
                for(Map.Entry<String,String> entry: props.entrySet()) {
                    String key=entry.getKey();
                    String val=entry.getValue();
                    key=trim(key);
                    val=trim(val);
                    sb.append(key).append("=\"").append(val).append("\" ");
                }
                sb.append(" />\n");
            }
        }

        sb.append("</config>\n");
        return sb.toString();
    }

    private static String trim(String val) {
        String retval="";
        int index;

        val=val.trim();
        while(true) {
            index=val.indexOf('\n');
            if(index == -1) {
                retval+=val;
                break;
            }
            retval+=val.substring(0, index);
            val=val.substring(index + 1);
        }

        return retval;
    }

    private static String inputAsString(InputStream input) throws IOException {
        int len=input.available();
        byte[] buf=new byte[len];
        input.read(buf, 0, len);
        return new String(buf);
    }

    public static String replace(String input, final String expr, String replacement) {
        StringBuilder sb=new StringBuilder();
        int new_index=0, index=0, len=expr.length(), input_len=input.length();

        while(true) {
            new_index=input.indexOf(expr, index);
            if(new_index == -1) {
                sb.append(input.substring(index, input_len));
                break;
            }
            sb.append(input.substring(index, new_index));
            sb.append(replacement);
            index=new_index + len;
        }

        return sb.toString();
    }

    static void help() {
        System.out.println("XmlConfigurator -file <input XML file> [-old]");
        System.out.println("(-old: converts old (plain-text) input format into new XML format)");
    }
}
