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

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.sql.SQLException;
import java.util.Collection;
import java.util.Iterator;

import edu.udo.cs.miningmart.db.ConfigReader;
import edu.udo.cs.miningmart.db.DB;
import edu.udo.cs.miningmart.exception.M4CompilerError;
import edu.udo.cs.miningmart.exception.M4CompilerWarning;
import edu.udo.cs.miningmart.exception.M4Exception;
import edu.udo.cs.miningmart.exception.UserError;
import edu.udo.cs.miningmart.m4.BaseAttribute;
import edu.udo.cs.miningmart.m4.Columnset;
import edu.udo.cs.miningmart.m4.Concept;
import edu.udo.cs.miningmart.m4.EstimatedStatistics;
import edu.udo.cs.miningmart.m4.M4Interface;
import edu.udo.cs.miningmart.m4.Step;
import edu.udo.cs.miningmart.m4.Value;

/**
 * This operator creates a YALE experiment file (XML) to ease
 * the combination of MiningMart preprocessing and YALE learning.
 * The YALE experiment created by this operator will contain a single
 * YALE operator, an instance of DatabaseExampleSource, which reads 
 * the data for subsequent YALE operations from the Columnset specified
 * by the InputConcept of this operator. If the InputConcept is connected
 * to more than one Columnset, the YALE experiment chain will include
 * an ExampleSetIterator which runs the YALE experiment for each input,
 * ie here for the data of each Columnset.
 * 
 * @author Timm Euler
 * @version $Id: PrepareForYale.java,v 1.7 2006/04/11 14:10:10 euler Exp $
 */
public class PrepareForYale extends ExecutableOperator {

	// parameter names for this operator:
	private static final String PARAMETER_INPUT_CONCEPT   = "TheInputConcept";
	private static final String PARAMETER_PRIMARY_KEY     = "ThePrimaryKey";
	private static final String PARAMETER_LABEL           = "TheLabel";
	private static final String PARAMETER_EXPERIMENT_FILE = "ExperimentFileName";
	private static final String PARAMETER_QUERY_FILE      = "QueryFilePrefix"; // used if more than one input columnset
	
	/**
	 * Creates XML output that can be read by the YALE software.
	 * 
	 * @see miningmart.compiler.operator.ExecutableOperator#createStatement(boolean)
	 */
	public void createStatement(boolean lazy)
		throws SQLException, M4CompilerError {
			
		// get the columnsets attached to input concept:
		Collection colsets;
		try {
		    colsets = this.getTheInputConcept().getColumnSets();
		}
		catch (M4Exception m4e) {
			throw new M4CompilerError("Operator PrepareForYale: M4Exception caught when reading the " +
			                          "Columnsets for the input concept: " + m4e.getMessage());
		}
		if ((colsets == null) || colsets.isEmpty()) {
			throw new M4CompilerError("Operator PrepareForYale: No Columnsets found in input concept!");
		}
		
		// if there is more than one columnset, create query files:
		if (colsets.size() > 1 && ( ! lazy)) {
			this.createQueryFiles(colsets);
		}
		
		// write the YALE experiment file:
		this.createYaleFile(colsets, lazy);
	}

	/**
	 * This method is overridden in order to switch off multiple runs of the operator
	 * due to multistep support in the compiler's control structure.
	 * You should not change this part, unless you know what you are doing!
	 * @throws UserError
	 * @throws M4CompilerError
	 * @throws SQLException
	 * @throws M4Exception
	 * @see edu.udo.cs.miningmart.m4.core.operator.Operator#execute(boolean)
	 */
	public void execute(boolean lazy) throws UserError, M4CompilerWarning, M4CompilerError, M4Exception, SQLException {
		super.execute(lazy);
		try {
			if (!lazy) {
				Concept inputConcept = this.getTheInputConcept();
				while (inputConcept.hasNextColumnSet())
					inputConcept.getNextColumnSet();
			}
		}
   		catch (M4Exception m4e)
   		{   throw new M4CompilerError("M4 interface error in 'execute' method in PrepareForYale: " + m4e.getMessage());  } 
	}

	/**
	 * Empty method in this operator.
	 * 
	 * @see miningmart.compiler.operator.ExecutableOperator#compileStatement()
	 */
	public void compileStatement() throws SQLException, M4CompilerError {
	}

