/*
 * JBoss, the OpenSource EJB server
 *
 * Distributable under LGPL license.
 * See terms of license at gnu.org.
 */
package org.jboss.metadata;

import java.net.URL;
import java.io.IOException;
import java.io.Reader;
import java.io.InputStreamReader;
import java.io.InputStream;
import java.util.Hashtable;

import org.w3c.dom.Document;
import org.w3c.dom.Element;

import org.xml.sax.*;
import javax.xml.parsers.*;

import org.jboss.ejb.DeploymentException;

/**
 *   XmlFileLoader class is used to read ejb-jar.xml, standardjboss.xml, jboss.xml
 *   files, process them using DTDs and create ApplicationMetaData object for future using
 *
 *   @see <related>
 *   @author <a href="mailto:sebastien.alborini@m4x.org">Sebastien Alborini</a>
 *   @author <a href="mailto:WolfgangWerner@gmx.net">Wolfgang Werner</a>
 *   @author <a href="mailto:Darius.D@jbees.com">Darius Davidavicius</a>
 *   @version $Revision: 1.13 $
 */
public class XmlFileLoader {
   	// Constants -----------------------------------------------------

   	// Attributes ----------------------------------------------------
	ClassLoader classLoader;

	ApplicationMetaData metaData;

	// Static --------------------------------------------------------

	// Constructors --------------------------------------------------
   	public XmlFileLoader() {
	}

	// Public --------------------------------------------------------
	public ApplicationMetaData getMetaData() {
		return metaData;
    }

   /**
   Set the class loader
   @param ClassLoader cl - class loader
   */
	public void setClassLoader(ClassLoader cl) {
		classLoader = cl;
	}

   /**
   Gets the class loader
   @return ClassLoader - the class loader
   */
   public ClassLoader getClassLoader() {
		return classLoader;
	}

	/**
	* load()
	*
	* This method creates the ApplicationMetaData.
	* The configuration files are found in the classLoader.
	* The default jboss.xml and jaws.xml files are always read first, then we override
	* the defaults if the user provides them
	*
	*/
   	public ApplicationMetaData load() throws Exception {
      	// create the metadata
		metaData = new ApplicationMetaData();

		// Load ejb-jar.xml

		// we can always find the files in the classloader
		URL ejbjarUrl = getClassLoader().getResource("META-INF/ejb-jar.xml");

		if (ejbjarUrl == null) {
			throw new DeploymentException("no ejb-jar.xml found");
		}

//		Logger.debug("Loading ejb-jar.xml : " + ejbjarUrl.toString());
		Document ejbjarDocument = getDocument(ejbjarUrl);

		// the url may be used to report errors
		metaData.setUrl(ejbjarUrl);
		metaData.importEjbJarXml(ejbjarDocument.getDocumentElement());

		// Load jbossdefault.xml from the default classLoader
		// we always load defaults first
      // we use the context classloader, because this guy has to know where
      // this file is
		URL defaultJbossUrl = Thread.currentThread().getContextClassLoader().getResource("standardjboss.xml");

		if (defaultJbossUrl == null) {
			throw new DeploymentException("no standardjboss.xml found");
		}

		Document defaultJbossDocument = getDocument(defaultJbossUrl);

		metaData.setUrl(defaultJbossUrl);
		metaData.importJbossXml(defaultJbossDocument.getDocumentElement());

		// Load jboss.xml
		// if this file is provided, then we override the defaults
		URL jbossUrl = getClassLoader().getResource("META-INF/jboss.xml");

		if (jbossUrl != null) {
//			Logger.debug(jbossUrl.toString() + " found. Overriding defaults");
			Document jbossDocument = getDocument(jbossUrl);

			metaData.setUrl(jbossUrl);
			metaData.importJbossXml(jbossDocument.getDocumentElement());
		}

		return metaData;
	}


 	// Package protected ---------------------------------------------

   // Protected -----------------------------------------------------


	// Private -------------------------------------------------------

      /**
      Gets Document object from URL
      @return Document
      */
      public static Document getDocument(URL url) throws DeploymentException {

         try {
            return getDocument (url.openStream(), url.getPath());
         } catch (IOException _ioe) {
            throw new DeploymentException(_ioe.getMessage());
         }
      }

      /**
      Gets Document object from URL. Here DTD validation and error hander is set
      @return Document
      */
      public static Document getDocument(InputStream _in, String inPath) throws DeploymentException 
      {
         try 
         {

            DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
            //forsing DTD validation
            docBuilderFactory.setValidating(true);
            DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder();

            LocalResolver lr = new LocalResolver();
            ErrorHandler eh = new LocalErrorHandler( inPath, lr );
            docBuilder.setEntityResolver(lr);
            docBuilder.setErrorHandler(eh );

            Document doc = docBuilder.parse(_in);                

            return doc;
         } catch (SAXParseException e) {
            System.out.println(e.getMessage()+":"+e.getColumnNumber()+":"+e.getLineNumber());
                      e.printStackTrace();
            throw new DeploymentException(e.getMessage(), e);
         } catch (SAXException e) {
            System.out.println(e.getException());
            throw new DeploymentException(e.getMessage(), e);
         } catch (Exception e) {
            throw new DeploymentException(e.getMessage(), e);
         }
      }

