/*
 * @(#)JnlpResource.java	1.9 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.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.Date;
import java.util.Map;
import java.util.jar.JarFile;
import java.util.jar.Pack200;
import java.util.jar.Pack200.Packer;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.GZIPOutputStream;

import javax.servlet.ServletContext;

/** 
 *  A JnlpResource encapsulate the information about a resource that is
 *  needed to process a JNLP Download Request.
 *
 *  The pattern matching arguments are: name, version-id, os, arch, and locale.
 *
 *  The outgoing arguments are:
 *     - path to resource in (WAR File)
 *     - product version-id (Version-id to return or null. Typically same as version-id above)
 *     - mime-type for content
 *     - lastModified date of WAR file resource
 * 
 */
public class JnlpResource {
	private static final Logger logger              = Logger.getLogger(JnlpResource.class.getName());
	private static final String JNLP_MIME_TYPE      = "application/x-java-jnlp-file";
	private static final String JAR_MIME_TYPE       = "application/x-java-archive";

	private static final String JAR_MIME_TYPE_NEW   = "application/java-archive";

	// Default extension for the JNLP file
	private static final String JNLP_EXTENSION      = ".jnlp";
	private static final String JAR_EXTENSION       = ".jar";

	private static String _jnlpExtension = JNLP_EXTENSION;
	private static String _jarExtension = JAR_EXTENSION;

	public static void setDefaultExtensions(String jnlpExtension, String jarExtension) {
		if (jnlpExtension != null && jnlpExtension.length() > 0) {
			if (!jnlpExtension.startsWith(".")) jnlpExtension = "." + jnlpExtension;
			_jnlpExtension = jnlpExtension;	
		}
		if (jarExtension != null && jarExtension.length() > 0) {
			if (!jarExtension .startsWith(".")) jarExtension  = "." + jarExtension ;
			_jarExtension = jarExtension;	
		}
	}
	
	private static Packer packer;
	
	static {
		packer = Pack200.newPacker();
		Map<String, String> p = packer.properties();
	}

	/* Pattern matching arguments */
	private String _name;	  // Name of resource with path (this is the same as path for non-version based)
	private String _versionId;    // Version-id for resource, or null if none
	private String[] _osList;     // List of OSes for which resource should be returned
	private String[] _archList;   // List of architectures for which the resource should be returned
	private String[] _localeList; // List of locales for which the resource should be returned
	/* Information used for reply */
	private String _path;            // Path to resource in WAR file (unique)
	private URL    _resource;        // URL to resource in WAR file (unique - same as above really)
	private long   _lastModified;    // Last modified in WAR file
	private String _mimeType;        // Mime-type for resource
	private String _returnVersionId; // Version Id to return
	private String _encoding;        // Accept encoding
	private Boolean _packOnTheFly;
	private String _packCommand;

	public JnlpResource(ServletContext context, String path) { 
		this(context, null, null, null, null, null, path, null); 
	}

	public JnlpResource(ServletContext context, 
			String name, 
			String versionId, 
			String[] osList, 
			String[] archList, 
			String[] localeList,
			String path,
			String returnVersionId) {
		this(context, name, versionId, osList, archList, localeList, path,
				returnVersionId, null);
	}

	public JnlpResource(ServletContext context, 
			String name, 
			String versionId, 
			String[] osList, 
			String[] archList, 
			String[] localeList,
			String path,
			String returnVersionId,
			String encoding) {	
		// Matching arguments
		_encoding = encoding;
		_name = name;
		_versionId = versionId;	
		_osList = osList;
		_archList = archList;
		_localeList = localeList;

		_returnVersionId = returnVersionId;
		_packOnTheFly = Boolean.valueOf(context.getInitParameter("packOnTheFly"));
		_packCommand = context.getInitParameter("packCommand");
		if (_packCommand == null) {
			_packCommand = "pack200";
		}

		/* Check for existance and get last modified timestamp */
		try {
			_findResource(context, path, encoding, 0);
		} catch(IOException ioe) {
			_resource = null;
		}				    			    
	}

