/*
 * MiningMart Version 1.0
 * 
 * Copyright (C) 2006 Martin Scholz, Timm Euler, 
 *                    Daniel Hakenjos, Katharina Morik
 *
 * Contact: miningmart@ls8.cs.uni-dortmund.de
 *
 * A list of contributing developers (other than the copyright 
 * holders) can be found at
 * http://mmart.cs.uni-dortmund.de/downloads/download.html
 * 
 * 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, see the file MM_HOME/LICENSE; if not, write
 * to the Free Software Foundation, Inc., 51 Franklin Street, Fifth
 * Floor, Boston, MA 02110-1301, USA.
 */
package edu.udo.cs.miningmart.installer;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.LineNumberReader;
import java.io.PrintStream;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Vector;

import javax.swing.JOptionPane;

import edu.udo.cs.miningmart.db.ConfigReader;
import edu.udo.cs.miningmart.db.DB;
import edu.udo.cs.miningmart.exception.DbConnectionClosed;
import edu.udo.cs.miningmart.exception.M4Exception;
import edu.udo.cs.miningmart.gui.application.MiningMartApplication;
import edu.udo.cs.miningmart.gui.util.DbConfigEditor;
import edu.udo.cs.miningmart.gui.util.DbConfigEditorWindow;
import edu.udo.cs.miningmart.m4.M4Interface;

/**
 * @author Timm Euler
 *
 * A class that handles creating and updating the M4 schema.
 */
public class MmInstallerTools {

	private DB db;
	boolean usingPostgres;
	boolean usingOracle;
	
	// the following static variables are used to decide what needs 
	// to be installed and what is already installed:
	private static boolean mmHomeOk = false;
	private static boolean dbConfigOk = false;
	private static boolean m4SchemaOk = false;
	private static boolean aprioriOk = false;
	private static boolean svmOk = false;
	private static boolean c45Ok = false;
	private static boolean kmeansOk = false;
	private static boolean featSelectOk = false;
	private static boolean opsOk = false;
	
	/**
	 * The constructor gets the DB connection object needed to
	 * access the M4 database schema.
	 */
	public MmInstallerTools(DB myM4Db) throws M4Exception {
		try {
			this.db = myM4Db;
			usingOracle = (this.db.getM4Dbms() == DB.ORACLE);
			usingPostgres = (this.db.getM4Dbms() == DB.POSTGRES);
		}
		catch (DbConnectionClosed dbc) {
			throw new M4Exception("MmInstallerTools: error accessing DB (connection closed): " + dbc.getMessage());
		}
	}
	
	public static boolean installationIsValid() {

		// step 1: check home directory
		String home = System.getProperty(MiningMartApplication.SYSTEM_PROP_MM_HOME);
		if (home != null) {
			File f = new File(home);
			mmHomeOk = f.exists();
		}
		// additional check: config directory is important!
		if (mmHomeOk) {
			home = home + File.separator + "config";
			File f = new File(home);
			mmHomeOk = f.exists();
		}
		
		// step 2: check db connection
		String dbFileName = System.getProperty(M4Interface.SYSTEM_PROP_DB_CONFIG_PATH);
		if (dbFileName == null) {
			dbConfigOk = false;
		}
		else {
			File f = new File(dbFileName);
			if ( ! f.exists()) { 
				dbConfigOk = false;
			}
			else {
				try {
					new ConfigReader(dbFileName);				
					M4Interface.getInstance();
					dbConfigOk = true;				
				}
				catch (M4Exception m) {
					dbConfigOk = false;
				}
			}
		}
		// step 3: check existence of M4 tables
		if ( ! dbConfigOk) {
			m4SchemaOk = false;
		}
		else {
			try {
				MmInstallerTools m4sc = new MmInstallerTools(M4Interface.getInstance().getM4db());
				m4SchemaOk = m4sc.m4TablesExist();
			}
			catch (M4Exception m) {
				m4SchemaOk = false;
			}
		}
		// step 4: check operators entries
		if ( ! m4SchemaOk) {
			opsOk = false;
		}
		else {
			try {
				MmInstallerTools m4sc = new MmInstallerTools(M4Interface.getInstance().getM4db());
				opsOk = m4sc.staticOperatorEntriesExist();
			}
			catch (M4Exception m) {
				opsOk = false;
			}			
		}
		// step 5: check which external operators are installed
		if (mmHomeOk) {			
			String osName = System.getProperty("os.name");
			String[] filesToCheck = new String[0];
			home = System.getProperty(MiningMartApplication.SYSTEM_PROP_MM_HOME);
			if ( ! home.endsWith(File.separator)) {
				home += File.separator;
			}
			String path = null;
        	// under windows no external operators are available:
			if ( ! osName.substring(0, 3).equalsIgnoreCase("win")) {
				// under linux two operators are available, under solaris some more
				if (osName.equalsIgnoreCase("SunOS")) {
					path = home + "ext/bin/SunOS/";
				}
				else if (osName.equalsIgnoreCase("Linux")) {
					path = home + "ext/bin/Linux";
				}
				// apriori and svm might be available for both systems:
				filesToCheck = new String[] { path + "apriori" };
	        	if (allFilesExist(filesToCheck)) {
	        		aprioriOk = true;
	        	}
	        	filesToCheck = new String[] { path + "mysvm", path + "predict" };
	        	if (allFilesExist(filesToCheck)) {
	        		svmOk = true;
	        	}
	        	// the rest of external operators exists only for solaris:
	        	if ( ! osName.equalsIgnoreCase("Linux")) {
	        		filesToCheck = new String[] { path + "C4.5_script", path + "c4.5", 
	        				path + "c4.5rules", path + "rules2dlf", path + "tree2dlf", };	        		
	        		if (allFilesExist(filesToCheck)) {
	        			c45Ok = true;
	        		}
	        		filesToCheck = new String[] { path + "mms3" };
	        		if (allFilesExist(filesToCheck)) {
	        			kmeansOk = true;
	        		}
	        		filesToCheck = new String[] { path + "fsgenetic", 
							path + "c4.5_fselect", path + "fsstat" };
	        		if (allFilesExist(filesToCheck)) {
	        			featSelectOk = true;
	        		}
	        	}
	        }
		}
		return mmHomeOk && dbConfigOk && m4SchemaOk && opsOk;
	}
	
