package org.codehaus.mojo.xml;

/*
 * 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.
 */

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.UndeclaredThrowableException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.transform.Source;
import javax.xml.transform.TransformerException;
import javax.xml.transform.URIResolver;
import javax.xml.transform.sax.SAXSource;

import org.apache.maven.plugin.MojoExecutionException;
import org.apache.xml.resolver.CatalogManager;
import org.apache.xml.resolver.tools.CatalogResolver;
import org.codehaus.plexus.resource.ResourceManager;
import org.codehaus.plexus.resource.loader.ResourceNotFoundException;
import org.w3c.dom.ls.LSInput;
import org.w3c.dom.ls.LSResourceResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.ext.EntityResolver2;


/**
 * An implementation of {@link org.xml.sax.EntityResolver},
 * {@link URIResolver}, and {@link LSResourceResolver},
 * based on the Apache catalog resolver.
 */
public class Resolver
    implements EntityResolver2, URIResolver, LSResourceResolver
{
	private final ResourceManager locator;
	private final File baseDir;
    private final CatalogResolver resolver;
    private boolean validating;

    /** Creates a new instance.
     * @param pFiles A set of files with catalog definitions to load
     * @throws MojoExecutionException An error occurred while loading the resolvers catalogs.
     */
    Resolver( File pBaseDir, List pFiles, List pUrls, ResourceManager pLocator, boolean pLogging )
        throws MojoExecutionException
    {
        baseDir = pBaseDir;
        locator = pLocator;
        CatalogManager manager = new CatalogManager();
        manager.setIgnoreMissingProperties( true );
        if ( pLogging )
        {
        	System.err.println("Setting resolver verbosity to maximum.");
        	manager.setVerbosity( Integer.MAX_VALUE );
        }
        resolver = new CatalogResolver( manager );
        for ( int i = 0; i < pFiles.size(); i++ )
        {
        	File file = (File) pFiles.get( i );
            try
            {
                resolver.getCatalog().parseCatalog( file.getPath() );
            }
            catch ( IOException e )
            {
                throw new MojoExecutionException( "Failed to parse catalog file: "
                                                  + file.getPath() + ": "
                                                  + e.getMessage(), e );
            }
        }
        for ( int i = 0;  i < pUrls.size();  i++ )
        {
        	URL url = (URL) pUrls.get( i );
        	try
        	{
        		resolver.getCatalog().parseCatalog( url );
        	}
        	catch ( IOException e )
        	{
        		throw new MojoExecutionException( "Failed to parse catalog URL: "
        				+ url + ": " + e.getMessage(), e );
        	}
        }
    }

    /**
     * Implementation of {@link org.xml.sax.EntityResolver#resolveEntity(String, String)}.
     */
    public InputSource resolveEntity( String pPublicId, String pSystemId )
        throws SAXException, IOException
    {
    	final InputSource source = resolver.resolveEntity( pPublicId, pSystemId );
    	if ( source != null )
    	{
    		return source;
    	}

    	URL url = resolve( pSystemId );
        if ( url != null )
        {
            return asInputSource( url );
        }
        return null;
    }

    private InputSource asInputSource( URL url ) throws IOException
    {
        InputSource isource = new InputSource( url.openStream() );
        isource.setSystemId( url.toExternalForm() );
        return isource;
    }

    /**
     * Implementation of {@link URIResolver#resolve(String, String)}.
     */
    public Source resolve( String pHref, String pBase )
        throws TransformerException
    {
    	final Source source = resolver.resolve( pHref, pBase );
    	if ( source != null )
    	{
    		return source;
    	}

    	URL url = resolve( pHref );
        if ( url != null )
        {
            try
            {
                return asSaxSource( asInputSource( url ) );
            }
            catch ( IOException e )
            {
                throw new TransformerException( e );
            }
            catch ( SAXException e )
            {
                throw new TransformerException( e );
            }
            catch ( ParserConfigurationException e )
            {
                throw new TransformerException( e );
            }
        }
        return null;
    }

    private Source asSaxSource( InputSource isource )
        throws SAXException, ParserConfigurationException
    {
        SAXParserFactory spf = SAXParserFactory.newInstance();
        spf.setValidating( validating );
        spf.setNamespaceAware( true );
        XMLReader xmlReader = spf.newSAXParser().getXMLReader();
        xmlReader.setEntityResolver( this );
        return new SAXSource( xmlReader, isource );
    }

    private final LSInput newLSInput( InputSource pSource )
    {
    	final LSInputImpl lsInput = new LSInputImpl();
    	lsInput.setByteStream( pSource.getByteStream() );
    	lsInput.setCharacterStream( pSource.getCharacterStream() );
    	lsInput.setPublicId( lsInput.getPublicId() );
    	lsInput.setSystemId( pSource.getSystemId() );
    	lsInput.setEncoding( pSource.getEncoding() );
    	return lsInput;
    }
    
    /**
     * Implementation of {@link LSResourceResolver#resolveResource(String, String, String, String, String)}.
     */
    public LSInput resolveResource( String pType, String pNamespaceURI, String pPublicId, String pSystemId,
                                    String pBaseURI )
    {
    	if ( pPublicId != null )
    	{
    		final InputSource isource = resolver.resolveEntity( pPublicId, pSystemId );
    		if ( isource != null )
    		{
    			return newLSInput( isource );
    		}
    	}
    	InputSource isource = resolver.resolveEntity( pNamespaceURI, pSystemId );
    	if ( isource != null )
    	{
    		return newLSInput( isource );
    	}
    	
        URL url = resolve( pSystemId );
        if ( url != null )
        {
            try
            {
                isource = asInputSource( url );
            }
            catch ( IOException e )
            {
                throw new UndeclaredThrowableException( e );
            }
        }
        return newLSInput( isource );
    }

    /**
     * Sets, whether the Resolver should create validating parsers.
     */
    public void setValidating ( boolean pValidating )
    {
        validating = pValidating;
    }

    /**
     * Returns, whether the Resolver should create validating parsers.
     */
    public boolean isValidating ( )
    {
        return validating;
    }

    private URL resolveAsResource( String pResource )
    {
        return Thread.currentThread().getContextClassLoader().getResource( pResource );
    }

    private URL resolveAsFile( String pResource )
    {
        File f = new File( baseDir, pResource );
        if ( !f.isFile() )
        {
            f = new File( pResource );
            if ( !f.isFile() )
            {
                return null;
            }
        }
        try
        {
            return f.toURI().toURL();
        }
        catch ( MalformedURLException e )
        {
            return null;
        }
    }

    private URL resolveAsURL( String pResource )
    {
        InputStream stream = null;
        try
        {
            final URL url = new URL( pResource );
            stream = url.openStream();
            stream.close();
            stream = null;
            return url;
        }
        catch ( IOException e )
        {
            return null;
        }
        finally
        {
            if ( stream != null )
            {
                try
                {
                    stream.close();
                }
                catch ( Throwable t )
                {
                    // Ignore me
                }
            }
        }
    }

    /**
     * Attempts to resolve the given URI.
     */
    public URL resolve( String pResource )
    {
        if ( pResource == null )
        {
            return null;
        }

        if ( pResource.startsWith( "resource:" ) )
        {
            String res = pResource.substring( "resource:".length() );
            return resolveAsResource( res );
        }

        URL url = resolveAsResource( pResource );
        if ( url == null )
        {
            url = resolveAsURL( pResource );
            if ( url == null )
            {
                url = resolveAsFile( pResource );
            }
        }

        try
        {
        	return locator.getResource( pResource ).getURL();
        }
        catch ( ResourceNotFoundException e )
        {
        	return null;
        }
        catch ( IOException e )
        {
        	return null;
        }
    }

    /**
     * Implementation of {@link EntityResolver2#getExternalSubset(String, String)}
     */
    public InputSource getExternalSubset( String name, String baseURI ) throws SAXException, IOException
    {
        return null;
    }

    /**
     * Implementation of {@link EntityResolver2#resolveEntity(String, String, String, String)}
     */
    public InputSource resolveEntity( String pName, String pPublicId, String pBaseURI, String pSystemId )
        throws SAXException, IOException
    {
    	final InputSource source = resolver.resolveEntity( pPublicId, pSystemId );
    	if ( source != null )
    	{
    		return source;
    	}
    	
    	URL url = resolve( pSystemId );
        if ( url != null )
        {
            return asInputSource( url );
        }
        return null;
    }
}
