// Utils.java
//    A common code...
//
//    senger@ebi.ac.uk
//    November 2002
//

package org.biomoby.shared;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.Charset;
import java.util.HashSet;
import java.util.zip.GZIPInputStream;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;

/**
 * This is a set of several utility methods which may be useful for
 * writing both registry and client code. Some methods are specific
 * for Apache Axis framework.
 *<P>
 * @author <A HREF="mailto:senger@ebi.ac.uk">Martin Senger</A>
 * @version $Id: Utils.java,v 1.21 2008/03/17 14:29:53 kawas Exp $
 */

public abstract class Utils {

    /** 
     * In 'string', replace all occurrences of
     * 'from' by 'to' and return the resulting string. 
     */
    static String replace (String string, String from, String to) {
        if (from.equals("")) 
            return string;
        StringBuffer buf = new StringBuffer (2*string.length());

        int previndex = 0;
        int index = 0;
        int flen = from.length();
        while (true) { 
            index = string.indexOf (from, previndex);
            if (index == -1) {
                buf.append (string.substring (previndex));
                break;
            }
            buf.append (string.substring (previndex, index) + to);
            previndex = index + flen;
        }
        return buf.toString();
    }


    /*************************************************************************
     *
     *************************************************************************/
    static public String format (Object objectToBeFormatted, int indent) {
	StringBuffer buf = new StringBuffer();
	while (indent-- > 0) {
	    buf.append ("   ");
	}
	String strIndent = new String (buf);
	if (objectToBeFormatted == null)
	    return strIndent;
	String result = strIndent +
	    replace (objectToBeFormatted.toString(), "\n", "\n" + strIndent);
	if (result.endsWith (strIndent))
	    return result.substring (0, result.length() - strIndent.length());
	else
	    return result;
    }

    /*************************************************************************
     * Create a file and fill it with given contents. <p>
     *
     * @param file to be created
     * @param contents what to put in the created file
     * @throws MobyException if something goes wrong
     *************************************************************************/
    public static void createFile (File file, String contents)
	throws MobyException {
	createFile (file, new StringBuffer (contents));
    }

    /*************************************************************************
     * Create a file and fill it with given contents. <p>
     *
     * @param file to be created
     * @param contents what to put in the created file
     * @throws MobyException if something goes wrong
     *************************************************************************/
    public static void createFile (File file, StringBuffer contents)
	throws MobyException {
 	try {
	    PrintWriter fileout = new PrintWriter
		(new BufferedWriter (new FileWriter (file)));
	    fileout.print (contents);
	    fileout.close();
 	} catch (IOException e) {
	    throw new MobyException
		("Cannot create file '" + file.getAbsolutePath() + "'. " + e.toString());
 	}
    }

    /*************************************************************************
     * Give back an elapsed time (given in milllis) in a human
     * readable form. <p>
     *
     * @param millis is a time interval in milliseconds
     * @return formatted, human-readable, time
     *************************************************************************/
    public static String ms2Human (long millis) {
	StringBuffer buf = new StringBuffer (100);
	long seconds = millis / 1000;
	long minutes = seconds / 60;
	long hours = minutes / 60;
	long days = hours / 24;
	if (days > 0)
	    buf.append (days + " days and ");
	buf.append ((hours % 24) + ":" + (minutes % 60) + ":" + (seconds % 60) + "." + (millis % 1000));
	return new String (buf);
    }

    /*************************************************************************
     * Return just the last part of the LSID identifier. An example of
     * an LSID identifier as used by and returned from the Moby
     * registry is <tt>urn:lsid:biomoby.org:objectclass:object</tt>.
     * <p>
     *
     * @param lsid is an input
     * @return the last part of 'lsid', or the whole 'lsid' if it does
     * not contain any colon (which is a delimiter used by LSID
     * identifiers)
     *************************************************************************/
    public static String pureName (String lsid) {
	if (lsid == null) return lsid;
	int pos = lsid.lastIndexOf (":");
	if (pos < 0 || pos == lsid.length() - 1) return lsid;
	return lsid.substring (pos + 1);
    }

