/**
*#########################################################################
*
* 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.
*
* Author: John Thompson, Greenstone Digital Library, University of Waikato
*
* Copyright (C) 1999 New Zealand Digital Library Project
*
* 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.
*
* 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.
*
* 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;

import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.lang.*;
import java.net.*;
import java.util.*;
import javax.swing.*;
import javax.swing.plaf.*;
import javax.swing.text.*;

import org.webswing.toolkit.api.WebswingUtil;

import org.greenstone.gatherer.Configuration;
import org.greenstone.gatherer.GAuthenticator;
import org.greenstone.gatherer.FedoraInfo;
import org.greenstone.gatherer.collection.CollectionManager;
import org.greenstone.gatherer.feedback.ActionRecorderDialog;
import org.greenstone.gatherer.feedback.Base64;
import org.greenstone.gatherer.file.FileManager;
import org.greenstone.gatherer.file.FileAssociationManager;
import org.greenstone.gatherer.file.RecycleBin;
import org.greenstone.gatherer.greenstone.Classifiers;
import org.greenstone.gatherer.greenstone.LocalGreenstone;
import org.greenstone.gatherer.greenstone.LocalLibraryServer;
import org.greenstone.gatherer.greenstone.Plugins;
import org.greenstone.gatherer.greenstone3.LibraryAddressProperties;
import org.greenstone.gatherer.greenstone3.ServletConfiguration;
import org.greenstone.gatherer.gui.GUIManager;
import org.greenstone.gatherer.gui.URLField;
import org.greenstone.gatherer.gui.WarningDialog;
import org.greenstone.gatherer.gui.FedoraLogin;
import org.greenstone.gatherer.gui.TestingPreparation;
import org.greenstone.gatherer.metadata.FilenameEncoding;
import org.greenstone.gatherer.remote.RemoteGreenstoneServer;
import org.greenstone.gatherer.util.GS3ServerThread;
import org.greenstone.gatherer.util.JarTools;
import org.greenstone.gatherer.util.SafeProcess;
import org.greenstone.gatherer.util.StaticStrings;
import org.greenstone.gatherer.util.Utility;


/** Containing the top-level "core" for the Gatherer, this class is the
* common core for the GLI application and applet. It first parses the
* command line arguments, preparing to update the configuration as
* required. Next it loads several important support classes such as the
* Configuration and Dictionary. Finally it creates the other important
* managers and sends them on their way.
* @author John Thompson, Greenstone Digital Library, University of Waikato
* @version 2.3
*/
public class Gatherer
{
	/** The name of the GLI. */
	static final public String PROGRAM_NAME = "Greenstone Librarian Interface";
	/** The current version of the GLI.
		* Note: the gs3-release-maker relies on this variable being declared
		* in a line which matches this java regex:
		* ^(.*)String\s*PROGRAM_VERSION\s*=\s*"trunk";
		* If change the declaration and it no longer matches the regex, please
		* change the regex in the gs3-release-maker code and in this message
		*/

	static final public String PROGRAM_VERSION = "3.13";

	static private Dimension size = new Dimension(800, 540);
	static public RemoteGreenstoneServer remoteGreenstoneServer = null;
	static public WebswingAuthenticator webswingAuthenticator = null;
  

	/** Has the exit flag been set? */
	static final public int EXIT_THEN_RESTART= 2;
	static public boolean exit = false;
	static public int exit_status = 0;

	static private String gli_directory_path = null;
	static private String gli_user_directory_path = null;

	static public String client_operating_system = null;

	/** All of the external applications that must exit before we close the Gatherer. */
	static private Vector apps = new Vector();
	static private String non_standard_collect_directory_path = null;
	static public String open_collection_file_path = null;
	static public String gsdlsite_collecthome = "";
	/** A public reference to the FileAssociationManager. */
	static public FileAssociationManager assoc_man;
	/** A public reference to the CollectionManager. */
	static public CollectionManager c_man;
	/** A public reference to the RecycleBin. */
	static public RecycleBin recycle_bin;
	/** a reference to the Servlet Configuration is GS3 */
	static public ServletConfiguration servlet_config;
	/** A public reference to the FileManager. */
	static public FileManager f_man;
	/** A public reference to the GUIManager. */
	static public GUIManager g_man = null;
	static private boolean g_man_built = false;

	/** We are using the GLI for GS3 */
	static public boolean GS3 = false;

	static public boolean isApplet = false;
	static public boolean isGsdlRemote = false;
	static public boolean isLocalLibrary = false;
	static public boolean isWebswing = false;

	// for storing the original proxy settings on GLI startup    
	private static Properties startup_proxy_settings = new Properties();
    
	/* TODO: If we're using local GLI, collections are built locally. If we're using client-GLI
	* and it contains a gs2build folder in it, then localBuild will also be true (if this is not
	* turned off in Preferences). If we're remote and this is turned off in Prefs, build remotely. */
	/*static public boolean buildingLocally = true;*/
	/** If we're using local GLI, we can always download. If we're using client-GLI, we can only
	* download if we have a gs2build folder inside it. And if we don't turn off downloadEnabling
	* in the preferences.
	*/
	static public boolean isDownloadEnabled = true;

	// feedback stuff
	/** is the feedback feature enabled? */
	static public boolean feedback_enabled = true;
	/** the action recorder dialog */
	static public ActionRecorderDialog feedback_dialog = null;

	// Refresh reasons
	static public final int COLLECTION_OPENED   = 0;
	static public final int COLLECTION_CLOSED   = 1;
	static public final int COLLECTION_REBUILT  = 2;
	static public final int PREFERENCES_CHANGED = 3;

	//////kk added////////  
	static public String cgiBase="";
	/////////////////

