/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
/*
 * $Id$
 */

package org.apache.xml.utils;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.URL;
import java.net.URLConnection;

import javax.xml.transform.ErrorListener;
import javax.xml.transform.SourceLocator;
import javax.xml.transform.TransformerException;

import org.apache.xml.res.XMLErrorResources;
import org.apache.xml.res.XMLMessages;

import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;


/**
 * Sample implementation of similar SAX ErrorHandler and JAXP ErrorListener.  
 *
 * <p>This implementation is suitable for various use cases, and 
 * provides some basic configuration API's as well to control 
 * when we re-throw errors, etc.</p>
 *
 * @author shane_curcuru@us.ibm.com
 * @version $Id$
 * @xsl.usage general
 */
public class ListingErrorHandler implements ErrorHandler, ErrorListener
{
    protected PrintWriter m_pw = null;
  

    /**
     * Constructor ListingErrorHandler; user-supplied PrintWriter.  
     */
    public ListingErrorHandler(PrintWriter pw)
    {
        if (null == pw)
            throw new NullPointerException(XMLMessages.createXMLMessage(XMLErrorResources.ER_ERRORHANDLER_CREATED_WITH_NULL_PRINTWRITER, null));
            // "ListingErrorHandler created with null PrintWriter!");
            
        m_pw = pw;
    }

    /**
     * Constructor ListingErrorHandler; uses System.err.  
     */
    public ListingErrorHandler()
    {
        m_pw = new PrintWriter(System.err, true);
    }


    /* ======== Implement org.xml.sax.ErrorHandler ======== */
    /**
     * Receive notification of a warning.
     *
     * <p>SAX parsers will use this method to report conditions that
     * are not errors or fatal errors as defined by the XML 1.0
     * recommendation.  The default behaviour is to take no action.</p>
     *
     * <p>The SAX parser must continue to provide normal parsing events
     * after invoking this method: it should still be possible for the
     * application to process the document through to the end.</p>
     *
     * <p>Filters may use this method to report other, non-XML warnings
     * as well.</p>
     *
     * @param exception The warning information encapsulated in a
     *                  SAX parse exception.
     * @exception org.xml.sax.SAXException Any SAX exception, possibly
     * wrapping another exception; only if setThrowOnWarning is true.
     * @see org.xml.sax.SAXParseException 
     */
    public void warning (SAXParseException exception)
    	throws SAXException
    {
    	logExceptionLocation(m_pw, exception);
        // Note: should we really call .toString() below, since 
        //  sometimes the message is not properly set?
        m_pw.println("warning: " + exception.getMessage());
        m_pw.flush();

        if (getThrowOnWarning())
            throw exception;
    }
    
    
    /**
     * Receive notification of a recoverable error.
     *
     * <p>This corresponds to the definition of "error" in section 1.2
     * of the W3C XML 1.0 Recommendation.  For example, a validating
     * parser would use this callback to report the violation of a
     * validity constraint.  The default behaviour is to take no
     * action.</p>
     *
     * <p>The SAX parser must continue to provide normal parsing events
     * after invoking this method: it should still be possible for the
     * application to process the document through to the end.  If the
     * application cannot do so, then the parser should report a fatal
     * error even if the XML 1.0 recommendation does not require it to
     * do so.</p>
     *
     * <p>Filters may use this method to report other, non-XML errors
     * as well.</p>
     *
     * @param exception The error information encapsulated in a
     *                  SAX parse exception.
     * @exception org.xml.sax.SAXException Any SAX exception, possibly
     * wrapping another exception; only if setThrowOnErroris true.
     * @see org.xml.sax.SAXParseException 
     */
    public void error (SAXParseException exception)
    	throws SAXException
    {
    	logExceptionLocation(m_pw, exception);
        m_pw.println("error: " + exception.getMessage());
        m_pw.flush();

        if (getThrowOnError())
            throw exception;
    }
    
    
    /**
     * Receive notification of a non-recoverable error.
     *
     * <p>This corresponds to the definition of "fatal error" in
     * section 1.2 of the W3C XML 1.0 Recommendation.  For example, a
     * parser would use this callback to report the violation of a
     * well-formedness constraint.</p>
     *
     * <p>The application must assume that the document is unusable
     * after the parser has invoked this method, and should continue
     * (if at all) only for the sake of collecting addition error
     * messages: in fact, SAX parsers are free to stop reporting any
     * other events once this method has been invoked.</p>
     *
     * @param exception The error information encapsulated in a
     *                  SAX parse exception.  
     * @exception org.xml.sax.SAXException Any SAX exception, possibly
     * wrapping another exception; only if setThrowOnFatalError is true.
     * @see org.xml.sax.SAXParseException
     */
    public void fatalError (SAXParseException exception)
    	throws SAXException
    {
    	logExceptionLocation(m_pw, exception);
        m_pw.println("fatalError: " + exception.getMessage());
        m_pw.flush();

        if (getThrowOnFatalError())
            throw exception;
    }


    /* ======== Implement javax.xml.transform.ErrorListener ======== */

