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

package org.apache.cxf.bus.spring;

import java.io.IOException;
import java.net.URL;
import java.net.URLConnection;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.stream.XMLStreamException;

import org.w3c.dom.Document;

import org.xml.sax.InputSource;

import org.apache.cxf.common.util.SystemPropertyAction;
import org.springframework.beans.factory.BeanDefinitionStoreException;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.core.io.support.EncodedResource;

/**
 * CXF reads a series of Spring XML files as part of initialization.
 * The time it takes to parse them, especially if validating, builds up.
 * The XML files shipped in a release in the JARs are valid and invariant.
 * To speed things up, this class implements two levels of optimization.
 * When a CXF distribution is fully-packaged, each of the Spring XML 
 * bus extension .xml files is accompanied by a FastInfoset '.fixml' file.
 * These read much more rapidly. When one of those is present, this classs
 * reads it instead of reading the XML text file. 
 * 
 * Absent a .fixml file, this class uses WoodStox instead of Xerces (or
 * whatever the JDK is providing).
 * 
 * The Woodstox optimization also applies to user cxf.xml or cxf-servlet.xml files
 * if the user has disabled XML validation of Spring files with
 * the org.apache.cxf.spring.validation.mode system property.
 * 
 * Note that the fastInfoset optimization is only applied for the 
 * methods here that start from a Resource. If this is called with an InputSource,
 * that optimization is not applied, since we can't reliably know the
 * location of the XML. 
 */
public class ControlledValidationXmlBeanDefinitionReader extends XmlBeanDefinitionReader {

    /**
     * Exception class used to avoid reading old FastInfoset files.
     */
    private static class StaleFastinfosetException extends Exception {

        private static final long serialVersionUID = -3594973504794187383L;

    }

    // the following flag allows performance comparisons with and 
    // without fast infoset processing.
    private boolean noFastinfoset;
    // Spring has no 'getter' for this, so we need our own copy.
    private int visibleValidationMode = VALIDATION_AUTO;
    // We need a reference to the subclass.
    private TunedDocumentLoader tunedDocumentLoader;
    /**
     * @param beanFactory
     */
    public ControlledValidationXmlBeanDefinitionReader(BeanDefinitionRegistry beanFactory) {
        super(beanFactory);
        tunedDocumentLoader = new TunedDocumentLoader();
        this.setDocumentLoader(tunedDocumentLoader);
        noFastinfoset = SystemPropertyAction.getPropertyOrNull("org.apache.cxf.nofastinfoset") != null 
            || !TunedDocumentLoader.hasFastInfoSet();
    }

    @Override
    protected int doLoadBeanDefinitions(InputSource inputSource, 
                                        Resource resource) throws BeanDefinitionStoreException {
        // sadly, the Spring class we are extending has the critical function
        // getValidationModeForResource
        // marked private instead of protected, so trickery is called for here.
        boolean suppressValidation = false;
        try {
            URL url = resource.getURL();
            if (url.getFile().contains("META-INF/cxf/")) {
                suppressValidation = true;
            }
        } catch (IOException e) {
            // this space intentionally left blank.
        }
        
        int savedValidation = visibleValidationMode;
        if (suppressValidation) {
            setValidationMode(VALIDATION_NONE);
        }
        int r = super.doLoadBeanDefinitions(inputSource, resource);
        setValidationMode(savedValidation);
        return r;
    }

    @Override
    public void setValidationMode(int validationMode) {
        visibleValidationMode = validationMode;
        super.setValidationMode(validationMode);
    }

    @Override
    public int loadBeanDefinitions(final EncodedResource encodedResource)
        throws BeanDefinitionStoreException {
        if (!noFastinfoset) {
            try {
                return fastInfosetLoadBeanDefinitions(encodedResource);
            } catch (BeanDefinitionStoreException bdse) {
                throw bdse;
            } catch (Throwable e) {
                //ignore - just call the super to load them
            }
        }
        try {
            return AccessController.doPrivileged(new PrivilegedExceptionAction<Integer>() {
                public Integer run() throws Exception {
                    return internalLoadBeanDefinitions(encodedResource);
                }
                
            });
        } catch (PrivilegedActionException e) {
            if (e.getException() instanceof RuntimeException) {
                throw (RuntimeException)e.getException();
            }
            throw (BeanDefinitionStoreException)e.getException();
        }
    }
    
    private int internalLoadBeanDefinitions(EncodedResource encodedResource) {
        return super.loadBeanDefinitions(encodedResource);
    }
    
    private int fastInfosetLoadBeanDefinitions(EncodedResource encodedResource)
        throws IOException, StaleFastinfosetException, 
        ParserConfigurationException, XMLStreamException {
        
        URL resUrl = encodedResource.getResource().getURL();
        // There are XML files scampering around that don't end in .xml.
        // We don't apply the optimization to them.
        if (!resUrl.getPath().endsWith(".xml")) {
            throw new StaleFastinfosetException();
        }
        String fixmlPath = resUrl.getPath().replaceFirst("\\.xml$", ".fixml");
        String protocol = resUrl.getProtocol();
        // beware of the relative URL rules for jar:, which are surprising.
        if ("jar".equals(protocol)) {
            fixmlPath = fixmlPath.replaceFirst("^.*!", "");
        }
        
        URL fixmlUrl = new URL(resUrl, fixmlPath);

        // if we are in unpacked files, we take some extra time
        // to ensure that we aren't using a stale Fastinfoset file.
        if ("file".equals(protocol)) {
            URLConnection resCon = null;
            URLConnection fixCon = null;
            resCon = resUrl.openConnection();
            fixCon = fixmlUrl.openConnection();
            if (resCon.getLastModified() > fixCon.getLastModified()) {
                throw new StaleFastinfosetException();
            }
        }
        
        Resource newResource = new UrlResource(fixmlUrl); 
        Document doc = TunedDocumentLoader.loadFastinfosetDocument(fixmlUrl);
        if (doc == null) {
            //something caused FastinfoSet to not be able to read the doc
            throw new StaleFastinfosetException();
        }
        return registerBeanDefinitions(doc, newResource);
    }

}
