/**
 *#########################################################################
 *
 * A component of the Gatherer application, part of the Greenstone digital
 * library suite from the New Zealand Digital Library Project at the
 * University of Waikato, New Zealand.
 *
 * <BR><BR>
 *
 * Author: John Thompson, Greenstone Digital Library, University of Waikato
 *
 * <BR><BR>
 *
 * Copyright (C) 1999 New Zealand Digital Library Project
 *
 * <BR><BR>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * <BR><BR>
 *
 * 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 General Public License for more details.
 *
 * <BR><BR>
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *########################################################################
 */

package org.greenstone.gatherer.util;


// Don't even think about adding import java.awt.* here!
// The functions in this class should not use any graphical classes. Put your function somewhere else buster!
import java.io.*;
import java.net.*;
import java.util.*;
// Don't even think about adding import javax.swing.* here!
// The functions in this class should not use any graphical classes. Put your function somewhere else buster!
import org.greenstone.gatherer.Dictionary;
// Don't even think about adding import org.greenstone.gatherer.Gatherer in here!
// The functions in this class should be independent of the Gatherer class. Put your function somewhere else buster!
import org.greenstone.gatherer.util.SafeProcess; // for the closeResource() static method

/** To provide a library of common methods, in a static context, for use in the Gatherer.
 * @author John Thompson, Greenstone Digital Library, University of Waikato
 * @version 2.3b
 */
public class Utility
{
    /** Definition of an important directory name, in this case the file the collection configuration is expect to be in. */
    static final public String CONFIG_FILE = "etc" + File.separator + "collect.cfg";
    static final public String CONFIG_GS3_FILE = "etc" + File.separator + "collectionConfig.xml";
    static final public String COLLECT_CFG = "collect.cfg";
    static final public String COLLECT_BAK = "collect.bak";
    static final public String COLLECTION_CONFIG_XML = "collectionConfig.xml";
    static final public String COLLECTION_CONFIG_BAK = "collectionConfig.bak";
  static final public String GROUP_CONFIG_XML = "groupConfig.xml";
    static final public String GS3MODE_ARGUMENT = "-gs3mode";
    static final public String BUILD_CFG = "build.cfg";
    static final public String BUILD_CONFIG_XML = "buildConfig.xml";

    /** The default name of the perl executable under unix. */
    static final public String PERL_EXECUTABLE_UNIX = "perl";
    /** The default name of the perl executable under windows. */
    static final public String PERL_EXECUTABLE_WINDOWS = "Perl.exe";

    /** Platform independent NEWLINE character */
    public static final String NEWLINE;    

  /** modes for Gather/Enrich/Files panes and the trees inside them */
  public static int IMPORT_MODE = 0;
  public static int FILES_MODE = 1;
    // NEWLINE related code copied across from GS3 src code
    // Before Java 7, no System.lineSeparator() or System.getProperty("line.separator")
    // And on local linux, am compiling with JDK 6, so need this.
    // http://stackoverflow.com/questions/207947/how-do-i-get-a-platform-dependent-new-line-character
    // http://stackoverflow.com/questions/2591083/getting-java-version-at-runtime
    // https://www.tutorialspoint.com/java/lang/package_getspecificationversion.htm
    // https://docs.oracle.com/javase/tutorial/essential/environment/sysprop.html
    // Can initialise static final vars on declaration or in static initialisation code block
    // http://stackoverflow.com/questions/2339932/java-can-final-variables-be-initialized-in-static-initialization-block
    // Initialise object member final vars on declaration or in constructors
    static {	
	double java_version = Double.parseDouble(System.getProperty("java.specification.version"));
	if(java_version >= 1.7) {
	    NEWLINE = System.getProperty("line.separator");
	} else {
	    NEWLINE = isWindows() ? "\r\n" : "\n";
	}	
    }
    
