/*
 * @(#)JarDiffHandler.java	1.10 10/01/12
 * 
 * Copyright (c) 2006 Sun Microsystems, Inc. All Rights Reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * -Redistribution of source code must retain the above copyright notice, this
 *  list of conditions and the following disclaimer.
 *
 * -Redistribution 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 Sun Microsystems, Inc. or the names of contributors may
 * be used to endorse or promote products derived from this software without
 * specific prior written permission.
 *
 * This software is provided "AS IS," without a warranty of any kind. ALL
 * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING
 * ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE
 * OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN MICROSYSTEMS, INC. ("SUN")
 * AND ITS LICENSORS SHALL NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE
 * AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS
 * DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR ANY LOST
 * REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL,
 * INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY
 * OF LIABILITY, ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE,
 * EVEN IF SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
 *
 * You acknowledge that this software is not designed, licensed or intended
 * for use in the design, construction, operation or maintenance of any
 * nuclear facility.
 */

package jnlp.sample.servlet;
import java.io.*;
import java.util.*;
import jnlp.sample.jardiff.*;
import javax.servlet.*;
import javax.servlet.http.*;
import jnlp.sample.util.VersionString;
import java.net.URL;
/*
 * A class that generates and caches information about JarDiff files
 *
 */
public class JarDiffHandler {
    // Default size of download buffer
    private static final int BUF_SIZE = 32 * 1024;
    
    // Default JARDiff mime type
    private static final String JARDIFF_MIMETYPE    = "application/x-java-archive-diff";
    
    /** List of all generated JARDiffs */
    private HashMap _jarDiffEntries = null;
    
    /** Reference to ServletContext and logger object */    
    private static Logger _log = null;
    private ServletContext _servletContext = null;
    private String _jarDiffMimeType = null;
    
    /* Contains information about a particular JARDiff entry */
    private static class JarDiffKey implements Comparable{	
	private String  _name;		// Name of file
	private String  _fromVersionId;	// From version
	private String  _toVersionId;	// To version
	private boolean _minimal;       // True if this is a minimal jardiff
	
	/** Constructor used to generate a query object */
	public JarDiffKey(String name, String fromVersionId, String toVersionId, boolean minimal) {	    	    
	    _name = name;
	    _fromVersionId = fromVersionId;
	    _toVersionId = toVersionId;	
	    _minimal = minimal;
	}
	
	// Query methods		
	public String getName() 		{ return _name; }
	public String getFromVersionId() 	{ return _fromVersionId; }
	public String getToVersionId() 		{ return _toVersionId; }
	public boolean isMinimal()              { return _minimal; }
	
	// Collection framework interface methods
	
	public int compareTo(Object o) {
	    // All non JarDiff entries are less
	    if (!(o instanceof JarDiffKey)) return -1;
	    JarDiffKey other = (JarDiffKey)o;    
	    
	    int n = _name.compareTo(other.getName());
	    if (n != 0) return n;
	    
	    n = _fromVersionId.compareTo(other.getFromVersionId());
	    if (n != 0) return n;

	    if (_minimal != other.isMinimal())	return -1;
	    
	    return _toVersionId.compareTo(other.getToVersionId());	    
	}
	
	public boolean equals(Object o) {
	    return compareTo(o) == 0;
	}
	
	public int hashCode() {
	    return _name.hashCode() + 
		_fromVersionId.hashCode() + 
		_toVersionId.hashCode();
	}		
    }
    
    static private class JarDiffEntry {		
	private File  	_jardiffFile;	// Location of JARDiff file
	
	public JarDiffEntry(File jarDiffFile) {		    
	    _jardiffFile = jarDiffFile;
	}
	
	public File   getJarDiffFile() 		{ return _jardiffFile; }	
    }
    
    /** Initialize JarDiff handler */
    public JarDiffHandler(ServletContext servletContext, Logger log) {	
	_jarDiffEntries = new HashMap();
	_servletContext = servletContext;
	_log = log;
	
	_jarDiffMimeType  = _servletContext.getMimeType("xyz.jardiff");
	if (_jarDiffMimeType == null) _jarDiffMimeType = JARDIFF_MIMETYPE;	         
    }    
    
