/*
 *  eXist Open Source Native XML Database
 *  Copyright (C) 2001-06 Wolfgang M. Meier
 *  wolfgang@exist-db.org
 *  http://exist.sourceforge.net
 *
 *  This program is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public License
 *  as published by the Free Software Foundation; either version 2
 *  of the License, or (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 *  $Id: XQueryServlet.java 4747 2006-10-27 10:09:58Z dizzzz $
 */
package org.exist.http.servlets;

import java.io.File;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.URISyntaxException;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.xml.transform.OutputKeys;
import org.apache.log4j.Logger;

import org.exist.http.Descriptor;
import org.exist.source.FileSource;
import org.exist.source.Source;
import org.exist.xmldb.CollectionImpl;
import org.exist.xmldb.XQueryService;
import org.exist.xmldb.XmldbURI;
import org.exist.xquery.Constants;
import org.exist.xquery.XPathException;
import org.exist.xquery.functions.request.RequestModule;
import org.exist.xquery.functions.response.ResponseModule;
import org.exist.xquery.functions.session.SessionModule;
import org.exist.xquery.util.HTTPUtils;
import org.exist.xquery.value.Sequence;
import org.xmldb.api.DatabaseManager;
import org.xmldb.api.base.Collection;
import org.xmldb.api.base.Database;
import org.xmldb.api.base.Resource;
import org.xmldb.api.base.ResourceIterator;
import org.xmldb.api.base.ResourceSet;
import org.xmldb.api.base.XMLDBException;

/**
 * Servlet to generate HTML output from an XQuery file.
 *
 * The servlet responds to an URL pattern as specified in the
 * WEB-INF/web.xml configuration file of the application. It will
 * interpret the path with which it is called as leading to a valid
 * XQuery file. The XQuery file is loaded, compiled and executed.
 * Any output of the script is sent back to the client.
 *
 * The servlet accepts the following initialization parameters in web.xml:
 *
 * <table border="0">
 * 	<tr><td>user</td><td>The user identity with which the script is executed.</td></tr>
 * 	<tr><td>password</td><td>Password for the user.</td></tr>
 * 	<tr><td>uri</td><td>A valid XML:DB URI leading to the root collection used to
 * 	process the request.</td></tr>
 * 	<tr><td>encoding</td><td>The character encoding used for XQuery files.</td></tr>
 * 	<tr><td>container-encoding</td><td>The character encoding used by the servlet
 * 	container.</td></tr>
 * 	<tr><td>form-encoding</td><td>The character encoding used by parameters posted
 * 	from HTML forms.</td></tr>
 * </table>
 *
 * User identity and password may also be specified through the HTTP session attributes
 * "user" and "password". These attributes will overwrite any other settings.
 *
 * @author Wolfgang Meier (wolfgang@exist-db.org)
 */
public class XQueryServlet extends HttpServlet {
    
    private static final Logger LOG = Logger.getLogger(XQueryServlet.class);
    
    public final static String DEFAULT_USER = "guest";
    public final static String DEFAULT_PASS = "guest";
    public final static XmldbURI DEFAULT_URI = XmldbURI.EMBEDDED_SERVER_URI.append(XmldbURI.ROOT_COLLECTION_URI);
    public final static String DEFAULT_ENCODING = "UTF-8";
    public final static String DEFAULT_CONTENT_TYPE = "text/html";
    
    public final static String DRIVER = "org.exist.xmldb.DatabaseImpl";
    
    private String user = null;
    private String password = null;
    private XmldbURI collectionURI = null;
    
    private String containerEncoding = null;
    private String formEncoding = null;
    private String encoding = null;
    private String contentType = null;
    
    /* (non-Javadoc)
     * @see javax.servlet.GenericServlet#init(javax.servlet.ServletConfig)
     */
    public void init(ServletConfig config) throws ServletException {
        super.init(config);
        user = config.getInitParameter("user");
        if(user == null)
            user = DEFAULT_USER;
        password = config.getInitParameter("password");
        if(password == null)
            password = DEFAULT_PASS;
        String confCollectionURI = config.getInitParameter("uri");
        if(confCollectionURI == null) {
            collectionURI = DEFAULT_URI;
        } else {
            try {
                collectionURI = XmldbURI.xmldbUriFor(confCollectionURI);
            } catch (URISyntaxException e) {
                throw new ServletException("Invalid XmldbURI for parameter 'uri': "+e.getMessage(),e);
            }
        }
        formEncoding = config.getInitParameter("form-encoding");
        if(formEncoding == null)
            formEncoding = DEFAULT_ENCODING;
        LOG.info("form-encoding = " + formEncoding);
        containerEncoding = config.getInitParameter("container-encoding");
        if(containerEncoding == null)
            containerEncoding = DEFAULT_ENCODING;
        LOG.info("container-encoding = " + containerEncoding);
        encoding = config.getInitParameter("encoding");
        if(encoding == null)
            encoding = DEFAULT_ENCODING;
        LOG.info("encoding = " + encoding);
        contentType = config.getInitParameter("content-type");
        if(contentType == null)
            contentType = DEFAULT_CONTENT_TYPE;
        
        try {
            Class driver = Class.forName(DRIVER);
            Database database = (Database)driver.newInstance();
            database.setProperty("create-database", "true");
            DatabaseManager.registerDatabase(database);
        } catch(Exception e) {
            String errorMessage="Failed to initialize database driver";
            LOG.error(errorMessage,e);
            throw new ServletException(errorMessage+": " + e.getMessage(), e);
        }
    }
    