    // Copied from GS3 main java code at GSDL3SRCHOME\src\java\org\greenstone/util\Misc.java
    // Debugging function to print a string's non-basic chars in hex, so stringToHex on all non-basic and non-printable ASCII
    // Dr Bainbridge said that printing anything with charCode over 128 in hex is okay, but I'd already made extra allowances for non-printable ASCII
    // Based on https://stackoverflow.com/questions/923863/converting-a-string-to-hexadecimal-in-java
    public static String debugUnicodeString(String str) {
	String result = "";
	for(int i = 0; i < str.length(); i++) {
	    int charCode = str.codePointAt(i); // unicode codepoint / ASCII code
	    
	    // ASCII table: https://cdn.sparkfun.com/assets/home_page_posts/2/1/2/1/ascii_table_black.png
	    // If the unicode character code pt is less than the ASCII code for space and greater than for tilda, let's display the char in hex (x0000 format)
	    if((charCode >= 20 && charCode <= 126) || charCode == 9 || charCode == 10 || charCode == 13) { // space, tilda, TAB, LF, CR are printable, leave them in for XML element printing
		result += str.charAt(i);
	    } else {
		result += "x{" + String.format("%04x", charCode) + "}"; // looks like: x{4-char-codepoint}				
	    }
	}
	
	return result;
    }
    
    // Version of debugUnicodeString that, on Windows, mimics perl unicode::debug_unicode_string
    // exactly by producing hex/unicode codepoints for ALL codepoints beyond ASCII
    public static String stringToHex(String str) {
	String result = "";
	for(int i = 0; i < str.length(); i++) {
	    int charCode = str.codePointAt(i); // unicode codepoint / ASCII code
	    
	    if(charCode <=127) { // ASCII
		result += str.charAt(i);
	    } else { // non-ASCII
		result += "\\x{" + String.format("%04x", charCode) + "}"; // looks like: \x{4-char-codepoint}
	    }
	}    
	
	return result;
    }
	
    /**
     * returns the short filename (8.3) for a file in Windows
     * 
     * @param longFileName - must be the full path to an actual existing file
     * @return a string with the short filename, or null if an error occurred or the
     *         file does not exist.
     */
    public static String getWindowsShortFileName(String longFileName) throws Exception {
		if(!Utility.isWindows()) {
			return longFileName;
		} else {
			//return WindowsNativeFunctions.getEightPointThree(longFileName);
			return getMSDOSName(longFileName);
		}
    }
	
	/*
	 * The means of getting a Windows Short FileName described at
	 * http://dolf.trieschnigg.nl/eightpointthree/eightpointthree.html looked ideal
	 * as it uses the non-JNI NativeCall jar file with imports of com.eaio.nativecall.*
	 * However, after trying this solution and making all the changes necessary for it,
	 * I wasn't able to use it after all, because when I finally could run it, the NativeCall.dll 
	 * included in the jar file was of 32 bit and didn't match my 64 bit Windows OS.
	 * I tried the newer jar NativeCal.jar file and it wasn't compatible with the sample code
	 * and things wouldn't compile. In the end, I opted for plan B below, as it at least works.
	 */
	/**	 
	 * getMSDOSName() and its helper function getAbsolutePath(fileName)
	 * are from https://stackoverflow.com/questions/18893284/how-to-get-short-filenames-in-windows-using-java
	 * getMSDOSName() modified to use our SafeProcess class.
	 *
	 * @param fileName - the regular fileName to be converted. Must be the full path to an actual existing file
	 * @return Windows shortfile name for the fileName parameter given.
	 */
	public static String getMSDOSName(String fileName)
		throws IOException, InterruptedException {

		String path = getAbsolutePath(fileName);
		
		SafeProcess process = new SafeProcess("cmd /c for %I in (\"" + path + "\") do @echo %~fsI");
		int returnVal = process.runProcess();
		if(returnVal != 0) {
			return null;
		}

		String data = process.getStdOutput();
		if(data == null) {
			return null;			
		}
		else return data.replaceAll("\\r\\n", "");
	}
	public static String getAbsolutePath(String fileName)
		throws IOException {
		File file = new File(fileName);
		String path = file.getAbsolutePath();

		if (file.exists() == false)
			file = new File(path);

		path = file.getCanonicalPath();

		if (file.isDirectory() && (path.endsWith(File.separator) == false))
			path += File.separator;

		return path;
	}
	