    /**
     * @see edu.udo.cs.miningmart.operator.ExecutableOperator#estimateStatistics(Step)
     */
    public EstimatedStatistics estimateStatistics(Step theStep) throws M4Exception {
    	return null;
    }
	/*
	 * If there are multiple columnsets attached to the input concept,
	 * create a query file for each. Each file will be read in one "loop"
	 * of the YALE operator 'IteratingOperatorChain'.
	 */
	private void createQueryFiles(Collection columnsets) throws M4CompilerError {
		String filePrefix = this.getQueryFilePrefix();		
		if (filePrefix == null) {
			throw new M4CompilerError("Operator PrepareForYale: found more than one columnset " +
			                          "attached to input concept; need Parameter '" +
			                          PARAMETER_QUERY_FILE + "'!");
		} 
		
		// loop through the columnsets:
		Iterator it = columnsets.iterator();
		int suffix = 1;
		try {
			while (it.hasNext()) {
				Columnset cs = (Columnset) it.next();
				PrintStream out = new PrintStream(new FileOutputStream(filePrefix + suffix));
				out.println(this.getQuery(cs));
				out.close();
				suffix++;
			}
		}
		catch (IOException ioe) {
			throw new M4CompilerError("Operator PrepareForYale: query file prefix not valid; IO error: " + ioe.getMessage());
		}
	}
	
	private String getQuery(Columnset cs) throws M4CompilerError {
		if (cs == null) {
			throw new M4CompilerError("Operator PrepareForYale: columnset for query creation was null!");
		}
		try {
			String theQuery = cs.getCompleteSQLQuery();
			
			// sometimes theQuery includes virtual column definitions;
			// then we need to replace XML-reserved names:
			theQuery = theQuery.replaceAll("<", "&lt;");
			theQuery = theQuery.replaceAll(">", "&gt;");
			
			return theQuery;
		}
		catch (M4Exception m4e) {
			throw new M4CompilerError("Operator PrepareForYale: could not generate query for columnset '" +
			                          cs.getName() + "': " + m4e.getMessage());
		}
	}
	
	private void createYaleFile(Collection colsets, boolean lazy) throws M4CompilerError {
		
		boolean useIteratingOperatorChain = (colsets.size() > 1) && ( ! lazy);		
		PrintStream out = this.getPrinterToExperimentFile();
		
		// file header:
		out.println("<!-- This file was created by the MiningMart operator 'PrepareForYale'. -->");
		out.println("<operator name=\"Root\" class=\"Experiment\">");
		
		// use iteration if there is more than one input columnset:
		String indent;
		if (useIteratingOperatorChain) {
			out.println("  <operator name=\"IterateOverColumnsets\" class=\"IteratingOperatorChain\">");
			indent = "    ";
			this.addParameterToYaleFile(out, indent, "start", "1");
			this.addParameterToYaleFile(out, indent, "end", "" + colsets.size()); 
		}
		else {
			indent = "  ";
		}
		
		// insert DatabaseExampleSource operator:
		out.println(indent + "<operator name=\"ReadDataFromDB\" class=\"DatabaseExampleSource\">");
		
		indent = indent + "  ";
		
		// database connection parameters:
		String urlprefix, dbname, username, passw;
		try {
		    String dbConfigFileName = System.getProperty(M4Interface.SYSTEM_PROP_DB_CONFIG_PATH);
			ConfigReader cr = new ConfigReader(dbConfigFileName);
			
			// read Business Db infos:
			dbname = cr.getBusDbName();
			username = cr.getBusUser();
			passw = cr.getBusPw();
			urlprefix = cr.getBusUrl();
		}
		catch (M4Exception m4e) {
			throw new M4CompilerError("Operator PrepareForYale: " + m4e.getMessage());
		}
		String driver = null;
		if (this.getM4Db().getBusinessDbms() == DB.ORACLE) {
			driver = "oracle.jdbc.driver.OracleDriver";
		}
		if (this.getM4Db().getBusinessDbms() == DB.POSTGRES) {
			driver = "org.postgresql.Driver";
		}
		this.addParameterToYaleFile(out, indent, "driver", driver);
		this.addParameterToYaleFile(out, indent, "urlprefix", urlprefix);
		this.addParameterToYaleFile(out, indent, "databasename", dbname);
		this.addParameterToYaleFile(out, indent, "username", username);
		this.addParameterToYaleFile(out, indent, "password", passw);
		
		String label, primkey;
		if ((label = this.getNameOfBaParam(PARAMETER_LABEL)) != null) {
			this.addParameterToYaleFile(out, indent, "label_attribute", label.toUpperCase());
		}
		if ((primkey = this.getNameOfBaParam(PARAMETER_PRIMARY_KEY)) != null) {
			this.addParameterToYaleFile(out, indent, "id_attribute", primkey.toUpperCase());
		}
		// use either a query or a query file:
		if (useIteratingOperatorChain) {
			this.addParameterToYaleFile(out, indent, "query_file", this.getQueryFilePrefix() + "%a");
		}
		else {
			try {
				this.addParameterToYaleFile(out, indent, "query", 
				                            this.getQuery(this.getTheInputConcept().getCurrentColumnSet()));
			}
			catch (M4Exception m4e) {
				throw new M4CompilerError("Operator PrepareForYale: M4 exception caught " +
				                          "when accessing current input columnset: " + m4e.getMessage());
			}
		}
		
		indent = indent.substring(0, indent.length()-2);		
		out.println(indent + "</operator>");
		
		// file footer:
		if (useIteratingOperatorChain) {			
			out.println("  </operator>");
		}
		out.println("</operator>");
	}
	