	// Inner classes -------------------------------------------------
	/**
	 * Local entity resolver to handle EJB 1.1 DTD. With this a http connection
	 * to sun is not needed during deployment.
    * Function boolean hadDTD() is added here because i force validation using 
    * docBuilderFactory.setValidating(true) but if EJB developer dont want 
    * validation and he not added DTD validation in XML file. In such case I use 
    * LocalErrorHandler where i reject errors if DTD was not found.
	 * @author <a href="mailto:WolfgangWerner@gmx.net">Wolfgang Werner</a>
    * @author <a href="mailto:Darius.D@jbees.com">Darius Davidavicius</a>
	 **/
	private static class LocalResolver implements EntityResolver {
		private Hashtable dtds = new Hashtable();
      private boolean _hasDTD = false;

		public LocalResolver() {
          
			registerDTD("-//Sun Microsystems, Inc.//DTD Enterprise JavaBeans 1.1//EN", "ejb-jar.dtd");
			registerDTD("-//Sun Microsystems, Inc.//DTD J2EE Application 1.2//EN", "application_1_2.dtd");
			registerDTD("-//Sun Microsystems, Inc.//DTD Connector 1.0//EN", "connector_1_0.dtd");
   		registerDTD("-//JBoss//DTD JAWS//EN", "jaws.dtd");
   		registerDTD("-//JBoss//DTD JBOSS//EN","jboss.dtd");
		}

      /**
      Registers available DTDs
      @param String publicId    - Public ID of DTD
      @param String dtdFileName - the file name of DTD
      */
		public void registerDTD(String publicId, String dtdFileName) {
			dtds.put(publicId, dtdFileName);
		}

      /**
      Returns DTD inputSource. Is DTD was found in the hashtable and inputSource was created
      flad hasDTD is ser to true.
      @param String publicId    - Public ID of DTD
      @param String dtdFileName - the file name of DTD
      @return InputSource of DTD
      */
		public InputSource resolveEntity (String publicId, String systemId) {

         _hasDTD = false;
			String dtd = (String)dtds.get(publicId);

			if (dtd != null) {
             _hasDTD = true;
				try {
					InputStream dtdStream = getClass().getResourceAsStream(dtd);
               InputSource aInputSource = new InputSource(dtdStream);
               return aInputSource;
				} catch( Exception ex ) {
					// ignore
				}
			}
			return null;
		}

      /**
      Returns the boolean value to inform id DTD was found in the XML file or not
      @return boolean - true if DTD was found in XML
      */
      public boolean hasDTD ()
      {
          return _hasDTD;
      }

	}

	/**
	 * Local error handler for entity resolver to DocumentBuilder parser.
    * Error is printed to output just if DTD was detected in the XML file. 
    * If DTD was not found in XML file I assume EJB biulder dont want to use DTD validation
    * But validation goes anyway that is why i need hasDTD() function in the LocalResolver 
    * for rejecting errors if DTD not used.
	 * @author <a href="mailto:WolfgangWerner@gmx.net">Wolfgang Werner</a>
    * @author <a href="mailto:Darius.D@jbees.com">Darius Davidavicius</a>
	 **/
   private static class LocalErrorHandler implements ErrorHandler {
      private String theFileName;
      private LocalResolver theLr;
      public LocalErrorHandler( String inFileName, LocalResolver inLr ) {
          theFileName = inFileName;
          theLr = inLr;
      }

      public void error(SAXParseException exception) 
      {
          if ( theLr.hasDTD() )
          {
              System.out.println("XmlFileLoader: File "
                                  + theFileName
                                  + " process error. Line: "
                                  + String.valueOf(exception.getLineNumber())
                                  + ". Error message: "
                                  + exception.getMessage()
                                 );
          }//end if

      }
      public void fatalError(SAXParseException exception) 
      {
          if ( theLr.hasDTD() )
          {
              System.out.println("XmlFileLoader: File "
                                  + theFileName
                                  + " process fatal error. Line: "
                                  + String.valueOf(exception.getLineNumber())
                                  + ". Error message: "
                                  + exception.getMessage()
                                );
          }//end if
      }
      public void warning(SAXParseException exception) 
      {
          if ( theLr.hasDTD() )
          {
              System.out.println("XmlFileLoader: File "
                                  + theFileName
                                  + " process warning. Line: "
                                  + String.valueOf(exception.getLineNumber())
                                  + ". Error message: "
                                  + exception.getMessage()
                                );
          }//end if
      }
   }// end class LocalErrorHandler


}