    /**
     * Receive notification of a warning.
     *
     * <p>{@link javax.xml.transform.Transformer} can use this method to report
     * conditions that are not errors or fatal errors.  The default behaviour
     * is to take no action.</p>
     *
     * <p>After invoking this method, the Transformer must continue with
     * the transformation. It should still be possible for the
     * application to process the document through to the end.</p>
     *
     * @param exception The warning information encapsulated in a
     *                  transformer exception.
     *
     * @throws javax.xml.transform.TransformerException  only if 
     * setThrowOnWarning is true.
     *
     * @see javax.xml.transform.TransformerException
     */
    public void warning(TransformerException exception)
        throws TransformerException
    {
    	logExceptionLocation(m_pw, exception);
        m_pw.println("warning: " + exception.getMessage());
        m_pw.flush();

        if (getThrowOnWarning())
            throw exception;
    }

    /**
     * Receive notification of a recoverable error.
     *
     * <p>The transformer must continue to try and provide normal transformation
     * after invoking this method.  It should still be possible for the
     * application to process the document through to the end if no other errors
     * are encountered.</p>
     *
     * @param exception The error information encapsulated in a
     *                  transformer exception.
     *
     * @throws javax.xml.transform.TransformerException  only if 
     * setThrowOnError is true.
     *
     * @see javax.xml.transform.TransformerException
     */
    public void error(TransformerException exception)
        throws TransformerException
    {
    	logExceptionLocation(m_pw, exception);
        m_pw.println("error: " + exception.getMessage());
        m_pw.flush();

        if (getThrowOnError())
            throw exception;
    }

    /**
     * Receive notification of a non-recoverable error.
     *
     * <p>The transformer must continue to try and provide normal transformation
     * after invoking this method.  It should still be possible for the
     * application to process the document through to the end if no other errors
     * are encountered, but there is no guarantee that the output will be
     * useable.</p>
     *
     * @param exception The error information encapsulated in a
     *                  transformer exception.
     *
     * @throws javax.xml.transform.TransformerException  only if 
     * setThrowOnError is true.
     *
     * @see javax.xml.transform.TransformerException
     */
    public void fatalError(TransformerException exception)
        throws TransformerException
    {
    	logExceptionLocation(m_pw, exception);
        m_pw.println("error: " + exception.getMessage());
        m_pw.flush();

        if (getThrowOnError())
            throw exception;
    }



    /* ======== Implement worker methods ======== */


    /**
     * Print out location information about the exception.  
     *
     * Cribbed from DefaultErrorHandler.printLocation() 
     * @param pw PrintWriter to send output to
     * @param exception TransformerException or SAXParseException
     * to log information about
     */
    public static void logExceptionLocation(PrintWriter pw, Throwable exception)
    {
        if (null == pw)
            pw = new PrintWriter(System.err, true);
        
        SourceLocator locator = null;
        Throwable cause = exception;

        // Try to find the locator closest to the cause.
        do
        {
            // Find the current locator, if one present
            if(cause instanceof SAXParseException)
            {
                // A SAXSourceLocator is a Xalan helper class 
                //  that implements both a SourceLocator and a SAX Locator
                //@todo check that the new locator actually has 
                //  as much or more information as the 
                //  current one already does
                locator = new SAXSourceLocator((SAXParseException)cause);
            }
            else if (cause instanceof TransformerException)
            {
                SourceLocator causeLocator = ((TransformerException)cause).getLocator();
                if(null != causeLocator)
                {
                    locator = causeLocator;
                }
            }
            
            // Then walk back down the chain of exceptions
            if(cause instanceof TransformerException)
                cause = ((TransformerException)cause).getCause();
            else if(cause instanceof WrappedRuntimeException)
                cause = ((WrappedRuntimeException)cause).getException();
            else if(cause instanceof SAXException)
                cause = ((SAXException)cause).getException();
            else
                cause = null;
        }
        while(null != cause);

        // Formatting note: mimic javac-like errors:
        //  path\filename:123: message-here
        //  systemId:L=1;C=2: message-here
        if(null != locator)
        {
            String id = (locator.getPublicId() != locator.getPublicId())
                      ? locator.getPublicId()
                        : (null != locator.getSystemId())
                          ? locator.getSystemId() : "SystemId-Unknown";

            pw.print(id + ":Line=" + locator.getLineNumber()
                             + ";Column=" + locator.getColumnNumber()+": ");
            pw.println("exception:" + exception.getMessage());
            pw.println("root-cause:" 
                       + ((null != cause) ? cause.getMessage() : "null"));
            logSourceLine(pw, locator); 
        }
        else
        {
            pw.print("SystemId-Unknown:locator-unavailable: ");
            pw.println("exception:" + exception.getMessage());
            pw.println("root-cause:" 
                       + ((null != cause) ? cause.getMessage() : "null"));
        }
    }


