/**
 * Copyright (c) 2008, Aberystwyth University
 *
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without 
 * modification, are permitted provided that the following conditions 
 * are met:
 * 
 *  - Redistributions of source code must retain the above 
 *    copyright notice, this list of conditions and the 
 *    following disclaimer.
 *  
 *  - Redistributions in binary form must reproduce the above copyright 
 *    notice, this list of conditions and the following disclaimer in 
 *    the documentation and/or other materials provided with the 
 *    distribution.
 *    
 *  - Neither the name of the Centre for Advanced Software and 
 *    Intelligent Systems (CASIS) nor the names of its 
 *    contributors may be used to endorse or promote products derived 
 *    from this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 
 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR 
 * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF 
 * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 
 * SUCH DAMAGE.
 */

package org.purl.sword.server;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.StringTokenizer;
import java.util.concurrent.atomic.AtomicInteger;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.codec.binary.Base64;
import org.apache.log4j.Logger;
import org.purl.sword.atom.Summary;
import org.purl.sword.atom.Title;
import org.purl.sword.base.ChecksumUtils;
import org.purl.sword.base.Deposit;
import org.purl.sword.base.DepositResponse;
import org.purl.sword.base.ErrorCodes;
import org.purl.sword.base.HttpHeaders;
import org.purl.sword.base.SWORDAuthenticationException;
import org.purl.sword.base.SWORDErrorDocument;
import org.purl.sword.base.SWORDException;
import org.purl.sword.base.SWORDErrorException;

/**
 * DepositServlet
 * 
 * @author Stuart Lewis
 */
public class DepositServlet extends HttpServlet {

	/** Sword repository */
	protected SWORDServer myRepository;

	/** Authentication type */
	private String authN;
	
	/** Maximum file upload size in kB **/
	private int maxUploadSize;

	/** Temp directory */
	private String tempDirectory;

	/** Counter */
	private static AtomicInteger counter = new AtomicInteger(0);

	/** Logger */
	private static Logger log = Logger.getLogger(DepositServlet.class);

	/**
	 * Initialise the servlet
	 * 
	 * @throws ServletException
	 */
	public void init() throws ServletException {
		// Instantiate the correct SWORD Server class
		String className = getServletContext().getInitParameter("sword-server-class");
		if (className == null) {
			log.fatal("Unable to read value of 'sword-server-class' from Servlet context");
		} else {
			try {
				myRepository = (SWORDServer) Class.forName(className)
						.newInstance();
				log.info("Using " + className + " as the SWORDServer");
			} catch (Exception e) {
				log
						.fatal("Unable to instantiate class from 'sword-server-class': "
								+ className);
				throw new ServletException(
						"Unable to instantiate class from 'sword-server-class': "
								+ className);
			}
		}

		authN = getServletContext().getInitParameter("authentication-method");
		if ((authN == null) || (authN.equals(""))) {
			authN = "None";
		}
		log.info("Authentication type set to: " + authN);

		String maxUploadSizeStr = getServletContext().getInitParameter("maxUploadSize");
		if ((maxUploadSizeStr == null) || 
		    (maxUploadSizeStr.equals("")) || 
		    (maxUploadSizeStr.equals("-1"))) {
			maxUploadSize = -1;
			log.warn("No maxUploadSize set, so setting max file upload size to unlimited.");
		} else {
			try {
				maxUploadSize = Integer.parseInt(maxUploadSizeStr);
				log.info("Setting max file upload size to " + maxUploadSize);
			} catch (NumberFormatException nfe) {
				maxUploadSize = -1;
				log.warn("maxUploadSize not a number, so setting max file upload size to unlimited.");
			}
		}

		tempDirectory = getServletContext().getInitParameter(
				"upload-temp-directory");
		if ((tempDirectory == null) || (tempDirectory.equals(""))) {
			tempDirectory = System.getProperty("java.io.tmpdir");
		}
		File tempDir = new File(tempDirectory);
		log.info("Upload temporary directory set to: " + tempDir);
		if (!tempDir.exists()) {
			if (!tempDir.mkdirs()) {
				throw new ServletException(
						"Upload directory did not exist and I can't create it. "
								+ tempDir);
			}
		}
		if (!tempDir.isDirectory()) {
			log.fatal("Upload temporary directory is not a directory: "
					+ tempDir);
			throw new ServletException(
					"Upload temporary directory is not a directory: " + tempDir);
		}
		if (!tempDir.canWrite()) {
			log.fatal("Upload temporary directory cannot be written to: "
					+ tempDir);
			throw new ServletException(
					"Upload temporary directory cannot be written to: "
							+ tempDir);
		}
	}