	public static boolean doInstallation(String possibleHomeDirectory) {
		MmInstallerTools m4sc = null;
		// first see if the property for home directory is properly set in the properties file:
		String home = System.getProperty(MiningMartApplication.SYSTEM_PROP_MM_HOME);
		File f = null;
		if (home != null) {
			f = new File(home);
			if (f.exists()) {
				mmHomeOk = true;
			}
			else {
				// second, try the automatically generated home directory:
				home = possibleHomeDirectory;
				f = new File(home);
				if (f.exists()) {
					mmHomeOk = true;
				}
			}
		}
		while ( ! mmHomeOk) {
			// third, ask user for MM-Home directory
			home = JOptionPane.showInputDialog(null, 
					"Please enter the Home directory for MiningMart,\n" +
					"which is the complete path up to and including 'MiningMart-1.0'.",
					home);
			if (home != null) {
				f = new File(home);
				mmHomeOk = f.exists();
			}
			else {
				mmHomeOk = false;
				return false;
			}
		}
		// now the home directory is found; set the properties:
		String propertiesFile = home + "config" + File.separator + "MiningMartHome.properties";
		f = new File(propertiesFile);
		if ( ! f.exists()) {
			JOptionPane.showConfirmDialog(null,
					"File '" + propertiesFile + "' should exist but doesn't!",
					"Error during installation",
					JOptionPane.OK_CANCEL_OPTION,
					JOptionPane.ERROR_MESSAGE);
			return false;
		}
		if (mmHomeOk) {
			if ( ! home.endsWith(File.separator)) {
				home += File.separator;
			}			
			setKeyValuePair(propertiesFile, MiningMartApplication.SYSTEM_PROP_MM_HOME, home);
			System.setProperty(MiningMartApplication.SYSTEM_PROP_MM_HOME, home);
			MiningMartApplication.setDefaultProperties();
		}
		else {
			return false;
		}
		// Next, check the db connection info:
		if ( ! dbConfigOk) {
			String dbFileName = System.getProperty(M4Interface.SYSTEM_PROP_DB_CONFIG_PATH);
			boolean dbConfFileOk = false;
			f = null;
			if (dbFileName != null) {
				f = new File(dbFileName);
			}
			if (dbFileName == null || (f != null && ( ! f.exists()))) { 		
				// ask user for file name and path for db.config
				boolean endsWithSlash = home.endsWith(File.separator);
				String suggestion = home + (endsWithSlash ? "" : File.separator) + "config" + File.separator + "db.config.myname";
				dbFileName = JOptionPane.showInputDialog(null, 
						"Please enter the complete path and file name for a\n" +
						"file that contains the database connection information.",
						suggestion);
				if (dbFileName == null) {
					return false;
				}
				f = new File(dbFileName);
				// check if file exists and is valid:
				if ( ! f.exists()) {
					boolean ready = makeDbConfigFile(dbFileName, propertiesFile);
					if ( ! ready) {
						return false;
					}
				}
				else {
					setKeyValuePair(propertiesFile, M4Interface.SYSTEM_PROP_DB_CONFIG_PATH, dbFileName);
				}
				System.setProperty(M4Interface.SYSTEM_PROP_DB_CONFIG_PATH, dbFileName);
			}
			try {
				new ConfigReader(dbFileName);
				dbConfFileOk = true;
			}
			catch (M4Exception m4e) {
				dbConfFileOk = false;
			}
			if ( ! dbConfFileOk) {
				// file is not valid, so ask user for db connection infos and create file
				boolean ready = makeDbConfigFile(dbFileName, home);
				if ( ! ready) {
					return false;
				}
				setKeyValuePair(propertiesFile, M4Interface.SYSTEM_PROP_DB_CONFIG_PATH, dbFileName);
				System.setProperty(M4Interface.SYSTEM_PROP_DB_CONFIG_PATH, dbFileName);
			}		
		}
		
		// create MmInstallerTools object:
		try {
			m4sc = new MmInstallerTools(M4Interface.getInstance().getM4db());
			dbConfigOk = true;
		}
		catch (M4Exception m4e) {
			JOptionPane.showConfirmDialog(
					null,
					"Error trying to connect to database:\n" + m4e.getMessage(),
					"Error during installation",
					JOptionPane.OK_CANCEL_OPTION,
					JOptionPane.ERROR_MESSAGE);				
			return false;
		}
		
		// now that mmHomeOk and dbConfigOk, check the other flags again since they could not have been
		// checked without database connection in the first call to 'installationIsValid()':
		installationIsValid();
		
		if ( ! m4SchemaOk) {
			// create M4 tables + sequence
			try {
				if (m4sc.m4TablesExist()) {
					m4SchemaOk = true;
				}
				else {
					int choice = JOptionPane.showConfirmDialog(
							null,
							"Warning: the system has not found all M4 tables. It will now create the M4 tables in the schema that was given\n" +
							"as M4 schema in the database. If you have old M4 tables or tables with names identical\n" +
							"to any M4 table, they will be deleted! Do you want to continue?",
							"Database warning",
							JOptionPane.OK_CANCEL_OPTION,
							JOptionPane.WARNING_MESSAGE);	
					if (choice == JOptionPane.CANCEL_OPTION) {
						return false;
					}
					boolean usingOracle = (M4Interface.getInstance().getM4db().getM4Dbms() == DB.ORACLE);
					boolean usingPostgres = (M4Interface.getInstance().getM4db().getM4Dbms() == DB.POSTGRES);
					String s = File.separator;
					String fileName = home + s + "m4install" + s;
					if (usingOracle) {
						fileName += "oracle";
					}
					if (usingPostgres) {
						fileName += "postgres";
					}
					String createTablesFileName = fileName + s + "CreateM4Tables.sql";
					m4sc.removeM4Tables();
					m4sc.createM4Tables(createTablesFileName);
					m4sc.installM4Sequence();
					String insertOpsFileName = fileName + s + "Operators.sql";
					m4sc.writeOperatorsIntoSchema(insertOpsFileName);
					try {
						m4sc.db.commitM4Transactions();
					}
					catch (SQLException sq) {
						throw new M4Exception("No COMMIT was possible after M4 tables were created: " + sq.getMessage());
					}
				}
			}
			catch (M4Exception m4e) {
				JOptionPane.showConfirmDialog(
						null,
						"Error trying to create M4 tables:\n" + m4e.getMessage(),
						"Error during installation",
						JOptionPane.OK_CANCEL_OPTION,
						JOptionPane.ERROR_MESSAGE);				
				return false;
			}
			catch (DbConnectionClosed d) {
				JOptionPane.showConfirmDialog(
						null,
						"Error trying to create M4 tables: database connection closed!\n" + d.getMessage(),
						"Error during installation",
						JOptionPane.OK_CANCEL_OPTION,
						JOptionPane.ERROR_MESSAGE);				
				return false;
			}
		}
		
		// check the operator entries:
		if ( ! opsOk) {
			try {
				opsOk = m4sc.staticOperatorEntriesExist();
				if ( ! opsOk) {
					ensureOperatorsAreInserted();
				}
			}
			catch (M4Exception m4e) {
				JOptionPane.showConfirmDialog(
						null,
						"An error occurred when checking for operator entries or\n" +
						"when inserting operators into M4 tables. Error message:\n" + m4e.getMessage(),
						"Error during installation",
						JOptionPane.OK_CANCEL_OPTION,
						JOptionPane.ERROR_MESSAGE);				
				return false;
			}
		}
		
		// at last check if external operators should be removed:		
		try {			
			if ( ! aprioriOk) {
				m4sc.removeOperatorFromM4("Apriori");
			}
			if ( ! c45Ok) {
				String[] opsToDelete = { "MissingValuesWithDecisionTree", 
						"MissingValuesWithDecisionRules", "PredictionWithDecisionTree",
						"PredictionWithDecisionRules", "DecisionTreeForRegression",
						"AssignPredictedValueCategorial"};				
				for (int i = 0; i < opsToDelete.length; i++) {
					m4sc.removeOperatorFromM4(opsToDelete[i]);
				}
			}
			if ( ! svmOk) {
				String[] opsToDelete = { "SupportVectorMachineForClassification", 
						"MissingValuesWithRegressionSVM", "FeatureSelectionWithSVM",
						"ComputeSVMError", "SupportVectorMachineForRegression" };				
				for (int i = 0; i < opsToDelete.length; i++) {
					m4sc.removeOperatorFromM4(opsToDelete[i]);
				}
			}
			if ( ! featSelectOk) {
				String[] opsToDelete = { "StatisticalFeatureSelection", 
						"GeneticFeatureSelection", "SGFeatureSelection" };				
				for (int i = 0; i < opsToDelete.length; i++) {
					m4sc.removeOperatorFromM4(opsToDelete[i]);
				}
			}
			if ( ! kmeansOk) {
				m4sc.removeOperatorFromM4("SegmentationWithKMean");
			}
		}
		catch (M4Exception m4e) {
			JOptionPane.showConfirmDialog(
					null,
					"An error occurred when some operators that depend on external algorithms\n" +
					"were disabled. Depending on the type of error some of your machine learning\n" +
					"operators may be defect. Error message:\n" + m4e.getMessage(),
					"Error during installation",
					JOptionPane.OK_CANCEL_OPTION,
					JOptionPane.WARNING_MESSAGE);
			
			return true; // some invalid ops don't matter... so much
		}
		return true;
	}