    /*************************************************************************
     * Find the resource with the given 'filename', read it and return
     * it. A resource is some data (images, audio, text, etc) that can
     * be accessed by class code in a way that is independent of the
     * location of the code, typically such resource file sits
     * anywhere on the CLASSPATH. <p>
     *
     * @param filename of a resource is a '/'-separated path name that
     * identifies the resource
     *
     * @param resourceOwner is any object whose class loader is used
     * to find and get the resource; typically one would put here
     * "this" when calling this method
     *
     * @return contents of the resource, or null if the resource could
     * not be found
     *
     * @throws MobyException if resource was found but an error
     * occured during its reading (IO problem, memory problem etc.)
     *************************************************************************/
    public static String readResource (String filename, Object resourceOwner)
	throws MobyException {

	InputStream ins =
	    resourceOwner.getClass().getClassLoader().getResourceAsStream
	    (filename);
	if (ins != null) {
	    BufferedReader data = null;
	    try {
		StringBuffer contents = new StringBuffer();
		data = new BufferedReader (new InputStreamReader (ins));
		String line;
		while ((line = data.readLine()) != null) {
		    contents.append (line);
		    contents.append ("\n");
		}
		return new String (contents);
	    } catch (IOException e) {
		throw new MobyException ("Problem when reading resource '" + filename +
					 "'. " + e.toString());
	    } catch (Error e) {     // be prepare for "out-of-memory" error
		throw new MobyException ("Problem when reading resource '" + filename +
					 "'. " + e.toString(), e);
	    } finally {
		try {
		    if (data != null)
			data.close();
		} catch (IOException e) {
		}
	    }
	}
	return null;
    }
    
    /*************************************************************************
     * Work in progress. <p>
     *
     * Slightly richer version of {@link
     * #readResource(String,Object)}. It reads the resource using
     * platform default encoding (which may be not what you
     * want... something to be done better (TBD). <p>
     *
     * @return contents of the resource, or null if the resource could
     * not be found
     *
     * @throws IOException if resource was found but an error
     * occured during its reading (IO problem, memory problem etc.)
     *************************************************************************/
    public static String readResource (String path, Class c)
	throws IOException {

	// path can be empty
	if (path == null) return null;

	// seems that we are going to read something - so prepare a
	// default encoding
 	String encoding = Charset.defaultCharset().name();

	// path can be absolute...
	File file = new File (path);
	if (file.isAbsolute())
	    return FileUtils.readFileToString (file, encoding);

	// ...or consider it a resource and load it as a resource of
	// the given class
	StringWriter result = new StringWriter();
	InputStream is = null;
	if (c != null) {
	    is = c.getClassLoader().getResourceAsStream (path);
	    if (is != null) {
		IOUtils.copy (is, result, encoding);
		return result.toString();
	    }
	    // ...or extend the path by the package name of the given
	    // class
	    String className = c.getName();
	    int pkgEndIndex = className.lastIndexOf ('.');
	    if (pkgEndIndex > 0) {
		String packageName = className.substring (0, pkgEndIndex);
		String newPath = packageName.replace ('.', '/') + "/" + path;
		is = c.getClassLoader().getResourceAsStream (newPath);
		if (is != null) {
		    IOUtils.copy (is, result, encoding);
		    return result.toString();
		}
	    }
	}

	// ...or (finally) try some general class loader
	is = Thread.currentThread().getContextClassLoader().getResourceAsStream (path);
	if (is != null) {
	    IOUtils.copy (is, result, encoding);
	    return result.toString();
	}

	// sorry, I cannot do more
	return null;
    }

    /*************************************************************************
     * Work in progress. <p>
     *
     * Similar to {@link #readResource(String,Class)} but return just
     * an URL of a resource, not the resource itself. <p>
     *
     * @return URL of the resource, or null if the resource could not
     * be found
     *************************************************************************/
    public static URL getResourceURL (String path, Class c) {

	// path can be empty
	if (path == null) return null;

	// path can be absolute...
	File file = new File (path);
	if (file.isAbsolute()) {
	    try {
		return file.toURI().toURL();
	    } catch (MalformedURLException e) {
		return null;
	    }
	}

	// ...or consider it a resource of the given class
	URL url = null;
	if (c != null) {
	    url = c.getClassLoader().getResource (path);
	    if (url != null) return url;

	    // ...or extend the path by the package name of the given class
	    String className = c.getName();
	    int pkgEndIndex = className.lastIndexOf ('.');
	    if (pkgEndIndex > 0) {
		String packageName = className.substring (0, pkgEndIndex);
		String newPath = packageName.replace ('.', '/') + "/" + path;
		url = c.getClassLoader().getResource (newPath);
		if (url != null) return url;
	    }
	}

	// ...or (finally) try some general class loader
	return Thread.currentThread().getContextClassLoader().getResource (path);
    }