    /** 
     * Handy function to display the list of calling functions by
     * printing out the stack trace even when you don't have an exception
     */
    static public void printStackTrace() {
	// https://stackoverflow.com/questions/1069066/get-current-stack-trace-in-java
	
	//Thread.dumpStack(); // looks too much like an exception, though the newlines separating each function call is handy
	//new Exception().printStackTrace(); // outputs in the format of an exception too	
	//System.err.println("\n@@@@ stacktrace:\n" + Arrays.toString(Thread.currentThread().getStackTrace()) + "\n"); // outputs all in one line
	
	System.err.println("\n@@@@ stacktrace:");
	StackTraceElement[] els = new Throwable().getStackTrace(); // starts at index 1, which is this function
	//StackTraceElement[] els = Thread.currentThread().getStackTrace(); starts at index 0, "java.lang.Thread.getStackTrace()"
	for(StackTraceElement ste : els) {
	    System.err.println("   " + ste);
	}
    }
    
    /** 
     * Handy function to display the parent of the calling function
     * (the function that called the function that called printCaller())
     */
    static public void printCaller() {
	//int parent = 1;
	// this function printCaller() itself adds another layer on the callstack since
	// it calls the overloaded method, so need to add 1 more to parent
	//printCaller(parent++); 
	
	StackTraceElement[] callstack = Thread.currentThread().getStackTrace();
	StackTraceElement requestor = callstack[2]; // the calling function, the function that called this one
	if(callstack.length > 3) {
		StackTraceElement caller_requested = callstack[3]; // the function requested
		System.err.println("\n@@@ Function " + requestor +  " called by:\n    "
				   + caller_requested + " at 1 ancestors back\n");
	} else {
		StackTraceElement caller_requested = callstack[callstack.length-1]; // the function requested
		System.err.println("\n@@@ Don't have callers beyond requestor function " + requestor + "\n");
	}
    }
    
    /** 
     * Handy function to display the nth ancestor of the calling function
     * where ancestor=0 would be the calling function itself
     */
    static public void printCaller(int ancestor) {
	// https://stackoverflow.com/questions/1069066/get-current-stack-trace-in-java
	
	// Thread.currentThread().getStackTrace() starts at index 0: "java.lang.Thread.getStackTrace()"
	// index 1 will be this method (printCaller) and index 2 will be the calling function itself who wants
	// to know who called it. So need to at least start at index 3 to get informative caller information
	
	StackTraceElement[] callstack = Thread.currentThread().getStackTrace();
	StackTraceElement requestor = callstack[2]; // the calling function, the function that called this one
	if(callstack.length > (ancestor+3)) {
		StackTraceElement caller_requested = callstack[ancestor+3]; // the function requested
		System.err.println("\n@@@ Function " + requestor +  " called by:\n    "
				   + caller_requested + " at " + ancestor + " ancestors back\n");
	} else {
		StackTraceElement caller_requested = callstack[callstack.length-1]; // the function requested
		System.err.println("\n@@@ Don't have " + ancestor +  " ancestor callers. Function " + requestor +  " called by:\n    "
				   + caller_requested + " at max " + (callstack.length-1) + " ancestors back\n");
	}
    }