	private void _findResource(ServletContext context, String path, String encoding, int count)
			throws MalformedURLException {
		if (count > 1) { return; } // only run through this twice at most
		String orig_path = path.trim();
		String search_path = orig_path;
		_resource = context.getResource(orig_path);	
		_mimeType = getMimeType(context, orig_path);
		if (_resource != null) {

			boolean found = false;
			// pack200 compression
			if (encoding != null && _mimeType != null &&
					(_mimeType.compareTo(JAR_MIME_TYPE) == 0 || _mimeType.compareTo(JAR_MIME_TYPE_NEW) == 0) &&
					encoding.toLowerCase().indexOf(DownloadResponse.PACK200_GZIP_ENCODING) > -1){
				search_path = orig_path + ".pack.gz";
				_resource = context.getResource(search_path);
				// Get last modified time
				if (_resource != null) {
					_lastModified = getLastModified(context, _resource, search_path);
					if (_lastModified != 0) {
						_path = search_path;
						found = true;
					} else {
						_resource = null;
					}
				}
			}

			// gzip compression
			if (found == false && encoding != null &&
					encoding.toLowerCase().indexOf(DownloadResponse.GZIP_ENCODING) > -1){
				search_path = orig_path + ".gz";
				_resource = context.getResource(search_path);
				// Get last modified time
				if (_resource != null) {
					_lastModified = getLastModified(context, _resource, search_path);
					if (_lastModified != 0) {
						_path = search_path;
						found = true;
					} else {
						_resource = null;
					}
				}
			}

			if (found == false) {
				// no compression
				search_path = orig_path;
				_resource = context.getResource(search_path);
				// Get last modified time
				if (_resource != null) {
					_lastModified = getLastModified(context, _resource, search_path);
					if (_lastModified != 0) {
						_path = search_path;
						found = true;
						if (count == 0 && _packOnTheFly) {
							String realPath = context.getRealPath(_path);
							_packAndCompress(realPath);
							_findResource(context, path, encoding, count+1);
						}
					} else {
						_resource = null;
					}
				}
			}		
		}
	}
	
	private void _packAndCompress(String path) {
		// Can't use internal packing because of a memory leak.
		// _packAndCompressInternal(path);
		_packAndCompressExternal(path);
	}

	/**
	 * Can't use this right now due to a memory leak.
	 * See: http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6927240
	 * @param path
	 */
	private void _packAndCompressInternal(String path) {
		try {
			JarFile jar = new JarFile(path);
			GZIPOutputStream gzos = new GZIPOutputStream(new FileOutputStream(path + ".pack.gz"));
			packer.pack(jar, gzos);
			jar.close();
			gzos.close();
		} catch (IOException ioe) {
			// we tried. it'll revert to sending the unpacked jar.
			// FIXME It'll probably continually try to create this pack file on subsequent requests. Can we avoid that?
			logger.log(Level.WARNING, "Failed to create .pack.gz file: " + path + ".pack.gz" + "\n" + toString(), ioe);
		}
	}
	
	private void _packAndCompressExternal(String path) {
		try {
			String command = _packCommand + " -q " + path + ".pack.gz " + path;
			logger.info("Running pack200 command: " + command);
			Process p = Runtime.getRuntime().exec(command);
			p.waitFor();
		} catch (Exception ex) {
			// we tried. it'll revert to sending the unpacked jar.
			// FIXME It'll probably continually try to create this pack file on subsequent requests. Can we avoid that?
			logger.log(Level.WARNING, "Failed to create .pack.gz file: " + path + ".pack.gz" + "\n" + toString(), ex);
		}
	}

	long getLastModified(ServletContext context, URL resource, String path) {
		long lastModified = 0;
		URLConnection conn;
		try {
			// Get last modified time
			conn = resource.openConnection();
			lastModified = conn.getLastModified();
		} catch (Exception e) {
			// do nothing
		}

		if (lastModified == 0) {
			// Arguably a bug in the JRE will not set the lastModified for file URLs, and
			// always return 0. This is a workaround for that problem.
			String filepath = context.getRealPath(path); 
			if (filepath != null) {
				File f = new File(filepath);	    
				if (f.exists()) {
					lastModified = f.lastModified();
				}
			}
		}
		return lastModified;
	}

	/* Get resource specific attributes */
	public String getPath() { return _path; }
	public URL getResource() { return _resource; }             
	public String getMimeType() { return _mimeType; }    
	public long getLastModified() { return _lastModified; }    
	public boolean exists() { return _resource != null; }    
	public boolean isJnlpFile() { return _path.endsWith(_jnlpExtension); }
	public boolean isJarFile() { return _path.endsWith(_jarExtension); }

	/* Get JNLP version specific attributes */
	public String getName() { return _name; }	            
	public String getVersionId() { return _versionId; }	            
	public String[] getOSList()  { return _osList; }
	public String[] getArchList()  { return _archList; }
	public String[] getLocaleList()  { return _localeList; }
	public String   getReturnVersionId() { return _returnVersionId; }

	private String getMimeType(ServletContext context, String path) {	
		String mimeType = context.getMimeType(path);
		if (mimeType != null) return mimeType;	
		if (path.endsWith(_jnlpExtension)) return JNLP_MIME_TYPE;
		if (path.endsWith(_jarExtension)) return JAR_MIME_TYPE;
		return "application/unknown";
	}        

	/** Print info about an entry */
	public String toString() { 
		return "JnlpResource[WAR Path: " + _path + 
				showEntry(" versionId=",_versionId) +
				showEntry(" name=", _name) +
				" lastModified=" + new Date(_lastModified) +
				showEntry(" osList=", _osList) +
				showEntry(" archList=", _archList) +
				showEntry(" localeList=", _localeList) + "]" +
				showEntry(" returnVersionId=", _returnVersionId) + "]";

	}

	private String showEntry(String msg, String value) {
		if (value == null) return "";
		return msg + value;		
	}

	private String showEntry(String msg, String[] value) {
		if (value == null) return "";
		return msg + java.util.Arrays.asList(value).toString();	
	}        
}