	/**
	 * Process the Get request. This will return an unimplemented response.
	 */
	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		// Send a '501 Not Implemented'
		response.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED);
	}

	/**
	 * Process a post request.
	 */
	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		// Create the Deposit request
		Deposit d = new Deposit();
		Date date = new Date();
		log.debug("Starting deposit processing at " + date.toString() + " by "
				+ request.getRemoteAddr());

		// Are there any authentication details?
		String usernamePassword = getUsernamePassword(request);
		if ((usernamePassword != null) && (!usernamePassword.equals(""))) {
			int p = usernamePassword.indexOf(":");
			if (p != -1) {
				d.setUsername(usernamePassword.substring(0, p));
				d.setPassword(usernamePassword.substring(p + 1));
			}
		} else if (authenticateWithBasic()) {
			String s = "Basic realm=\"SWORD\"";
			response.setHeader("WWW-Authenticate", s);
			response.setStatus(401);
			return;
		}
		
		// Set up some variables
		String filename = null;
		File f = null;
		FileInputStream fis = null;

		// Do the processing
		try {
			// Write the file to the temp directory
			filename = tempDirectory + "SWORD-"
					+ request.getRemoteAddr() + "-" + counter.addAndGet(1);
			InputStream fin = request.getInputStream();
			OutputStream fout = new FileOutputStream(new File(filename));
			try
			{
			    byte[] buf = new byte[1024];
			    int len;
			    while ((len = fin.read(buf)) > 0)
			    {
			        fout.write(buf, 0, len);
			    }
			}
			finally
			{
			    fin.close();
			    fout.close();
			}
			
			// Check the size is OK
			File file = new File(filename);
		    long fLength = file.length() / 1024;
		    if ((maxUploadSize != -1) && (fLength > maxUploadSize)) {
		    	this.makeErrorDocument(ErrorCodes.MAX_UPLOAD_SIZE_EXCEEDED, 
		    			               HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE, 
		    			               "The uploaded file exceeded the maximum file size this server will accept (the file is " + 
		    			               fLength + "kB but the server will only accept files as large as " + 
		    			               maxUploadSize + "kB)",
		    			               request,
		    			               response);
		    	return;
		    }
		    
			// Check the MD5 hash
			String receivedMD5 = ChecksumUtils.generateMD5(filename);
			log.debug("Received filechecksum: " + receivedMD5);
			d.setMd5(receivedMD5);
			String md5 = request.getHeader("Content-MD5");
			log.debug("Received file checksum header: " + md5);
			if ((md5 != null) && (!md5.equals(receivedMD5))) {
				// Return an error document
				this.makeErrorDocument(ErrorCodes.ERROR_CHECKSUM_MISMATCH, 
						               HttpServletResponse.SC_PRECONDITION_FAILED,
						               "The received MD5 checksum for the deposited file did not match the checksum sent by the deposit client",
						               request,
						               response);
				log.debug("Bad MD5 for file. Aborting with appropriate error message");
				return;
			} else {
				// Set the file
				f = new File(filename);
				fis = new FileInputStream(f);
				d.setFile(fis);

				// Set the X-On-Behalf-Of header
                String onBehalfOf = request.getHeader(HttpHeaders.X_ON_BEHALF_OF.toString());
				if ((onBehalfOf != null) && (onBehalfOf.equals("reject"))) {
                    // user name is "reject", so throw a not know error to allow the client to be tested
                    throw new SWORDErrorException(ErrorCodes.TARGET_OWNER_UKNOWN,"unknown user \"reject\"");
                } else {
                    d.setOnBehalfOf(onBehalfOf);
                }

				// Set the X-Packaging header
				d.setPackaging(request.getHeader(HttpHeaders.X_PACKAGING));

				// Set the X-No-Op header
				String noop = request.getHeader(HttpHeaders.X_NO_OP);
                log.error("X_NO_OP value is " + noop);
				if ((noop != null) && (noop.equals("true"))) {
					d.setNoOp(true);
				} else if ((noop != null) && (noop.equals("false"))) {
					d.setNoOp(false);
                }else if (noop == null) {
                    d.setNoOp(false);
				} else {
                    throw new SWORDErrorException(ErrorCodes.ERROR_BAD_REQUEST,"Bad no-op");
                }

				// Set the X-Verbose header
				String verbose = request.getHeader(HttpHeaders.X_VERBOSE);
				if ((verbose != null) && (verbose.equals("true"))) {
					d.setVerbose(true);
				} else if ((verbose != null) && (verbose.equals("false"))) {
					d.setVerbose(false);
                }else if (verbose == null) {
                    d.setVerbose(false);
				} else {
                    throw new SWORDErrorException(ErrorCodes.ERROR_BAD_REQUEST,"Bad verbose");
                }

				// Set the slug
				String slug = request.getHeader(HttpHeaders.SLUG);
				if (slug != null) {
					d.setSlug(slug);
				}

				// Set the content disposition
				d.setContentDisposition(request.getHeader(HttpHeaders.CONTENT_DISPOSITION));

				// Set the IP address
				d.setIPAddress(request.getRemoteAddr());

				// Set the deposit location
				d.setLocation(getUrl(request));

				// Set the content type
				d.setContentType(request.getContentType());

				// Set the content length
				String cl = request.getHeader(HttpHeaders.CONTENT_LENGTH);
				if ((cl != null) && (!cl.equals(""))) {
					d.setContentLength(Integer.parseInt(cl));
				}

				// Get the DepositResponse
				DepositResponse dr = myRepository.doDeposit(d);
				
				// Echo back the user agent
				if (request.getHeader(HttpHeaders.USER_AGENT.toString()) != null) {
					dr.getEntry().setUserAgent(request.getHeader(HttpHeaders.USER_AGENT.toString()));
				}
				
				// Echo back the packaging format
				if (request.getHeader(HttpHeaders.X_PACKAGING.toString()) != null) {
					dr.getEntry().setPackaging(request.getHeader(HttpHeaders.X_PACKAGING.toString()));
				}
				
				// Print out the Deposit Response
				response.setStatus(dr.getHttpResponse());
				if ((dr.getLocation() != null) && (!dr.getLocation().equals("")))
				{
					response.setHeader("Location", dr.getLocation());
				}
				response.setContentType("application/atom+xml; charset=UTF-8");
				PrintWriter out = response.getWriter();
				out.write(dr.marshall());
				out.flush();
			}
		} catch (SWORDAuthenticationException sae) {
			// Ask for credentials again
			if (authN.equals("Basic")) {
				String s = "Basic realm=\"SWORD\"";
				response.setHeader("WWW-Authenticate", s);
				response.setStatus(401);
			}
		} catch (SWORDErrorException see) {
			// Get the details and send the right SWORD error document
			log.error(see.toString());
			this.makeErrorDocument(see.getErrorURI(), 
		               			   see.getStatus(),
		               			   see.getDescription(),
		                           request,
		                           response);
			return;
		} catch (SWORDException se) {
			response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
			log.error(se.toString());
		} catch (NoSuchAlgorithmException nsae) {
			response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
			log.error(nsae.toString());
		}
		
		finally {
			// Close the input stream if it still open
			if (fis != null) {
				fis.close();
			}

			// Try deleting the temp file
			if (filename != null) {
				f = new File(filename);
				f.delete();
			}
		}
	}
	
	/**
	 * Utility method to construct a SWORDErrorDocumentTest
	 * 
	 * @param errorURI The error URI to pass
	 * @param status The HTTP status to return
	 * @param summary The textual description to give the user
	 * @param request The HttpServletRequest object
	 * @param response The HttpServletResponse to send the error document to
	 * @throws IOException 
	 */
	protected void makeErrorDocument(String errorURI, int status, String summary, 
			                       HttpServletRequest request, HttpServletResponse response) throws IOException
	{
		SWORDErrorDocument sed = new SWORDErrorDocument(errorURI);
		Title title = new Title();
		title.setContent("ERROR");
		sed.setTitle(title);
		Calendar calendar = Calendar.getInstance();
		String utcformat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";
		SimpleDateFormat zulu = new SimpleDateFormat(utcformat);
		String serializeddate = zulu.format(calendar.getTime());
		sed.setUpdated(serializeddate);
		Summary sum = new Summary();
		sum.setContent(summary);
		sed.setSummary(sum);
		if (request.getHeader(HttpHeaders.USER_AGENT.toString()) != null) {
			sed.setUserAgent(request.getHeader(HttpHeaders.USER_AGENT.toString()));
		}
		response.setStatus(status);
    	response.setContentType("application/atom+xml; charset=UTF-8");
		PrintWriter out = response.getWriter();
    	out.write(sed.marshall().toXML());
		out.flush();
	}

	/**
	 * Utility method to return the username and password (separated by a colon
	 * ':')
	 * 
	 * @param request
	 * @return The username and password combination
	 */
	protected String getUsernamePassword(HttpServletRequest request) {
		try {
			String authHeader = request.getHeader("Authorization");
			if (authHeader != null) {
				StringTokenizer st = new StringTokenizer(authHeader);
				if (st.hasMoreTokens()) {
					String basic = st.nextToken();
					if (basic.equalsIgnoreCase("Basic")) {
						String credentials = st.nextToken();
						String userPass = new String(Base64
								.decodeBase64(credentials.getBytes()));
						return userPass;
					}
				}
			}
		} catch (Exception e) {
			log.debug(e.toString());
		}
		return null;
	}

	/**
	 * Utility method to decide if we are using HTTP Basic authentication
	 * 
	 * @return if HTTP Basic authentication is in use or not
	 */
	protected boolean authenticateWithBasic() {
		if (authN.equalsIgnoreCase("Basic")) {
			return true;
		} else {
			return false;
		}
	}

	/**
	 * Utility method to construct the URL called for this Servlet
	 * 
	 * @param req The request object
	 * @return The URL
	 */
	protected static String getUrl(HttpServletRequest req) {
		String reqUrl = req.getRequestURL().toString();
		String queryString = req.getQueryString();
		if (queryString != null) {
			reqUrl += "?" + queryString;
		}
		return reqUrl;
	}
}