    /** Returns a JarDiff for the given request */
    public synchronized DownloadResponse getJarDiffEntry(ResourceCatalog catalog, DownloadRequest dreq, JnlpResource res) {
	if (dreq.getCurrentVersionId() == null) return null;
	
	// check whether the request is from javaws 1.0/1.0.1
	// do not generate minimal jardiff if it is from 1.0/1.0.1
	boolean doJarDiffWorkAround = isJavawsVersion(dreq, "1.0*");

	// First do a lookup to find a match
	JarDiffKey key = new JarDiffKey(res.getName(), 
					dreq.getCurrentVersionId(), 
					res.getReturnVersionId(), 
					!doJarDiffWorkAround);		
	

	JarDiffEntry entry = (JarDiffEntry)_jarDiffEntries.get(key);
	// If entry is not found, then the querty has not been made. 	
	if (entry == null) {	    	    	
	    if (_log.isInformationalLevel()) {
		_log.addInformational("servlet.log.info.jardiff.gen", 
				      res.getName(), 
				      dreq.getCurrentVersionId(), 
				      res.getReturnVersionId());
	    }
	    File f = generateJarDiff(catalog, dreq, res, doJarDiffWorkAround);	    
	    if (f == null) {
		_log.addWarning("servlet.log.warning.jardiff.failed",
				res.getName(), 
				dreq.getCurrentVersionId(), 
				res.getReturnVersionId());
	    }
	    // Store entry in table
	    entry = new JarDiffEntry(f);	    
	    _jarDiffEntries.put(key, entry);	    
	}	
	
	
	
	// Check for no JarDiff to return
	if (entry.getJarDiffFile() == null) {
	    return null;
	} else {
	    return DownloadResponse.getFileDownloadResponse(entry.getJarDiffFile(),
							    _jarDiffMimeType,
							    entry.getJarDiffFile().lastModified(),
							    res.getReturnVersionId());	
	}
    }
    

    public static boolean isJavawsVersion(DownloadRequest dreq, String version) {
	String javawsAgent = "javaws";
	String jwsVer = dreq.getHttpRequest().getHeader("User-Agent");


	// check the request is coming from javaws
	if (!jwsVer.startsWith("javaws-")) {
	    // this is the new style User-Agent string
	    // User-Agent: JNLP/1.0.1 javaws/1.4.2 (b28) J2SE/1.4.2
	    StringTokenizer st = new StringTokenizer(jwsVer);
	    while (st.hasMoreTokens()) {
		String verString = st.nextToken();
		int index = verString.indexOf(javawsAgent);
		if (index != -1) {
		    verString = verString.substring(index + javawsAgent.length() + 1);
		    return VersionString.contains(version, verString);
		}
	    }
	    return false;
	}

	// extract the version id from the download request
	int startIndex = jwsVer.indexOf("-");

	if (startIndex == -1) {
	    return false;
	}

	int endIndex = jwsVer.indexOf("/");

	if (endIndex == -1 || endIndex < startIndex) {
	    return false;
	}

	String verId = jwsVer.substring(startIndex + 1, endIndex);


	// check whether the versionString contains the versionId
	return VersionString.contains(version, verId);

    }

    /** Download resource to the given file */
    private boolean download(URL target, File file) {

	_log.addDebug("JarDiffHandler:  Doing download");	

	boolean ret = true;
	boolean delete = false;
	// use bufferedstream for better performance
	BufferedInputStream in = null;
	BufferedOutputStream out = null;
	try {
	    in = new BufferedInputStream(target.openStream());	 
	    out  = new BufferedOutputStream(new FileOutputStream(file));
	    int read = 0;
	    int totalRead = 0;
	    byte[] buf = new byte[BUF_SIZE];
	    while ((read = in.read(buf)) != -1) {
		out.write(buf, 0, read);	
		totalRead += read;	
	    }

	    _log.addDebug("total read: " + totalRead);	 
	    _log.addDebug("Wrote URL " + target.toString() + " to file " + file);
	   
	} catch(IOException ioe) {
	  
	    _log.addDebug("Got exception while downloading resource: " + ioe);
	    
	    ret = false;
	  
	    if (file != null) delete = true;
		
	} finally {
	    
	    try {
		in.close();
		in = null;
	    } catch (IOException ioe) {
		_log.addDebug("Got exception while downloading resource: " + ioe);
	    }

	    try {
		out.close();
		out = null;
	    } catch (IOException ioe) {
		_log.addDebug("Got exception while downloading resource: " + ioe);
	    }

	    if (delete) {
		file.delete();
	    }

	}
	return ret;
    }