    /* (non-Javadoc)
     * @see javax.servlet.http.HttpServlet#doGet(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
     */
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        process(request, response);
    }
    
    /* (non-Javadoc)
     * @see javax.servlet.http.HttpServlet#doPost(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
     */
    protected void doPost(HttpServletRequest req, HttpServletResponse response) throws ServletException, IOException {
        HttpServletRequest request = null;
        
        //For POST request, If we are logging the requests we must wrap HttpServletRequest in HttpServletRequestWrapper
        //otherwise we cannot access the POST parameters from the content body of the request!!! - deliriumsky
        Descriptor descriptor = Descriptor.getDescriptorSingleton();
        if(descriptor != null) {
            if(descriptor.allowRequestLogging()) {
                request = new HttpServletRequestWrapper(req, formEncoding);
            } else {
                request = req;
            }
        } else {
            request = req;
        }
        
        process(request, response);
    }
    
    /**
     * Processes incoming HTTP requests for XQuery
     */
    protected void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        //first, adjust the path
        String path = request.getPathTranslated();
        if(path == null) {
            path = request.getRequestURI().substring(request.getContextPath().length());
            int p = path.lastIndexOf(';');
            if(p != Constants.STRING_NOT_FOUND)
                path = path.substring(0, p);
            path = getServletContext().getRealPath(path);
        }
        
        //second, perform descriptor actions
        Descriptor descriptor = Descriptor.getDescriptorSingleton();
        if(descriptor != null) {
            //logs the request if specified in the descriptor
            descriptor.doLogRequestInReplayLog(request);
            
            //map's the path if a mapping is specified in the descriptor
            path = descriptor.mapPath(path);
        }
        
        
        if (request.getCharacterEncoding() == null)
            request.setCharacterEncoding(formEncoding);
        ServletOutputStream sout = response.getOutputStream();
        PrintWriter output = new PrintWriter(new OutputStreamWriter(sout, formEncoding));
        response.setContentType(contentType + "; charset=" + formEncoding);
        response.addHeader( "pragma", "no-cache" );
        response.addHeader( "Cache-Control", "no-cache" );
        
        File f = new File(path);
        if(!f.canRead()) {
            sendError(output, "Cannot read source file", path);
            return;
        }
        
        //allow source viewing for GET?
        if(request.getMethod().toUpperCase().equals("GET")) {
            String option;
            boolean source = false;
            if((option = request.getParameter("_source")) != null)
                source = option.equals("yes");
            
            //Should we display the source of the XQuery or execute it
            if(source && descriptor != null) {
                //show the source
                
                //check are we allowed to show the xquery source - descriptor.xml
//                System.out.println("path="+path);
                if(descriptor.allowSourceXQuery(path)) {
                    //Show the source of the XQuery
                    //writeResourceAs(resource, broker, stylesheet, encoding, "text/plain", outputProperties, response);
                    response.setContentType("text/plain;charset=" + formEncoding);
                    FileSource fs = new FileSource(f, encoding, true);
                    output.write(fs.getContent());
                    output.flush();
                    return;
                } else {
                   
                   response.sendError(HttpServletResponse.SC_FORBIDDEN, "Permission to view XQuery source for: " + path + " denied. Must be explicitly defined in descriptor.xml");
                   return;
                }
            }
        }
        
        
        //-------------------------------
        // Added by Igor Abade (igoravl@cosespseguros.com.br)
        // Date: Aug/06/2004
        //-------------------------------
        
        String contentType = this.contentType;
        try {
            contentType = getServletContext().getMimeType(path);
            if (contentType == null)
                contentType = this.contentType;
        } catch (Throwable e) {
            contentType = this.contentType;
        } finally {
            if (contentType.startsWith("text/") || (contentType.endsWith("+xml")))
                contentType += ";charset=" + formEncoding;
            response.setContentType(contentType );
        }
        
        //-------------------------------
        
