/*
 * 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.m4.core;

import java.io.Serializable;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collection;
import java.util.Iterator;
import java.util.Vector;
import java.util.logging.Level;

import edu.udo.cs.miningmart.db.CompilerDatabaseService;
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.exception.MiningMartException;
import edu.udo.cs.miningmart.m4.Case;
import edu.udo.cs.miningmart.m4.Concept;
import edu.udo.cs.miningmart.m4.M4Interface;
import edu.udo.cs.miningmart.m4.M4InterfaceContext;
import edu.udo.cs.miningmart.m4.Operator;
import edu.udo.cs.miningmart.m4.OperatorGroup;
import edu.udo.cs.miningmart.m4.utils.Print;
import edu.udo.cs.miningmart.schemamatching.EditDistanceMatcher;
import edu.udo.cs.miningmart.schemamatching.MatchingResult;
import edu.udo.cs.miningmart.schemamatching.MmSchemaMatcher;
import edu.udo.cs.miningmart.schemamatching.NgramMatcher;
import edu.udo.cs.miningmart.schemamatching.SchemaMatchException;
import edu.udo.cs.miningmart.schemamatching.SoundexMatcher;
import edu.udo.cs.miningmart.schemamatching.StringEqualityMatcher;

/**
 * @author Martin Scholz
 * @version $Id: M4InterfaceImpl.java,v 1.12 2006/04/11 14:10:14 euler Exp $
 */
