/* ======================================================================
   Parts Copyright 2006 University of Leeds, Oxford University, University of the Highlands and Islands.

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.

====================================================================== */

package org.bodington.server.userimport;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.Properties;
import org.apache.log4j.Logger;
import org.bodington.pool.ObjectPoolException;
import org.bodington.server.BuildingServer;
import org.bodington.server.BuildingServerException;
import org.bodington.server.realm.PassPhrase;


/** Creates and saves new users and groups. <strong>This should NOT be used
directly!</strong> Use ImportManager instead.
<p>
<br/>$HeadURL: https://svn.oucs.ox.ac.uk/sysdev/src/u/usrgrpgen/tags/2.0-3/uk/ac/ox/usrgrpgen/UserGroupImporter.java $
<br/>$LastChangedRevision:3331 $
<br/>$LastChangedDate:2006-11-14 10:28:09 +0000 (Tue, 14 Nov 2006) $
<br/>$LastChangedBy:buckett $
</p>
@author	Mats Henrikson
@version $LastChangedRevision:3331 $
*/
public class UserGroupImporter implements Runnable {


	/** The Logger instance to use. */
	private static final Logger log = Logger.getLogger(UserGroupImporter.class);
	/** Flag that is checked on every loop to see if we should quit. */
	private boolean keepRunning = true;
	/** Flag set to true whenever an import is going on. */
	private boolean isRunning = false;
	/** Flag set to false if we should VACUUM the database once in a while. */
	private boolean doVacuum = true;
	/** The InformationResource to get the GeneratedUsers from. */
	private InformationResource ir;
	/** The GroupManager that creates groups. */
	private GroupManager grpmgr;
	/** The username to save new groups as. */
	private String userName;
	/** The number of processed users. */
	private int processedUsers;


	/** Initialise a new instance of an importer.
	<p>The properties object needs to have all the keys used to initialise the
	UserGroupImporter, the InformationResource implementation, the
	GeneratedUser implementation, and the GeneratedGroup implementation.</p>
	<p>These are the keys that it expects in the properties object:
	<dl>
	<dt><strong>usrgrpgen.informationresource.impl</strong></dt>
	<dd>The value should be the class name to use as an implementation for the
	org.bodington.server.userimport.InformationResource interface. This is what generates
	the users we are adding. Read the documentation for your specific
	implementation class to see what keys it in turn expects to get from the
	properties object.</dd>
	<dt><strong>usrgrpgen.username</strong></dt>
	<dd>The username to create the users and groups as, usually something like
	sysadmin.</dd>
	<dt><strong>usergrpgen.vacuum</strong></dt>
	<dd>If set to <code>false</code> then we don't attempt to do VACUUM 
	commands when importing users. Defaults to true.</dd>
	</dl>
	</p>
	@param	props	A properties instance to use.
	@throws	ConfigurationException	Throws a CE if it can't find all the
			parameters it needs to initialise everything.
	*/
	protected UserGroupImporter(Properties props)
			throws ConfigurationException {

	    doVacuum = !props.getProperty("usrgrpgen.vacuum", "true").equalsIgnoreCase("false");
	    
	    //	  init InformationResource 
		String irClassName =
				props.getProperty("usrgrpgen.informationresource.impl");
		
		if(irClassName == null) {
			throw new ConfigurationException("The "
					+"'usrgrpgen.informationresource.impl' property was not "
					+"found.");
		}
		try {
			ir = (InformationResource)Class.forName(irClassName)
					.newInstance();
		}
		catch(Exception e) { throw new ConfigurationException(e); }
		userName = props.getProperty("usrgrpgen.username");
		if(userName == null) {
			throw new ConfigurationException("The 'usrgrpgen.username' property"
					+" was not found.");
		}
		ir.init(props);
		grpmgr = ir.getGroupManager(props);
	}


	/** Start a Bodington context for this thread.
	@return	Returns true if it everything went ok.
	@throws	ConfigurationException	Throws a CE if something was wrong with the
			config, which made it impossible to start Bodington.
	*/
	private boolean startBodContext() throws ConfigurationException {
		BuildingServer server = BuildingServer.getInstance();

		/* The below should never happen when running in bodington, only when
		 * running in an OfflineGenerator. */
		if(server == null) {
			log.fatal("No BuildingServer running.");
			return false;
		}
		server.startContext();
		
		try {
			/* Need to set a user for the session to be able to set an ACL
			 * resource on new groups. */
			PassPhrase p = PassPhrase.findPassPhraseByUserName(userName);
			server.getContext().setUser(p.getUser());
		}
		catch(BuildingServerException bse) {
			throw new ConfigurationException("It was not possible to initialise"
					+" Bodington, caught an exception when trying to set the "
					+"context user to '"+userName+"'.", bse);
		}

		if(server.getContext().getUser() == null) {
			throw new ConfigurationException("The user to save groups as does "
					+"not exist, please specify a valid user.");
		}

		log.info("Bodington context started.");
		return true;
	}