//        URI baseUri;
//        try {
//            baseUri = new URI(request.getScheme(),
//                    null/*user info?*/, request.getLocalName(), request.getLocalPort(),
//                    request.getRequestURI(), null, null);
//        } catch(URISyntaxException e) {
//            baseUri = null;
//        }
        
        String requestPath = request.getRequestURI();
        int p = requestPath.lastIndexOf("/");
        if(p != Constants.STRING_NOT_FOUND)
            requestPath = requestPath.substring(0, p);
        String moduleLoadPath = getServletContext().getRealPath(requestPath.substring(request.getContextPath().length()));
        String actualUser = null;
        String actualPassword = null;
        HttpSession session = request.getSession();
        if(session != null && request.isRequestedSessionIdValid()) {
            actualUser = getSessionAttribute(session, "user");
            actualPassword = getSessionAttribute(session, "password");
        }
        if(actualUser == null) actualUser = user;
        if(actualPassword == null) actualPassword = password;
        
        try {
            Collection collection = DatabaseManager.getCollection(collectionURI.toString(), actualUser, actualPassword);
            XQueryService service = (XQueryService)
            collection.getService("XQueryService", "1.0");
            service.setProperty("base-uri", collectionURI.toString());
            service.setModuleLoadPath(moduleLoadPath);
            //service.setNamespace(prefix, RequestModule.NAMESPACE_URI);
            if(!((CollectionImpl)collection).isRemoteCollection()) {
                service.declareVariable(RequestModule.PREFIX + ":request", new HttpRequestWrapper(request, formEncoding, containerEncoding));
                service.declareVariable(ResponseModule.PREFIX + ":response", new HttpResponseWrapper(response));
                service.declareVariable(SessionModule.PREFIX + ":session", new HttpSessionWrapper(session));
            }
            
            Source source = new FileSource(f, encoding, true);
            ResourceSet result = service.execute(source);
            String mediaType = service.getProperty(OutputKeys.MEDIA_TYPE);
            if (mediaType != null) {
                if (!response.isCommitted())
                    response.setContentType(mediaType + "; charset=" + formEncoding);
            }
            for(ResourceIterator i = result.getIterator(); i.hasMoreResources(); ) {
                Resource res = i.nextResource();
                output.println(res.getContent().toString());
            }
        } catch (XMLDBException e) {
            LOG.debug(e);
            sendError(output, e.getMessage(), e);
        }
        output.flush();
    }
    
    private String getSessionAttribute(HttpSession session, String attribute) {
        Object obj = session.getAttribute(attribute);
        if(obj == null)
            return null;
        if(obj instanceof Sequence)
            try {
                return ((Sequence)obj).getStringValue();
            } catch (XPathException e) {
                return null;
            }
        return obj.toString();
    }
    
    private void sendError(PrintWriter out, String message, XMLDBException e) {
        out.print("<html><head>");
        out.print("<title>XQueryServlet Error</title>");
        out.print("<link rel=\"stylesheet\" type=\"text/css\" href=\"error.css\"></link></head>");
        out.print("<body><div id=\"container\"><h1>Error found</h1>");
        Throwable t = e.getCause();
        if (t instanceof XPathException) {
            XPathException xe = (XPathException) t;
            out.println(xe.getMessageAsHTML());
        } else {
            out.print("<h2>Message:");
            out.print(message);
            out.print("</h2>");
        }
        
        if(t!=null){
            // t can be null
            out.print(HTTPUtils.printStackTraceHTML(t));
        }
        
        
        out.print("</div></body></html>");
    }
    
    private void sendError(PrintWriter out, String message, String description) {
        out.print("<html><head>");
        out.print("<title>XQueryServlet Error</title>");
        out.print("<link rel=\"stylesheet\" type=\"text/css\" href=\"error.css\"></link></head>");
        out.println("<body><h1>Error found</h1>");
        out.print("<div class='message'><b>Message: </b>");
        out.print(message);
        out.print("</div><div class='description'>");
        out.print(description);
        out.print("</div></body></html>");
        out.flush();
    }
    
    // -jmvanel : never used locally
    
//	private static final class CachedQuery {
//
//		long lastModified;
//		String sourcePath;
//		CompiledExpression expression;
//
//		public CachedQuery(File sourceFile, CompiledExpression expression) {
//			this.sourcePath = sourceFile.getAbsolutePath();
//			this.lastModified = sourceFile.lastModified();
//			this.expression = expression;
//		}
//
//		public boolean isValid() {
//			File f = new File(sourcePath);
//			if(f.lastModified() > lastModified)
//				return false;
//			return true;
//		}
//
//		public CompiledExpression getExpression() {
//			return expression;
//		}
//	}
    
}