    // fix for 4720897
    // if the jar file resides in a war file, download it to a temp dir
    // so it can be used to generate jardiff
    private String getRealPath(String path) throws IOException{

	URL fileURL =  _servletContext.getResource(path);
	
	File tempDir  = (File)_servletContext.getAttribute("javax.servlet.context.tempdir");

	// download file into temp dir
	if (fileURL != null) {
	    File newFile = File.createTempFile("temp", ".jar", tempDir);
	    if (download(fileURL, newFile)) {	       
		String filePath = newFile.getPath();
		return filePath;
	    }
	}
	return null;
    }


    private File generateJarDiff(ResourceCatalog catalog, DownloadRequest dreq, JnlpResource res, boolean doJarDiffWorkAround) {	
	boolean del_old = false;
	boolean del_new = false;

	// Lookup up file for request version
	DownloadRequest fromDreq = dreq.getFromDownloadRequest();
	try {
	    JnlpResource fromRes = catalog.lookupResource(fromDreq);
	    
	    /* Get file locations */
	    String newFilePath = _servletContext.getRealPath(res.getPath());
	    String oldFilePath = _servletContext.getRealPath(fromRes.getPath());	    	    	   
	    	    	    
	    // fix for 4720897
	    if (newFilePath == null) {
		newFilePath = getRealPath(res.getPath());
		if (newFilePath != null) del_new = true;
	    }

	    if (oldFilePath == null) {
		oldFilePath = getRealPath(fromRes.getPath());
		if (oldFilePath != null) del_old = true;
	    }
	    	  	         	   
	    if (newFilePath == null || oldFilePath == null) {
		return null;
	    }
	    
	    // Create temp. file to store JarDiff file in
	    File tempDir  = (File)_servletContext.getAttribute("javax.servlet.context.tempdir");

	    // fix for 4653036: JarDiffHandler() should use javax.servlet.context.tempdir to store the jardiff	  
	    File outputFile = File.createTempFile("jnlp", ".jardiff", tempDir);
	   	  
	    _log.addDebug("Generating Jardiff between " + oldFilePath + " and " + 
			      newFilePath + " Store in " + outputFile);
	    
	    // Generate JarDiff
	    OutputStream os = new FileOutputStream(outputFile);	   	   

	    JarDiff.createPatch(oldFilePath, newFilePath, os, !doJarDiffWorkAround);
	    os.close();
	    
	    try {

		// Check that Jardiff is smaller, or return null
		if (outputFile.length() >= (new File(newFilePath).length())) {
		    _log.addDebug("JarDiff discarded - since it is bigger");
		    return null;
		}
		
		// Check that Jardiff is smaller than the packed version of
		// the new file, if the file exists at all
		File newFilePacked = new File(newFilePath + ".pack.gz");
		if (newFilePacked.exists()) {
		    _log.addDebug("generated jardiff size: " + outputFile.length());
		    _log.addDebug("packed requesting file size: " + newFilePacked.length());
		    if (outputFile.length() >= newFilePacked.length()) {
			_log.addDebug("JarDiff discarded - packed version of requesting file is smaller");
			return null;
		    }
		}
		
		_log.addDebug("JarDiff generation succeeded");
		return outputFile;

	    } finally {		
		// delete the temporarily downloaded file
		if (del_new) {
		    new File(newFilePath).delete();
		}
		
		if (del_old) {
		    new File(oldFilePath).delete();
		}
	    }	  
	} catch(IOException ioe) {
	    _log.addDebug("Failed to genereate jardiff", ioe);
	    return null;	    	    
	} catch(ErrorResponseException ere) {
	    _log.addDebug("Failed to genereate jardiff", ere);
	    return null;	    
	}		
    }                        
}

