/**
 * 
 */
package org.biomoby.registry.rdfagent.util;

import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.Vector;

import org.biomoby.shared.MobyException;
import org.biomoby.shared.MobyService;
import org.biomoby.shared.extended.ServiceInstanceParser;

import com.hp.hpl.jena.rdf.model.Model;
import com.hp.hpl.jena.rdf.model.ModelFactory;
import com.hp.hpl.jena.shared.JenaException;

/**
 * This class retrieves the RDF from a url. Moreover, depending on the constants
 * in org.biomoby.registry.rdfagent.util.Constants, the registries data store
 * may be updated to reflect the success or failure of retrieval.
 * 
 * @author Eddie created Feb 1, 2006
 */
@SuppressWarnings("unchecked")
public class SignatureURLConnection {

	private String newline = System.getProperty("line.separator");

	// map of url to count
	private Map badURLs = new HashMap();

	private Vector invalidServicesFound = new Vector();

	/**
	 * Constructor
	 * 
	 */
	public SignatureURLConnection() {

	}

	/**
	 * 
	 * @param url
	 *            the url that is used to build the RDF model
	 * @return the model created by parsing the entity described by the url.
	 */
	public Model getRdfFromURL(String url) {
		Log.info("Trying to retrieve the RDF document located at " + url);
		Model model = ModelFactory.createDefaultModel();
		HttpURLConnection urlConnection = null;
		int code = -1;
		try {
			Log.info("Opening a connection to the url");
			urlConnection = (HttpURLConnection) new URL(url).openConnection();
			urlConnection.setRequestProperty("User-Agent", "RDFAgent/1.0");
			urlConnection.setDefaultUseCaches(false);
			urlConnection.setUseCaches(false);

		} catch (MalformedURLException e) {
			Log.exception(this.getClass().getName(), "getRdfFromURL(String)", e);
			Log.severe("Malformed url: " + url);
			code = 404;

		} catch (IOException e) {
			Log.exception(this.getClass().getName(), "getRdfFromURL(String)", e);
			Log.severe("IO Exception on the following url: " + url
					+ ". Please ensure that you have an internet connection.");
			code = 404;
		}

		if (urlConnection != null) {
			try {
				code = urlConnection.getResponseCode();
				Log.info("Obtained response code " + code);
			} catch (IOException e) {
				Log.exception(this.getClass().getName(), "getRdfFromURL(String)", e);
			}

			if (code == 200 || code == 202) {
				// we successfully performed a GET
				try {
					Log.info("reading from the url");

					// model.read(urlConnection.getInputStream(), "");
					// urlConnection.disconnect();

					Connector c = new Connector();
					c.setModel(model);
					c.setConnection(urlConnection);
					c.start();
					long endTime = System.currentTimeMillis() + (1000 * 60);// one
					// minute
					while (c.isAlive()) {
						if (System.currentTimeMillis() > endTime) {
							Log
									.severe("The URL, "
											+ url
											+ ", timed out while trying to read the RDF."
											+ newline
											+ "Please ensure that you have an internet connection and that the remote location is readable.");
							if (c.isAlive()) {
								// check one last time ...
								code = 404;
								ModelFactory.createDefaultModel();
								c.interrupt();
							}
							break;
						}
						try {
							Thread.sleep(4000);
						} catch (InterruptedException ie) {
						}
					}

					code = c.getCode();
					model = c.getModel();

					if (code == 200 || code == 202) {
						Log.info("Closing the url connection.");
						Log.info("Got an RDF model.");
						return model;
					}
					// } catch (IOException e) {
					// Log.exception(this.getClass().getName(),
					// "getRdfFromURL(String)", e);
					// Log.severe("IO Exception on the url " + url + " while
					// trying to read the RDF."
					// + newline + "Please ensure that you have an internet
					// connection.");
					// code = 404;
				} catch (JenaException e) {
					Log.exception(this.getClass().getName(), "getRdfFromURL(String)", e);
					Log.severe("Couldn't parse the RDF document located at: " + url + "." + newline
							+ "This document is invalid.");
					// custom code that says that the RDF is invalid
					code = 415;
				}
			}
		}
		// if you get here, then an 'error' occurred (invalid rdf, i/o exception
		// or the url was 'bad' - let's log that information
		performURLErrorHandling(url, code);

		// model is empty
		return model;
	}