	/** End the bodington context for this thread. */
	private void endBodContext() {
		BuildingServer server = BuildingServer.getInstance();
		server.endContext();
		// server.shutdown();
		log.info("System is shut down at "+new Date());
	}


	/** Method required as a result of implementing Runnable. This is where
	the real work takes place with the main loops in here. */
	public void run() {
		String id = null;
		try {
			if(keepRunning && !isRunning) {
				isRunning = true;
				keepRunning = startBodContext();
			}
			else { return; }

			/* Get an iterator for the users, this may take a while depending
			 * on the InformationResource implementation. */
			Iterator users = ir.userIterator();

			// this is where all the main work goes on
			commitVacuum();
			for(processedUsers = 0; keepRunning && users.hasNext();
					processedUsers++) {
				GeneratedUser user = null;
				try {
					user = (GeneratedUser)users.next();
					id = user.getUniqueID();
					if(!user.load()) { user.create(); }
					user.update();
                    user.save(); // An unsaved user can't be added to groups.
					grpmgr.createAllGroups(user);
					grpmgr.putInGroups(user);
                    user.save();
				}
				catch(GroupCreationException gce) {
					log.error("Could not create certain groups needed to save "
							+"the user "
							+(user==null?"(unknown)":user.getUniqueID())
							+". The exception below may give more info:\n"
							+getExceptionLog(gce));
				}
                catch (UserCreationException uce) {
                    log.error("Could not create user "+ user+ " "+ uce.getMessage());
                }
                catch (BuildingServerException bse) {
                	log.error("Serious problem trying to update: "+ user, bse);
                }
                // only commit and vacuum every 500 users
				if(processedUsers % 500 == 0) { commitVacuum(); }
			}
			commitVacuum();
			log.info("Importer Completed. Users Processed: "+ getProcessedUsers());
		}
		// catch any exception thrown here to save the bodington system
		catch(Exception e) {
			log.fatal(
					"Caught an exception when running, user and group importing"
					+" has been stopped, please investigate and rectify the "
					+"problem before running the importer again. The user "
					+"under consideration was "+id+". The following "
					+"exception trace might give more info on the problem:\n"
					+getExceptionLog(e));
		}
		finally {
			// make sure this is always done last!
			endBodContext();
			ir = null;
			grpmgr = null;
			isRunning = false;
		}
	}


	/** Commit the changes to the database and does a vacuum. It is possible
	that the commit is not necessary (quite likely really), but do it anyway as
	it doesn't hurt.
	@throws	ObjectPoolException	Throws an OPE if something goes wrong.
	@throws	SQLException		Throws an SQLE if something goes wrong.
	*/
	private void commitVacuum() throws ObjectPoolException, SQLException {
	    if (!doVacuum) {
	        log.debug("vacuum is turned off");
	        return;
	    }
		long start = System.currentTimeMillis();
		Connection con = BuildingServer.getInstance().getContext()
				.getConnection();
		con.commit();
		con.createStatement().execute("END;");
		con.createStatement().execute("VACUUM;");
		long end = System.currentTimeMillis();
		log.debug("commit/vacuum took "+(end-start)+" milliseconds");
	}


	/** Gets an error log from an exception, and all it's nested causes.
	@param	exception	The exception to get the error log from.
	@return	Returns a String with the stack trace and all messages starting from
			the exception the method was called with, and then all the nested
			exceptions.
	*/
	public static String getExceptionLog(Throwable exception) {
		StringWriter trace = new StringWriter();
		PrintWriter writer = new PrintWriter(trace);
		do { exception.printStackTrace(writer); }
		while((exception = exception.getCause()) != null);
		return trace.toString();
	}



	/** Stop a currently running import dead in it's tracks*. Note that this
	method may return before the import has actually stopped.
	<p>* Not strictly true, it will actually stop it nicely at the next
	convenient opportunity, but dieDieDie() is easier to remember than
	pleaseStopAtNextConvenientOpportunity().</p> */
	public void dieDieDie() { keepRunning = false; }


	/** Returns true if there is currently an import going on.
	@return	Returns true if there is currently an import going on.
	*/
	public boolean isRunning() { return isRunning; }

	/**
	 * Check to see if the importer is trying to shutdown.
	 * @return True if the importer is shutting down.
	 */
	public boolean isInShutdown() { return !keepRunning; }

	/** Returns the number of users that have been processed so far.
	@return	The total number of users processed so far.
	*/
	public int getProcessedUsers() { return processedUsers; }


	/** Returns the number of new groups that have been created.
	@return	The number of new groups created, this will return -1 if there is no
			GroupManager to ask.
	*/
	public int getCreatedGroups() {
		if(grpmgr == null) { return -1; }
		return grpmgr.getCreatedGroups();
	}


	/** Returns the number of new resources that have been created.
	@return	The number of new resources created, this will return -1 if there is
			no GroupManager to ask.
	*/
	public int getCreatedResources() {
		if(grpmgr == null) { return -1; }
		return grpmgr.getCreatedResources();
	}
}