    /*************************************************************************
     * Return a stringified version of the given exception, containing
     * - for more serious errors, such as NullPointerException - also
     * a stack trace. <p>
     *
     * @param e to be reported
     *************************************************************************/
    public static String stackTraceIfSerious (Throwable e) {
	if (e == null)
	    return "";
	boolean seriousError =
	    ( (e instanceof java.lang.NullPointerException)              ||
	      (e instanceof java.lang.ClassCastException)                ||
	      (e instanceof java.lang.reflect.InvocationTargetException) ||
	      (e instanceof java.lang.ArrayIndexOutOfBoundsException)    ||
	      (e instanceof java.lang.ClassNotFoundException) );
	if (seriousError) {
	    StringWriter sw = new StringWriter (500);
	    e.printStackTrace (new PrintWriter (sw));
	    return sw.toString();
	} else {
	    return e.toString();
	}
    }

    /*************************************************************************
     * Return just the last part of a Java class name (after the last
     * dot). It is useful for displaying purposes. <p>
     *
     * @param className whose last part is being looked for
     * @return the last part of 'className', or the whole 'className' if it does
     * not contain any dots
     *************************************************************************/
    public static String simpleClassName (String className) {
	int pos = className.lastIndexOf (".");
	int len = className.length();
	if (pos == -1)    return className;
	if (pos == len-1) return className.substring (0, pos);
	return className.substring (pos + 1);
    }

    /*********************************************************************
     * Check or create an article name. Biomoby uses term "article
     * name" for naming Biomoby objects by context where they
     * appear. For example, article name must be used for Biomoby
     * objects that are children (members) of other Biomoby objects. <p>
     *
     * This method checks if the given 'articleName' is not empty -
     * and if it is then it replaces it with the given 'className'
     * (each Biomoby object always has a class name). <p>
     *
     * It also trims the article name (removes starting and ending
     * whitespaces). <p>
     *
     * @param articleName will be checked, or created if it is empty
     * @param className will be used to create an article name (if
     * necessary)
     * @return original 'articleName' or a new (created) one
     ********************************************************************/
    static public String checkOrCreateArticleName (String articleName,
						   String className) {

	// triming whitespaces
	if (articleName.length() != articleName.trim().length())
	    articleName = articleName.trim();

	// return back the original article name (if it was good)
	if (articleName != null && ! "".equals (articleName.trim()))
	    return articleName;

	// if there is no article name, take the last part of the
	// class name
	return Utils.simpleClassName (className);
    }

    /*************************************************************************
     * Make sure that an article name does not collide with the member
     * names in the top-level MobyObject. The members in question are
     * 'id', 'name', 'namespace' and 'value'. This does not mean that
     * Biomoby data types are not allowed to have such article names,
     * but that Java code generator must generate slightly different
     * names for its methods in these cases. <p>
     *
     * I must admit that this replacement is not full-proof: if a data
     * type will have both article names 'value' and 'the_value' then
     * this arrangement breaks... <p>
     *
     * This method is used at least from two places now (an XML Moby
     * Parser and a MoSeS code generator) - that's why it ended up
     * here in general utilities. <p>
     *
     * @param value will be checked (already expected not to be empty)
     * @return the same 'value' if nothing wrong with it was found, or
     * a new string resembling the 'value' but having some characters
     * replaced
     *************************************************************************/
    public static String mobyEscape (String value) {
	if ( "value".equals (value) ||
	     "name".equals (value) ||
	     "namespace".equals (value) ||
	     "id".equals (value) )
	    return "the_" + value;
	else
	    return value;
    }