	/**
	 * 
	 * @param url
	 *            to extract services from
	 * @return an array of MobyService objects
	 */
	public MobyService[] extractServicesFromURL(String url) {

		Log.info("Loading the RDF model.");
		Model m = getRdfFromURL(url);
		Log.info("Loaded the RDF model.");
		if (m != null && !m.isEmpty()) {

			ServiceInstanceParser parser = null;

			try {
				parser = new ServiceInstanceParser(url);
			} catch (MobyException e) {
				Log.exception(this.getClass().getName(), "extractServicesFromURL(String)", e);
				Log.severe(e.getLocalizedMessage());
				performURLErrorHandling(url, 400);
				return new MobyService[] {};
			}
			Log.info("Parsing services from the model.");
			// parser != null
			MobyService[] services = null;
			try {
				services = parser.getMobyServicesFromRDF(m);
			} catch (MobyException e) {
				Log.exception(this.getClass().getName(), "extractServicesFromURL(String)", e);
				Log.severe("There was something wrong with the model: " + newline
						+ e.getLocalizedMessage());
				performURLErrorHandling(url, 501);
				return new MobyService[] {};
			}

			Log.info("Parsing complete.");
			if (parser.isRDFValid()) {
				// RDF was valid and no bad service found
				if (services.length == 0) {
					// there was nothing in the document
					performURLErrorHandling(url, 501);
					return services;
				}
				if (services != null) {
					// RDF document was ok after all, so remove url from hitlist
					removeFromHitlist(url);
					return services;
				}
			} else {
				Log.info("RDF contained some invalid services.");
				// RDF was found to contain bad services
				// process the bad ones and return the good ones.
				String errors = parser.getErrors();
				String[] badServices = errors.split(newline);
				Map duplicates = new HashMap();
				for (int i = 0; i < badServices.length; i++) {
					// Log.info(i + " errors");
					// each line is in the format name,authority{text}
					// String[] line = badServices[i].split("{"); this line
					// killed the jvm on bioinfo!?!
					StringTokenizer st = new StringTokenizer(badServices[i], "{");
					String line = (st.hasMoreTokens() ? st.nextToken() : null);
					if (line != null) {
						if (duplicates.containsKey(line))
							continue;
						String[] contents = line.split(",");
						if (contents != null && contents.length == 2) {
							duplicates.put(line, "");
							MobyService s = new MobyService(contents[0]);
							s.setAuthority(contents[1]);
							invalidServicesFound.add(s);
						}
					}
				}
				if (services.length == 0) {
					// there was nothing in the document
					performURLErrorHandling(url, 501);
					return services;
				}
				if (services != null) {
					// RDF document was ok after all, so remove url from hitlist
					removeFromHitlist(url);
					return services;
				}
			}
		}
		return new MobyService[] {};
	} /*
		 * This method inserts/updates the error count and logs certain
		 * information
		 * 
		 */

	/**
	 * @param url
	 */
	private void removeFromHitlist(String url) {
		if (Constants.REGISTRY_ENABLE_DEREGISTER) {
			// remove the url from the hit list if necessary and if
			// deregistration is enabled
			CentralDataAccess central = null;
			try {
				central = new CentralDataAccessImpl();
				int count = central.getErrorCountForURL(url);
				if (count != 0) {
					Log.info("Attempting to remove " + url + " from the 'hit list'.");
					if (central.deleteErrorCountForURL(url)) {
						Log.info("Removal success!");
					} else {
						Log.info("Removal failed.");
					}
				}
				if (central != null)
					central.cleanup();
			} catch (MobyException e) {
				Log.exception(this.getClass().getName(), "getRdfFromURL(String)", e);
				Log.severe(e.getLocalizedMessage());
			}
		}
	}

