/*
 * 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.
 *
 * $Header:$
 */
package org.apache.beehive.netui.compiler.grammar;

import org.apache.beehive.netui.compiler.AnnotationGrammar;
import org.apache.beehive.netui.compiler.FlowControllerInfo;
import org.apache.beehive.netui.compiler.LocalFileEntityResolver;
import org.apache.beehive.netui.compiler.typesystem.declaration.AnnotationValue;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;
import org.w3c.dom.Document;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;


/**
 * A type that requires a valid XML file.  Can accept a specific XML schema, and will fall back to DTD-checking.
 */
public class ValidXmlFileType
        extends WebappPathType
{
    private String _schemaFileName;
    private static Map _parseResults = Collections.synchronizedMap( new HashMap() );

    public ValidXmlFileType( String schemaFileName, String requiredRuntimeVersion, AnnotationGrammar parentGrammar,
                             FlowControllerInfo fcInfo )
    {
        super( false, requiredRuntimeVersion, parentGrammar, fcInfo );
        _schemaFileName = schemaFileName;
    }


    protected boolean checkAnyExtension()
    {
        return true;
    }


    protected boolean doFatalError()
    {
        return true;
    }


    protected boolean ignoreDirectories()
    {
        return false;
    }


    protected boolean allowFileInPageFlowSourceDir()
    {
        return true;
    }


    protected void runAdditionalChecks( File file, AnnotationValue value )
    {

        //
        // We cache the results of parsing the file until the file is actually modified,
        // so we don't end up continually re-parsing it.
        //
        ParseResults prevResults = ( ParseResults ) _parseResults.get( file.getPath() );
        long lastModTime = file.lastModified();

        if ( prevResults == null || lastModTime > prevResults.getFileModTime() )
        {
            try
            {
                DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
                LocalFileEntityResolver entityResolver = LocalFileEntityResolver.getInstance();
                
                // If a schema was specified, we'll validate against that; otherwise, we'll just use the DTD.
                if (_schemaFileName != null) {
                    InputSource schemaInput =  entityResolver.resolveLocalEntity(_schemaFileName);
                    assert schemaInput != null : "could not get schema resource for " + _schemaFileName;
                    factory.setNamespaceAware(true);
                    factory.setAttribute( "http://java.sun.com/xml/jaxp/properties/schemaLanguage",
                                          "http://www.w3.org/2001/XMLSchema");
                    factory.setAttribute( "http://java.sun.com/xml/jaxp/properties/schemaSource", schemaInput);
                }
                factory.setValidating(true);
                DocumentBuilder builder = factory.newDocumentBuilder();
                builder.setEntityResolver(LocalFileEntityResolver.getInstance());
                ParseResults results = new ParseResults(lastModTime);
                Validator handler = new Validator(results);
                builder.setErrorHandler(handler);
                Document doc = builder.parse(file);
                if (doc.getDoctype() == null && _schemaFileName == null) {
                    // If the doctype is null, then we don't want to add errors -- there was no DTD identified.
                    results = new ParseResults(lastModTime);
                }
                _parseResults.put(file.getPath(), results);
                addErrorDiagnostics(file, results, value);
            }
            catch ( SAXParseException e )
            {
                _parseResults.put( file.getPath(), new ParseResults( lastModTime, e ) );
                addDiagnostic(file, e, value, true);
                return;
            }
            catch ( Exception e )
            {
                _parseResults.put( file.getPath(), new ParseResults( lastModTime, e ) );
                addError( value, "error.xml-read-error",
                          new Object[]{ file.getPath(), e.getClass().getName(), e.getMessage() } );
                return;
            }
        }
        else
        {
            addErrorDiagnostics(file, prevResults, value);
        }
    }

    private void addErrorDiagnostics(File file, ParseResults results, AnnotationValue value) {
        List errors = results.getErrors();

        for (Iterator i = errors.iterator(); i.hasNext();) {
            Exception e = (Exception) i.next();
            
            if (e instanceof SAXParseException) {
                addDiagnostic(file, (SAXParseException) e, value, true);
            } else {
                addError( value, "error.xml-read-error",
                        new Object[]{ file.getPath(), e.getClass().getName(), e.getMessage() } );
            }
        }
        
        List warnings = results.getWarnings();

        for (Iterator i = warnings.iterator(); i.hasNext();) {
            Exception e = (Exception) i.next();
            assert e instanceof SAXParseException : e.getClass().getName();
            addDiagnostic(file, (SAXParseException) e, value, false);
        }
    }
    
    private void addDiagnostic(File file, SAXParseException err, AnnotationValue value, boolean isError)
    {
        if ( err.getColumnNumber() != -1 && err.getLineNumber() != -1 )
        {
            Object[] args =
                    {
                        file.getPath(),
                        new Integer( err.getLineNumber() ),
                        new Integer( err.getColumnNumber() ),
                        err.getMessage()
                    };

            if (isError) {
                addError( value, "error.xml-parse-error", args );
            } else {
                addWarning( value, "error.xml-parse-error", args );
            }
        }
        else if ( err.getLineNumber() != -1 )
        {
            Object[] args =
            {
                file.getPath(),
                new Integer( err.getLineNumber() ),
                err.getMessage()
            };

            if (isError) {
                addError( value, "error.xml-parse-error-nocolumn", args );
            } else {
                addWarning( value, "error.xml-parse-error-nocolumn", args );
            }
        }
        else
        {
            Object[] args =
            {
                file.getPath(),
                err.getMessage()
            };

            if (isError) {
                addError( value, "error.xml-parse-error-nolinecolumn", args );
            } else {
                addWarning( value, "error.xml-parse-error-nolinecolumn", args );
            }
        }
    }

    private static class ParseResults
    {
        private long _fileModTime;
        private List _errors = null;
        private List _warnings = null;

        public ParseResults(long fileModTime) {
            _fileModTime = fileModTime;
        }
        
        public ParseResults(long fileModTime, Exception ex) {
            _fileModTime = fileModTime;
            addError(ex);
        }

        public long getFileModTime() {
            return _fileModTime;
        }

        public void setFileModTime(long fileModTime) {
            _fileModTime = fileModTime;
        }

        public void addError(Exception e) {
            if (_errors == null) {
                _errors = new ArrayList();
            }
            _errors.add(e);
        }
        
        public void addWarning(Exception e) {
            if (_warnings == null) {
                _warnings = new ArrayList();
            }
            _warnings.add(e);
        }
        
        public List getErrors() {
            return _errors != null ? _errors : Collections.EMPTY_LIST;
        }
        
        public List getWarnings() {
            return _warnings != null ? _warnings : Collections.EMPTY_LIST;
        }
    }

    private static class Validator extends DefaultHandler {

        private ParseResults _results;

        public Validator(ParseResults results) {
            _results = results;
        }

        public ParseResults getResults() {
            return _results;
        }

        public void error(SAXParseException ex)
                throws SAXException {
            _results.addError(ex);
        }

        public void fatalError(SAXParseException ex)
                throws SAXException {
            _results.addError(ex);
        }

        public void warning(SAXParseException ex)
                throws SAXException {
            _results.addWarning(ex);
        }
    }
}