	private void addParameterToYaleFile(PrintStream out, String indent, String key, String value) {
		if (value == null) {
			value = "UNKNOWN";
		}
		out.println(indent + "<parameter key=\"" + key + "\" value=\"" + value + "\"/>");
	}
	
    private Concept getTheInputConcept() throws M4CompilerError {
       return (Concept) this.getSingleParameter(PARAMETER_INPUT_CONCEPT);
    }
    
    private PrintStream getPrinterToExperimentFile() throws M4CompilerError {
    	Value v = (Value) this.getSingleParameter(PARAMETER_EXPERIMENT_FILE);
    	if (v == null) {
    		throw new M4CompilerError("Operator PrepareForYale: Parameter '" +
    		                          PARAMETER_EXPERIMENT_FILE + "' not found!");
    	}
    	String nameOfFile = v.getValue();
    	if (nameOfFile == null || nameOfFile.equals("")) {
    		throw new M4CompilerError("Operator PrepareForYale: Parameter '" +
    		                          PARAMETER_EXPERIMENT_FILE + "' not found!");
    	}
		PrintStream out;
    	try {
		    out = new PrintStream(new FileOutputStream(nameOfFile));
    	} 
    	catch (IOException ioe) {
    		throw new M4CompilerError("Operator PrepareForYale: Filename found in Parameter '" +
    		                          PARAMETER_EXPERIMENT_FILE + "' is not valid!");
    	}
 		
 		return out;
    }
    
    /*
     * If the input concept has only one columnset, a query can be used in YALE.
     * Otherwise a query file has to be used. Therefore the parameter QueryFilePrefix
     * is optional here.
     */
    private String getQueryFilePrefix() throws M4CompilerError {
    	Value v = (Value) this.getSingleParameter(PARAMETER_QUERY_FILE);
    	if (v == null || v.getValue() == null || v.getValue().equals("")) {
    		return null;
    	}
    	return v.getValue();
    }
    
    private String getNameOfBaParam(String paramName) throws M4CompilerError {
    	BaseAttribute ba = (BaseAttribute) this.getSingleParameter(paramName);
    	if (ba == null) {
    		return null;
    	}
    	try {
    		return ba.getCurrentColumn().getName();
    	}
    	catch (NullPointerException npe) {
    		throw new M4CompilerError("Operator PrepareForYale: found no column attached to BA specified by parameter '" + 
    		                          paramName + "'!");
    	}
    	catch (M4Exception m4e) {
    		throw new M4CompilerError("Operator PrepareForYale: M4 error accessing BA specified by parameter '" + 
    		                          paramName + "': " + m4e.getMessage());
    	}
    }
}
/*
 * Historie
 * --------
 * 
 * $Log: PrepareForYale.java,v $
 * Revision 1.7  2006/04/11 14:10:10  euler
 * Updated license text.
 *
 * Revision 1.6  2006/04/06 16:31:10  euler
 * Prepended license remark.
 *
 * Revision 1.5  2006/03/23 11:13:44  euler
 * Improved exception handling.
 *
 * Revision 1.4  2006/03/19 21:17:13  scholz
 * refactoring
 *
 * Revision 1.3  2006/03/19 17:00:37  scholz
 * refactoring
 *
 * Revision 1.2  2006/01/18 16:58:58  euler
 * Added some basic estimations of statistics.
 * Will need improvements.
 *
 * Revision 1.1  2006/01/03 09:54:21  hakenjos
 * Initial version!
 *
 */