	/** Magic to allow Enter to fire the default button. */
	static {
		KeyStroke enter = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0);
		Keymap map = JTextComponent.getKeymap(JTextComponent.DEFAULT_KEYMAP);
		map.removeKeyStrokeBinding(enter);
	}

	static private URL default_gliserver_url=null;

  static public String local_tomcat_context_url_string = null;
  
    //******************* CODE FOR IMPORTANT HELPER FUNCTIONS FOR GUI TESTING *****************

    // Changes made to GLI execute GUI code in the Event Dispatch Thread (EDT) allowing GUI
    // testing with AssertJ Swing can be beneficial for GLI overall. In case it breaks GLI
    // or you suspect it has, set this to true. There's still extra method invocations, but
    // it's otherwise the same linear sequence and gets executed on the same thread as originally
    public static final boolean TURN_OFF_EVENTDISPATCH_CHANGES = false;
    public static boolean PRINT_THREAD_NAMES = false; // verbose info for debugging, can change
    
    // SYNC executes code with invokeAndWait on the EDT (waiting until termination of the runnable)
    // ASYNC calls invokeLater() which is the same as calling aThread.start(), not waiting for
    // any result of the code being executed.
    public static final boolean SYNC = true;
    public static final boolean ASYNC = false;
    // If you know some changes are undesirable for regular GLI, but they're needed for
    // testing to not throw EDT exceptions, then pass in DO_NOT_BYPASS_FOR_TESTING as 3rd arg
    // to Gatherer.invokeInEDT...(). This is OK for *while debugging* what the difference
    // between testing and regular GLI is, or while you're still busy implementing GLI automated
    // tests and want to return to debug the differences later.
    public static final boolean BYPASS_WHEN_NOT_TESTING = true;
    public static final boolean DO_NOT_BYPASS_WHEN_NOT_TESTING = false;

    // When turning off event dispatch changes globally (with TURN_OFF_EVENTDISPATCH_CHANGES
    // set to true) or when bypassing EDT changes on a case by case call to invokeInEDT...
    // we need to know what the *original* behaviour of GLI was that was replaced by the
    // invokeInEDT call, so that we return to running it in the same thread as GLI did,
    // instead of on the EDT.
    // So we need to know if GLI ran the Runnable code in the new Runnable Thread (passed in
    // as param Runnable) by calling start() on it, or whether GLI ran the Runnable code in
    // whatever the current thread was at the time/whatever the thread was before switch to EDT.
    private static final int RUN_IN_THREAD_PARAM_WITH_START = 1;
    private static final int PROCEED_IN_CURRENT_THREAD = 2;
    
    /*
    public static void invokeInEDT(boolean invokeAs, Runnable runnable) {
	invokeInDispatchThread("", invokeAs, DO_NOT_BYPASS_WHEN_NOT_TESTING,
			       PROCEED_IN_CURRENT_THREAD, runnable);
    }    
    public static void invokeInEDT(String callerDescription, boolean invokeAs, Runnable runnable)
    {
	invokeInDispatchThread(callerDescription, invokeAs, DO_NOT_BYPASS_WHEN_NOT_TESTING, PROCEED_IN_CURRENT_THREAD, runnable);
    }
    */
    // Call like:
    /*
      Gatherer.invokeInEDT_replacesProceedInCurrThread(
         "Gatherer.init()", // Class.methodName from where you're calling invokeInEDT...()
         Gatherer.<SYNC|ASYNC>,
	 //Gatherer.<DO_NOT_BYPASS_WHEN_NOT_TESTING|BYPASS_WHEN_NOT_TESTING>, // for debugging
	 new Runnable() {
            public void run() {
               .... some GUI/event code that should happen on EDT
	    }
      });
    */
    // BEWARE: in the code you embed in run() on rewriting any existing code,
    // change any This references to <Outerclass>.this
    
    
    // Versions that replace (new MyThreadSubclass()).start() coding pattern in GLI
    // when it needs invoking on EDT. If EDT changes are turned off globally or bypassed on
    // a case by case basis, the Runnable code will be run on the thread passed in as param, as
    // happens by calling start() on the thread instead of running the code on the calling thread
    public static void invokeInEDT_replacesThreadStart(String callerDescription,
	       boolean invokeAs, boolean bypassIfNotTesting, Thread newThread)
    {
	invokeInDispatchThread(callerDescription, invokeAs, bypassIfNotTesting,
			       RUN_IN_THREAD_PARAM_WITH_START, newThread);
    }
    public static void invokeInEDT_replacesThreadStart(String callerDescription,
	       boolean invokeAs, Thread newThread)
    {
	invokeInDispatchThread(callerDescription, invokeAs, DO_NOT_BYPASS_WHEN_NOT_TESTING,
			       RUN_IN_THREAD_PARAM_WITH_START, newThread);
    }
    
    // The more common alternative: versions of invokeInEDT_replacesProceedInCurrThread().
    // This version replaces any block of code Runnable code to be run in the Event Dispatch
    // Thread, EDT (unless changes for testing are turned off globally or bypassed on a case by
    // basis, in which case the block of Runnable code gets run in whatever is the calling thread)
    // @param invokeAs can be SYNC or ASYNC, see description of the constants.
    // SYNC does invokeAndWait() and is the preferrable choice to replace existing code by
    // embedding a call to invokeInEDT(), except if SYNC ends up blocking.
    // ASYNC is invokeLater() use it if SYNC blocks, to avoid blocking.    
    public static void invokeInEDT_replacesProceedInCurrThread(String callerDescription,
							       boolean invokeAs, boolean bypassIfNotTesting, Runnable runnable)
    {
	invokeInDispatchThread(callerDescription, invokeAs, bypassIfNotTesting, PROCEED_IN_CURRENT_THREAD, runnable);
    }
    public static void invokeInEDT_replacesProceedInCurrThread(String callerDescription,
	       boolean invokeAs, Runnable runnable)
    {
	invokeInDispatchThread(callerDescription, invokeAs, DO_NOT_BYPASS_WHEN_NOT_TESTING,
			       PROCEED_IN_CURRENT_THREAD, runnable);
    }
    
    // Skeleton structure and basic idea of function taken from
    // https://stackoverflow.com/questions/2684049/check-if-thread-is-edt-is-necessary
    private static void invokeInDispatchThread(String callerDescription, boolean invokeAs,
		   boolean bypassIfNotTesting, int runVariant, Runnable runnable)
    {
	if(PRINT_THREAD_NAMES) {
	    // https://stackoverflow.com/questions/467224/renaming-threads-in-java
	    Map<Thread,StackTraceElement[]> threadsInfo = Thread.getAllStackTraces();
	    Set<Thread> allActiveThreads = threadsInfo.keySet();
	    //for(Map.Entry<Thread,StackTraceElement[]> : threadsInfo) {
	    for(Thread aThread : allActiveThreads) {
		System.err.println("thread name: " + aThread.getName()
				   + " - class " + aThread.getClass().getName());
	    }
	}

	// print calling thread's name and the thread we run on if we're bypassing EDT changes
	// or would have bypassed if we *weren't* testing
	if(bypassIfNotTesting) {
	    System.err.println("Calling thread name: " + Thread.currentThread().getName()
			       + " - class " + Thread.currentThread().getClass().getName());
	    if(runnable instanceof Thread) {
		Thread t = (Thread)runnable;
		System.err.println("\tRunnable thread name: " + t.getName()
				   + " - class " + t.getClass().getName());
	    }
	}

	// If we're turning off EDT changes globally or on a case-by-case basis,
	// we're not running the Runnable code on the EDT any more, but whatever
	// thread GLI *would* have run the code on if we hadn't made the EDT changes
	if(TURN_OFF_EVENTDISPATCH_CHANGES
	   || (bypassIfNotTesting && !TestingPreparation.TEST_MODE)) {
	    	    
	    if (bypassIfNotTesting && !TestingPreparation.TEST_MODE) {
		System.err.println("@@@@" + callerDescription + " - Bypassing EDT change");
	    }
	    if(!EventQueue.isDispatchThread()) {
		System.err.println("@@@@" + callerDescription + "\n"
				   + "\tExpected to be on the Event Dispatch Thread but not so."
				   + "\n\tThis is potentially dangerous!!!");
	    }
	    
	    // Run it as before the EDT changes to GLI: one of 2 ways as specified
	    if(runVariant == RUN_IN_THREAD_PARAM_WITH_START) {
		// start off the param thread to run the runnable in thread of param
		// instead of whatever the current thread of the calling function is
		Thread runInThread = (Thread)runnable;
		// TODO: warning when runnable is not the event dispatch thread
		runInThread.start(); // async by nature		
	    } else if(runVariant == PROCEED_IN_CURRENT_THREAD) {
		// run the Runnable code (whether Thread object or not) in
		// whatever the current thread of the calling function is
		runnable.run(); // synchronous by nature of Runnable code running on current thread
	    }	    
	    
	} else { // We want the EDT changes to GLI to be active, meaning we *want* to run
	    // the Runnable code in the Event Dispatch Thread, not in any other Threads

	    if (EventQueue.isDispatchThread()) {
		// We're already in the Dispatch Thread.
		// So just run the Runnable code in the Dispatch thread, regardless of how GLI
		// used run it. Specifically, don't call start() on any Thread param to run
		// the code in the non-EDT thread param
		runnable.run();
		    
	    } else { // whether the Runnable is a new Thread or other Runnable object
		// whether GLI before EDT changes launched it with start() in its own Thread
		// or it was code run in whatever the current thread was, we run it on the
		// Dispatch Thread (Event Dispatch Thread, EDT)
		if(invokeAs == ASYNC) {
		    SwingUtilities.invokeLater(runnable); // asynchronous
		} else {
		    try {
			SwingUtilities.invokeAndWait(runnable); // synchronous
		    } catch(Exception e) {
			String message = "@@@ Exception during ";
			if(callerDescription != null && !callerDescription.equals("")) {
			    message += callerDescription + " - ";
			}
			message += "invokeAndWait on EDT: ";
			System.err.println(message + e.getMessage());
			e.printStackTrace();
		    }
		}
	    }
	}
    }
    
    //**********************END CODE ORIGINALLY MADE FOR GUI TESTING************************
    
	public Gatherer(GetOpt go) 
	{
		// Display the version to make error reports a lot more useful
		System.err.println("Version: " + PROGRAM_VERSION + "\n");

		if (go.webswing) {
		    WebswingUtil.getWebswingApi().sendActionEvent("setCursor", "wait", null);
		}
		
		JarTools.initialise(this); 


		// Remember the GSDLOS value
		client_operating_system = go.client_operating_system;

		// If feedback is enabled, set up the recorder dialog
		if (go.feedback_enabled) {
			// Use the default locale for now - this will be changed by the Gatherer run method
			feedback_enabled = true;
			feedback_dialog = new ActionRecorderDialog(Locale.getDefault());
		}

                // Are we greenstone 2 or 3?
                if (go.gsdl3_web_path != null && !go.gsdl3_web_path.equals("")) {
                  this.GS3 = true;
                  Configuration.config_xml = Configuration.CONFIG_XML_3;
		} else {
                  go.gsdl3_web_path = null;
                  go.gsdl3_writableweb_path = null;
                  go.gsdl3_src_path = null;
		}
                
		// Are we using a remote Greenstone?
		if (go.use_remote_greenstone) {
			isGsdlRemote = true;

			// We don't have a local Greenstone!
			go.gsdl3_web_path=null;
			go.gsdl3_writableweb_path=null;
			go.gsdl3_src_path=null;

			// Don't set go.gsdl_path to null, since gdsl_path may still be set 
			// if we have a client-gli containing gs2build folder. 
			// However, keep track of whether we can download.
			if(go.gsdl_path == null) {
				isDownloadEnabled = false;
			}

			// We have to use our own collect directory since we can't use the Greenstone one
			setCollectDirectoryPath(getGLIUserDirectoryPath() + "collect" + File.separator);
		}
		if (go.webswing) {
		    isWebswing = true;
		}
		// We have a local Greenstone. OR we have a gs2build folder inside 
		// the client-GLI folder (with which the Download panel becomes enabled) 
		if(isDownloadEnabled) {
			LocalGreenstone.setDirectoryPath(go.gsdl_path);
		}

		// Users may specify a non-standard collect directory (eg. when running one GLI in a network environment)
		// Processing of custom non-standard collect directory passed into gli now happens in init()
		//if (go.collect_directory_path != null) { setCollectDirectoryPath(go.collect_directory_path); }

		// More special code for running with a remote Greenstone
		if (isGsdlRemote) {
			if (go.fedora_info.isActive()) {
                          Configuration.template_config_xml = Configuration.TEMPLATE_CONFIG_PREFIX + Configuration.FEDORA_CONFIG_PREFIX + Configuration.CONFIG_REMOTE_XML; 
			}
			else {
                          Configuration.template_config_xml = Configuration.TEMPLATE_CONFIG_PREFIX +  Configuration.CONFIG_REMOTE_XML; 
			}

			Configuration.config_xml = Configuration.CONFIG_REMOTE_XML; 

			File collect_directory = new File(Gatherer.getCollectDirectoryPath());
			if (!collect_directory.exists() && !collect_directory.mkdir()) {
				System.err.println("Warning: Unable to make directory: " + collect_directory);
			}
		}
		else if (isWebswing) {
		    
		    Configuration.template_config_xml = Configuration.TEMPLATE_CONFIG_PREFIX +  Configuration.CONFIG_WEBSWING_XML; 
		    Configuration.config_xml = Configuration.CONFIG_WEBSWING_XML;
		    
		    if(go.username != null && !go.username.equals("")
		       && go.userJSessionID != null && !go.userJSessionID.equals("")) {
			
			webswingAuthenticator = new WebswingAuthenticator(go.username, go.usergroups, go.userJSessionID);
		    }
		}
		else {	    
			if (go.fedora_info.isActive()) {
				Configuration.template_config_xml = Configuration.TEMPLATE_CONFIG_PREFIX + Configuration.FEDORA_CONFIG_PREFIX + Configuration.config_xml;
			}
			// else, the CONFIG_XML uses the default config file, which is for the local GS server
		}

		if(go.testing_mode) {
		    TestingPreparation.TEST_MODE = true;
		}
		
		init(go.gsdl_path, go.gsdl3_web_path, go.gsdl3_writableweb_path, go.gsdl3_src_path, 
		go.fedora_info,
		go.local_library_path, go.library_url_string, 
		go.gliserver_url_string, go.debug, go.perl_path, go.no_load, go.filename, go.site_name,
                     go.servlet_path, go.collect_directory_path, go.language);
	}

        public Gatherer(String[] args)
        {
	    this(new GetOpt(args));
	}

	public void init(String gsdl_path, String gsdl3_web_path, String gsdl3_writableweb_path, String gsdl3_src_path, 
	FedoraInfo fedora_info,
	String local_library_path,
	String library_url_string, String gliserver_url_string, boolean debug_enabled,
	String perl_path, boolean no_load, String open_collection, 
                         String site_name, String servlet_path, String collect_directory_path, String startup_lang) 
	{

          Properties build_props = null; // in case we need to load buildProperties
          LibraryAddressProperties library_address_props= null;

	    if (isWebswing) {
		// library_url must be defined in the options
              if (library_url_string == null) {
		    System.err.println("Please set the library_url gli option in the webswing config");
		    System.exit(0);
              }
              if ( servlet_path == null) {
                System.err.println("The servlet path option was not set for GLI options in webswing config, defaulting to /library");
                servlet_path = "/library";
              }
              // we need to set up the locahost url
              build_props = loadBuildProperties(gsdl3_src_path);
              if (build_props != null) {
                library_address_props = new LibraryAddressProperties(build_props);
				    
                local_tomcat_context_url_string = library_address_props.getLocalHttpAddress();
                System.err.println("local url = "+local_tomcat_context_url_string);
              }

			    
                if (local_tomcat_context_url_string == null) {
                  System.err.println("Setting base configuration URL to Greenstone3 default:");
                  local_tomcat_context_url_string = "http://127.0.0.1:8383/greenstone3";
                  System.err.println("    " + local_tomcat_context_url_string);
                }

                // we need to authenticate the user
		//if(webswingAuthenticator == null || webswingAuthenticator.getUserName() == null) {
		if(webswingAuthenticator == null) {
		    webswingAuthenticator = new WebswingAuthenticator();
		}
		if (!webswingAuthenticator.authenticate(local_tomcat_context_url_string+servlet_path)) {
		    System.err.println("Authentication error ("+local_tomcat_context_url_string+"), quitting GLI");
		    System.exit(0);
		}
		
		String username = webswingAuthenticator.getUsername();

		// In webswing case, GathererProg does not have a username when it was initializing this
		String gli_no_username_dir_path = Gatherer.getGLIUserDirectoryPath();

		String gli_user_directory_path = gli_no_username_dir_path  + username + File.separator;
		
		File gli_user_directory = new File(gli_user_directory_path);
		if (!gli_user_directory.exists() && !gli_user_directory.mkdir()) {
		    System.err.println("Warning: Unable to make directory: " + gli_user_directory_path);
		}
		else {
		    Gatherer.setGLIUserDirectoryPath(gli_user_directory_path);
		}
		
	    }
		// Create the debug stream if required
		if (debug_enabled) {
			DebugStream.enableDebugging();

			Calendar now = Calendar.getInstance();
			String debug_file_path = "debug" + now.get(Calendar.DATE) + "-" + now.get(Calendar.MONTH) + "-" + now.get(Calendar.YEAR) + ".txt";

			// Debug file is created in the user's GLI directory
			debug_file_path = getGLIUserDirectoryPath() + debug_file_path;
			DebugStream.println("Debug file path: " + debug_file_path);
			DebugStream.setDebugFile(debug_file_path);
			DebugStream.print(System.getProperties());
		}

		// Delete plugins.dat and classifiers.dat files from previous versions of the GLI (no longer used)
		File plugins_dat_file = new File(Gatherer.getGLIUserDirectoryPath() + "plugins.dat");
		if (plugins_dat_file.exists()) {
			System.err.println("Deleting plugins.dat file...");
			Utility.delete(plugins_dat_file);
		}
		File classifiers_dat_file = new File(Gatherer.getGLIUserDirectoryPath() + "classifiers.dat");
		if (classifiers_dat_file.exists()) {
			System.err.println("Deleting classifiers.dat file...");
			Utility.delete(classifiers_dat_file);
		}

		try {
			// Load GLI config file
                  new Configuration(getGLIUserDirectoryPath(), gsdl_path, gsdl3_web_path, gsdl3_writableweb_path, gsdl3_src_path, site_name, servlet_path,
                                    fedora_info, startup_lang);
			
			// Check we know where Perl is
			Configuration.perl_path = perl_path;
			if (isGsdlRemote && !isDownloadEnabled && Utility.isWindows() && Configuration.perl_path != null) {
				if (Configuration.perl_path.toLowerCase().endsWith("perl.exe")) {
					Configuration.perl_path = Configuration.perl_path.substring(0, Configuration.perl_path.length() - "perl.exe".length());
				}
				if (Configuration.perl_path.endsWith(File.separator)) {
					Configuration.perl_path = Configuration.perl_path.substring(0, Configuration.perl_path.length() - File.separator.length());
				}
			}

			// the feedback dialog has been loaded with a default locale, 
			// now set the user specified one
			if (feedback_enabled && feedback_dialog != null) {
				feedback_dialog.setLocale(Configuration.getLocale("general.locale", true));
			}

			// Read Dictionary
			new Dictionary(Configuration.getLocale("general.locale", true), Configuration.getFont("general.font", true));

			// check that we are using Sun Java
			String java_vendor = System.getProperty("java.vendor");
			String java_runtime_name = System.getProperty("java.runtime.name");

			
			if (!java_vendor.equals("Sun Microsystems Inc.")
			    && !java_vendor.equals("Oracle Corporation")
			    && !java_runtime_name.equals("OpenJDK Runtime Environment")) {
			    // OK, so the dictionary key label is a bit apocryphal these days, but it gets the job done
			    System.err.println(Dictionary.get("General.NotSunJava", java_vendor));
			}

			// Unless we're using remote building, we need to know where the local Greenstone is
			if (!isGsdlRemote && gsdl_path == null) {
				missingGSDL();
			}

			if (fedora_info.isActive()) {

			    // if we have either a remote GS 2/3 server, or if we are dealing with a 
			    // local GS2, or else if it's a local GS3 but with fedora installed elsewhere, 
			    // we ask for fedora login details now
			    File fedora_webapp = new File(gsdl3_src_path+File.separator+"packages"+File.separator+"tomcat"+File.separator+"webapps"+File.separator+"fedora");
			    
			    if(isGsdlRemote || !GS3 || !fedora_webapp.exists()) {
				popupFedoraInfo();
			    }

			    // else if we have a local GS3 with fedora installed within it, we'll be starting 
			    // up fedora further below, together with the local tomcat
			}

			
			// Before calling setProxy, store anything already present
			// Downloading happens on the machine where GLI is running from, even with clientgli
			// For downloading with wget, we use ftp/http(s)_proxy env vars, so check if they're
			// already set
			if(System.getenv("http_proxy") != null) System.err.println("http_proxy already set");
			if(System.getenv("https_proxy") != null) System.err.println("https_proxy already set");
			if(System.getenv("ftp_proxy") != null) System.err.println("ftp_proxy already set");

			
			// Finally, we're ready to find out the version of the remote Greenstone server 
			if(isGsdlRemote) {
				// instantiate the RemoteGreenstoneServer object first
				remoteGreenstoneServer = new RemoteGreenstoneServer();

				// Set up proxy
				setProxy();
				// Now we set up an Authenticator
				Authenticator.setDefault(new GAuthenticator());

				int greenstoneVersion = 2;
				String prev_gliserver_url_string = Configuration.getString("general.gliserver_url", true);
				requestGLIServerURL(); // opens up the gliserver dialog
				gliserver_url_string = Configuration.gliserver_url.toString();
				
				
				greenstoneVersion = remoteGreenstoneServer.getGreenstoneVersion();
				// Display the version to make error reports a lot more useful
				System.err.println("Remote Greenstone server version: " + greenstoneVersion);
				if(greenstoneVersion == -1) { // remote server not running
					Gatherer.exit();
				}
				if(greenstoneVersion >= 3) {
					this.GS3 = true;
					if (!prev_gliserver_url_string.equals(gliserver_url_string)) {
						System.err.println("Using a new GLI server url, clear site and servlet info");
						Configuration.setSiteAndServlet("", "");
					} else {
					    Configuration.prepareForGS3();
					}
				} 
				
				if(fedora_info.isActive()) { 
					// when GS server is remote, FEDORA_HOME resides on the remote server side,
					// but we know the library URL from user-provided information
					library_url_string = fedora_info.getLibraryURL();
				} else {
					library_url_string = remoteGreenstoneServer.getLibraryURL(Configuration.gliserver_url.toString());
				}
				// write it into the config file
				Configuration.setString("general.library_url", true, library_url_string);
			}
			else if(!isWebswing) { // local greenstone: add shutdown hooks to forcibly stop the server on irregular termination
			    // And start up the local library server, if that's what we want
			    /// but we don't need to do this if we are webswing.

			    // The Java virtual machine shuts down in response to two kinds of events:
			    // - The program exits normally, when the last non-daemon thread exits or when the exit (equivalently, System.exit) method is invoked, or
			    // - The virtual machine is terminated in response to a user interrupt, such as typing ^C, or a system-wide event, such as user logoff or system shutdown.
			    // https://coderanch.com/t/328888/java/Killing-process-spawned-Runtime-exec
			    // So here we add a shutdown hook to take care of Ctrl-C situations where GLI is irregularly terminated
			    // Sadly, the shutdownhook never gets called on Windows, whether GS2 or GS3,
			    // when a Ctrl-C is sent to the DOS prompt that launched GLI. Because GLI on Windows
			    // currently waits to respond to a Ctrl-C until AFTER GLI is already (properly) exited
			    Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
				    public void run() {
					
					if(Gatherer.exit != true) { // unexpected termination, such as Ctrl-C won't set Gatherer.exit
					    // so still need to at least issue the call to stop any running GS server
					    
					    System.err.println("ShutDownHook called...");

					    if (GS3) { // issue the ant call to stop any running GS3
						// (tomcat and networked Derby Server) by calling GS3ServerThread.stopServer()
						if(!GS3ServerThread.wasServerLaunchedOutsideGLI()) {
						    System.err.println("Attempting to forcibly terminate the GS server...");
						    GS3ServerThread.stopServer();
						} else {
						    System.err.println("Tomcat was launched outside GLI. Leaving it running...");
						}
					    } else { // issue the call to stop any running GS2 local library server
						if (LocalLibraryServer.isRunning() == true) {
						    System.err.println("Attempting to forcibly terminate the GS server...");
						    LocalLibraryServer.forceStopServer();
						}
					    }
					}
				    }
				}));
					
			    
				if (!GS3) {		
					isLocalLibrary = LocalLibraryServer.start(gsdl_path, local_library_path);				
					
				}
				else { // local GS3, start the local tomcat

				    if(!GS3ServerThread.wasServerLaunchedOutsideGLI()) {
                                       //System.err.println("@@@@ Launching tomcat from GLI");
                                        // configure servlets xml as we need to use this before the restart has taken effect
                                        GS3ServerThread.prepareXML(); // should we check the result?
					GS3ServerThread thread = new GS3ServerThread(gsdl3_src_path, "restart");
					thread.start();
				    }
				    
					// If fedora installed inside this local GS3, then ask for fedora login details now.
					if (fedora_info.isActive()) { 
					    File fedora_webapp = new File(gsdl3_src_path+File.separator+"packages"+File.separator+"tomcat"+File.separator+"webapps"+File.separator+"fedora");
					    
					    if(fedora_webapp.exists()) {
						System.err.println("**** Waiting for the local Greenstone server to start up fedora...");
						popupFedoraInfo();
					    }
					}
				}
				// else web library: GS server is local, but doesn't use the webserver included with GS2
			}
			
			// The "-library_url" option overwrites anything in the config files
			if (library_url_string != null && library_url_string.length() > 0) {
				try {
					System.err.println("Setting library_url to " + library_url_string + "...");
					Configuration.library_url = new URL(library_url_string);
				}
				catch (MalformedURLException error) {
					DebugStream.printStackTrace(error);
				}
			}


			// Check that we now know the Greenstone library URL, since we need this for previewing collections
			// It is not necessary if an independent GSI was launched and the user hasn't pressed Enter Library yet
			DebugStream.println("Configuration.library_url = " + Configuration.library_url);
			if (Configuration.library_url == null) {
				if(isLocalLibrary) {
					if(!LocalLibraryServer.isURLPending()) {
						missingEXEC(null);
					}
					// else LocalLibraryServer is expecting a URL soon, so don't need to display the dialog
				} else { // GS2 webLibrary or GS3 or remote Greenstone
                                  String external_url_string = null;
                                  if (GS3) {
                                    external_url_string = "http://127.0.0.1:8383/greenstone3"; // default
                                    // lets work out the actual url to pass in
                                    if (build_props == null ) {
                                      build_props = loadBuildProperties(gsdl3_src_path);
                                    }
                                    if (build_props != null) {
                                      library_address_props = new LibraryAddressProperties(build_props);
                                    }
                                    
                                    if (library_address_props != null) {
                                      external_url_string = library_address_props.getExternalAddress();
                                    }
                                  }
                                  missingEXEC(external_url_string);
				}
			}

			// The "-gliserver_url" option overwrites anything in the config files
			// why do this? we have already asked the user for gliserver url and set it...
			if (gliserver_url_string != null && gliserver_url_string.length() > 0) {
				try {
					System.err.println("Setting gliserver_url to " + gliserver_url_string + "...");
					Configuration.gliserver_url = new URL(gliserver_url_string);
				}
				catch (MalformedURLException error) {
					DebugStream.printStackTrace(error);
				}
			}

			// If we're using a remote Greenstone we need to know where the gliserver script is
			DebugStream.println("Configuration.gliserver_url = " + Configuration.gliserver_url);

			if (GS3) {
				// Load Greenstone 3 servlet configuration
				if (isGsdlRemote){
                                  servlet_config = new ServletConfiguration(Configuration.gli_user_directory_path, Configuration.gli_user_directory_path);
				}else{
                                  servlet_config= new ServletConfiguration(gsdl3_web_path, gsdl3_writableweb_path);
				}
			

				if (Configuration.servlet_path == null || Configuration.servlet_path.equals("")) {
				    String my_servlet_path = null;
				    if (isGsdlRemote) {
					System.err.println("servlet path is null, get new one from remote server");
					my_servlet_path = remoteGreenstoneServer.getDefaultServletPath().trim();
				    } else {
					// we need to look up build.properties
					System.err.println("servlet path is null, get one from build.properties");
                                        if (build_props == null ) {
                                          build_props = loadBuildProperties(gsdl3_src_path);
                                        }
					
					my_servlet_path = build_props.getProperty("greenstone.default.servlet", "/library");
					
				    }
				    // set site name to match
				    String my_site_name = servlet_config.getSiteForServlet(my_servlet_path);
				    System.err.println("setting site and servlet to " +my_site_name+", "+my_servlet_path);
				    Configuration.setSiteAndServlet(my_site_name, my_servlet_path);
				    
				}
			}
                        
                        if (isWebswing) {
                          Configuration.local_url = new URL(local_tomcat_context_url_string);
                        }
			// have we got a specified collection to open?
			// the no_load flag to GLI is processed at the end of handling open_collection_file_path
			open_collection_file_path = open_collection;
			if (open_collection_file_path == null) {
			    open_collection_file_path = Configuration.getString(
				"general.open_collection"+Configuration.gliPropertyNameSuffix(), true);
			}
			else {
			    // see if it is expressed as a relative filename, in which case, prepend
			    // collect_directory_path
			    File open_collection_file = new File(open_collection_file_path);
			    if (!open_collection_file.isAbsolute()) {
				// dealing with a relative dir
				// => prefix collect_directory_path
				// but first need to work out this out for ourselves, as standard/non-standard collect_dir
				// has not yet been determined

				String resolved_collect_directory_path =
				    (collect_directory_path != null)
				    ? collect_directory_path : getDefaultGSCollectDirectoryPath(false);  // false => without dir sep at end

				open_collection_file_path = resolved_collect_directory_path + File.separator + open_collection_file_path;
			    }
    
			}
			
			if (no_load || (isGsdlRemote && open_collection_file_path.equals(""))) {
				open_collection_file_path = null;
			}
                        if (isWebswing && open_collection_file_path !=null && !open_collection_file_path.equals("")) {
                          // remove /gli.col
                          String coll_path = open_collection_file_path.substring(0, open_collection_file_path.lastIndexOf(File.separator));
                          String coll_name = coll_path.substring(coll_path.lastIndexOf(File.separator)+1);
                          // in case permissions have changed
                          if (!webswingAuthenticator.canEditCollection(coll_name)) {
                            open_collection_file_path = "";
                          }
                        }
			initCollectDirectoryPath();
			// ensure that a directory called 'cache' exists in the GLI user directory  
			File user_cache_dir = new File(Gatherer.getGLIUserCacheDirectoryPath());
			System.err.println("User cache dir: " + Gatherer.getGLIUserCacheDirectoryPath());
			if (!user_cache_dir.exists() && !user_cache_dir.mkdir()) {
				System.err.println("Warning: Unable to make directory: " + user_cache_dir);
			}


			if (Gatherer.isGsdlRemote) {
				DebugStream.println("Not checking for perl path/exe");
			}
			else {
				// Perl path is a little different as it is perfectly ok to
				// start the GLI without providing a perl path
				boolean found_perl = false;
				if (Configuration.perl_path != null) {
					// See if the file pointed to actually exists
					File perl_file = new File(Configuration.perl_path);
					found_perl = perl_file.exists();
					perl_file = null;
				}
				if (Configuration.perl_path == null || !found_perl) {
					// Run test to see if we can run perl as is.
					PerlTest perl_test = new PerlTest();
					if (perl_test.found()) {
						// If so replace the perl path with the system
						// default (or null for unix).
						Configuration.perl_path = perl_test.toString();
						found_perl = true;
					}
				}
				if (!found_perl) {
					// Time for an error message.
					missingPERL();
				}
			}


			// Check for ImageMagick - dependent on perl_path
			if (Gatherer.isGsdlRemote) {
				DebugStream.println("Not checking for ImageMagick.");
			}
			else if (!(new ImageMagickTest()).found()) {
				// Time for a warning message
				missingImageMagick();
			}
			
			// Check for PDFBox
			if (Gatherer.isGsdlRemote) {
				DebugStream.println("Not checking for PDFBox.");
			}
			else {			    
				String gs_dir = gsdl_path; // for GS3, pdf-box is in gs2build/ext (not in GS3/ext), for GS2 it's also in GS2/ext
			    File pdfboxExtensionFolder = new File(gs_dir+File.separator+"ext"+File.separator+"pdf-box");
			    if (!(pdfboxExtensionFolder.exists() && pdfboxExtensionFolder.isDirectory())) {
				// The user doesn't have PDFBox, inform them of it
				String zipExtension = Utility.isWindows() ? "zip" : "tar.gz";
				missingPDFBox(zipExtension, pdfboxExtensionFolder.getParent());
			    }
			}

			// Check that the locale can support multiple filename encodings
			//System.err.println("#### Java identifies current Locale as (file.encoding): "
			//	+ System.getProperty("file.encoding"));
			if(System.getProperty("file.encoding").equals("UTF-8")){
				// If the locale is UTF-8, Java will interpret all filename bytes as UTF-8, 
				// which is a destructive process as it will convert characters not recognised
				// by UTF-8 into the invalid character, rather than preserving the bytecodes.
				// This has the effect that non-UTF8 encoded filenames on a system set to a 
				// UTF-8 locale are not 'seen' by Java (if they contain non-ASCII characters).

			        // This message popping up first thing after a successful GS install is thought
			        // to be unnecessarily alarming. Turning off.
				//multipleFilenameEncodingsNotSupported();
				FilenameEncoding.MULTIPLE_FILENAME_ENCODINGS_SUPPORTED = false; 
				FilenameEncoding.URL_FILE_SEPARATOR = File.separator;
			} else {
				FilenameEncoding.MULTIPLE_FILENAME_ENCODINGS_SUPPORTED = true;
				FilenameEncoding.URL_FILE_SEPARATOR = "/"; // URL file separator is always "/" 
			}
			
			// Set the default font for all Swing components.
			FontUIResource default_font = Configuration.getFont("general.font", true);
			Enumeration keys = UIManager.getDefaults().keys();
			while (keys.hasMoreElements()) {
				Object key = keys.nextElement();
				Object value = UIManager.get(key);
				if (value instanceof FontUIResource) {
					UIManager.put(key, default_font);
				}
			}

			// At this point (which is where this code originally used to be), we can set up the proxy for the
			// non-remote case. The remote Greenstone server would already have setup its proxy when required.
			if(!isGsdlRemote) {
				setProxy();
				// Now we set up an Authenticator
				Authenticator.setDefault(new GAuthenticator());
			}

			// TODO: I feel now it should be invokeAndWait() to force it to happen
			// synchronously, so objects exist during subsequent code after try block
			// Broke GLI
			Gatherer.invokeInEDT_replacesProceedInCurrThread("Gatherer.init() - File/Coll managers", Gatherer.SYNC, new Runnable() {
				public void run() {
				    assoc_man = new FileAssociationManager();
				    // Create File Manager
				    f_man = new FileManager();
				    // Create Collection Manager
				    c_man = new CollectionManager();
				    // Create Recycle Bin
				    recycle_bin = new RecycleBin();
				}
			    });
			
			
			if (GS3) {
				if (site_name==null) {
					site_name = Configuration.site_name;
					servlet_path = null; // need to reset this
				}
				if (servlet_path == null) {
					servlet_path = Configuration.getServletPath();
				}
			}
			
			
		} catch (Exception exception) {
			DebugStream.printStackTrace(exception);
		}

		
		// Create GUI Manager (last) or else suffer the death of a thousand NPE's
		Gatherer.invokeInEDT_replacesProceedInCurrThread("Gatherer.init() - GUImanager", Gatherer.SYNC, new Runnable() {
			public void run() {
			    g_man = new GUIManager(size);
			}
		    });
		
		// Get a list of the core Greenstone classifiers and plugins
		Classifiers.loadClassifiersList(null);
		Plugins.loadPluginsList(null);

		// Users may specify a non-standard collect directory (eg. when running one GLI in a network environment)
		// Set to any custom collection directory provided to gli. This happens if gli was run as:
		// ./gli.sh -collectdir </full/path/to/custom/collect>
		if(collect_directory_path != null) {

		    // create a version of the collect_dir_path without a file-separator at end
		    String collectDir = collect_directory_path; 
		    if(collectDir.endsWith(File.separator)) {
			collectDir = collectDir.substring(0, collectDir.length()-1); // remove file separator at end
		    }

		    // update .gli/config.xml to contain the version of the path without file-separator at end
		    if(collect_directory_path.equals(getDefaultGSCollectDirectoryPath(false))) {
			Configuration.setString("general.open_collection"+Configuration.gliPropertyNameSuffix(), 
						true, "");
		    } else {
			Configuration.setString("general.open_collection"+Configuration.gliPropertyNameSuffix(), 
						true, collectDir);
		    }

		    // set non_standard_collect_directory_path variable. Ensures file_separator at end
		    Gatherer.setCollectDirectoryPath(collect_directory_path); 

		    // Use version of path without file-separator at end to set collecthome in gsdlsite(3).cfg
		    if(collectDir != null) {
			collectDir = "\""+collectDir+"\"";
		    }
		    Utility.updatePropertyConfigFile(getGsdlSiteConfigFile(), "collecthome", collectDir);
		    // if gsdlsite.cfg does not exist (if using server.exe for instance), the above method will just return
		}
		
		// If using a remote Greenstone we need to download the collection configurations now
		if (Gatherer.isGsdlRemote) {
			if (remoteGreenstoneServer.downloadCollectionConfigurations().equals("")) {
				// !! Something went wrong downloading the collection configurations
				System.err.println("Error: Could not download collection configurations.");
				if(!Gatherer.isApplet) { // don't close the browser if it is an applet!
					System.exit(0);
				}
			}
		}
            }

    
	/** Returns the correct version of the (local or remote) Greenstone server if init() has already been called. */
	public static int serverVersionNumber() { 
		return GS3 ? 3 : 2;
	} 

	/** Returns "Server: version number" if init() has already been called. */
	public static String getServerVersionAsString() { 
		return "Server: v" + serverVersionNumber();
	} 

	public void openGUI()
	{
		// Size and place the frame on the screen
		Rectangle bounds = Configuration.getBounds("general.bounds", true);
		if (bounds == null) {
			// Choose a sensible default value
			bounds = new Rectangle(0, 0, 640, 480);
		}
		
		// Ensure width and height are reasonable
		size = bounds.getSize();
		
		if (size.width < 640) {
			size.width = 640;
		}
		else if (size.width > Configuration.screen_size.width && Configuration.screen_size.width > 0) {
			size.width = Configuration.screen_size.width;
		}
		if (size.height < 480) {
			size.height = 480;
		}
		else if (size.height > Configuration.screen_size.height && Configuration.screen_size.height > 0) {
			size.height = Configuration.screen_size.height;
		}
		
		if (!g_man_built) {
		        g_man.setSize(size);		    
			g_man.display();

			// Place the window in the desired location on the screen, if this is do-able (not under most linux window managers apparently. In fact you're lucky if they listen to any of your screen size requests).
			g_man.setLocation(bounds.x, bounds.y);
			g_man.setVisible(true);

			// After the window has been made visible, check that it is in the correct place
			// sometimes java places a window not in the correct place, 
			// but with an offset. If so, we work out what the offset is
			// and change the desired location to take that into account 
			Point location = g_man.getLocation();
			int x_offset = bounds.x - location.x;
			int y_offset = bounds.y - location.y;
			// If not, offset the window to move it into the correct location
			if (x_offset > 0 || y_offset > 0) {
				///ystem.err.println("changing the location to "+(bounds.x + x_offset)+" "+ (bounds.y + y_offset));
				g_man.setLocation(bounds.x + x_offset, bounds.y + y_offset);
			}

			// The 'after-display' triggers several events which don't occur until after the visual components are actually available on screen. Examples of these would be the various html renderings, as they can't happen offscreen.
			g_man.afterDisplay();
			g_man_built = true;
		}
		else {
			g_man.setVisible(true);
		}
		
		if (isWebswing) {
		    WebswingUtil.getWebswingApi().sendActionEvent("setCursor", "default", null);
		}

		// Get a list of the core Greenstone classifiers and plugins
		/*Classifiers.loadClassifiersList(null);
	Plugins.loadPluginsList(null);

	// If using a remote Greenstone we need to download the collection configurations now
	if (Gatherer.isGsdlRemote) {
		if (remoteGreenstoneServer.downloadCollectionConfigurations().equals("")) {
		// !! Something went wrong downloading the collection configurations
		System.err.println("Error: Could not download collection configurations.");
		System.exit(0);
		}
	}*/

		// if we're tutorial testing GLI, want names assigned to the GUI components	
		TestingPreparation.setNamesRecursively(g_man);
		
		
		// If there was a collection left open last time, reopen it	
		if (open_collection_file_path == null || new File(Gatherer.open_collection_file_path).isDirectory()) {
			
			//the menu bar items, file and edit, are disabled from the moment of their creation. if there is no left-over collection from the last session, enable them; otherwise it's disabled until the collection finishes loading. They will be enabled in collectionManager.java
		    setMenuBarEnabled(true);
		} else {

			// If there was a collection left open last time, reopen it
			c_man.openCollectionFromLastTime();
		}
	}

	public static void setMenuBarEnabled(boolean enabled) {
	    Gatherer.invokeInEDT_replacesProceedInCurrThread("Gatherer.init() - enabling menubar", Gatherer.ASYNC, new Runnable() {
		    public void run() {
			g_man.menu_bar.file.setEnabled(enabled);
			g_man.menu_bar.edit.setEnabled(enabled);
		    }
		});
	}

	/** Exits the Gatherer after ensuring that things needing saving are saved.
	* @see java.io.FileOutputStream
	* @see java.io.PrintStream
	* @see java.lang.Exception
	* @see javax.swing.JOptionPane
	* @see org.greenstone.gatherer.Configuration
	* @see org.greenstone.gatherer.collection.CollectionManager
	* @see org.greenstone.gatherer.gui.GUIManager
	*/
	static public void exit(int new_exit_status)
	{
		DebugStream.println("In Gatherer.exit()...");
		exit = true;
		if (new_exit_status != 0) {
			// default exit_status is already 0 
			// only remember a new exit status if it is non-trivial
			exit_status = new_exit_status;
		}

		// Save the file associations
		if (assoc_man != null) {
			assoc_man.save();
			assoc_man = null;
		}

		// Get the gui to deallocate
		if(g_man != null) {
			g_man.destroy();
			g_man_built = false;
		}

		// Flush debug
		DebugStream.closeDebugStream();
		
		// If we started a server, we should try to stop it.
		if (LocalLibraryServer.isRunning() == true) {		
			LocalLibraryServer.stop();
		}

		// If we're using a remote Greenstone server we need to make sure that all jobs have completed first
		if (isGsdlRemote) {
			remoteGreenstoneServer.exit();
		} else if (GS3) { // stop the local tomcat web server when running GS3
		    // can't call ant stop from its own thread - what if GLI has exited by then?
		    // issue call to ant stop from the main GLI thread
		    //GS3ServerThread thread = new GS3ServerThread(Configuration.gsdl_path, "stop");
		    //thread.start();
		    if(!GS3ServerThread.wasServerLaunchedOutsideGLI()) {
			GS3ServerThread.stopServer();
		    } else {
			System.err.println("Tomcat was launched outside GLI. Leaving it running...");
		    }
		    
		}

		// Make sure we haven't started up any processes that are still running
		if (apps.size() == 0) {
		    // If we're running as an applet we don't actually quit (just hide the main GLI window)
		    // Same for when testing mode. Considering System.exit() here stops JUnit
		    // testing, presumably it also terminates the JRE running the GLI which may
		    // be the JRE running the JUnit tests
		    if (!Gatherer.isApplet && !TestingPreparation.TEST_MODE) {
			// This is the end...
			System.exit(exit_status);
		    } else if(TestingPreparation.TEST_MODE) {
			g_man.dispose(); // already set GUIManager to DISPOSE_ON_CLOSE (unless
			// this on-window-closing handler countermands it. We don't want to
			// countermand it for GLI automated testing mode. Calling dispose() here
			// is just to be expliciit.
		    }
		}
		else {
			JOptionPane.showMessageDialog(g_man, Dictionary.get("General.Outstanding_Processes"), Dictionary.get("General.Outstanding_Processes_Title"), JOptionPane.ERROR_MESSAGE);
			g_man.setVisible(false);
		}
	}

	static public void exit()
	{
		exit(0);
	}

	/** Returns the path of the current collect directory. */
	static public String getCollectDirectoryPath()
	{
		if (non_standard_collect_directory_path != null) {
			return non_standard_collect_directory_path;
		}
		
		return getDefaultGSCollectDirectoryPath(true); // file separator appended
		
	}
	
	// if we need to know whether the local server we are running is server.exe vs apache web server
	static public boolean isPersistentServer() {
		return (!isGsdlRemote && LocalLibraryServer.isPersistentServer());
	}

	/** Returns the path of the Greenstone "collect" directory. */
	static public String getDefaultGSCollectDirectoryPath(boolean appendSeparator) {
		String colDir;
		if (GS3) {
			colDir = getSitesDirectoryPath() + Configuration.site_name + File.separator + "collect";			
		}
		else {
			colDir = Configuration.gsdl_path + "collect";
		}
		
		if(appendSeparator) {
			colDir += File.separator;
		}
		return colDir;
	}	

	/** Returns the path of the GLI directory. */
	static public String getGLIDirectoryPath()
	{
		return gli_directory_path;
	}


	/** Returns the path of the GLI "metadata" directory. */
	static public String getGLIMetadataDirectoryPath()
	{
		return getGLIDirectoryPath() + "metadata" + File.separator;
	}


	/** Returns the path of the GLI user directory. */
	static public String getGLIUserDirectoryPath()
	{
		return gli_user_directory_path;
	}


	/** Returns the path of the GLI user "cache" directory. */
	static public String getGLIUserCacheDirectoryPath()
	{
		return getGLIUserDirectoryPath() + "cache" + File.separator;
	}


	/** Returns the path of the GLI user "log" directory. */
	static public String getGLIUserLogDirectoryPath()
	{
		return getGLIUserDirectoryPath() + "log" + File.separator;
	}


	static public String getSitesDirectoryPath()
	{
		return Configuration.gsdl3_web_path + "sites" + File.separator;
	}

  static public String getCurrentSiteDirectoryPath() {
    return getSitesDirectoryPath()+Configuration.site_name+File.separator;
  }
  static public String getCurrentCollectionDirectoryPath() {
    return getCollectDirectoryPath()+File.separator+c_man.getCollection().getName()+File.separator;
  }


	static public void setCollectDirectoryPath(String collect_directory_path)
	{
		non_standard_collect_directory_path = collect_directory_path;
		if (!non_standard_collect_directory_path.endsWith(File.separator)) {
			non_standard_collect_directory_path = non_standard_collect_directory_path + File.separator;
		}
	}


	static public void setGLIDirectoryPath(String gli_directory_path_arg)
	{
		gli_directory_path = gli_directory_path_arg;
	}


	static public void setGLIUserDirectoryPath(String gli_user_directory_path_arg)
	{
		gli_user_directory_path = gli_user_directory_path_arg;

		// Ensure the GLI user directory exists
		File gli_user_directory = new File(gli_user_directory_path);
		if (!gli_user_directory.exists() && !gli_user_directory.mkdirs()) {
			System.err.println("Error: Unable to make directory: " + gli_user_directory);
		}
	}


	public static void initCollectDirectoryPath() {		
		String defaultColdir = getDefaultGSCollectDirectoryPath(false); // no file separator at end
		String coldir = defaultColdir;
		// If local GS and opening a collection outside the standard GS collect folder,
		// need to open the non-standard collect folder that the collection resides in
		if (!isGsdlRemote
		    && !open_collection_file_path.startsWith(defaultColdir))
		{
                  if (isWebswing) {
                    // webswing, not allowed colls outside of current collectdir
                    open_collection_file_path = defaultColdir;
                    setCollectDirectoryPath(defaultColdir);
                  } else {
                    
			File collectFolder = null;
			
			if(!open_collection_file_path.equals("")) {
				if(!open_collection_file_path.endsWith("gli.col")) { // then it's a collect folder
					collectFolder = new File(open_collection_file_path);
				} else {
					// the filepath is a gli.col file. To get the collect folder: the 1st level up
					// is the collection folder, 2 two levels up is the containing collect folder
					collectFolder = new File(open_collection_file_path).getParentFile().getParentFile();
				}	
				
				// Need to deal with colgroups as well: while there's an etc/collect.cfg
				// in the current collectFolder, move one level up
				String cfg_file = (Gatherer.GS3)? Utility.CONFIG_GS3_FILE : Utility.CONFIG_FILE;
				if(new File(collectFolder.getAbsolutePath()+File.separator+cfg_file).exists()) { // colgroup
					collectFolder = collectFolder.getParentFile();		    
				}

				// Inform the user that their collecthome is non-standard (not inside GS installation)
				nonStandardCollectHomeMessage(collectFolder.getAbsolutePath(), defaultColdir); // display message
			}		
			
			if(collectFolder == null || !collectFolder.exists()) {
				// if GLI config file specified no collectDir (open_collection_file_path is "")
				// OR if dealing with a local server but the collectdir no longer exists, 
				// use the default greenstone collect directory, and write that to affected files
				
				open_collection_file_path = defaultColdir;		// default GS collect dir
				// Configuration.setString("general.open_collection"+Configuration.gliPropertyNameSuffix(), true, "");
			} else { // use the coldir value specified in the flags to GLI or from the last GLI session				
				coldir = collectFolder.getAbsolutePath();
			}			
			// set it as the current folder
			setCollectDirectoryPath(coldir); // will ensure the required file separator at end
                  }
                }
		
		if(!isGsdlRemote) {
			// LocalLibraryServer would  already have set glisite.cfg to the correct collecthome for server.exe
			// Here we set collecthome in gsdl(3)site.cfg for the GS2 apache web server and GS3 tomcat server
			String gsdlsitecfg = getGsdlSiteConfigFile();			
			// update the gsdlsite config file and store the old value for use when we exit GLI
			if(coldir.equals(defaultColdir)) {				
				gsdlsite_collecthome = Utility.updatePropertyConfigFile(
				gsdlsitecfg, "collecthome", null);
			} else {
				gsdlsite_collecthome = Utility.updatePropertyConfigFile(
				gsdlsitecfg, "collecthome", "\""+coldir+"\""); // no file separator
				// if gsdlsite.cfg does not exist (if using server.exe for instance), the above method will just return
			}
		}		
	}

	/** depending on the version of GS being run, return the path to the current GS' installation's gsdl(3)site.cfg */
	public static String getGsdlSiteConfigFile() {
		if(Gatherer.GS3) { // web/WEB-INF/cgi/gsdl3site.cfg
			return Configuration.gsdl3_writableweb_path + File.separator + "WEB-INF" 
			+ File.separator + "cgi" + File.separator + "gsdl3site.cfg";
		} else { // cgi-bin/gsdlsite.cfg
		    String gsdlarch = System.getenv("GSDLARCH");
		    if(gsdlarch == null) {
			gsdlarch = "";
		    }
		    return Configuration.gsdl_path /* + File.separator */
			+ "cgi-bin" + File.separator + client_operating_system+gsdlarch + File.separator + "gsdlsite.cfg";
		}
	}

	public static void collectDirectoryHasChanged(
	String oldCollectPath, String newCollectPath, final Component container) 
	{		
		if(oldCollectPath.equals(newCollectPath)) {
			return; // nothing to be done
		}
		
		// Will use a busy cursor if the process of changing the collect directory takes more 
		// than half a second/500ms. See http://www.catalysoft.com/articles/busyCursor.html
		Cursor originalCursor = container.getCursor();
		java.util.TimerTask timerTask = new java.util.TimerTask() {
			public void run() {
				// set the cursor on the container:
				container.setCursor(new Cursor(Cursor.WAIT_CURSOR));
			}
		};
		java.util.Timer timer = new java.util.Timer(); 
		
		try {
			timer.schedule(timerTask, 500);
			
			// first save any open collection in the old location, then close it
			if(Gatherer.c_man.getCollection() != null) {
				Gatherer.g_man.saveThenCloseCurrentCollection(); // close the current collection first
			}
			
			// change to new collect path
			if(newCollectPath.equals(getDefaultGSCollectDirectoryPath(true))) {
				Configuration.setString("general.open_collection"+Configuration.gliPropertyNameSuffix(), 
				true, "");
			} else {
				Configuration.setString("general.open_collection"+Configuration.gliPropertyNameSuffix(), 
				true, newCollectPath);
			}
			Gatherer.setCollectDirectoryPath(newCollectPath);
			

			// refresh the Documents in Greenstone Collections
			//WorkspaceTreeModel.refreshGreenstoneCollectionsNode();
			Gatherer.g_man.refreshWorkspaceTreeGreenstoneCollections();
			
			// The web server needs to be told where a new (non-standard) collecthome home is.
			// The web server reads collecthome from cgi-bin/<OS>/gsdlsite.cfg, where the property
			// collecthome can be specified if a non-standard collecthome is to be used. If no 
			// such property is specified in the file, then it assumes the standard GS collecthome.
			// This method does nothing for a remote Greenstone.	
			if(Gatherer.isGsdlRemote) {
				return;
			}	
			
			// non-destructive update of gsdl(3)site.cfg (comments preserved)
			String collectDir = Gatherer.getCollectDirectoryPath();
			//collectDir = "\"" + collectDir.substring(0, collectDir.length()-1) + "\""; // remove file separator at end	
			collectDir = collectDir.substring(0, collectDir.length()-1); // remove file separator at end
			if(collectDir != null) {
				collectDir = "\""+collectDir+"\"";
			}
			Utility.updatePropertyConfigFile(getGsdlSiteConfigFile(), "collecthome", collectDir);
			// if gsdlsite.cfg does not exist (if using server.exe for instance), the above method will just return
			
			if(!Gatherer.GS3 && Gatherer.isLocalLibrary) {
				// for Images in the collection to work, the apache web server 
				// configuration's COLLECTHOME should be updated on collectdir change. 
				// Does nothing for server.exe at the moment

				LocalLibraryServer.reconfigure();
			}
		} finally { // Note try-finally section without catch:
			// "Java's finally clause is guaranteed to be executed even when
			// an exception is thrown and not caught in the current scope."
			// See http://www.catalysoft.com/articles/busyCursor.html
			// the following code fragment is guaranteed to restore the original
			// cursor now the custom actionPerformed() processing is complete, regardless
			// of whether the processing method terminates normally or throws an exception
			// and regardless of where in the call stack the exception is caught.			
			
			timer.cancel();
			container.setCursor(originalCursor);
		}
	}


	static public void refresh(int refresh_reason)
	{
		if (g_man != null) {

			g_man.refresh(refresh_reason, c_man.ready());
		}

		// Now is a good time to force a garbage collect
		System.gc();
	}


	// used to send reload coll messages to the tomcat server
	static public void configGS3Server(String site, String command) {

	    // Do not do configGS3Server for a GS3 solr collection. Not at present at least, when we're stopping
	    // and starting the GS3 server from GLI for solr cols, since this function clears the solr.xml file.
	    /*if(Gatherer.c_man.isSolrCollection()) {
		return;
		}*/

		if (Configuration.library_url == null){
			System.err.println("Error: you have not provided the Greenstone Library address.");  
			return;

		} 
		
		try {
			String full_context_url = null;
		    
			if (!isWebswing) {
			    full_context_url = Configuration.library_url.toString();
			}
			else {
                          // webswing, we use the localhost version
                          full_context_url = Configuration.local_url.toString();
			}

                        // should start with  / eg /library
			String library_servlet_name = Configuration.getServletPath();

			String raw_url = full_context_url + library_servlet_name + command;						
			
			URL url = new URL(raw_url);
			DebugStream.println("Action: " + raw_url);
			
			HttpURLConnection library_connection = (HttpURLConnection) url.openConnection();
			int response_code = library_connection.getResponseCode();
			if(HttpURLConnection.HTTP_OK <= response_code && response_code < HttpURLConnection.HTTP_MULT_CHOICE) {
				DebugStream.println("200 - Complete.");
			}
			else {
				DebugStream.println("404 - Failed.");
			}
			url = null;
		}
		catch(java.net.ConnectException connectException) {
			JOptionPane.showMessageDialog(g_man, Dictionary.get("Preferences.Connection.Library_Path_Connection_Failure", Configuration.library_url.toString()), Dictionary.get("General.Warning"), JOptionPane.WARNING_MESSAGE);
			DebugStream.println(connectException.getMessage());
		}
		catch (Exception exception) {            
			DebugStream.printStackTrace(exception);
		}
	}


	/** Used to 'spawn' a new child application when a file is double clicked.
	* @param file The file to open 
	* @see org.greenstone.gatherer.Gatherer.ExternalApplication
	*/
	static public void spawnApplication(File file) {
		String [] commands = assoc_man.getCommand(file);
		if(commands != null) {
			ExternalApplication app = new ExternalApplication(commands);
			apps.add(app);
			app.start();
		}
		else {
			///ystem.err.println("No open command available.");
		}
	}


	static public void spawnApplication(String command)
	{
		ExternalApplication app = new ExternalApplication(command);
		apps.add(app);
		app.start();
	}

	static public void spawnApplication(String command, String ID)
	{
		ExternalApplication app = new ExternalApplication(command, ID);
		apps.add(app);
		app.start();
	}

	static public void spawnApplication(String[] commands, String ID)
	{
		ExternalApplication app = new ExternalApplication(commands, ID);
		apps.add(app);
		app.start();
	}

	static public void terminateApplication(String ID) {
		for(int i = 0; i < apps.size(); i++) {
			ExternalApplication app = (ExternalApplication)apps.get(i);
			if(app.getID() != null && app.getID().equals(ID)) {
				app.stopExternalApplication();
				apps.remove(app);
			}
		}
	}


	/** Used to 'spawn' a new  browser application or reset an existing one when the preview button is clicked 
	* @param url The url to open the browser at
	* @see org.greenstone.gatherer.Gatherer.BrowserApplication
	*/    
	static public void spawnBrowser(String url) {
		String command = assoc_man.getBrowserCommand(url);
		if (command != null) {
			BrowserApplication app = new BrowserApplication(command, url);
			apps.add(app);
			app.start();
		}
		else {
			///ystem.err.println("No browser command available."); 
		}
	}


	/** Prints a warning message about a missing library path, which means the final collection cannot be previewed in the Gatherer.
	*/
	static public void missingEXEC(String default_url) {
	    try {
	    SwingUtilities.invokeAndWait(new Runnable() {
	      public void run() {
		WarningDialog dialog;
		String configPropertyName = "general.library_url"+Configuration.gliPropertyNameSuffix();

		if (GS3) {
			// Warning dialog with no cancel button and no "turn off warning" checkbox 
			dialog = new WarningDialog("warning.MissingEXEC", Dictionary.get("MissingEXEC_GS3.Title"), Dictionary.get("MissingEXEC_GS3.Message"), configPropertyName, false, false);
		} else { // local case
			dialog = new WarningDialog("warning.MissingEXEC", Dictionary.get("MissingEXEC.Title"), Dictionary.get("MissingEXEC.Message"), configPropertyName, false);
		}

		JTextField field = new URLField.Text(Configuration.getColor("coloring.editable_foreground", false), Configuration.getColor("coloring.editable_background", false));
                if (default_url !=null) {
                  field.setText(default_url);
                  field.selectAll();
                }
		dialog.setValueField(field);
		dialog.display();
		dialog.dispose();
		dialog = null;

		String library_url_string = Configuration.getString(configPropertyName, true);
		if (!library_url_string.equals("")) {
			try {
				// WarningDialog does not allow invalid URLs, so the following is ignored:
				// make sure the URL the user provided contains the http:// prefix
				// and then save the corrected URL
				if(!library_url_string.startsWith("http://") 
						&& !library_url_string.startsWith("https://")) {
					library_url_string = "http://"+library_url_string;
					Configuration.setString(configPropertyName, true, library_url_string); 
				}
				Configuration.library_url = new URL(library_url_string);
			}
			catch (MalformedURLException exception) {
				DebugStream.printStackTrace(exception);
			}
		}
	      }
	    });
	  } catch(Exception e) { //InterruptedException or InvocationTargetException
	      e.printStackTrace();
	  }
	}

    static private Properties loadBuildProperties(String gsdl3_src_path) {

	File buildPropsFile = new File(gsdl3_src_path + File.separator + "build.properties");
	Properties props = null;
	if(buildPropsFile.exists()) {
	    props = new Properties();
	    try{
		props.load(new FileInputStream(buildPropsFile));
	    } catch (Exception e) {
		System.err.println("couldn't load properties "+e.toString());
		props = null;
	    }
	}
	return props;
    }

	/** Prints a warning message about a missing library path, which means the final collection cannot be previewed in the Gatherer.
	*/
	static private void popupFedoraInfo() {
	    try {
	    SwingUtilities.invokeAndWait(new Runnable() {
	      public void run() {
		FedoraLogin dialog = new FedoraLogin("Fedora Login", false);

		if (Configuration.library_url == null) {
			
			String library_url_string = dialog.getLibraryURL();
			if (!library_url_string.equals("")) {
				try {
					Configuration.library_url = new URL(library_url_string);
				}
				catch (MalformedURLException exception) {
					DebugStream.printStackTrace(exception);
				}
			}
		}
		
		boolean showLogin = true;
		do {
			if(!dialog.loginRequested()) { // user pressed cancel to exit the FedoraLogin dialog
				System.exit(0);
			} else {
				showLogin = dialog.loginRequested();
				String hostname = dialog.getHostname();
				String port     = dialog.getPort();
				String username = dialog.getUsername();
				String password = dialog.getPassword();
				String protocol = dialog.getProtocol();
				
				Configuration.fedora_info.setHostname(hostname);
				Configuration.fedora_info.setPort(port);
				Configuration.fedora_info.setUsername(username);
				Configuration.fedora_info.setPassword(password);
				Configuration.fedora_info.setProtocol(protocol);
				
				String ping_url_str = protocol + "://" + hostname + ":" + port + "/fedora";
				String login_str = username + ":" + password;
				
				String login_encoding = Base64.encodeBytes(login_str.getBytes());
				
				try {
					URL ping_url = new URL(ping_url_str);
					URLConnection uc = ping_url.openConnection();
					uc.setRequestProperty  ("Authorization", "Basic " + login_encoding);
					// Attempt to access some content ...
					InputStream content = (InputStream)uc.getInputStream();
					
					// if no exception occurred in the above, we would have come here:
					showLogin = false;
					dialog.dispose();
				}
				catch (Exception exception) {
					// TODO: move into dictionary
					String[] errorMessage = {"Failed to connect to the Fedora server.", "It might not be running, or",
						"incorrect username and/or password."};
					dialog.setErrorMessage(errorMessage);
					//DebugStream.printStackTrace(exception);
					// exception occurred, show the dialog again (do this after printing to
					// debugStream, else the above does not get done for some reason).
					dialog.setVisible(true);
				}
			} 
		} while(showLogin);

		dialog = null; // no more need of the dialog
		
		// Now we are connected.
	      }
	    });
	  } catch(Exception e) { //InterruptedException or InvocationTargetException
	      e.printStackTrace();
	  }	
	}



	static private void requestGLIServerURL()
	{
	    try {
	    SwingUtilities.invokeAndWait(new Runnable() {
	      public void run() {
		WarningDialog dialog;
		String[] defaultURLs = {
			"http://localhost:8383/greenstone3/cgi-bin/gliserver.pl", 
			"http://localhost:8080/gsdl/cgi-bin/gliserver.pl"
		};

		// Warning dialog with no cancel button and no "turn off warning" checkbox 
		// (since user-input of the gliserver script is mandatory)
		dialog = new WarningDialog("warning.MissingGLIServer", Dictionary.get("MissingGLIServer.Title"), Dictionary.get("MissingGLIServer.Message"), "general.gliserver_url", false, false); 

		dialog.setValueField(new URLField.DropDown(Configuration.getColor("coloring.editable_foreground", false), 
		Configuration.getColor("coloring.editable_background", false), 
		defaultURLs, "general.gliserver_url", 
		"general.open_collection"+Configuration.gliPropertyNameSuffix(), 
		"gliserver.pl")); 

		if (Gatherer.default_gliserver_url!=null){
			dialog.setValueField(Gatherer.default_gliserver_url.toString());
		}

		// A WarningDialog cannot always be made to respond (let alone to exit the program) on close. We
		// handle the response of this particular WarningDialog here: a URL for gliserver.pl is a crucial
		// piece of user-provided data. Therefore, if no URL was entered for gliserver.pl, it'll exit safely.
		dialog.addWindowListener(new WindowAdapter() {
			public void windowClosing(WindowEvent e) {
				Gatherer.exit();
			}
		});
		
		dialog.display();
		dialog.dispose();
		dialog = null;

		
		String gliserver_url_string = Configuration.getString("general.gliserver_url", true);
		if (!gliserver_url_string.equals("")) {
			try {
				Configuration.gliserver_url = new URL(gliserver_url_string);
				Configuration.setString("general.gliserver_url", true, gliserver_url_string);
			}
			catch (MalformedURLException exception) {
				DebugStream.printStackTrace(exception);
			}
		}
	      }
	    });
	  } catch(Exception e) { //InterruptedException or InvocationTargetException
	      e.printStackTrace();
	  }
	}


	/** Prints a warning message about a missing GSDL path, which although not fatal pretty much ensures nothing will work properly in the GLI.
	*/
	static private void missingGSDL() {
	    try {
	    SwingUtilities.invokeAndWait(new Runnable() {
	      public void run() {
		WarningDialog dialog = new WarningDialog("warning.MissingGSDL", Dictionary.get("MissingGSDL.Title"), Dictionary.get("MissingGSDL.Message"), null, false);
		dialog.display();
		dialog.dispose();
		dialog = null;
	      }
	    });
	  } catch(Exception e) { //InterruptedException or InvocationTargetException
	      e.printStackTrace();
	  }
	}

	/** Prints a warning message about missing a valid ImageMagick path, which although not fatal means building image collections won't work */
	static private void missingImageMagick() {
	    try {
	    SwingUtilities.invokeAndWait(new Runnable() {
	      public void run() {
		WarningDialog dialog = new WarningDialog("warning.MissingImageMagick", Dictionary.get("MissingImageMagick.Title"), Dictionary.get("MissingImageMagick.Message"), null, false);
		dialog.display();
		dialog.dispose();
		dialog = null;
	      }
	    });
	  } catch(Exception e) { //InterruptedException or InvocationTargetException
	      e.printStackTrace();
	  }
	}

    	/** Prints a message informing the user where they can get PDFBox from to process PDF files of v1.5 and greater */
    static private void missingPDFBox(final String zipExtension, final String extFolder) {
     try {
      SwingUtilities.invokeAndWait(new Runnable() {
       public void run() {

	// point to the correct version of the PDFBox extension for this Greenstone release
	String releaseTag = "";
	if(!PROGRAM_VERSION.equals("trunk")) { // assume it's a release version
	    releaseTag = "main/tags/"+PROGRAM_VERSION+"/";
	}

	WarningDialog dialog = new WarningDialog("warning.MissingPDFBox", Dictionary.get("MissingPDFBox.Title"), Dictionary.get("MissingPDFBox.Message", new String[]{releaseTag, zipExtension, extFolder}), null, false);
		dialog.display();
		dialog.dispose();
		dialog = null;
	      }
	    });
	  } catch(Exception e) { //InterruptedException or InvocationTargetException
	      e.printStackTrace();
	  }		
	}

	/** Prints a warning message about missing a valid PERL path, which although not fatal pretty much ensures no collection creation/building will work properly in the GLI. */
	static private void missingPERL() {
	    try {
	    SwingUtilities.invokeAndWait(new Runnable() {
	      public void run() {
		WarningDialog dialog = new WarningDialog("warning.MissingPERL", Dictionary.get("MissingPERL.Title"), Dictionary.get("MissingPERL.Message"), null, false);
		dialog.display();
		dialog.dispose();
		dialog = null;
	      }
	    });
	  } catch(Exception e) { //InterruptedException or InvocationTargetException
	      e.printStackTrace();
	  }
	}

    /** Prints a message informing the user that their collecthome is non-standard (not inside GS installation) */
    static private void nonStandardCollectHomeMessage(final String open_collection_file_path, final String defaultColDir) {
     try {
      SwingUtilities.invokeAndWait(new Runnable() {
       public void run() {
	WarningDialog dialog = new WarningDialog("warning.NonStandardCollectHome", Dictionary.get("NonStandardCollectHome.Title"), Dictionary.get("NonStandardCollectHome.Message", new String[]{open_collection_file_path, defaultColDir}), null, false);
		dialog.display();
		dialog.dispose();
		dialog = null;
	      }
	    });
	  } catch(Exception e) { //InterruptedException or InvocationTargetException
	      e.printStackTrace();
	  }
	}

	/** Prints a warning message about the OS not supporting multiple filename encodings.  */
	static private void multipleFilenameEncodingsNotSupported() {
	    try {
	    SwingUtilities.invokeAndWait(new Runnable() {
	      public void run() {
		WarningDialog dialog = new WarningDialog("warning.NoEncodingSupport", 
		Dictionary.get("NoEncodingSupport.Title"), 
		Dictionary.get("NoEncodingSupport.Message"), null, false);
		dialog.display();
		dialog.dispose();
		dialog = null;
	      }
	    });
	  } catch(Exception e) { //InterruptedException or InvocationTargetException
	      e.printStackTrace();
	  }
	}

	/** Sets up the proxy connection by setting JVM Environment flags and creating a new Authenticator.
	* @see java.lang.Exception
	* @see java.lang.System
	* @see java.net.Authenticator
	* @see org.greenstone.gatherer.Configuration
	* @see org.greenstone.gatherer.GAuthenticator
	*/
	static public void setProxy() {
		try {// Can throw several exceptions
		    boolean use_proxy = Configuration.get("general.use_proxy", true);
		    
		    setProxyForProtocol("http", use_proxy);
		    setProxyForProtocol("https", use_proxy);
		    setProxyForProtocol("ftp", use_proxy);
			  
		} catch (Exception error) {
			DebugStream.println("Error in Gatherer.initProxy(): " + error);
			DebugStream.printStackTrace(error);
		}
	}

    private static void setProxyForProtocol(String protocol, boolean use_proxy) throws Exception {
	// These are Java properties, therefore see
	// https://docs.oracle.com/javase/7/docs/api/java/net/doc-files/net-properties.html
	// https://docs.oracle.com/javase/8/docs/technotes/guides/net/proxies.html
	// https://stackoverflow.com/questions/14243590/proxy-settings-in-java	

	// Just need to warn the user that we're overriding proxy settings using custom defined proxy settings
	// and returning to using the original proxy settings when custom defined proxy settings are turned off
	// We don't actually need to store/restore the original settings, as Java system vars like http.proxyHost
	// http.proxyPort don't seem to be set even if on GLI startup the env vars like http(s)_proxy were already set.

	
	if(use_proxy) {
	    if(System.getenv(protocol+"_proxy") != null) {
		System.err.println("Overriding original "+protocol+" proxy settings");
	    }
	    
	    // set the custom proxy defined through GLI for this protocol
	    System.setProperty(protocol+".proxyHost", Configuration.getString("general."+protocol.toUpperCase()+"_proxy_host", true));
	    System.setProperty(protocol+".proxyPort", Configuration.getString("general."+protocol.toUpperCase()+"_proxy_port", true));

	    // Not sure what proxyType is. And proxySet ceased to exist since JDK 6 or before. See links above.
	    // But we used to set them both for HTTP before, so still doing so for proxyType, since it may or may not exist
	    // But proxySet doesn't exist anymore, so not continuing with that.
	    if(protocol.equals("http")) {
		System.setProperty(protocol+".proxyType", "4");
		//System.setProperty(protocol+".proxySet", "true");
	    }
	    
	}

	else { // use_proxy=false, not using proxy defined through GLI, so
	    // either unset proxy vars for this protocol, or restore any original settings for them

	    if(System.getenv(protocol+"_proxy") != null) {
		System.err.println("Restoring original "+protocol+" proxy settings");
	    }
	    
	    System.setProperty(protocol+".proxyHost", "");
	    System.setProperty(protocol+".proxyPort", "");
	    //if(protocol.equals("http")) {
		//System.setProperty(protocol+".proxySet", "false");
	    //}	     
	}
    }
    
	
	/** This private class contains an instance of an external application running within a JVM shell. It is important that this process sits in its own thread, but its more important that when we exit the Gatherer we don't actually System.exit(0) the Gatherer object until the user has voluntarily ended all of these child processes. Otherwise when we quit the Gatherer any changes the users may have made in external programs will be lost and the child processes are automatically deallocated. */
	static private class ExternalApplication
	extends Thread {
		private SafeProcess process = null;
		/** The initial command string given to this sub-process. */
		private String command = null;
		private String[] commands = null;

		private String ID = null;

		/** Constructor.
	* @param command The initial command <strong>String</strong>.
	*/
		public ExternalApplication(String command) {
			this.command = command;
		}

		public ExternalApplication(String[] commands) {
			this.commands = commands;
		}

		public ExternalApplication(String command, String ID) {
			this.command = command;
			this.ID = ID;
		}

		public ExternalApplication(String[] commands, String ID) {
			this.commands = commands;
			this.ID = ID;
		}
		
		public String getID() { 
			return ID; 
		}

		/** We start the child process inside a new thread so it doesn't block the rest of Gatherer.
	* @see java.lang.Exception
	* @see java.lang.Process
	* @see java.lang.Runtime
	* @see java.lang.System
	* @see java.util.Vector
	*/
		public void run() {
			// Call an external process using the args.
			try {
				if(commands != null) {
					StringBuffer whole_command = new StringBuffer();
					for(int i = 0; i < commands.length; i++) {						
						// get rid of any quotes around parameters in file associations
						if(commands[i].startsWith("\"") || commands[i].startsWith("\'")) {
						    commands[i] = commands[i].substring(1);
						}
						if(commands[i].endsWith("\"") || commands[i].endsWith("\'")) {
						    commands[i] = commands[i].substring(0, commands[i].length()-1);
						}

						if (i>0) {
						    whole_command.append(" ");
						}
						whole_command.append(commands[i]);		    
					}
					DebugStream.println("Running " + whole_command.toString());
					process = new SafeProcess(commands);
				}
				else {
					DebugStream.println("Running " + command);
					process = new SafeProcess(command);
				}
				process.runProcess();
			}
			catch (Exception exception) {
				DebugStream.printStackTrace(exception);
			}
			// Remove ourself from Gatherer list of threads.
			apps.remove(this);
			// Call exit if we were the last outstanding child process thread.
			if (apps.size() == 0 && exit == true) {
				// In my opinion (DB) there is no need to exit here,
				// the 'run' method ending naturally brings this
				// thread to an end.  In fact it is potentially
				// dangerous to exit here, as the main thread in the
				// Gatherer class may be stopped prematurely.  As it so
				// happens the point at which the ExternalApplication thread
				// is asked to stop (Back in the main Gatherer thread) is after
				// various configuration files have been saved.
				// 
				// A similar argument holds for BrowserApplication thread below.
				System.exit(exit_status);
			}
		}
		public void stopExternalApplication() {
			if(process != null) {
			    SafeProcess.log("*** stopExternalApplication called.");
			    process.cancelRunningProcess();
			}
		}
	}
	/** This private class contains an instance of an external application running within a JVM shell. It is important that this process sits in its own thread, but its more important that when we exit the Gatherer we don't actually System.exit(0) the Gatherer object until the user has volunteerily ended all of these child processes. Otherwise when we quit the Gatherer any changes the users may have made in external programs will be lost and the child processes are automatically deallocated. */
	static private class BrowserApplication
	extends Thread {
		private SafeProcess process = null;
		/** The initial command string given to this sub-process. */
		private String command = null;
		private String url = null;
		private String[] commands = null;

		public BrowserApplication(String command, String url) {
			StringTokenizer st = new StringTokenizer(command);
			int num_tokens = st.countTokens();
			this.commands = new String [num_tokens];
			int i=0;
			while (st.hasMoreTokens()) {
				commands[i] = st.nextToken();
				i++;
			}
			//this.commands = commands;
			this.url = url;
		}
		/** We start the child process inside a new thread so it doesn't block the rest of Gatherer.
	* @see java.lang.Exception
	* @see java.lang.Process
	* @see java.lang.Runtime
	* @see java.lang.System
	* @see java.util.Vector
	*/
		public void run() {
			// Call an external process using the args.
			if(commands == null) {
				apps.remove(this);
				return;
			}
			try {
				String prog_name = commands[0];
				String lower_name = prog_name.toLowerCase();
				if (lower_name.indexOf("mozilla") != -1 || lower_name.indexOf("netscape") != -1) { 
					DebugStream.println("found mozilla or netscape, trying remote it");
					// mozilla and netscape, try using a remote command to get things in the same window
					String [] new_commands = new String[] {prog_name, "-raise", "-remote", "openURL("+url+",new-tab)"};
					printArray(new_commands);

					process = new SafeProcess(new_commands);
					int exitCode = process.runProcess();
					if (exitCode != 0) { // if Netscape or mozilla was not open
						DebugStream.println("couldn't do remote, trying original command");
						printArray(commands);
						process = null;
						process = new SafeProcess(commands); // try the original command
						process.runProcess();
					}
				} else {
					// just run what we have been given
					StringBuffer whole_command = new StringBuffer();
					for(int i = 0; i < commands.length; i++) {
						whole_command.append(commands[i]);
						whole_command.append(" ");
					}
					DebugStream.println("Running " + whole_command.toString());
					process = new SafeProcess(commands);
					process.runProcess();
				}
			}
			
			catch (Exception exception) {
				DebugStream.printStackTrace(exception);
			}
			// Remove ourself from Gatherer list of threads.
			apps.remove(this);
			// Call exit if we were the last outstanding child process thread.
			if (apps.size() == 0 && exit == true) {
				System.exit(exit_status);
			}
		}
		public void printArray(String [] array) {
			for(int i = 0; i < array.length; i++) {
				DebugStream.print(array[i]+" ");
				System.err.println(array[i]+" ");
			}
		}
		public void stopBrowserApplication() {
			if(process != null) {
			    SafeProcess.log("*** stopBrowserApplication called.");
			    process.cancelRunningProcess();
			}
		}
	}


	private class ImageMagickTest
	{
		public boolean found()
		{
		    // at this stage, GLI has already sourced setup.bash, and the necessary
		    // env variables will be available to the perl process we're about to launch
		    boolean found = false;

		    
			    // run the command `/path/to/perl -S gs-magick.pl identify -version`
			    ArrayList cmd_list = new ArrayList();
			    if (!Gatherer.isGsdlRemote) {
				if(Configuration.perl_path != null) {
				    cmd_list.add(Configuration.perl_path);
				} else {
				    System.err.println("Warning: ImageMagickTest::found() perl_path not set, calling 'perl' instead.");
				    cmd_list.add("perl");
				}
				cmd_list.add("-S");
			    }
			    cmd_list.add("gs-magick.pl");
			    if(Utility.isWindows()) {
				cmd_list.add("identify.exe");
			    } else {
				cmd_list.add("identify");
			    }
			    cmd_list.add("-version");

			    String[] command_parts = (String[]) cmd_list.toArray(new String[0]);

			    String cmd_str = "";
			    for(int i = 0; i < command_parts.length; i++) {
			    	cmd_str += command_parts[i] + " ";
			    }
			    DebugStream.println("***** Running ImageMagickTest command: " + cmd_str);
			   
			    SafeProcess image_magick_process = new SafeProcess(command_parts);
			    int exitValue = image_magick_process.runProcess(); // default process iostream handling
			    //new way of detection of ImageMagick
			    // Inspect the standard output of the process and seach for two particular occurrences: Version and ImageMagick.
			    String output = image_magick_process.getStdOutput().toLowerCase();
			    if (output.indexOf("version") != -1 || output.indexOf("imagemagick") != -1) {
				found = true;
			    } // else found var remains false	
			    
			    return found;
			    //return (image_magick_process.exitValue() == 0);
			    
		}
	}


	private class PerlTest
	{
		private String[] command = new String[2];

		public PerlTest()
		{
			command[0] = (Utility.isWindows() ? Utility.PERL_EXECUTABLE_WINDOWS : Utility.PERL_EXECUTABLE_UNIX);
			command[1] = "-version";
		}

		public boolean found()
		{
			try {
			    SafeProcess perl_process = new SafeProcess(command);
			    int exitValue = perl_process.runBasicProcess();
			    return (exitValue == 0);
			}
			catch (Exception exception) {
				return false;
			}
		}

		public String toString() {
			return command[0];
		}
	}

    // Tests if a commandline program is installed
    static public class ProgramInstalledTest implements SafeProcess.ExceptionHandler
    {
	private final String[] command;
	private boolean returnValue = false;

	public ProgramInstalledTest(String commandOnlyNoArgs) {
	    this.command = new String[]{ commandOnlyNoArgs, "--version" }; // --help also works for linux open commands   
	}
	
	public ProgramInstalledTest(String commandOnlyNoArgs, String basicTestParam) {
	    this.command = new String[]{ commandOnlyNoArgs, basicTestParam };
	}
	
	public boolean found()
	{
	    try {
		SafeProcess file_open_test_process = new SafeProcess(command);
		file_open_test_process.setExceptionHandler(this);
		int exitValue = file_open_test_process.runBasicProcess();

		// returns 0 if program installed and correct params passed in
		// like --version/--help. Mostly returns 1 if wrong params passed in/launched wrong
		// Both return values indicate the program is installed.
		// e.g java --help or --version are wrong, as -help and -version are expected by java command.
		// 127 is returned if program not installed		
		this.returnValue = (exitValue == 0 || exitValue == 1); 
	    }
	    catch (Exception exception) {
		this.returnValue = false;
	    }


	    // returnValue could have been set by an exception during SafeProcess.runBasicProcess()
	    // (see gotException()) so the returnValue is only being returned here at end of function
	    return this.returnValue;
	}


	public void gotException(Exception e) {
	    this.returnValue = false; // default program not found,
	    // or not working which works out the same for my purposes
	    
	    if (!(e instanceof IOException)) {
		SafeProcess.log("Gatherer.ProgramInstalledTest: got exception" + e.getMessage(), e);
	    }
	}
	
	public String toString() {
	    return command[0];
	}
    }
    
	static public class WebswingAuthenticator
	    extends GAuthenticator
	{
	    static protected String username = null;
	    static protected HashSet<String> groups;
	    static protected String userJSessionID;
	    static protected String rawgroups;

	    protected void displayError(String error_message) {
		WarningDialog dialog = new WarningDialog("warning.AuthenticationError", Dictionary.get("WebswingAuthenticationError.Title"), error_message, null, false, false); 

		dialog.display();
		dialog.dispose();
		dialog = null;
	    }

	    public WebswingAuthenticator() {}

	    public WebswingAuthenticator(String username, String usergroups, String sessionID) {
		this.username = username;
		this.rawgroups = usergroups;
		this.userJSessionID = sessionID;
	    }

	    public String getUsername() {
		return username;
	    }
	    
	    public String doRequest(String new_url, boolean forSession) {
		String result;
		try {
		    URL authenticationURL = new URL(new_url);
		    HttpURLConnection conn = (HttpURLConnection)authenticationURL.openConnection();
		    if(forSession) {
			conn.setRequestProperty("Cookie", "JSESSIONID="+this.userJSessionID);
		    }
		    
		    BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
		    result = "";
		    String line = null;
		    
		    while((line = reader.readLine()) != null) {
			result += line;
		    }
		} catch (Exception e) {
		    System.err.println("There was an exception "+e.getMessage());
		    displayError("There was an exception "+e.getMessage());
		    return null;
		}
		// Parse out the content nested inside <div ... id="gs_content"> </div>
		int start = result.indexOf("id=\"gs_content\"");
		if(start != -1) {
			start = result.indexOf(">", start);
			int end = result.indexOf("<", start);
			result = result.substring(start+1, end);
			result = result.trim();
		}		
		if (result.startsWith("Authentication failed:")) {
		    System.err.println("Authentication Error: "+result);
		    displayError(result.replaceAll("&apos;", "'"));
		    return null;
		}

		return result;
	    }

	    
	    public boolean authenticate(String library_url_string) {
		String result = null;
		boolean authenticated = false;
		if(username != null) {
		    String new_url = library_url_string+"?a=s&sa=get-groups-from-session&excerptid=gs_content&un="+username;
		    
		    result = doRequest(new_url, true);
		    if(result != null) {
			authenticated = true;
		    }
		    
		}
		if(!authenticated) {
                  PasswordAuthentication pa = getPasswordAuthentication();
                  if (pa == null) {
		    // user cancelled 
		    System.err.println("Authentication cancelled.");
		    displayError(Dictionary.get("WebswingAuthenticationError.Cancelled"));
		    return false;
                    
                  }
                  username = pa.getUserName();
		
                  String password = new String(pa.getPassword());
                  String new_url = library_url_string+"?a=s&sa=authenticated-ping&excerptid=gs_content&un="+username+"&pw="+password;
                  result = doRequest(new_url, false);		
                  
                  if(result == null) {
		    return false;
                  }
		}

		groups = new HashSet<String>();
		String[] contents = result.split(",");
		for (int i=0; i<contents.length; i++) {
		    String g = contents[i];
                    // add any valid authenticating groups
                    if (g.equals("administrator") || g.equals("all-collections-editor") || g.equals("personal-collections-editor") || g.equals("shared-collections-editor") || g.endsWith("-collection-editor")) {
			groups.add(g);
		    }
		}
		if (groups.size()==0) {
		    System.err.println("User not in any collection editing groups");
		    displayError(Dictionary.get("WebswingAuthenticationError.NoPermissions"));
		    return false; // user has no editing privileges
		}
		return true;
	    }
	    
	    public boolean canEditCollection(String collection) {
		if (groups.contains("administrator") || groups.contains("all-collections-editor")) {
		    return true;
		}
                if (groups.contains("shared-collections-editor") && !collection.contains("@")) {
                  return true;
                }
		if (groups.contains("personal-collections-editor") && collection.startsWith(username+"@")) {
		    return true;
		}
		if (groups.contains(collection+"-collection-editor")) {
		    return true;
		}
		return false;
	    }
	    
          public boolean canEditAllCollections() {
             if (groups.contains("administrator") || groups.contains("all-collections-editor")) {
              return true;
            }
            return false;
          }
          public boolean canEditSharedCollections() {
            if (groups.contains("administrator") || groups.contains("all-collections-editor") || groups.contains("shared-collections-editor")) {
              return true;
            }
            return false;
          }

          public boolean canEditPersonalCollections() {
            if (groups.contains("administrator") || groups.contains("all-collections-editor") || groups.contains("personal-collections-editor")) {
              return true;
            }
            return false;
          }
          public boolean canCreateNewCollections() {
            if (groups.contains("administrator") || groups.contains("all-collections-editor") || groups.contains("shared-collections-editor") || groups.contains("personal-collections-editor")) {
              return true;
            }
            return false;
          }
	    public PasswordAuthentication getAuthentication(String username, String password)
	    {
		return getPasswordAuthentication(username,password);
	    }

	    public PasswordAuthentication getAuthentication()
	    {
		return getPasswordAuthentication();
	    }

	    
	    protected String getMessageString()
	    {
		return Dictionary.get("RemoteGreenstoneServer.Authentication_Message");
	    }

	    
 	}

    
}