public class M4InterfaceImpl
	extends M4Interface
	implements M4InterfaceContext, Serializable
{
	/** The database connection (subclass of DB) for the M4 Interface */
	private DB db;


	// ***** Constructor *****

	/**
	 * Constructor
	 *  
	 * Will set up the database connections and the <code>Print</code> object for
	 * log outputs
	 */
	public M4InterfaceImpl() {
		Level printVerb = Print.DB_READ;
		
		String printVerbS = System.getProperty(M4Interface.SYSTEM_PROP_PRINT_VERBOSITY);
		if (printVerbS != null) {
			printVerb=Print.getVerbosityLevelForName(printVerbS);
/*			try {
				printVerb = Integer.parseInt(printVerbS);
			}catch (NumberFormatException e) {}
			}	
*/				
		}
		
		print=Print.getGlobalPrintObject();
		print.setMinimumVerbosityLevel(printVerb);
		this.getNewDbConnection();
	}
	
	public void getNewDbConnection() {
		if (this.db != null) {
			try {
				this.db.updateDatabase();
			}
			catch (M4Exception e) {
				System.err.println("Warning: Could not update Database!\n" + e.getMessage());
			}
			this.db.clearM4Cache();
			setCurrentCompilerAccess(null);
		}
		
		String dbConfigPath = System.getProperty(M4Interface.SYSTEM_PROP_DB_CONFIG_PATH);
		if (dbConfigPath == null || dbConfigPath.length() == 0) {
			this.getPrintObject().doPrint(Print.MAX,
				"Fatal error: Could not read System property "
				+ M4Interface.SYSTEM_PROP_DB_CONFIG_PATH
				+ " to set up database connections!!");
			this.db = null;
		}
		else {
			DB d = null;
			try {
		        ConfigReader cr = new ConfigReader(dbConfigPath);
				
	    		d = new CompilerDatabaseService( 
	    				cr.getM4Url(),
						cr.getM4DbName(),
						cr.getM4User(),
						cr.getM4Pw(),
						cr.getBusUrl(),
						cr.getBusDbName(),
						cr.getBusUser(),
						cr.getBusPw(),
					    true,
					    this);
			}
			catch (SQLException e) {
				this.getPrintObject().doPrint(Print.ERROR,e.getMessage(),e);
				System.out.println("SQLException:"+e.getMessage());
			}
			catch (M4Exception m4e) {
				this.getPrintObject().doPrint(Print.ERROR,"Could not connect to DB: "+m4e.getMessage(),m4e);
				System.err.println("Could not connect to DB: " + m4e.getMessage());
			}
			this.db = d;
        }
	}

	
	// ***** Implementations of the CompilerAccessLogic interface
	
	public boolean getStopRequest() {
		return false; // always false for an HCI-only M4 interface
	}

	public edu.udo.cs.miningmart.m4.Case getCase() {
		// Just used by CompilerAccessLogic, so we may leave it out here!
		return null; // Does a "single case restriction" make sense here?
	}

	public DB getM4db() {
		return this.db;	
	}

	public Print getPrintObject() {
		return print;
	}


	// ***** Implementations of the M4Interface interface *****
	
	/**
	 * @see M4Interface#createCase(String, boolean)
	 */
	public edu.udo.cs.miningmart.m4.Case createCase(String name, boolean setAsCurrentCase) throws M4Exception
	{
		Case m4case = new edu.udo.cs.miningmart.m4.core.Case(this.getM4db());
		((edu.udo.cs.miningmart.m4.core.Case) m4case).setName(name);
		
		if (setAsCurrentCase) {
			M4Interface.setCurrentCase(m4case);
		}
		
		return m4case;
	}

	/**
	 * @see M4Interface#createOperator(String)
	 */
	public edu.udo.cs.miningmart.m4.Operator createOperator(String name) throws M4Exception
	{
		Operator op = new edu.udo.cs.miningmart.m4.core.Operator(this.getM4db());
		((edu.udo.cs.miningmart.m4.core.Operator) op).setName(name);
		return op;
	}
	
	/**
	 * Current implementation:
	 * Reads case names directly from the database, so it has to be taken care for
	 * writing back the cache in time!
	 * 
	 * @see M4Interface#getAllCaseNames()
	 */
	public Collection getAllCaseNames() {
		String query =
			"SELECT "  + edu.udo.cs.miningmart.m4.core.Case.ATTRIB_CASE_NAME
			+ " FROM " + edu.udo.cs.miningmart.m4.core.Case.M4_TABLE_NAME;
		
		Vector result = null;
		ResultSet rs = null;
		try {
			rs = this.getM4db().executeM4SqlRead(query);
			result = new Vector();
			while (rs.next()) {
				String caseName = rs.getString(1);
				if (caseName != null) {
					result.add(caseName);
				}
			}
		}
		catch (SQLException e) {
			this.getPrintObject().doPrint(Print.ERROR,e.getMessage(),e);
		}
		catch (DbConnectionClosed e) {
			this.getPrintObject().doPrint(Print.ERROR,e.getMessage(),e);	
		}
		finally {
			this.closeResultSet(rs);
		}
		return result;
	}
	
	/**
	 * @see edu.udo.cs.miningmart.m4.M4Interface#getNamesOfBusinessTablesAndViews()
	 */
	public Collection getNamesOfBusinessTablesAndViews() throws M4Exception {
		return this.getM4db().getNamesOfBusSchemaDbObjects();
	}
	
	/**
	 * @see edu.udo.cs.miningmart.m4.M4Interface#isBusinessTable(java.lang.String)
	 */
	public boolean isBusinessTable(String name) throws M4Exception {
		try {
			return this.getM4db().isBusinessTable(name);
		}
		catch (SQLException sqle) {
			throw new M4Exception("SQL error trying to find out if '" + name + 
					"' is a table or view in the business schema: " + sqle.getMessage());
		}
		catch (DbConnectionClosed dbc) {
			throw new M4Exception("Connection to DB suddenly closed when trying to find out if '" + name + 
					"' is a table or view in the business schema: " + dbc.getMessage());			
		}
	}
	
	/**
	 * @see edu.udo.cs.miningmart.m4.M4Interface#isBusinessView(java.lang.String)
	 */
	public boolean isBusinessView(String name) throws M4Exception {
		try {
			return this.getM4db().isBusinessView(name);
		}
		catch (SQLException sqle) {
			throw new M4Exception("SQL error trying to find out if '" + name + 
					"' is a table or view in the business schema: " + sqle.getMessage());
		}
		catch (DbConnectionClosed dbc) {
			throw new M4Exception("Connection to DB suddenly closed when trying to find out if '" + name + 
					"' is a table or view in the business schema: " + dbc.getMessage());			
		}
	}
	
	
	/**
	 * @see edu.udo.cs.miningmart.m4.M4Interface#existsCase(String)
	 */
	public boolean existsCase(String name) {
		String query =
			"SELECT COUNT(*) "
			+ " FROM " + edu.udo.cs.miningmart.m4.core.Case.M4_TABLE_NAME
			+ " WHERE " + edu.udo.cs.miningmart.m4.core.Case.ATTRIB_CASE_NAME+ "=\'"+name+"\'";
		
		boolean result=false;
		ResultSet rs = null;
		try {
			rs = this.getM4db().executeM4SqlRead(query);

			if (rs.next()) {
				String counter = rs.getString(1);
				if (Integer.parseInt(counter)==0){
					result=false;
				}
				if (Integer.parseInt(counter)==1){
					result=true;
				}
			}
		}
		catch (SQLException e) {
			this.getPrintObject().doPrint(Print.ERROR,e.getMessage(),e);
		}
		catch (DbConnectionClosed e) {
			this.getPrintObject().doPrint(Print.ERROR,e.getMessage(),e);	
		}
		finally {
			this.closeResultSet(rs);
		}
		return result;
	}

	

	/** 
	 * Helper method to close ResultSet without any exception thrown.
	 * Exceptions will be printed using the local <code>Print</code> object.
	 * If the <code>ResultSet</code> is <code>null</code> nothing is done.
	 */
	private void closeResultSet(ResultSet rs) {
		String msg = DB.closeResultSet(rs);
		if (msg != null) {
			Print print = this.getPrintObject();
			if (print != null) {
				print.doPrint(Print.MAX, msg);
			}
		}	
	}

	/**
	 * @see M4Interface#findOperator(String)
	 */
	public Operator findOperator(String name) {
		if (name != null) {
	
			String query =
				"SELECT "   + edu.udo.cs.miningmart.m4.core.Operator.ATTRIB_OPERATOR_ID
				+ " FROM "  + edu.udo.cs.miningmart.m4.core.Operator.M4_TABLE_NAME
				+ " WHERE " + edu.udo.cs.miningmart.m4.core.Operator.ATTRIB_OPERATOR_NAME
				+ " = '" + name + "'";
		
			try {
				Long opId = this.getM4db().executeM4SingleValueSqlReadL(query);
				if (opId != null) {
					DB db = this.getM4db();
					Class targetClass = edu.udo.cs.miningmart.m4.core.Operator.class;
					return (Operator) db.getM4Object(opId.longValue(), targetClass);
				}
			}
			catch (MiningMartException e) {
				this.getPrintObject().doPrint(Print.ERROR,e.getMessage(),e);
			}
			catch (SQLException e) {
				this.getPrintObject().doPrint(Print.ERROR,e.getMessage(),e);	
			}
		}
			
		return null;
	}

	public Case findCase(String name, boolean setAsCurrent) {
		if (name != null) {
	
			String query =
				"SELECT "   + edu.udo.cs.miningmart.m4.core.Case.ATTRIB_CASE_ID
				+ " FROM "  + edu.udo.cs.miningmart.m4.core.Case.M4_TABLE_NAME
				+ " WHERE " + edu.udo.cs.miningmart.m4.core.Case.ATTRIB_CASE_NAME
				+ " = '" + name + "'";
		
			try {
				Long caId = this.getM4db().executeM4SingleValueSqlReadL(query);
				if (caId != null) {
					DB db = this.getM4db();
					Class targetClass = edu.udo.cs.miningmart.m4.core.Case.class;
					Case m4case = (Case) db.getM4Object(caId.longValue(), targetClass);
					
					// set this case as current case... this should be
					// the intended functionality:
					if (setAsCurrent){
						M4Interface.setCurrentCase(m4case);
					}
					
					return m4case;
				}
			}
			catch (MiningMartException e) {
				this.getPrintObject().doPrint(Print.ERROR,e.getMessage(),e);
			}
			catch (SQLException e) {
				this.getPrintObject().doPrint(Print.ERROR,e.getMessage(),e);	
			}
		}
			
		return null;
	}


	/**
	 * Current implementation:
	 * Reads operator names directly from the database, so it has to be taken care for
	 * writing back the cache in time!
	 * 
	 * @see M4Interface#getAllOperatorNames()
	 */
	public Collection getAllOperatorNames() {
		String query =
			"SELECT " + edu.udo.cs.miningmart.m4.core.Operator.ATTRIB_OPERATOR_NAME
			+ " FROM " + edu.udo.cs.miningmart.m4.core.Operator.M4_TABLE_NAME;
		
		Vector result = null;
		ResultSet rs = null;
		try {
			rs = this.getM4db().executeM4SqlRead(query);
			result = new Vector();
			while (rs.next()) {
				String opName = rs.getString(1);
				if (opName != null) {
					result.add(opName);
				}
			}
		}
		catch (SQLException e) {
			this.getPrintObject().doPrint(Print.ERROR,e.getMessage(),e);
		}
		catch (DbConnectionClosed e) {
			this.getPrintObject().doPrint(Print.ERROR,e.getMessage(),e);	
		}
		finally {
			this.closeResultSet(rs);
		}
		return result;
	}

	/**
	 * @see edu.udo.miningmart.m4.M4Interface#findConnection(Concept, Collection, String)
	 */
	public Collection findConnection( Concept theConcept, 
									  Collection namesOfDbObjects, 
									  String matcher) 
	throws M4Exception {
		
		// safety check:
		if (theConcept == null || namesOfDbObjects == null) {
			throw new M4Exception("M4 Interface, method 'findConnection': got NULL as first or second parameter!");
		}
		
		// check if concept is of type DB:		
		if ( ! theConcept.getType().equals(Concept.TYPE_DB)) {
			throw new M4Exception("Cannot re-connect concept '" + theConcept.getName() +
					"' to a table or view, it is not of type DB!");
		}	
		
		// create a temporary concept that contains only the DB features:
		Concept tempConWithDbFeatures = (Concept) this.getM4db().createNewInstance(Concept.class);
		tempConWithDbFeatures.setName(theConcept.getName());
		
		Iterator it = theConcept.getAllBaseAttributes().iterator();
		while (it.hasNext()) {
			BaseAttribute ba = (BaseAttribute) it.next();
			if (ba.getType().equals(BaseAttribute.TYPE_DB)) {
				ba.copy(tempConWithDbFeatures);
			}
		}
		
		// choose the matcher:
		MmSchemaMatcher myMatcher = null;
		if (matcher.equals(M4Interface.MATCHER_SIMPLE)) {
			myMatcher = new StringEqualityMatcher();
		}
		if (matcher.equals(M4Interface.MATCHER_SOUNDEX)) {
			myMatcher = new SoundexMatcher();
		}
		if (matcher.equals(M4Interface.MATCHER_EDITDISTANCE)) {
			myMatcher = new EditDistanceMatcher();
		}
		if (matcher.equals(M4Interface.MATCHER_NGRAM)) {
			int valueOfN = 3;
			myMatcher = new NgramMatcher(valueOfN);
		}
		// add other matchers here!
		if (myMatcher == null) {
			throw new M4Exception("M4InterfaceImpl.findConnection: got unknown type of matcher ('" +
					matcher + "')!");
		}		

		// We create a temporary M4 Case with only Concepts and Relations to represent 
		// the given Db objects:
		Case tempCase = this.readSchemaToMatch(namesOfDbObjects);
		
		// Attempt to match the concept to a table/view. This should always 
		// work, even if the matching is bad:
		it = tempCase.getConcepts().iterator();
		Columnset bestMatch = null;
		double bestSimilarity = -1.0;
		MatchingResult[][] myMatchingMatrix = null;
		while (it.hasNext()) {
			Concept candidateForMatching = (Concept) it.next();
			try {
				double candidateSimilarity = myMatcher.getSimilarity(tempConWithDbFeatures, candidateForMatching);
				if (candidateSimilarity >= bestSimilarity) {
					bestSimilarity = candidateSimilarity;
					bestMatch = (Columnset) candidateForMatching.getCurrentColumnSet();
					myMatchingMatrix = myMatcher.getSimilarityMatrix(tempConWithDbFeatures, candidateForMatching);
				}
			}
			catch (SchemaMatchException sme) {
				throw new M4Exception("SchemaMatchException caught when comparing concept '" +
						theConcept.getName() + "' with table '" + candidateForMatching.getName() + "': " + sme.getMessage());
			}
		}
		
		// As a second step, see if the result of any join among the given
		// Db objects gives an even better matching result:
		it = tempCase.getAllRelations().iterator();
		boolean bestMatchIsResultOfJoin = false;
		Concept from = null, to = null;
		// iterate through the relations:
		while (it.hasNext()) {
			Relation candidateRelationForJoin = (Relation) it.next();
			// create the join result:
			Columnset candidateCs = (Columnset) candidateRelationForJoin.getResultOfJoin();
			// create a concept that represents the join result:
			Concept candidateForMatching = tempCase.createConceptFromColumnset(candidateCs);
			try {
				// see how well the join result matches the given concept:
				double candidateSimilarity = myMatcher.getSimilarity(tempConWithDbFeatures, candidateForMatching);
				if (candidateSimilarity >= bestSimilarity) {
					bestSimilarity = candidateSimilarity;
					bestMatch = candidateCs;
					bestMatchIsResultOfJoin = true;
					myMatchingMatrix = myMatcher.getSimilarityMatrix(tempConWithDbFeatures, candidateForMatching);
					from = candidateRelationForJoin.getTheFromConcept();
					to = candidateRelationForJoin.getTheToConcept();
				}
			}
			catch (SchemaMatchException sme) {
				throw new M4Exception("SchemaMatchException caught when comparing concept '" +
						theConcept.getName() + "' with table '" + candidateForMatching.getName() + "': " + sme.getMessage());
			}
		}		
		
		// copy some information to the right (current) case:
		Columnset myFromCs = null, myToCs = null;
		Case currentCase = theConcept.getTheCase();
		if (bestMatchIsResultOfJoin) {
			Concept myFromCopy = from.copy(currentCase);
			Concept myToCopy = to.copy(currentCase);
			myFromCs = (Columnset) from.getCurrentColumnSet().copy(myFromCopy);
			myToCs = (Columnset) to.getCurrentColumnSet().copy(myToCopy);
			myFromCopy.deleteSoon();
			myToCopy.deleteSoon();
			// ensure that the result of the join does exist in the DB:
			this.getM4db().createSQLView(bestMatch);
		}

		// delete the temporary case:
		tempCase.deleteSoon();
		
		// safety check:
		if (bestMatch == null) {
			throw new M4Exception("M4InterfaceImpl.findConnection: Could not match concept '" + theConcept.getName() +
					"' to any of the given DB objects, nor to the result of a join among them!");			
		}
		
	  	// Now a new columnset is found for the concept.
		// Remove old connection to columnsets if it exists:		
		theConcept.setColumnSets(new Vector());
		
		// set new connection:
		Columnset csForTheConcept = (Columnset) bestMatch.copy(theConcept);
		
		// Now connect BaseAttributes to Columns. To this end,
		// first find the best match of any BA to any Column,
		// do the match, then remove the corresponding part from the matrix and continue. A greedy algo.
		while (myMatchingMatrix.length > 0 && myMatchingMatrix[0].length > 0) {
			double bestOfAllMatchingSimilarities = -1.0;
			int bestRow = 0;
			int bestColumn = 0;
			int currentNoOfRows = myMatchingMatrix.length;
			int noOfColumns = myMatchingMatrix[0].length;
			for (int n = 0; n < myMatchingMatrix.length; n++) {
				for (int m = 0; m < myMatchingMatrix[n].length; m++) {
					if (myMatchingMatrix[n][m].getSimilarity() >= bestOfAllMatchingSimilarities) {
						bestOfAllMatchingSimilarities = myMatchingMatrix[n][m].getSimilarity();
						bestRow = n;
						bestColumn = m;
					}
				}	
			}
			// do the match:
			MatchingResult result = myMatchingMatrix[bestRow][bestColumn];
			BaseAttribute baToMatch = (BaseAttribute) tempConWithDbFeatures.getBaseAttribute(result.getBaOfFirstSchema().getName());
			Column matchCandidate = (Column) result.getBaOfSecondSchema().getCurrentColumn();
			// find the corresponding BA/Col in the given concept/columnset 
			// rather than the temporary ones:
			BaseAttribute baToConnect = (BaseAttribute) theConcept.getBaseAttribute(baToMatch.getName());
			Column colToConnect = (Column) csForTheConcept.getColumn(matchCandidate.getName());
			baToConnect.removeLinkToColumns();
			baToConnect.addColumn(colToConnect);
				
			// remove this match from the matrix:
			MatchingResult[][] newMatrix = new MatchingResult[currentNoOfRows - 1][noOfColumns - 1];
			int rowIndexForNewMatrix = 0;
			for (int n = 0; n < myMatchingMatrix.length; n++) {
				int colIndexForNewMatrix = 0;
				if (n != bestRow) {
					for (int m = 0; m < myMatchingMatrix[n].length; m++) {
						if (m != bestColumn) {
							newMatrix[rowIndexForNewMatrix][colIndexForNewMatrix] = myMatchingMatrix[n][m];
							colIndexForNewMatrix++;
						}
					}
					rowIndexForNewMatrix++;
				}
			}
			myMatchingMatrix = newMatrix;			
		}
		
		// delete the temporary concept and columnset:
		tempConWithDbFeatures.deleteSoon();
		bestMatch.deleteSoon();
		
		// return the right things:
		if (bestMatchIsResultOfJoin) {
			Vector ret = new Vector();
			ret.add(myFromCs);
			ret.add(myToCs);
			return ret;			
		}
		else {
			return null;
		}
	}

	// creates a Case whose concepts and relations represent the DB Schema
	// given by the given list of tables/views
	private Case readSchemaToMatch(Collection tableNames) throws M4Exception {
		try {
			if (tableNames != null) {				
				boolean setAsCurrentCase = false;
				Case newCase = this.createCase("__TemporaryCaseWithSchemaToMatch", setAsCurrentCase);
				
				// iterate through names of tables/views:
				Iterator namesIt = tableNames.iterator();
				boolean conceptsCreated = false;
				while (namesIt.hasNext()) {
					String tableName = (String) namesIt.next();
					
					if (tableName.indexOf("$") == -1) {
						if ( ! this.conceptExistsInCase(newCase, tableName)) {								
							if ( ! this.checkForManyToManyRelation(tableName, newCase)) {
								newCase.createConceptAndRelationsFromTables(tableName);
							}
							conceptsCreated = true;
						}
					}
				}
				if (conceptsCreated) {
					return newCase;
				}
				else {
					return null;
				}
			}
			return null;
		}
		catch (M4Exception e) {
			throw new M4Exception("M4 error when trying to create a temporary Case representing a part of the business schema: " + e.getMessage());
		}
	}	

	private boolean conceptExistsInCase(Case theCase, String nameOfConcept) throws M4Exception {
		Iterator namesIt = theCase.getAllConceptNames().iterator();
		while (namesIt.hasNext()) {
			String oneConceptName = (String) namesIt.next();
			if (oneConceptName.equalsIgnoreCase(nameOfConcept)) {
				return true;
			}
		}
		return false;
	}
	
	/*
	 * Returns TRUE iff the given db object can be treated as a cross table.
	 * In that case all needed concepts, columnsets and many-to-many relations
	 * are created! Otherwise nothing is done and false is returned.
	 */
	private boolean checkForManyToManyRelation(String dbTableName, Case theCase) throws M4Exception {
		try {
			if (this.getM4db().isCrossTable(dbTableName)) {				
				theCase.createManyToManyRelationsFromCrossTable(dbTableName);
				return true;
			}
			else {
				return false;
			}
		}
		catch (SQLException sqle) {
			 throw new M4Exception("SQL error occurred when trying to see if '" +
			 		dbTableName + "' is a cross table: " + sqle.getMessage());			 
		}
		catch (DbConnectionClosed dbc) {
			 throw new M4Exception("Suddenly closed connection to DB when trying to see if '" +
			 		dbTableName + "' is a cross table: " + dbc.getMessage());				
		}
	}
	
	/*
	 * @see M4Interface#executeSQLQC1(String, int)
	 *
	public List executeSQLQC1(String sqlString, int maxRows)
		throws SQLException
	{
      Map tableData          = executeSQLQ(sqlString, maxRows);
      List columnNames       = (List)tableData.get(MM_COOLUMN_NAMES);
      String firstColumnName = columnNames.get(0).toString();
      List firstColumnData   = (List)tableData.get(firstColumnName);
      return firstColumnData;
	}

	/*
	 * @see M4Interface#executeSQLQ(String, int)
	 *
	public Map executeSQLQ(String sqlString, int maxRows) throws SQLException {
		HashMap aMap = new HashMap();
		ResultSet rs = null;
		try {
			rs = this.getM4db().executeM4SqlRead(sqlString);
			ResultSetMetaData rsmd = rs.getMetaData();
			int numColumns = rsmd.getColumnCount();
			int rowNum = 0;
			while (rs.next() && rowNum < maxRows) {
				for (int i = 1; i <= numColumns; i++) {
					final String columnName = rsmd.getColumnName(i);
					List aList = (List) aMap.get(columnName);
					if (aList == null) {
						aList = new ArrayList();
						aMap.put(columnName, aList);
					}
					aList.add(rs.getObject(i));
				}
				rowNum++;
			}
			ArrayList cList = new ArrayList();
			for (int i = 1; i <= numColumns; i++) {
				String columnName = rsmd.getColumnName(i);
				cList.add(columnName);
			}
			aMap.put(MM_COOLUMN_NAMES, cList);
		}
		catch (DbConnectionClosed e) {
			throw new SQLException("DbConnection closed when calling M4InterfaceImpl.executeSQLQ(String, int)");
		}
		finally {
			DB.closeResultSet(rs);
		}
		return aMap;
	}

	/*
	 * @see M4Interface#executeSQLU(String)
	 *
	public void executeSQLU(String sqlString) throws SQLException {
		if (sqlString != null) {
			try {
				this.getM4db().executeM4SqlWrite(sqlString);
			}
			catch (DbConnectionClosed e) {
				throw new SQLException(
					"DbConnection closed in M4InterfaceImpl.executeSQLU(String):\n"
					+ e.getMessage());	
			}
		}
	} */


	// ***** Methods handling the case locking mechanism *****

	private static final String TABLE_NAME  = "M4_ACCESS_T";
	private static final String ATTR_ID     = "OBJECT_ID";
	private static final String ATTR_TYPE   = "OBJECT_TYPE";
	private static final String ATTR_CLIENT = "CLIENT_NAME";
	private static final String ATTR_ACCESS = "ACCESS_TYPE"; 

	private static final String ACCESS_STRING_WRITE = "WRITE";
	private static final String ACCESS_STRING_READ  = "READ";
	
	/**
	 * @return a <code>Collection</code> of <code>String</code>s representing
	 * client names having just read locks to the specified case
	 */
	private Collection readAccessClients(String caseName) throws SQLException {			
		String query = "SELECT " + ATTR_CLIENT
				     + " FROM " + TABLE_NAME
				     + " WHERE " + ATTR_ID + " = '" + caseName + "'"
				     + " AND " + ATTR_TYPE + " = 'CASE'"
				     + " AND " + ATTR_ACCESS + " = '" + ACCESS_STRING_READ + "'";
		
		return this.resultCollection(query);
	}

	/**
	 * @return a <code>Collection</code> of <code>String</code>s representing
	 * client names having write locks to the specified case
	 */
	private Collection writeAccessClients(String caseName) throws SQLException {			
		String query = "SELECT " + ATTR_CLIENT
				     + " FROM " + TABLE_NAME
				     + " WHERE " + ATTR_ID + " = '" + caseName + "'"
				     + " AND " + ATTR_TYPE + " = 'CASE'"
				     + " AND " + ATTR_ACCESS + " = '" + ACCESS_STRING_WRITE + "'";
		
		return this.resultCollection(query);
	}

	/** Helper method of read/writeAccessClients */
	private Collection resultCollection(String query) throws SQLException {
		Collection result = new Vector();
		ResultSet rs = null;
		try {
			rs = this.getM4db().executeM4SqlRead(query);
			String name;
			while (rs.next() && (name = rs.getString(1)) != null) {
				result.add(name);
			}
		}
		catch (DbConnectionClosed e) {
			throw new SQLException(e.getMessage());
		}
		finally {
			this.closeResultSet(rs);
		}		
		return result;		
	}

	private void addM4AccessTuple(String caseName, String client, boolean forWriting) {	
		String query = "INSERT INTO " + TABLE_NAME
					 + " ( " + ATTR_ID + ", " + ATTR_TYPE + ", " + ATTR_CLIENT + ", " + ATTR_ACCESS + " )"
				     + " VALUES ( '" + caseName + "', 'CASE', '" + client + "', '"
				     + (forWriting ? ACCESS_STRING_WRITE : ACCESS_STRING_READ) + "' )";

		try {
			this.getM4db().executeM4SqlWrite(query);
		}
		catch (DbConnectionClosed e) {
			this.getPrintObject().doPrint(Print.ERROR,e.getMessage(),e);
		}
		catch (SQLException e) {
			this.getPrintObject().doPrint(Print.ERROR,e.getMessage(),e);
		}
	}

	private void removeM4AccessTuple(String caseName, String client) {	
		String query = "DELETE FROM " + TABLE_NAME
					 + " WHERE " + ATTR_ID + " = '" + caseName + "'"
					 + " AND "   + ATTR_TYPE + " = 'CASE'"
					 + " AND "   + ATTR_CLIENT + " = '" + client + "'";
		try {
			this.getM4db().executeM4SqlWrite(query);
		}
		catch (DbConnectionClosed e) {
			this.getPrintObject().doPrint(Print.ERROR,e.getMessage(),e);
		}
		catch (SQLException e) {
			this.getPrintObject().doPrint(Print.ERROR,e.getMessage(),e);
		}
	}
	
	/**
	 * @see M4Interface#canRemoveM4Access(String)
	 */
	public boolean canRemoveM4Access(String caseName){
	
		try{
			if (isCaseLockedForReading(caseName)){
		
				Collection writeClients = this.writeAccessClients(caseName);
				if ((writeClients.contains(this.getUserName()))&&(writeClients.size()==1)){
					return true;
				}
				return false;
		
			}else if (isCaseLockedForWriting(caseName)){
				Collection readClients = this.readAccessClients(caseName);
				if ((readClients.contains(this.getUserName()))&&(readClients.size()==1)){
					return true;
				}
				return false;
			}
		}
		catch (SQLException e) {
			this.getPrintObject().doPrint(Print.ERROR,e.getMessage(),e);
		}
		return false;
	}
	

	/**
	 * @see M4Interface#isCaseLockedForReading(String)
	 */
	public boolean isCaseLockedForReading(String caseName) {
		try {
			Collection c = this.writeAccessClients(caseName);
			return ((c != null) && (c.size() > 0));
		}
		catch (SQLException e) {
			this.getPrintObject().doPrint(Print.ERROR,e.getMessage(),e);
		}
		return true;
	}

	/**
	 * @see M4Interface#isCaseLockedForWriting(String)
	 */
	public boolean isCaseLockedForWriting(String caseName) {
		try {
			if (isCaseLockedForReading(caseName)) {
				return true;	
			}
			Collection c = this.readAccessClients(caseName);
			return ((c != null) && (c.size() > 0));
		}
		catch (SQLException e) {
			this.getPrintObject().doPrint(Print.ERROR,e.getMessage(),e);
		}
		return true;
	}

	/**
	 * @see M4Interface#findCaseForReadOnlyAccess(String, boolean)
	 */
	public Case findCaseForReadOnlyAccess(String name, boolean setAsCurrent)
		throws CaseLockedException
	{	
		if (!isCaseLockedForReading(name)) {
			this.addM4AccessTuple(name, this.getUserName(), false);
			return findCase(name,setAsCurrent);
		}
		else throw new CaseLockedException();
	}

	/**
	 * @see M4Interface#findCaseForUpdate(String, boolean)
	 */
	public Case findCaseForUpdate(String name, boolean setAsCurrent)
		throws CaseLockedException
	{
		if (!isCaseLockedForWriting(name)) {
			this.addM4AccessTuple(name, this.getUserName(), true);
			return findCase(name,setAsCurrent);
		}
		else throw new CaseLockedException();
	}

	/**
	 * @see M4Interface#releaseCase(String)
	 */
	public void releaseCase(String name) {
		try {
			this.removeM4AccessTuple(name, this.getUserName());
			Case m4case = this.findCase(name,false);
			if (m4case == null) {
				m4case = getCurrentCase();
			} 
			if (m4case != null) {
				((edu.udo.cs.miningmart.m4.core.Case) m4case).store();

			}
			this.getM4db().clearM4Cache();
		}
		catch (M4Exception e)   {
			this.getPrintObject().doPrint(Print.ERROR,e.getMessage(),e);
		}
		// no chance to throw an exception here
	}

	/**
	 * @see M4Interface#releaseCaseWithoutStoring(String)
	 */
	public void releaseCaseWithoutStoring(String name) {
		this.removeM4AccessTuple(name, this.getUserName());
		try {
			this.getM4db().commitM4Transactions();
		}
		catch (DbConnectionClosed bdc) {
			this.getPrintObject().doPrint(Print.ERROR, bdc.getMessage());
		}
		catch (SQLException e) {
			this.getPrintObject().doPrint(Print.ERROR, e.getMessage());
		}
		this.getM4db().clearM4Cache();
		// no chance to throw an exception here
	}
	
	/** @return the user name stored as a system property */
	private String getUserName() {
		return System.getProperty("user.name");
	}

	/** Caches an instance of OperatorGroup, because one instance should be sufficient. */
	private OperatorGroup opGroup;

	/** @return an object of class OperatorGroup */
	public OperatorGroup getOperatorGroup() {
		if (opGroup != null) {
			return opGroup;	
		}
		
		DB db = this.getM4db();
		if (db != null) {
			try {
				opGroup = new edu.udo.cs.miningmart.m4.core.OperatorGroup(db);
			}
			catch (Exception e) {
				if (getCurrentCase() != null) {
					((edu.udo.cs.miningmart.m4.core.Case) getCurrentCase()).doPrint(e);
				}
				else {
					System.err.println("M4InterfaceImpl.getOperatorGroup():\n");
					e.printStackTrace();
				}
			}
		}
		return opGroup;
	}

}
/*
 * Historie
 * --------
 * 
 * $Log: M4InterfaceImpl.java,v $
 * Revision 1.12  2006/04/11 14:10:14  euler
 * Updated license text.
 *
 * Revision 1.11  2006/04/06 16:31:14  euler
 * Prepended license remark.
 *
 * Revision 1.10  2006/03/19 17:00:37  scholz
 * refactoring
 *
 * Revision 1.9  2006/03/02 16:49:59  euler
 * Many bugfixes
 *
 * Revision 1.8  2006/01/12 09:41:39  euler
 * Added commit to method releaseCaseWithoutStoring
 *
 * Revision 1.7  2006/01/06 16:39:55  euler
 * Small bugfix
 *
 * Revision 1.6  2006/01/06 16:25:04  euler
 * Updates and bugfixes in the delete-Mechanism for M4Data objects.
 *
 * Revision 1.5  2006/01/03 16:19:38  euler
 * Bugfixes
 *
 * Revision 1.4  2006/01/03 14:51:34  euler
 * Two new schema matchers.
 *
 * Revision 1.3  2006/01/03 10:55:53  euler
 * Fixed wrong imports.
 *
 * Revision 1.2  2006/01/03 10:43:50  euler
 * Bugfixes
 *
 * Revision 1.1  2006/01/03 09:54:18  hakenjos
 * Initial version!
 *
 */