    /**
     * Print out the specific source line that caused the exception, 
     * if possible to load it.  
     *
     * @param pw PrintWriter to send output to
     * @param locator Xalan wrapper for either a JAXP or a SAX 
     * source location object
     */
    public static void logSourceLine(PrintWriter pw, SourceLocator locator)
    {
        if (null == locator)
            return;
            
        if (null == pw)
            pw = new PrintWriter(System.err, true);

        String url = locator.getSystemId();
        // Bail immediately if we get SystemId-Unknown
        //@todo future improvement: attempt to get resource 
        //  from a publicId if possible
        if (null == url)
        {
            pw.println("line: (No systemId; cannot read file)");
            pw.println();
            return;
        }
        
        //@todo attempt to get DOM backpointer or other ids

        try
        {
            int line = locator.getLineNumber();
            int column = locator.getColumnNumber();
            pw.println("line: " + getSourceLine(url, line));
            StringBuffer buf = new StringBuffer("line: ");
            for (int i = 1; i < column; i++)
            {
                buf.append(' ');
            }
            buf.append('^');
            pw.println(buf.toString());
        }
        catch (Exception e)
        {
            pw.println("line: logSourceLine unavailable due to: " + e.getMessage());
            pw.println();
        }
    }


    /**
     * Return the specific source line that caused the exception, 
     * if possible to load it; allow exceptions to be thrown.  
     *
     * @author shane_curcuru@us.ibm.com
     */
    protected static String getSourceLine(String sourceUrl, int lineNum)
            throws Exception
    {
        URL url = null;
        // Get a URL from the sourceUrl
        try
        {
            // Try to get a URL from it as-is
            url = new URL(sourceUrl);
        }
        catch (java.net.MalformedURLException mue)
        {
            int indexOfColon = sourceUrl.indexOf(':');
            int indexOfSlash = sourceUrl.indexOf('/');
            
            if ((indexOfColon != -1)
                && (indexOfSlash != -1)
                && (indexOfColon < indexOfSlash))
            {
                // The url is already absolute, but we could not get 
                //  the system to form it, so bail
                throw mue;
            }
            else
            {
                // The url is relative, so attempt to get absolute
                url = new URL(SystemIDResolver.getAbsoluteURI(sourceUrl));
                // If this fails, allow the exception to propagate
            }
        }
        
        String line = null;
        InputStream is = null;
        BufferedReader br = null;
        try
        {
            // Open the URL and read to our specified line
            URLConnection uc = url.openConnection();
            is = uc.getInputStream();
            br = new BufferedReader(new InputStreamReader(is));

            // Not the most efficient way, but it works
            // (Feel free to patch to seek to the appropriate line)
            for (int i = 1; i <= lineNum; i++)
            {
                line = br.readLine();
            }
            
        } 
        // Allow exceptions to propagate from here, but ensure 
        //  streams are closed!
        finally
        {
            br.close();
            is.close();
        }
        
        // Return whatever we found
        return line;
    }    


    /* ======== Implement settable properties ======== */

    /**
     * User-settable behavior: when to re-throw exceptions.  
     *
     * <p>This allows per-instance configuration of 
     * ListingErrorHandlers.  You can ask us to either throw 
     * an exception when we're called for various warning / 
     * error / fatalErrors, or simply log them and continue.</p>
     *
     * @param b if we should throw an exception on warnings
     */
    public void setThrowOnWarning(boolean b)
    {
        throwOnWarning = b;
    }

    /**
     * User-settable behavior: when to re-throw exceptions.  
     *
     * @return if we throw an exception on warnings
     */
    public boolean getThrowOnWarning()
    {
        return throwOnWarning;
    }

    /** If we should throw exception on warnings; default:false.  */
    protected boolean throwOnWarning = false;


    /**
     * User-settable behavior: when to re-throw exceptions.  
     *
     * <p>This allows per-instance configuration of 
     * ListingErrorHandlers.  You can ask us to either throw 
     * an exception when we're called for various warning / 
     * error / fatalErrors, or simply log them and continue.</p>
     *
     * <p>Note that the behavior of many parsers/transformers 
     * after an error is not necessarily defined!</p>
     *
     * @param b if we should throw an exception on errors
     */
    public void setThrowOnError(boolean b)
    {
        throwOnError = b;
    }

    /**
     * User-settable behavior: when to re-throw exceptions.  
     *
     * @return if we throw an exception on errors
     */
    public boolean getThrowOnError()
    {
        return throwOnError;
    }

    /** If we should throw exception on errors; default:true.  */
    protected boolean throwOnError = true;


    /**
     * User-settable behavior: when to re-throw exceptions.  
     *
     * <p>This allows per-instance configuration of 
     * ListingErrorHandlers.  You can ask us to either throw 
     * an exception when we're called for various warning / 
     * error / fatalErrors, or simply log them and continue.</p>
     *
     * <p>Note that the behavior of many parsers/transformers 
     * after a fatalError is not necessarily defined, most 
     * products will probably barf if you continue.</p>
     *
     * @param b if we should throw an exception on fatalErrors
     */
    public void setThrowOnFatalError(boolean b)
    {
        throwOnFatalError = b;
    }

    /**
     * User-settable behavior: when to re-throw exceptions.  
     *
     * @return if we throw an exception on fatalErrors
     */
    public boolean getThrowOnFatalError()
    {
        return throwOnFatalError;
    }

    /** If we should throw exception on fatalErrors; default:true.  */
    protected boolean throwOnFatalError = true;

}