	/**
	 * Creates the file MiningMartHome.properties and sets the system
	 * property "MM_HOME" into the file. The Db config property will be
	 * set to a nonexisting filename.
	 * 
	 * @param homeDirectory complete path of the MiningMart home directory
	 * @return the complete path and name of the created file
	 */
	public static String createHomePropertiesFile(String homeDirectory) {
		File f = new File(homeDirectory);
		if ( ! f.exists()) {
			JOptionPane.showConfirmDialog(null,
					"You have entered the home directory\n" +
					homeDirectory + "\n but it does not exist!");
			return null;
		}
		if ( ! homeDirectory.endsWith(File.separator)) {
			homeDirectory += File.separator;
		}
		String configDirectory = homeDirectory + "config" + File.separator;
		f = new File(configDirectory);
		if ( ! f.exists()) {
			JOptionPane.showConfirmDialog(null,
					"You have entered the home directory\n" +
					homeDirectory + "\n but it does not have the required subdirectory 'config'!");
			return null;			
		}
		String propFile = configDirectory + "MiningMartHome.properties";
		f = new File(propFile);
		try {
			if ( ! f.exists()) {
				f.createNewFile();
			}
			PrintStream out = new PrintStream(new FileOutputStream(f));
			out.println("# Main configuration file for MiningMart.\n#\n#");
			out.println("# You should enter here the path to the MiningMart system directory.");
			out.println("# Example (Windows): MM_HOME=C:\\Programs\\MiningMart-1.0\\");
			out.println("# Example (Linux):   MM_HOME=/home/myname/analysis/MiningMart-1.0/");
			out.println("\n" + MiningMartApplication.SYSTEM_PROP_MM_HOME + " = " + homeDirectory + "\n");
			out.println("# You may enter here the complete path to the file db.config; this file");
			out.println("# is usually in MM_HOME/config/ and if this is the case, the system");
			out.println("# finds out for itself.");
			out.println("\n" + M4Interface.SYSTEM_PROP_DB_CONFIG_PATH + " = ...");
			out.flush();
			out.close();
			return propFile;
		}
		catch (IOException ioe) {
			JOptionPane.showConfirmDialog(
					null,
					"Error trying to write properties to '" + propFile + ":\n" + ioe.getMessage(),
					"Error during installation",
					JOptionPane.OK_CANCEL_OPTION,
					JOptionPane.ERROR_MESSAGE);				
			return null;
		}		
	}
	