	private void performURLErrorHandling(String url, int code) {
		if (code == 400) {
			Log
					.warning("The request could not be understood by the server due to malformed syntax. The client SHOULD NOT repeat the request without modifications.");
		} else if (code == 401) {
			Log.warning("The request requires user authentication.");
		} else if (code == 402) {
			Log
					.warning("The request requires payment. The agent is unwilling to pay while it scavenges!");
		} else if (code == 403) {
			Log
					.warning("The server understood the request, but is refusing to fulfill it. Authorization will not help and the request SHOULD NOT be repeated.");
		} else if (code == 404) {
			Log.warning("The server has not found anything matching the Request-URI.");
		} else if (code == 415) {
			// already done
		} else if (code == 417) {
			Log.warning("The agent had connection problems with the data store.");
		} else if (code == 500) {
			Log
					.warning("The server encountered an unexpected condition which prevented it from fulfilling the request.");
		} else if (code == 501) {
			Log
					.warning("The agent encountered problems parsing the RDF document into valid services.");
		} else if (code == 502) {
			Log.warning("The agent encountered an invalid service in the registry.");
		} else {
			Log.warning("Error retrieving the RDF at " + url
					+ ". The HTTP response code given was: " + code + ".");
		}

		// if we are enabling deregistration, then we should update counts
		if (Constants.REGISTRY_ENABLE_DEREGISTER) {
			CentralDataAccess central = null;
			try {
				central = new CentralDataAccessImpl();
				int count = central.getErrorCountForURL(url);
				if (count == 0) {
					if (central.insertErrorCountForURL(url, code)) {
						Log.info("Error count for " + url + " was inserted! The count is 1.");
					} else {
						Log
								.warning("Error count for "
										+ url
										+ " was not inserted! Make sure that the DB parameters are correct!");
					}
				} else {
					if ((++count) >= Constants.REGISTRY_DEREGISTER_COUNT) {
						central.deleteErrorCountForURL(url);
					} else {
						central.incrementErrorCountForURL(url, code);
					}
				}
				badURLs.put(url, new Integer(count));

			} catch (MobyException e) {
				Log.exception(this.getClass().getName(), "perfromURLErrorHandling(String, int)", e);
				Log.severe(e.getLocalizedMessage());
			}
			if (central != null)
				central.cleanup();
		} else {
			badURLs.put(url, new Integer(0));
		}
	}

	/**
	 * 
	 * @return a map consisting of a url as the key and a count (the number of
	 *         failed attempts to parse or resolve the url) as the value.
	 */
	public Map getBadUrlMap() {
		return badURLs;
	}

	/**
	 * 
	 * @return a vector of MobyService objects that represent invalid services
	 *         found while parsing the rdf
	 */
	public Vector getInvalidServicesFoundInRDF() {
		return invalidServicesFound;
	}

	private class Connector extends Thread {

		private Model model = null;

		private HttpURLConnection con = null;

		private int code = -1;

		public void run() {
			try {
				model.read(con.getInputStream(), "");
				code = con.getResponseCode();
				con.disconnect();
			} catch (Exception e) {
				Log.exception(this.getClass().getName(), "getRdfFromURL(String)", e);
				Log.severe("IO Exception on the url " + con.getURL().toExternalForm()
						+ " while trying to read the RDF." + newline
						+ "Please ensure that you have an internet connection.");
				this.code = 404;
			}

		}

		public int getCode() {
			return code;
		}

		public void setConnection(HttpURLConnection c) {
			con = c;
		}

		public void setModel(Model m) {
			model = m;
		}

		public Model getModel() {
			return model;
		}
	}

}
