/*
 * 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: DocumentCache.java 468651 2006-10-28 07:04:25Z minchau $
 */

package org.apache.xalan.xsltc.dom;

import java.io.File;
import java.io.PrintWriter;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLDecoder;
import java.util.Date;
import java.util.Hashtable;

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

import org.apache.xalan.xsltc.DOM;
import org.apache.xalan.xsltc.DOMCache;
import org.apache.xalan.xsltc.DOMEnhancedForDTM;
import org.apache.xalan.xsltc.Translet;
import org.apache.xalan.xsltc.runtime.AbstractTranslet;
import org.apache.xalan.xsltc.runtime.BasisLibrary;
import org.apache.xalan.xsltc.runtime.Constants;
import org.apache.xml.utils.SystemIDResolver;

import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;

/**
 * @author Morten Jorgensen
 */
public final class DocumentCache implements DOMCache {

    private int       _size;
    private Hashtable _references;
    private String[]  _URIs;
    private int       _count;
    private int       _current;
    private SAXParser _parser;
    private XMLReader _reader;
    private XSLTCDTMManager _dtmManager;

    private static final int REFRESH_INTERVAL = 1000;

    /*
     * Inner class containing a DOMImpl object and DTD handler
     */
    public final class CachedDocument {
	
	// Statistics data
	private long _firstReferenced;
	private long _lastReferenced;
	private long _accessCount;
	private long _lastModified;
	private long _lastChecked;
	private long _buildTime;

	// DOM and DTD handler references
	private DOMEnhancedForDTM _dom = null;
	
	/**
	 * Constructor - load document and initialise statistics
	 */
	public CachedDocument(String uri) {
	    // Initialise statistics variables
	    final long stamp = System.currentTimeMillis();
	    _firstReferenced = stamp;
	    _lastReferenced  = stamp;
	    _accessCount     = 0;
	    loadDocument(uri);

	    _buildTime = System.currentTimeMillis() - stamp;
	}

	/**
	 * Loads the document and updates build-time (latency) statistics
	 */
	public void loadDocument(String uri) {

	    try {
		final long stamp = System.currentTimeMillis();
                _dom = (DOMEnhancedForDTM)_dtmManager.getDTM(
                                 new SAXSource(_reader, new InputSource(uri)),
                                 false, null, true, false);
		_dom.setDocumentURI(uri);

		// The build time can be used for statistics for a better
		// priority algorithm (currently round robin).
		final long thisTime = System.currentTimeMillis() - stamp;
		if (_buildTime > 0)
		    _buildTime = (_buildTime + thisTime) >>> 1;
		else
		    _buildTime = thisTime;
	    }
	    catch (Exception e) {
		_dom = null;
	    }
	}

	public DOM getDocument()       { return(_dom); }

	public long getFirstReferenced()   { return(_firstReferenced); }

	public long getLastReferenced()    { return(_lastReferenced); }

	public long getAccessCount()       { return(_accessCount); }

	public void incAccessCount()       { _accessCount++; }

	public long getLastModified()      { return(_lastModified); }

	public void setLastModified(long t){ _lastModified = t; }

	public long getLatency()           { return(_buildTime); }

	public long getLastChecked()       { return(_lastChecked); }
	
	public void setLastChecked(long t) { _lastChecked = t; }

	public long getEstimatedSize() {
	    if (_dom != null)
		return(_dom.getSize() << 5); // ???
	    else
		return(0);
	}

    }

    /**
     * DocumentCache constructor
     */
    public DocumentCache(int size) throws SAXException {
        this(size, null);
        try {
            _dtmManager = (XSLTCDTMManager)XSLTCDTMManager.getDTMManagerClass()
                                                          .newInstance();
        } catch (Exception e) {
            throw new SAXException(e);
        }
    }

    /**
     * DocumentCache constructor
     */
    public DocumentCache(int size, XSLTCDTMManager dtmManager) throws SAXException {
	_dtmManager = dtmManager;
	_count = 0;
	_current = 0;
	_size  = size;
	_references = new Hashtable(_size+2);
	_URIs = new String[_size];

	try {
	    // Create a SAX parser and get the XMLReader object it uses
	    final SAXParserFactory factory = SAXParserFactory.newInstance();
	    try {
		factory.setFeature(Constants.NAMESPACE_FEATURE,true);
	    }
	    catch (Exception e) {
		factory.setNamespaceAware(true);
	    }
	    _parser = factory.newSAXParser();
	    _reader = _parser.getXMLReader();
	}
	catch (ParserConfigurationException e) {
	    BasisLibrary.runTimeError(BasisLibrary.NAMESPACES_SUPPORT_ERR);
	}
    }