    public static void readSystemPropsFromHomeProperties(String propFilename) {
		File f = new File(propFilename);
		if ( ! f.exists()) {
			return;
		}
		try {
			LineNumberReader lnr = new LineNumberReader(new FileReader(f));
			String line = lnr.readLine();
			while (line != null) {
				line = line.trim();
				if (line.equals("") || line.startsWith("#")) {
					line = lnr.readLine();
					continue;
				}
				int indexOfEqualSign = line.indexOf("=");
				if (indexOfEqualSign == -1) {
					throw new IOException("Found invalid key-value pair, line: " + line);
				}
				String readKey = line.substring(0, indexOfEqualSign).trim();
				if (readKey.equals("")) {
					throw new IOException("Found invalid key-value pair, line: " + line);
				}
				String readValue = null;
				if (line.length() > indexOfEqualSign + 1) {
					readValue = line.substring(indexOfEqualSign + 1).trim();
				}
				if (readKey.equalsIgnoreCase(M4Interface.SYSTEM_PROP_DB_CONFIG_PATH) && readValue != null) {
					System.setProperty(M4Interface.SYSTEM_PROP_DB_CONFIG_PATH, readValue);
				}
				if (readKey.equalsIgnoreCase(MiningMartApplication.SYSTEM_PROP_MM_HOME) && readValue != null) {
					System.setProperty(MiningMartApplication.SYSTEM_PROP_MM_HOME, readValue);
				}
				line = lnr.readLine();
			}
			lnr.close();
		}
		catch (IOException i) {
			JOptionPane.showConfirmDialog(
					null,
					"Error reading properties from '" + propFilename + ":\n" + i.getMessage(),
					"Error during startup",
					JOptionPane.OK_CANCEL_OPTION,
					JOptionPane.ERROR_MESSAGE);				
			return;
		}
	}
	