    /*************************************************************************
     * Make 'value' a valid Java identifier by trimming it and by
     * replacing "unwanted" characters by underscores. <p>
     *
     * @param value will be checked
     * @return the same 'value' if nothing wrong with it was found, or
     * a new string resembling the 'value' but having some characters
     * replaced
     *************************************************************************/
    public static String javaEscape (String value) {
	if (value == null)
	    return "_";
	String trimmed = value.trim();
	if (value.length() > trimmed.length())
	    value = trimmed;
	if (value.equals (""))
	    return "_";

	if (javaReserved.contains (value))
	    return "_" + value;

	if (! Character.isJavaIdentifierStart (value.charAt (0)))
	    value = "_" + value;

	boolean found = false;   // if it becomes true we need to return a new string
	char[] s = value.toCharArray();
// 	if (s.length != value.length())
// 	    found = true;   // can happen because of trimming
// 	if (! Character.isJavaIdentifierStart (s[0])) {
// 	    s[0] = '_';
// 	    found = true;
// 	}
	for (int i = 1; i < s.length ; i++ ) {
	    if (! Character.isJavaIdentifierPart (s[i])) {
		s[i] = '_';
		found = true;
	    }
	}

	return (found ? new String (s) : value);
    }

    /*************************************************************************
     *
     *************************************************************************/
    protected static HashSet<String> javaReserved = new HashSet<String>();
    static {
        javaReserved.add ("abstract");
        javaReserved.add ("assert");
        javaReserved.add ("boolean");
        javaReserved.add ("break");
        javaReserved.add ("byte");
        javaReserved.add ("case");
        javaReserved.add ("catch");
        javaReserved.add ("char");
        javaReserved.add ("class");
        javaReserved.add ("const");
        javaReserved.add ("continue");
        javaReserved.add ("default");
        javaReserved.add ("do");
        javaReserved.add ("double");
        javaReserved.add ("else");
        javaReserved.add ("enum");
        javaReserved.add ("extends");
        javaReserved.add ("false");
        javaReserved.add ("final");
        javaReserved.add ("finally");
        javaReserved.add ("float");
        javaReserved.add ("for");
        javaReserved.add ("goto");
        javaReserved.add ("if");
        javaReserved.add ("implements");
        javaReserved.add ("import");
        javaReserved.add ("instanceof");
        javaReserved.add ("int");
        javaReserved.add ("interface");
        javaReserved.add ("long");
        javaReserved.add ("native");
        javaReserved.add ("new");
        javaReserved.add ("null");
        javaReserved.add ("package");
        javaReserved.add ("private");
        javaReserved.add ("protected");
        javaReserved.add ("public");
        javaReserved.add ("return");
        javaReserved.add ("short");
        javaReserved.add ("static");
        javaReserved.add ("strictfp");
        javaReserved.add ("super");
        javaReserved.add ("switch");
        javaReserved.add ("synchronized");
        javaReserved.add ("this");
        javaReserved.add ("throw");
        javaReserved.add ("throws");
        javaReserved.add ("transient");
        javaReserved.add ("true");
        javaReserved.add ("try");
        javaReserved.add ("void");
        javaReserved.add ("volatile");
        javaReserved.add ("while");
    }
    
    /***************************************************************************
     * Gets an InputStream on a URL 
     ***************************************************************************/
    public static InputStream getInputStream(URL url) throws MobyException {
	if (url == null)
	    throw new MobyException("Can't get RESOURCE from a null URL!");
	try {
	    URLConnection connection = url.openConnection();
	    // try gzip content encoding iff we have a HTTP url
	    if (connection instanceof HttpURLConnection) {
		HttpURLConnection urlConnection = null;
		urlConnection = (HttpURLConnection) connection;
		urlConnection.setRequestProperty("User-Agent",
			"jmoby-central/1.0");
		urlConnection.setRequestProperty("Accept-Encoding",
			"gzip, deflate");
		urlConnection.setDefaultUseCaches(false);
		urlConnection.setUseCaches(false);

		if (("gzip").equalsIgnoreCase(urlConnection
			.getContentEncoding())) {
		    // handle gzip encoded content
		    return new GZIPInputStream(urlConnection.getInputStream());
		} else {
		    return urlConnection.getInputStream();
		}
	    } else {
		return connection.getInputStream();
	    }

	} catch (IOException e) {
	    throw new MobyException("Error creating input stream: "
		    + e.toString());
	}
    }
    
}