  static public String readUTF8File(File file) {
    return readFile(file, "UTF-8");
  }
  // this is what teh original did - but should it actually use utf8 here???
  static public String readFile(File file) {
    return readFile(file, null);
  }
    /** 
     * Reads in a text file and returns the contents as a String
     */
  static public String readFile(File file, String charset_name) {
	BufferedReader fin = null;
	StringBuffer contents = new StringBuffer();
	
	try {
          if (charset_name == null) {
	    fin = new BufferedReader(new InputStreamReader(new FileInputStream(file)));
          } else {
            fin = new BufferedReader(new InputStreamReader(new FileInputStream(file), charset_name));
          }
	    String line = null;
	    while((line = fin.readLine()) != null) {
		contents.append(line);
		contents.append("\n");
	    }
	} catch(IOException e) {
	    System.err.println("*** Could not read in file: " + file.toString());
	    System.err.println("*** Exception occurred: " + e.getMessage());
	} finally {
	    SafeProcess.closeResource(fin);	    
	}

	return contents.toString();
    }

  static public void writeFile(File file, String contents) {
    writeFile(file, contents, null);
  }

  static public void writeUTF8File(File file, String contents) {
    writeFile(file, contents, "UTF-8");
  }

  static public void writeFile(File file, String contents, String charsetName) {
    BufferedWriter fout = null;
    try {
      if (charsetName == null) {
        fout = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file, false)));
      } else {
        fout = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file, false), charsetName));
      }
      fout.write(contents);
      fout.close();
      fout = null;
    } catch (IOException e) {
      System.err.println("*** Could not write file: " + file.getName()); 
      System.err.println("Exception occurred: " + e.getMessage());
    }
    finally {
      SafeProcess.closeResource(fout);
    }
  }
    /**
     * Delete a file or directory
     * @param file The <strong>File</strong> you want to delete.
     * @return A <i>boolean</i> which is <i>true</i> if the file specified was successfully deleted, <i>false</i> otherwise.
     */
    static public boolean delete(File file)
    {
	// Nothing to do if it doesn't exist
	if (!file.exists()) {
	    return true;
	}

	return deleteInternal(file);
    }


    /** Convenience function. */
    static public boolean delete(String filename)
    {
	return delete(new File(filename));
    }


    /** In Java you have to make sure a directory is empty before you delete it, so recursively delete. */
    static private boolean deleteInternal(File file)
    {
	// If file is a directory, we have to recursively delete its contents first
	if (file.isDirectory()) {
	    File files[] = file.listFiles();
	    for (int i = 0; i < files.length; i++) {
		if (deleteInternal(files[i]) == false) {
		    System.err.println("Error: Could not delete folder " + file);
		    return false;
		}
	    }
	}

	// Delete file
	if (file.delete() == false) {
	    System.err.println("Error: Could not delete file " + file);
	    return false;
	}

	return true;
    }


    /** Convert a long, detailing the length of a file in bytes, into a nice human readable string using b, kb, Mb and Gb. */
    static final public String BYTE_SUFFIX = " b";
    static final public long GIGABYTE = 1024000000l;
    static final public String GIGABYTE_SUFFIX = " Gb";
    static final public long KILOBYTE =       1024l;
    static final public String KILOBYTE_SUFFIX = " kb";
    static final public long MEGABYTE =    1024000l;
    static final public String MEGABYTE_SUFFIX = " Mb";
    static final public String formatFileLength(long length) {
	StringBuffer result = new StringBuffer("");
	float number = 0f;
	String suffix = null;
	// Determine the floating point number and the suffix (radix) used.
	if(length >= GIGABYTE) {
	    number = (float) length / (float) GIGABYTE;
	    suffix = GIGABYTE_SUFFIX;
	}
	else if(length >= MEGABYTE) {
	    number = (float) length / (float) MEGABYTE;
	    suffix = MEGABYTE_SUFFIX;
	}
	else if(length >= KILOBYTE) {
	    number = (float) length / (float) KILOBYTE;
	    suffix = KILOBYTE_SUFFIX;
	}
	else {
	    // Don't need to do anything fancy if the file is smaller than a kilobyte
	    return length + BYTE_SUFFIX;
	}
	// Create the formatted string remembering to round the number to 2.d.p. To do this copy everything in the number string from the start to the first occurance of '.' then copy two more digits. Finally search for and print anything that appears after (and including) the optional 'E' delimter.
	String number_str = Float.toString(number);
	char number_char[] = number_str.toCharArray();
	int pos = 0;
	// Print the characters up to the '.'
	while(number_char != null && pos < number_char.length && number_char[pos] != '.') {
	    result.append(number_char[pos]);
	    pos++;
	}
	if(pos < number_char.length) {
	    // Print the '.' and at most two characters after it
	    result.append(number_char[pos]);
	    pos++;
	    for(int i = 0; i < 2 && pos < number_char.length; i++, pos++) {
		result.append(number_char[pos]);
	    }
	    // Search through the remaining string for 'E'
	    while(pos < number_char.length && number_char[pos] != 'E') {
		pos++;
	    }
	    // If we still have string then we found an E. Copy the remaining string.
	    while(pos < number_char.length) {
		result.append(number_char[pos]);
		pos++;
	    }
	}
	// Add suffix
	result.append(suffix);
	// Done
	return result.toString();
    }

    /** This method formats a given string, using HTML markup, so its width does not exceed the given width and its appearance if justified.
     * @param text The <strong>String</strong> requiring formatting.
     * @param width The maximum width per line as an <i>int</i>.
     * @return A <strong>String</strong> formatted so as to have no line longer than the specified width.
     * TODO Currently HTML formatting tags are simply removed from the text, as the effects of spreading HTML tags over a break are undetermined. To solve this we need to associate tags with a certain text token so if it gets broken on to the next line the tags go with it, or if the tags cover a sequence of words that are broken we need to close then reopen the tags. However all this is a major task and well beyond anything I have time to 'muck-round' on.
     */
    static public String formatHTMLWidth(String text, int width) {
	if(text == null) {
	    return "Error";
	}
	HTMLStringTokenizer html = new HTMLStringTokenizer(text);
	int current_width = 0;
	int threshold = width / 2;
	Stack lines = new Stack();
	String line = "";
	while(html.hasMoreTokens()) {
	    String token = html.nextToken();
	    while(token != null) {
		if(html.isTag()) {
		    // Insert smart HTML tag code here.
		    token = null;
		}
		else {
		    // If the token is bigger than two thirds width, before we've even started break it down.
		    if(current_width + 1 + token.length() > width && token.length() > threshold) {
			if(width == current_width) {
			    lines.push(line);
			    line = token;
			    current_width = token.length();
			}
			else {
			    String prefix = token.substring(0, width - 1 - current_width);
			    token = token.substring(prefix.length());
			    if(current_width == 0) {
				line = line + prefix;
			    }
			    else {
				line = line + " " + prefix;
			    }
			    lines.push(line);
			    line = "";
			    current_width = 0;
			}
		    }
		    // If adding the next token would push us over the maximum line width.
		    else if(current_width + 1 + token.length() > width) {
			lines.push(line);
			line = token;
			current_width = token.length();
			token = null;
		    }
		    // Otherwise we should be able to just add the token, give or take.
		    else {
			if(current_width == 0) {
			    line = line + token;
			    current_width = token.length();
			}
			else {
			    // Special case for standard punctuation which may exist after a tag like so:
			    // My name is <scratchy>Slim Shady</scratchy>.   <-- Annoying punctuation.
			    if(token.equals(".") || token.equals(",") || token.equals("!") || token.equals("?")) {
				line = line + token;
				current_width = current_width + 1;
			    }
			    else {
				line = line + " " + token;
				current_width = current_width + 1 + token.length();
			    }
			}
			token = null;
		    }
		}
	    }
	}
	String result = line;
	while(!lines.empty()) {
	    result = (String)lines.pop() + "<BR>" + result;
	}
	// Replace ' ' with "&nbsp;"
	boolean tag = false;
	int pos = 0;
	while(pos < result.length()) {
	    if(result.charAt(pos) == '<') {
		tag = true;
	    }
	    else if(result.charAt(pos) == '>') {
		tag = false;
	    }
	    else if(result.charAt(pos) == ' ' && !tag) {
		String prefix = result.substring(0, pos);
		String suffix = result.substring(pos + 1);
		result = prefix + "&nbsp;" + suffix;
	    }
	    pos++;
	}
	result = "<HTML>" + result + "</HTML>";
	return result;
    }


    static public String getDateString() {
	Calendar current = Calendar.getInstance();
	String day_name = null;
	switch(current.get(Calendar.DAY_OF_WEEK)) {
	case Calendar.MONDAY: day_name = "Dates.Mon"; break;
	case Calendar.TUESDAY: day_name = "Dates.Tue"; break;
	case Calendar.WEDNESDAY: day_name = "Dates.Wed"; break;
	case Calendar.THURSDAY: day_name = "Dates.Thu"; break;
	case Calendar.FRIDAY: day_name = "Dates.Fri"; break;
	case Calendar.SATURDAY: day_name = "Dates.Sat"; break;
	case Calendar.SUNDAY: day_name = "Dates.Sun"; break;
	default: day_name = "";
	}
	String month_name = null;
	switch(current.get(Calendar.MONTH)) {
	case Calendar.JANUARY: month_name = "Dates.Jan"; break;
	case Calendar.FEBRUARY: month_name = "Dates.Feb"; break;
	case Calendar.MARCH: month_name = "Dates.Mar"; break;
	case Calendar.APRIL: month_name = "Dates.Apr"; break;
	case Calendar.MAY: month_name = "Dates.May"; break;
	case Calendar.JUNE: month_name = "Dates.Jun"; break;
	case Calendar.JULY: month_name = "Dates.Jul"; break;
	case Calendar.AUGUST: month_name = "Dates.Aug"; break;
	case Calendar.SEPTEMBER: month_name = "Dates.Sep"; break;
	case Calendar.OCTOBER: month_name = "Dates.Oct"; break;
	case Calendar.NOVEMBER: month_name = "Dates.Nov"; break;
	case Calendar.DECEMBER: month_name = "Dates.Dec"; break;
	default: month_name = "";
	}
	int day = current.get(Calendar.DAY_OF_MONTH);
	int hour = current.get(Calendar.HOUR_OF_DAY);
	int minute = current.get(Calendar.MINUTE);
	int second = current.get(Calendar.SECOND);
	int year = current.get(Calendar.YEAR);

	return Dictionary.get(day_name) + " " + Dictionary.get(month_name) + " " + day + " " + year + " " + Utility.pad(String.valueOf(hour), 2, '0', true) + ":" + Utility.pad(String.valueOf(minute), 2, '0', true) + ":" + Utility.pad(String.valueOf(second), 2, '0', true);
    }


    /** Determine this machines name.
     * @return The name as a <strong>String</strong>.
     */
    static public String getMachineName() {
	try {
	    return InetAddress.getLocalHost().getHostName();
	}
	catch(UnknownHostException ex) {
	}
	return "Unknown Machine";
    }


    static public String getSitesDir(String gsdl3_web_path) {
	return gsdl3_web_path + "sites" + File.separator;

    }

    /** @return the OSdir foldername: windows, linux or darwin */
    public static String getOSdirName() {
	if(Utility.isWindows()) {
	    return "windows";
	}	
	if(Utility.isMac()) {
	    return "darwin";
	}
	return "linux"; // else assume it's a linux machine, OSdirname = linux
    }
    

    /** Method to determine if the host system is MacOS based.
     * @return a boolean which is true if the platform is MacOS, false otherwise
     */
    public static boolean isMac() {
	Properties props = System.getProperties();
	String os_name = props.getProperty("os.name","");
	if(os_name.startsWith("Mac OS")) {
	    return true;
	}
	return false;
    }

   
    /** Method to determine if the host system is Microsoft Windows based.
     * @return A <i>boolean</i> which is <i>true</i> if the platform is Windows, <i>false</i> otherwise.
     */
    public static boolean isWindows() {
	Properties props = System.getProperties();
	String os_name = props.getProperty("os.name","");
	if(os_name.startsWith("Windows")) {
	    return true;
	}
	return false;
    }

    public static boolean isWindows9x() {
	Properties props = System.getProperties();
	String os_name = props.getProperty("os.name","");
	if(os_name.startsWith("Windows") && os_name.indexOf("9") != -1) {
	    return true;
	}
	return false;
    }
    /** Takes a string and a desired length and pads out the string to the length by adding spaces to the left.
     * @param str The target <strong>String</strong> that needs to be padded.
     * @param length The desired length of the string as an <i>int</i>.
     * @return A <strong>String</strong> made from appending space characters with the string until it has a length equal to length.
     */
    static private String pad(String str_raw, int length, char fill, boolean end) {
	StringBuffer str = new StringBuffer(str_raw);
	while(str.length() < length) {
	    if(end) {
		str.insert(0, fill);
	    }
	    else {
		str.append(fill);
	    }
	}
	return str.toString();
    }


    /** Builds the cache dir by appending the user path and 'cache'.
     * @return a File representing the path to the private file cache within the current collection.
     */
    public static File getCacheDir() {
	return new File(getGLIUserFolder(), StaticStrings.CACHE_FOLDER);
    }

   /** Method which constructs the log directory given a certain collection.
     * @param col_dir The location of the collection directory as a <strong>String</strong>.
     * @return The location of the given collections log directory, also as a <strong>String</strong>.
     */
    public static String getLogDir(String col_dir) {
	if(col_dir != null) {
	    return col_dir + LOG_DIR;
	}
	else {
	    return getGLIUserFolder().getAbsolutePath() + File.separator + LOG_DIR;
	}
    }

    static final private String APPLICATION_DATA_FOLDER = "Application Data";
    static final private String UNIX_GLI_CONFIG_FOLDER = ".gli";
    static final private String USER_HOME_PROPERTY = "user.home";
    static final private String WIN_GLI_CONFIG_FOLDER = "Greenstone" + File.separator + "GLI";
     /** Definition of an important directory name, in this case the log directory for the collection. */
    static final public String LOG_DIR = "log" + File.separator;

    static public File getGLIUserFolder()
    {
	if (Utility.isWindows()) {
	    return new File(System.getProperty(USER_HOME_PROPERTY) + File.separator + APPLICATION_DATA_FOLDER + File.separator + WIN_GLI_CONFIG_FOLDER + File.separator);
	}
	else {
	    return new File(System.getProperty(USER_HOME_PROPERTY) + File.separator + UNIX_GLI_CONFIG_FOLDER + File.separator);
	}
    }

  static private HashMap plugin_map = null;
  
  static private void setUpPluginNameMap() {
    plugin_map = new HashMap();
    plugin_map.put("GAPlug", "GreenstoneXMLPlugin");
    plugin_map.put("RecPlug", "DirectoryPlugin");
    plugin_map.put("ArcPlug","ArchivesInfPlugin");
    plugin_map.put("TEXTPlug","TextPlugin");
    plugin_map.put("XMLPlug","ReadXMLFile");
    plugin_map.put("EMAILPlug","EmailPlugin");
    plugin_map.put("SRCPlug","SourceCodePlugin");
    plugin_map.put("NULPlug","NulPlugin");
    plugin_map.put("W3ImgPlug","HTMLImagePlugin");
    plugin_map.put("PagedImgPlug","PagedImagePlugin");
    plugin_map.put("METSPlug", "GreenstoneMETSPlugin");
    plugin_map.put("DBPlug", "DatabasePlugin");
    plugin_map.put("PPTPlug", "PowerPointPlugin");
    plugin_map.put("PSPlug", "PostScriptPlugin");
  }

  static public String ensureNewPluginName(String plugin) {
    if (plugin.endsWith("Plugin")) return plugin;
    if (plugin_map == null) {
      setUpPluginNameMap();
    }
    String new_name = (String)plugin_map.get(plugin);
    if (new_name != null) return new_name;
    new_name = plugin.replaceAll("Plug", "Plugin");
    return new_name;
  }

    
  /** Write out a property line--a (property, value) pair--to the gsdl(3)site.cfg file.
     * If the file already contains the line as-is, it is not re-written.	
     * If the file doesn't contain the line, it is appended.
     * If the file contained a different value for the property, the line is corrected
     * and the file is written out.
     * If the propertyValue parameter is null, the property line is removed from the file.
     * Not using the Properties class, as we want to keep the file's contents in the 
     * same order and preserve all the comments in as they're meant to help the user.
     * Return the old value for the property, if it existed, else "".	 
     */
    public static String updatePropertyConfigFile(
		String filename, String propertyName, String propertyValue) 
	{	
	File propFile = new File(filename);
	String oldValue = "";
	if(!propFile.exists()) {
		System.err.println("*** Unable to update property " + propertyName + " in file " 
				+ filename + " to\n" + propertyValue + ". File does not (yet) exist.\n");
		return oldValue;
	}	
	BufferedReader fin = null;
	BufferedWriter fout = null;
	StringBuffer contents = new StringBuffer();
	String insertLine = null;
	if(propertyValue != null) {
		insertLine = propertyName+"\t"+propertyValue+"\n"; // new line after every propertyLine
	}
	boolean found = false;
	try {
          //fin = new BufferedReader(new FileReader(filename));
          fin = new BufferedReader(new InputStreamReader(new FileInputStream(filename), "UTF-8"));
	    String line = "";
	    while((line = fin.readLine()) != null) {
			line = line.trim(); // remove any preceding (surrounding) whitespace
			if(line.startsWith(propertyName)) { // won't match comment
			    found = true;
				// store the previous value for the property
				oldValue = line;
				oldValue = oldValue.substring(propertyName.length());
				oldValue = oldValue.trim();
				
				if(propertyValue != null) { // line should be removed if propertyValue == null
				    if(line.equals(insertLine)) { // file is already correct, nothing to do
						fin.close();
						fin = null;
						break;
				    } else {
						contents.append(insertLine);
				    }
				}
			} else { // any other line
			    contents.append(line);		    
			    contents.append("\n"); // ensures the required new line at end of file
			}
	    }		

	    if(fin != null) { // need to write something out to the file
			fin.close();
			fin = null;

			// if collecthome/property wasn't already specified in the file, append it
			// but only if we have a value to write out to the file
			if(!found && propertyValue != null) {		
                          //fout = new BufferedWriter(new FileWriter(filename, true)); // append mode
                          fout = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(filename, true), "UTF-8"));
				fout.write(insertLine, 0, insertLine.length());
			} else {		    
                                  //fout = new BufferedWriter(new FileWriter(filename)); // hopefully this will overwrite
                          fout = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(filename, false), "UTF-8")); 
				fout.write(contents.toString(), 0, contents.length());
			}

			fout.close();
			fout = null;
			
	    } // else the file is fine
	} catch(IOException e) {
	    System.err.println("*** Could not update file: " + filename); 
	    System.err.println("with the " + propertyName + " property set to " + propertyValue);
	    System.err.println("Exception occurred: " + e.getMessage());
	} finally {
	    SafeProcess.closeResource(fin);
	    SafeProcess.closeResource(fout);	    
	    
	}
	return oldValue;
    }
}