	/** 
	 * A method to create a syntactically valid db.config file from
	 * information given by the user.
	 * 
	 * @param nameForFile name and path of file to be written
	 * @return False if any error occurred
	 */ 
	public static boolean makeDbConfigFile(String nameForDbConfigFile, String homePropertiesFileName) {
		final HashMap theValues = new HashMap();
		final String dbFileName = nameForDbConfigFile;
		ActionListener a = new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				writeDbConfigValuesToFile(theValues, dbFileName);
			}
		};
		DbConfigEditorWindow w = new DbConfigEditorWindow(null, theValues, a);
		w.setRestartWarning(false);
		w.prepareWindow();
		w.show();
		if (w.isCanceled())
			return false;
		// enter the right value into the home properties file:
		setKeyValuePair(homePropertiesFileName, M4Interface.SYSTEM_PROP_DB_CONFIG_PATH, nameForDbConfigFile);
		System.setProperty(M4Interface.SYSTEM_PROP_DB_CONFIG_PATH, nameForDbConfigFile);
		return true;
	}
	
	private static boolean setKeyValuePair(String givenFile, String key, String value) {
		try {
			File f = new File(givenFile);
			if ( ! f.exists()) {
				f.createNewFile();
			}
		
			// parse the file to ensure valid key-value pairs:
			Vector newLines = new Vector();
			LineNumberReader lnr = new LineNumberReader(new FileReader(f));
			String line = lnr.readLine();
			while (line != null) {
				line = line.trim();
				if (line.equals("") || line.startsWith("#")) {
					newLines.add(line);
					line = lnr.readLine();
					continue;
				}
				int indexOfEqualSign = line.indexOf("=");
				if (indexOfEqualSign == -1) {
					throw new IOException("Found invalid key-value pair, line: " + line);
				}
				String readKey = line.substring(0, indexOfEqualSign).trim();
				if (readKey.equals("")) {
					throw new IOException("Found invalid key-value pair, line: " + line);
				}
				String readValue = null;
				if (line.length() > indexOfEqualSign + 1) {
					readValue = line.substring(indexOfEqualSign + 1).trim();
				}
				if ((value == null || value.equals("")) && readValue != null) {
					value = readValue;
				}
				if (readKey.equalsIgnoreCase(key)) {
					line = key + " = " + value;
				}
			
				newLines.add(line);
				line = lnr.readLine();
			}
		
			PrintStream out = new PrintStream(new FileOutputStream(f));
			Iterator it = newLines.iterator();
			while (it.hasNext()) {
				String nextLine = (String) it.next();
				out.println(nextLine);
			}
			out.flush();
			out.close();
		
			return true;
		}
		catch (IOException ioe) {
			JOptionPane.showConfirmDialog(
					null,
					"Error trying to read or write properties from/to '" + givenFile + ":\n" + ioe.getMessage(),
					"Error during installation",
					JOptionPane.OK_CANCEL_OPTION,
					JOptionPane.ERROR_MESSAGE);				
			return false;
		}		
	}
	
	private static boolean writeDbConfigValuesToFile(HashMap theMap, String nameForFile) {
		try {
			File f = new File(nameForFile);
			if ( ! f.exists()) {
				f.createNewFile();
			}
			PrintStream out = new PrintStream(new FileOutputStream(f));
			
			// write initial comment:
			writeStartComment(out);
			
			// M4 SCHEMA 
			
			// database name
			String dbName = (String) theMap.get(DbConfigEditor.VAR_DB_NAME);
			out.println("# Name of database used for M4 tables:");
			out.println(ConfigReader.M4_DBNAME + " = " + dbName + "\n");
			
			// driver
			String driver = (String) theMap.get(DbConfigEditor.VAR_DB_DRIV);
			out.println("# Path to JDBC driver used to access M4 tables.");
			out.println("# For Oracle this is probably 'jdbc:oracle:thin:'.");
			out.println("# For Postgres this is probably 'jdbc:postgresql:'.");
			out.println(ConfigReader.M4_DRIVER + " = " + driver + "\n");
			
			// database server access
			String host = (String) theMap.get(DbConfigEditor.VAR_DB_HOST);
			String port = (String) theMap.get(DbConfigEditor.VAR_DB_PORT);
			out.println("# Access to database server for M4 tables.");
			out.println("# Format for Oracle: @<host>:<port>:");
			out.println("# Format for Postgres: //<host>:<port>    (note: no final colon)");
			boolean oracle = (driver.indexOf("oracle") > -1);
			boolean postgr = (driver.indexOf("postgres") > -1);
			String location = null;
			if (oracle) {
				location = "@" + host + ":" + port + ":";
			}
			if (postgr) {
				location = "//" + host + ":" + port;
			}
			out.println(ConfigReader.M4_LOCATION + " = " + location + "\n");
			
			// m4 user
			String m4user = (String) theMap.get(DbConfigEditor.VAR_M4_USER);
			out.println("# Name of DB user who owns M4 tables:");
			out.println(ConfigReader.M4_USER + " = " + m4user + "\n");
			
			// m4 password
			String m4pass = (String) theMap.get(DbConfigEditor.VAR_M4_PASS);
			out.println("# Password for DB user who owns M4 tables:");
			out.println(ConfigReader.M4_PW + " = " + m4pass + "\n");			

			// BUSINESS SCHEMA 
			
			// database name
			out.println("# Name of database used for business tables:");
			out.println(ConfigReader.BUS_DBNAME + " = " + dbName + "\n");
			
			// driver
			out.println("# Path to JDBC driver used to access business tables.");
			out.println("# For Oracle this is probably 'jdbc:oracle:thin:'.");
			out.println("# For Postgres this is probably 'jdbc:postgresql:'.");
			out.println(ConfigReader.BUS_DRIVER + " = " + driver + "\n");
			
			// database server access
			out.println("# Access to database server for business tables.");
			out.println("# Format for Oracle: @<host>:<port>:");
			out.println("# Format for Postgres: //<host>:<port>    (note: no final colon)");
			out.println(ConfigReader.BUS_LOCATION + " = " + location + "\n");
			
			// m4 user
			String bususer = (String) theMap.get(DbConfigEditor.VAR_BUS_USER);
			out.println("# Name of DB user who owns business tables:");
			out.println(ConfigReader.BUS_USER + " = " + bususer + "\n");
			
			// m4 password
			String buspass = (String) theMap.get(DbConfigEditor.VAR_BUS_PASS);
			out.println("# Password for DB user who owns business tables:");
			out.println(ConfigReader.BUS_PW + " = " + buspass + "\n");
			
			out.flush();
			out.close();
			return true;
		}
		catch (IOException ioe) {
			JOptionPane.showConfirmDialog(
					null,
					"Error trying to read database connection info from user or writing it to a config file:\n" + ioe.getMessage(),
					"Error during installation",
					JOptionPane.OK_CANCEL_OPTION,
					JOptionPane.ERROR_MESSAGE);				
			return false;
		}
	}
	
	private static void writeStartComment(PrintStream out) {
		out.println("# This file contains the information that tells\n" +
				"# the MiningMart system how to connect to your database.\n" +
				"#\n" +
				"# There are two sets of values. The first set is for\n" + 
				"# the M4 database schema. This is the schema where you\n" +  
				"# installed the M4 tables when you followed the instructions\n" +  
				"# in the file MM_HOME/m4install/<DbmsName>/README.txt. \n" + 
				"# The second set of values is for the database schema that \n" + 
				"# holds the data which you want to prepare or analyse.\n" + 
				"# The values given in the two sets may be identical.\n" + 
				"#\n" + 
				"# You might want to entertain several copies of this file\n" + 
				"# with different database connection information. In order\n" + 
				"# to start MiningMart with the connection you choose, enter\n" + 
				"# the name and path of the correct version of this file as\n" + 
				"# the value of DB_CONFIG_PATH into the file\n" +  
				"# MM_HOME/config/MiningMartHome.properties.");
	}
	
	// checks if all files whose names are given exist
	private static boolean allFilesExist(String[] fileNames) {
		for (int i = 0; i < fileNames.length; i++) {
			File f = new File(fileNames[i]);
			if ( ! f.exists()) {
				return false;
			}
		}
		return true;
	}
	
	/**
	 * This method ensures that all M4 tables are removed.
	 */
	public void removeM4Tables() throws M4Exception {
		String[] tablenames = DB.ORDER_FOR_WRITING;
		try {
			for (int i = tablenames.length - 1; i >= 0; i--) {
				this.db.dropM4Table(tablenames[i]);
			}
			this.db.commitM4Transactions();
		}
		catch (SQLException s) {
			try {
				this.db.rollbackOnM4();
			}
			catch (DbConnectionClosed dbc) {
				throw new M4Exception("MmInstallerTools: error accessing DB (connection closed): " + dbc.getMessage());
			}
			catch (SQLException sqle) {
				throw new M4Exception("MmInstallerTools: SQL error accessing DB: " + sqle.getMessage());
			}
			throw new M4Exception("MmInstallerTools: SQL error trying to drop an M4 table: " + s.getMessage());			
		}
		catch (DbConnectionClosed dbc) {
			throw new M4Exception("MmInstallerTools: error accessing DB (connection closed): " + dbc.getMessage());
		}
	}
	

	String[] operatorTables = { "op_constr_t", "op_cond_t", "op_assert_t",
			                    "op_params_t", "op_group_t", "operator_t" };
	
	/**
	 * This method assumes that the M4 tables exist in the database,
	 * and updates the information about the operators (in the static
	 * part of M4) according to the SQL commands in the file whose name
	 * and path are given.
	 * 
	 * @param fileAndPathWithOperatorInfo name and path of a file with 
	 *        SQL commands
	 */
	public void writeOperatorsIntoSchema(String fileAndPathWithOperatorInfo)
	throws M4Exception {
		if ( ! m4TablesExist()) {
			throw new M4Exception("MmInstallerTools: cannot update operators since the M4 tables do not seem to exist!");
		}
		
		File f = new File(fileAndPathWithOperatorInfo);
		if ( ! f.exists()) {
			throw new M4Exception("MmInstallerTools: cannot update operators since Operator information file could not be found!");
		}
		try {			
			// remove constraints which would hinder the deletion of old operator information:
			String query = "ALTER TABLE step_t DROP CONSTRAINT stopid_fk";
			try {
				this.db.executeM4SqlWrite(query);
				query = "ALTER TABLE parameter_t DROP CONSTRAINT paropid_fk";
				this.db.executeM4SqlWrite(query);
			}
			catch (SQLException s) {
				// do nothing because the exception might be that the constraints do not exist...
			}
			
			// delete old operator information:
			query = "DELETE FROM docu_t WHERE doc_objtype = 'OPERATOR'";
			this.db.executeM4SqlWrite(query);
			for (int i = 0; i < operatorTables.length; i++) {
				query = "DELETE FROM " + operatorTables[i];
				this.db.executeM4SqlWrite(query);
			}
			
			// write the new operator information:
			this.writeSqlCommandsFromFileIntoDb(f, true);
			
			// re-create the constraints:
			query = "ALTER TABLE step_t ADD (CONSTRAINT stopid_fk FOREIGN KEY (st_opid) REFERENCES operator_t (op_id))";
			if (this.db.getM4Dbms() == DB.POSTGRES) {
				query = "ALTER TABLE step_t ADD CONSTRAINT stopid_fk FOREIGN KEY (st_opid) REFERENCES operator_t (op_id)";
			}
			this.db.executeM4SqlWrite(query);
			query = "ALTER TABLE parameter_t ADD (CONSTRAINT paropid_fk FOREIGN KEY (par_opid) REFERENCES operator_t (op_id))";
			if (this.db.getM4Dbms() == DB.POSTGRES) {
				query = "ALTER TABLE parameter_t ADD CONSTRAINT paropid_fk FOREIGN KEY (par_opid) REFERENCES operator_t (op_id)";
			}
			this.db.executeM4SqlWrite(query);
			this.db.commitM4Transactions();
		}
		catch (SQLException sql) {
			try {
				this.db.rollbackOnM4();
			}
			catch (DbConnectionClosed dbc) {
				throw new M4Exception("MmInstallerTools: error accessing DB (connection closed): " + dbc.getMessage());
			}
			catch (SQLException sqle) {
				throw new M4Exception("MmInstallerTools: SQL error accessing DB: " + sqle.getMessage());
			}
			throw new M4Exception("MmInstallerTools: SQL error updating operator information: " + sql.getMessage());
		}
		catch (DbConnectionClosed dbc) {
			throw new M4Exception("MmInstallerTools: error accessing DB (connection closed): " + dbc.getMessage());
		}
	}
	
	public static void ensureOperatorsAreInserted() {
		String homeDir = System.getProperty(MiningMartApplication.SYSTEM_PROP_MM_HOME);
		if ( ! homeDir.endsWith(File.separator)) {
			homeDir += File.separator;
		}
		DB mdb = null;
		try {
			mdb = M4Interface.getInstance().getM4db();
			boolean usingOracle = (mdb.getM4Dbms() == DB.ORACLE);
			boolean usingPostgres = (mdb.getM4Dbms() == DB.POSTGRES);
			String opsFile = homeDir + "m4install" + File.separator;
			if (usingOracle) {
				opsFile += "oracle";				
			}
			if (usingPostgres) {
				opsFile += "postgres";
			}
			opsFile += File.separator + "Operators.sql";
			MmInstallerTools m4sc = new MmInstallerTools(mdb);
			m4sc.writeOperatorsIntoSchema(opsFile);
		}
		catch (M4Exception m) {
			JOptionPane.showConfirmDialog(null,
					"Error trying to insert operators into M4 tables. System cannot start. Error message:\n" + m.getMessage());
			System.exit(-1);
		}
		catch (DbConnectionClosed dbc) {
			JOptionPane.showConfirmDialog(null,
					"Error trying to insert operators into M4 tables. System cannot start. Error message:\n" + dbc.getMessage());
			System.exit(-1);
		}		
	}
	
	/**
	 * This method assumes that no M4 tables exist in the database.
	 * It creates them if the file whose name and path are given contains
	 * the commands for creating them.
	 * 
	 * @param fileNameAndPath name and path of a file with 
	 *        SQL commands
	 */
	public void createM4Tables(String fileNameAndPath) throws M4Exception {
		if (m4TablesExist()) {
			throw new M4Exception("MmInstallerTools: cannot create M4 tables since they seem to exist already!");
		}		
		File f = new File(fileNameAndPath);
		if ( ! f.exists()) {
			throw new M4Exception("MmInstallerTools: cannot create M4 tables since the file with their creation commands could not be found!");
		}
		this.writeSqlCommandsFromFileIntoDb(f, false);		
	}
	
	public void removeM4Sequence() throws M4Exception {
		String seqname = "ALL_SQ";
		String query = "DROP SEQUENCE " + seqname;
		try {
			this.db.executeM4SqlWrite(query);
		}
		catch (SQLException s) {
			// do nothing because the exception may have been thrown because
			// the sequence hadn't existed
		}
		catch (DbConnectionClosed dbc) {
			throw new M4Exception("MmInstallerTools: error accessing DB (connection closed): " + dbc.getMessage());
		}
	}
	
	public void installM4Sequence() throws M4Exception {
		// if the sequence exists remove it:
		this.removeM4Sequence();
		String seqname = "ALL_SQ";
		String query;
		// now install the sequence:
		try {
			query = "CREATE SEQUENCE " + seqname + " INCREMENT ";
			if (this.usingOracle) {
				query += "BY ";
			}
			query += "1 START ";
			if (this.usingOracle) {
				query += "WITH ";
			}
			query += "100000000 MAXVALUE 199999999 MINVALUE 100000000 ";
			if (this.usingOracle) {
				query += "NOCYCLE ";				
			}
			query += "CACHE 20";
			if (this.usingOracle) {
				query += " NOORDER";
			}
			this.db.executeM4SqlWrite(query);
		}
		catch (SQLException sql) {
			try {
				this.db.rollbackOnM4();
			}
			catch (DbConnectionClosed dbc) {
				throw new M4Exception("MmInstallerTools: error accessing DB (connection closed): " + dbc.getMessage());
			}
			catch (SQLException sqle) {
				throw new M4Exception("MmInstallerTools: SQL error accessing DB: " + sqle.getMessage());
			}
			throw new M4Exception("MmInstallerTools: SQL error installing M4 sequence: " + sql.getMessage());
		}
		catch (DbConnectionClosed dbc) {
			throw new M4Exception("MmInstallerTools: error accessing DB (connection closed): " + dbc.getMessage());
		}
	}
	
	private void writeSqlCommandsFromFileIntoDb(File commandFile, boolean useDrops) throws M4Exception {
		String line = null;
		try {
			LineNumberReader lnr = new LineNumberReader(new FileReader(commandFile));			
			line = lnr.readLine();
			int lineCount = 0;			
			// insert new operator information:
			while (line != null) {
				line = line.trim();
				if (line.startsWith("--")) {
					line = lnr.readLine();
					continue;
				}
				if (line.equals("")){
					line = lnr.readLine();
					continue;
				}
				// append lines until we have a complete SQL command:
				while ( ! line.endsWith(";")) {
					line += " " + lnr.readLine().trim();
				}
				// remove the ";":
				line = line.substring(0, line.length()-1);
				
				// check if it is a DROP command:
				if ( ( ! useDrops) && line.toUpperCase().startsWith("DROP ")) {
					line = lnr.readLine();
					continue;
				}
				this.db.executeM4SqlWrite(line);
				lineCount++;
				if (lineCount % 500 == 0) {
					this.db.commitM4Transactions();
					this.db.getFreshM4Connection();
				}
				line = lnr.readLine();
			}
			this.db.commitM4Transactions();
			this.db.getFreshM4Connection();
		}
		catch (IOException ioe) {
			try {
				this.db.rollbackOnM4();
			}
			catch (DbConnectionClosed dbc) {
				throw new M4Exception("MmInstallerTools: error accessing DB (connection closed): " + dbc.getMessage());
			}
			catch (SQLException sqle) {
				throw new M4Exception("MmInstallerTools: SQL error accessing DB: " + sqle.getMessage());
			}
			throw new M4Exception("MmInstallerTools: error accessing file: " + ioe.getMessage());
		}
		catch (SQLException sql) {
			try {
				this.db.rollbackOnM4();
			}
			catch (DbConnectionClosed dbc) {
				throw new M4Exception("MmInstallerTools: error accessing DB (connection closed): " + dbc.getMessage());
			}
			catch (SQLException sqle) {
				throw new M4Exception("MmInstallerTools: SQL error accessing DB: " + sqle.getMessage());
			}
			throw new M4Exception("MmInstallerTools: SQL error executing command from file.\nLine: " + line + "\nMessage: " + sql.getMessage());
		}
		catch (DbConnectionClosed dbc) {
			throw new M4Exception("MmInstallerTools: error accessing DB (connection closed): " + dbc.getMessage());
		}
	}
	
	private boolean m4TablesExist() throws M4Exception {
		String[] tablenames = DB.ORDER_FOR_WRITING;
		for (int i = tablenames.length - 1; i >= 0; i--) {
			if ( ! this.db.tableExistsInM4(tablenames[i])) {
				return false;
			}
		}
		return true;
	}
	
	private boolean staticOperatorEntriesExist() throws M4Exception {
		for (int i = 0; i < operatorTables.length; i++) {
			if (this.m4TableIsEmpty(operatorTables[i])) {
				return false;
			}
		}
		return true;
	}
	
	private boolean m4TableIsEmpty(String tableName) throws M4Exception {
		String query = "SELECT COUNT(*) FROM " + tableName;
		try {
			Long l = this.db.executeM4SingleValueSqlReadL(query);
			if (l.longValue() == 0) {
				return true;
			}
			return false;
		}
		catch (DbConnectionClosed d) {
			throw new M4Exception("MmInstallerTools: Db connection closed when checking if table '" + tableName +
					"' is empty: " + d.getMessage());
		}
		catch (SQLException s) {
			throw new M4Exception("MmInstallerTools: SQL error when checking if table '" + tableName +
					"' is empty: " + s.getMessage());
		}
	}
	
	private void removeOperatorFromM4(String operatorName) throws M4Exception {
		String query = "SELECT op_id FROM operator_t WHERE op_name = '" + operatorName + "'";
		Long l = null;
		try {
			l = this.db.executeM4SingleValueSqlReadL(query);
			String[] operatorTableIdFields = { "constr_opid", "cond_opid", "assert_opid",
					                           "op_id", "opg_opid", "op_id" };			
			if (l != null) {
				query = "DELETE FROM docu_t WHERE doc_objid = " + l.longValue();
				this.db.executeM4SqlWrite(query);
				for (int i = 0; i < operatorTables.length; i++) {
					query = "DELETE FROM " + operatorTables[i] + 
							" WHERE " + operatorTableIdFields[i] + " = " + l.longValue();
					this.db.executeM4SqlWrite(query);
				}
			}
		}
		catch (SQLException sqle) {
			throw new M4Exception("SQL error removing operator '" + operatorName + "': " + sqle.getMessage());
		}
		catch (DbConnectionClosed dbc) {
			throw new M4Exception("MmInstallerTools: Connection to M4-DB was closed: " + dbc.getMessage());
		}
	}
}