    /**
     * Returns the time-stamp for a document's last update
     */
    private final long getLastModified(String uri) {
	try {
	    URL url = new URL(uri);
	    URLConnection connection = url.openConnection();
	    long timestamp = connection.getLastModified();
	    // Check for a "file:" URI (courtesy of Brian Ewins)
	    if (timestamp == 0){ // get 0 for local URI
	        if ("file".equals(url.getProtocol())){
	            File localfile = new File(URLDecoder.decode(url.getFile()));
	            timestamp = localfile.lastModified();
	        }
	    }
	    return(timestamp);
	}
	// Brutal handling of all exceptions
	catch (Exception e) {
	    return(System.currentTimeMillis());
	}
    }

    /**
     *
     */
    private CachedDocument lookupDocument(String uri) {
	return((CachedDocument)_references.get(uri));
    }

    /**
     *
     */
    private synchronized void insertDocument(String uri, CachedDocument doc) {
	if (_count < _size) {
	    // Insert out URI in circular buffer
	    _URIs[_count++] = uri;
	    _current = 0;
	}
	else {
	    // Remove oldest URI from reference Hashtable
	    _references.remove(_URIs[_current]);
	    // Insert our URI in circular buffer
	    _URIs[_current] = uri;
	    if (++_current >= _size) _current = 0;
	}
	_references.put(uri, doc);
    }

    /**
     *
     */
    private synchronized void replaceDocument(String uri, CachedDocument doc) {
	CachedDocument old = (CachedDocument)_references.get(uri);
	if (doc == null)
	    insertDocument(uri, doc);
	else
	    _references.put(uri, doc);
    }

    /**
     * Returns a document either by finding it in the cache or
     * downloading it and putting it in the cache.
     */
    public DOM retrieveDocument(String baseURI, String href, Translet trs) {
	CachedDocument doc;

    String uri = href;
    if (baseURI != null && !baseURI.equals("")) {
        try {
            uri = SystemIDResolver.getAbsoluteURI(uri, baseURI);
        } catch (TransformerException te) {
            // ignore    
        }
    }
    
	// Try to get the document from the cache first
	if ((doc = lookupDocument(uri)) == null) {
	    doc = new CachedDocument(uri);
	    if (doc == null) return null; // better error handling needed!!!
	    doc.setLastModified(getLastModified(uri));
	    insertDocument(uri, doc);
	}
	// If the document is in the cache we must check if it is still valid
	else {
	    long now = System.currentTimeMillis();
	    long chk = doc.getLastChecked();
	    doc.setLastChecked(now);
	    // Has the modification time for this file been checked lately?
	    if (now > (chk + REFRESH_INTERVAL)) {
		doc.setLastChecked(now);
		long last = getLastModified(uri);
		// Reload document if it has been modified since last download
		if (last > doc.getLastModified()) {
		    doc = new CachedDocument(uri);
		    if (doc == null) return null;
		    doc.setLastModified(getLastModified(uri));
		    replaceDocument(uri, doc);
		}
	    }
	    
	}

	// Get the references to the actual DOM and DTD handler
	final DOM dom = doc.getDocument();

	// The dom reference may be null if the URL pointed to a
	// non-existing document
	if (dom == null) return null;

	doc.incAccessCount(); // For statistics

	final AbstractTranslet translet = (AbstractTranslet)trs;

	// Give the translet an early opportunity to extract any
        // information from the DOM object that it would like.
	translet.prepassDocument(dom);

	return(doc.getDocument());
    }

    /**
     * Outputs the cache statistics
     */
    public void getStatistics(PrintWriter out) {
	out.println("<h2>DOM cache statistics</h2><center><table border=\"2\">"+
		    "<tr><td><b>Document URI</b></td>"+
		    "<td><center><b>Build time</b></center></td>"+
		    "<td><center><b>Access count</b></center></td>"+
		    "<td><center><b>Last accessed</b></center></td>"+
		    "<td><center><b>Last modified</b></center></td></tr>");

	for (int i=0; i<_count; i++) {
	    CachedDocument doc = (CachedDocument)_references.get(_URIs[i]);
	    out.print("<tr><td><a href=\""+_URIs[i]+"\">"+
		      "<font size=-1>"+_URIs[i]+"</font></a></td>");
	    out.print("<td><center>"+doc.getLatency()+"ms</center></td>");
	    out.print("<td><center>"+doc.getAccessCount()+"</center></td>");
	    out.print("<td><center>"+(new Date(doc.getLastReferenced()))+
		      "</center></td>");
	    out.print("<td><center>"+(new Date(doc.getLastModified()))+
		      "</center></td>");
	    out.println("</tr>");
	}

	out.println("</table></center>");
    }
}
