// GraphsServlet.java
//
//    A servlet producing Moby graphs.
//
//    senger@ebi.ac.uk
//    November 2003
//

package org.biomoby.client;

import org.biomoby.shared.CentralAll;
import org.biomoby.shared.MobyDataType;
import org.biomoby.shared.MobyException;
import org.biomoby.shared.MobyNamespace;
import org.biomoby.shared.MobyPrimaryDataSimple;
import org.biomoby.shared.MobyService;
import org.biomoby.shared.MobyServiceType;
import org.biomoby.shared.Utils;

import org.tulsoft.tools.servlets.Html;
import org.tulsoft.tools.servlets.HtmlConstants;
import org.tulsoft.tools.external.CatchOutputDefaultImpl;
import org.tulsoft.tools.external.Executor;
import org.tulsoft.tools.Sorter;
import org.tulsoft.shared.GException;
import org.tulsoft.shared.StringUtils;
import org.tulsoft.shared.UUtils;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Properties;
import java.util.Vector;

/**
 * A servlet making graphs of Moby service instances, Moby data types,
 * and Moby service types.  <P>
 *
 * @author <A HREF="mailto:senger@ebi.ac.uk">Martin Senger</A>
 * @version $Id: GraphsServlet.java,v 1.16 2008/02/14 06:13:36 senger Exp $
 */

public class GraphsServlet
    extends HttpServlet
    implements HtmlConstants {

    /**
     * 
     */
    private static final long serialVersionUID = 1L;
    // names of the init parameters (note that these same names are
    // also used in 'src/webapps/web.xml.template' file; if you wish
    // to change them, change them on both places)
    static final protected String MASTER_NAME = "provider_name";
    static final protected String MASTER_EMAIL = "provider_email";
    static final protected String REFRESH_INTERVAL = "refresh_interval";
    static final protected String CACHE_DIR = "cache_dir";
    static final protected String CACHE_URL = "cache_url";
    static final protected String DEFAULT_ENDPOINT = "default_endpoint";
    static final protected String DEFAULT_NAMESPACE = "default_namespace";
    static final protected String PROXY_SET = "proxySet";
    static final protected String PROXY_PORT = "http.proxyPort";
    static final protected String PROXY_HOST = "http.proxyHost";
    static final protected String DOT_PATH = "dot_path";
    static final protected String REGISTRY_CACHE_DIR = "registry_cache_dir";

    // expected/known form's element names
    static final protected String VERBOSE = "verbose";
    static final protected String DEBUG = "debug";
    static final protected String REGISTRY_URL = "endpoint";
    static final protected String REGISTRY_URI = "uri";
    static final protected String OUTPUT_TYPE_DATA  = "outtypedata";  // for data types
    static final protected String OUTPUT_TYPE_SERVI = "outtypeservi"; // for service instances
    static final protected String OUTPUT_TYPE_SERVT = "outtypeservt"; // for service types
    static final protected String RANKDIR = "rankdir";
    static final protected String REFRESH = "refresh";
    static final protected String MAX_DEPTH = "max_depth";
    static final protected String SEL_DEPTH = "sel_depth";
    static final protected String SEL_SERVI_MULTI = "sel_servi_multi";
    static final protected String SEL_SERVI_1 = "sel_servi1";
    static final protected String SEL_SERVI_2 = "sel_servi2";
    static final protected String SEL_AUTH_MULTI = "sel_auth_multi";
    static final protected String SEL_DATA_1 = "sel_data1";
    static final protected String SEL_DATA_2 = "sel_data2";
    static final protected String SEL_NS_1 = "sel_ns1";
    static final protected String SEL_NS_2 = "sel_ns2";
    static final protected String FILTER_DATAPATH = "datapath";
    static final protected String FILTER_PATH = "path";
    static final protected String FILTER_SELECT = "select";
    static final protected String FILTER_FULL = "full";
    static final protected String FILTER = "filter";

    // ...names of submit buttons
    static final protected String ACTION_ENTRY     = "entry";
    static final protected String ACTION_MAIN_FORM = "main";
    static final protected String ACTION_JOB_DATA  = "data";
    static final protected String ACTION_JOB_SERVI = "services";
    static final protected String ACTION_JOB_SERVT = "stypes";

    // names of the possible graph types
    static final protected String GRAPH_DATA  = "DT";   // data types
    static final protected String GRAPH_SERVI = "SI";   // service instances
    static final protected String GRAPH_SERVT = "ST";   // service types

    // colors for HTML pages
    static final protected String BG_COLOR    = "#FFFFCD";
    static final protected String LINK_COLOR  = "#0000FF";
    static final protected String VLINK_COLOR = "#0000FF";
    static final protected String TEXT_COLOR  = "#000000";
    static final protected String COL_HEADER  = "#FFCC00";

    // some default values
    static final protected long DEFAULT_REFRESH_IN_MINUTES = 360;

    // output types
    static final protected String T_PNG   = "png";
    static final protected String T_GIF   = "gif";
    static final protected String T_JPG   = "jpg";
    static final protected String T_PS    = "ps";
    static final protected String T_SVG   = "svg";
    static final protected String T_VRML  = "vrml";
    static final protected String T_MIF   = "mif";
    static final protected String T_IMAP  = "imap";
    static final protected String T_CMAP  = "cmap";
    static final protected String T_HPGL  = "hpgl";
    static final protected String T_PCL   = "pcl";
    static final protected String T_FIG   = "fig";
    static final protected String T_DOT   = "dot";
    static final protected String T_CANON = "canon";
    static final protected String T_PLAIN = "txt";
    static final protected String T_RDF   = "rdf";

    // and few other constants
    static final protected String WITHOUT_NAMESPACE = "(no namespace)";
    static final protected String RESULTWIN = "RESULTWIN";


    // table containing mapping between several Moby registries and
    // instances of this class:
    //    key (type String):          URL of Moby registries + "::" + its namespaces
    //    value (type GraphsServlet): an instance of this class dealing with that
    //                                registry
    static Hashtable registries;

    // servlet init parameters (read in when the servlet starts)
    static Hashtable initParams;

    // these are filled in the static initializer
    static String[] supportedTypesForData;
    static String[] supportedTypesForServices;
    static String[] supportedTypesForServiceTypes;
    static Hashtable contentTypes;
    static Hashtable displayNamesForTypes;

    // cache for created graphs (images)
    static SimpleFileCache cache;
    static ServletContext context;

    // cache location for all Moby registries
    static String registryCacheDir;

    // default Moby registry
    static String defaultEndpoint;
    static String defaultNamespace;

    // definition of a Moby registry
//     String endpoint;
//     String namespace;
    CentralAll registry;
    boolean debug = false;
    boolean verbose = false;
//     MobyDataType[] dataTypes;
//     MobyService[] services;
//     MobyServiceType[] serviceTypes;
    long lastRead = -1;         // in millis
    long refreshInterval = -1;  // in millis

    /*************************************************************************
     * A default constructor, used usually by a servlet engine.
     *************************************************************************/
    public GraphsServlet() {
    }

    /*************************************************************************
     * A real constructor that is called when it is known what Moby
     * registry we are going to serve for. All instances of
     * GraphsServlet are stored in a hashtable so when a call for
     * already known combination of 'mobyEndpoint' and 'mobyNamespace'
     * comes a proper instance of GraphsServlet is either restored from
     * the hashtable, or created.
     *************************************************************************/
    public GraphsServlet (String mobyEndpoint, String mobyNamespace)
	throws MobyException {

	// note that 'registryCacheDir' is shared by all registries -
	// and that's why it could be created already in init()
	registry =
	    new CentralDigestCachedImpl (mobyEndpoint, mobyNamespace, registryCacheDir);
    }

    /**************************************************************************
     * static initializer
     **************************************************************************/
    static {
	supportedTypesForData = new String[] {
	    T_PNG, T_GIF, T_JPG, T_PS, T_SVG, T_VRML, T_MIF, /* T_IMAP, T_CMAP, */
	    T_HPGL, T_PCL, T_FIG, T_DOT, T_CANON, T_PLAIN
	};
	supportedTypesForServices = new String[] {
	    T_PNG, T_GIF, T_JPG, T_PS, T_SVG, T_VRML, T_MIF, /* T_IMAP, T_CMAP, */
	    T_HPGL, T_PCL, T_FIG, T_DOT, T_CANON, T_PLAIN, T_RDF
	};
	supportedTypesForServiceTypes = supportedTypesForData;

	displayNamesForTypes = new Hashtable();
	displayNamesForTypes.put (T_PNG,   "PNG - Portable Network Graphics");
	displayNamesForTypes.put (T_GIF,   "GIF Bitmap image");
	displayNamesForTypes.put (T_JPG,   "JPEG Compressed image");
	displayNamesForTypes.put (T_PS,    "Postscript");
	displayNamesForTypes.put (T_SVG,   "SVG - Scalable Vector Graphics");
	displayNamesForTypes.put (T_VRML,  "VRML - VR Modelling language");
	displayNamesForTypes.put (T_MIF,   "FrameMaker MIF");
	displayNamesForTypes.put (T_IMAP,  "IMAP...(TBD)");
	displayNamesForTypes.put (T_CMAP,  "CMAP... (TBD)");
	displayNamesForTypes.put (T_HPGL,  "HP-GL/2 vector graphic printer language");
	displayNamesForTypes.put (T_PCL,   "PCL printer language");
	displayNamesForTypes.put (T_FIG,   "FIG graphics language");
	displayNamesForTypes.put (T_DOT,   "DOT language");
	displayNamesForTypes.put (T_CANON, "DOT language without layout");
	displayNamesForTypes.put (T_PLAIN, "Simple, line-based language");
	displayNamesForTypes.put (T_RDF,   "RDF - Resource Definition Framework");
    }

    /*************************************************************************
     * 
     *           Servlet initialization...
     *
     *************************************************************************/
    public void init()
	throws ServletException {
	registries = new Hashtable();
	initParams = new Hashtable();

	// read first context parameters (scope: all 'our' servlets)
	context = getServletContext();
	for (Enumeration en = context.getInitParameterNames(); en.hasMoreElements(); ) {
	    String name = (String)en.nextElement();
	    if (name != null)
		initParams.put (name, context.getInitParameter (name));
	}

	// then read config parameters (scope: this servlet) - they may overwrite
	// those read above
	for (Enumeration en = getInitParameterNames(); en.hasMoreElements(); ) {
	    String name = (String)en.nextElement();
	    if (name != null)
		initParams.put (name, getInitParameter (name));
	}

	// read some suggested defaults from the init parameters
	defaultEndpoint = (String)initParams.get (DEFAULT_ENDPOINT);
	if ( UUtils.isEmpty (defaultEndpoint) || defaultEndpoint.equals ("\"\"") )
	    defaultEndpoint = CentralImpl.DEFAULT_ENDPOINT;
	defaultNamespace = (String)initParams.get (DEFAULT_NAMESPACE);
	if ( UUtils.isEmpty (defaultNamespace) || defaultNamespace.equals ("\"\"") )
	    defaultNamespace = CentralImpl.DEFAULT_NAMESPACE;

	registryCacheDir = (String)initParams.get (REGISTRY_CACHE_DIR);
	if (UUtils.isEmpty (registryCacheDir)) {
	    String tmpDir = System.getProperty ("java.io.tmpdir");
	    String fileSeparator = System.getProperty ("file.separator");
	    registryCacheDir = tmpDir + fileSeparator + "mobycache";
	}

	// set HTTP proxy - this is probably useless because (I guess)
	// the proxy can be set in the Tomcat configuration file (and
	// not to let each servlet to do it) - but it's here, anyway
	String proxySet = (String)initParams.get (PROXY_SET);
	if (! UUtils.isEmpty (proxySet))
	    System.setProperty (PROXY_SET, proxySet);
	String proxyPort = (String)initParams.get (PROXY_PORT);
	if (! UUtils.isEmpty (proxyPort))
	    System.setProperty (PROXY_PORT, proxyPort);
	String proxyHost = (String)initParams.get (PROXY_HOST);
	if (! UUtils.isEmpty (proxyHost))
	    System.setProperty (PROXY_HOST, proxyHost);
    }

    /*************************************************************************
     * 
     *           An entry point...
     *
     *************************************************************************/
    public void doGet (HttpServletRequest req, HttpServletResponse res)
	throws ServletException, IOException {
	doPost (req, res);
    }

    public void doPost (HttpServletRequest req, HttpServletResponse res)
	throws ServletException, IOException {

	// initialize the cache (do it only the first time, but I
	// cannot do it in init() becuase I need at least one request
	// for finding the context path)
	if (cache == null)
	    cache = initCache (context, req.getContextPath());

	// set some admin options (keep their old values if they are not sent)
	if (getString (req, VERBOSE) != null)
	    verbose = getBoolean (req, VERBOSE);
	if (getString (req, DEBUG) != null) {
	    debug = getBoolean (req, DEBUG);
	    if (debug) verbose = true;
	}

	// for each Moby Registry (identified by its URL), there is an
	// instance of this class (because it keeps some data about
	// its registry cached) - there is a global table 'registries'
	// to keep these instances
	String endpoint = getString (req, REGISTRY_URL);
	if (endpoint == null)
	    endpoint = defaultEndpoint;
	String namespace = getString (req, REGISTRY_URI);
	if (namespace == null)
	    namespace = defaultNamespace;
	String key = endpoint + "::" + namespace;
	GraphsServlet worker = (GraphsServlet)registries.get (key);

	// working the first time with this registry
	if (worker == null) {
	    try {
		worker = new GraphsServlet (endpoint, namespace);
		worker.setVerbose (verbose);
		worker.setDebug (debug);
		worker.registry.setDebug (debug);
		registries.put (key, worker);
	    } catch (MobyException e) {
		error (res, HttpServletResponse.SC_SERVICE_UNAVAILABLE, e);
		return;
	    }
	}

	// maybe someone wants to re-read the registry
	if (getBoolean (req, REFRESH)) {
	    ((CentralDigestImpl)worker.registry).removeFromCache (null);
	}

	// do the main job
	if (exists (req, ACTION_MAIN_FORM))
	    worker.doFormPage (req, res);
	else if (exists (req, ACTION_JOB_DATA))
	    worker.doGraphDataTypes (req, res);
	else if (exists (req, ACTION_JOB_SERVI))
	    worker.doGraphServices (req, res);
 	else if (exists (req, ACTION_JOB_SERVT))
 	    worker.doGraphServiceTypes (req, res);
	else
	    worker.doEntryPage (req, res);
    }

    /*************************************************************************
     * Create an ENTRY page.
     *************************************************************************/
    protected void doEntryPage (HttpServletRequest req, HttpServletResponse res)
	throws ServletException, IOException {

	Html h = new Html (req);
	res.setContentType ("text/html");
	PrintWriter out = res.getWriter();

	out.print (h.startHtml (new String[] {
	    TITLE, "Moby Graphs Entry page",
	    BGCOLOR, BG_COLOR,
	    LINK, LINK_COLOR, VLINK, VLINK_COLOR, TEXT, TEXT_COLOR }));

	out.println (h.gen (FORM, new String[] {
	    METHOD, "POST"
	}));

	out.println (h.gen (H1, "Exploring BioMoby graphically"));

	out.println (h.gen (P, "What BioMoby registry do you wish to explore? The URL is essential - but usually the default value is fine. The namespace (also sometimes called URI) mostly does not need to be changed."));
	out.println ("URL of the BioMoby registry: ");
	out.println (h.gen (P, h.text (REGISTRY_URL, registry.getRegistryEndpoint(), 50)));
	out.println ("Namespace of the registry: ");
	out.println (h.gen (P, h.text (REGISTRY_URI, registry.getRegistryNamespace(), 50)));

 	out.println (h.gen (P, "The graphs are created faster if information about the registry are not retrieved again and again for each request. But if you wish to get really the most latest state check the box below."));
	out.println (h.checkbox (REFRESH));
	out.println (" Re-read all registry entries now");

 	out.println (h.gen (P, h.submit (" Continue ", ACTION_MAIN_FORM)));

 	out.print (h.end (FORM));

 	out.print (getSignature (h));
	out.print (h.endHtml());		    
    }

    /*************************************************************************
     * Create the main page with a FORM for defining what graph is wanted.
     * In order to do that we need read the registry - but it may be already
     * cached.
     *************************************************************************/
    protected void doFormPage (HttpServletRequest req, HttpServletResponse res)
	throws ServletException, IOException {

	MobyService[] services = null;
	MobyDataType[] dataTypes = null;
// 	Map namespaces = null;
	MobyNamespace[] namespaces = null;
	try {
	    readRegistryIfNeeded();
	    services = registry.getServices();
	    dataTypes = registry.getDataTypes();
	    namespaces  = registry.getFullNamespaces();
	    lastRead = getLastRead();
	} catch (MobyException e) {
 	    error (res, HttpServletResponse.SC_SERVICE_UNAVAILABLE, e);
	    return;
	}

	Html h = new Html (req);
	res.setContentType ("text/html");
	PrintWriter out = res.getWriter();

	out.print (h.startHtml (new String[] {
	    TITLE, "Moby Graphs Request page",
	    BGCOLOR, BG_COLOR,
	    LINK, LINK_COLOR, VLINK, VLINK_COLOR, TEXT, TEXT_COLOR }));

 	out.println (h.gen (FORM, new String[] {
 	    METHOD, "POST"
	}));
	out.println (getFieldsAsHidden (h));
	out.println (h.gen (H1, "Exploring BioMoby graphically"));
	out.println (h.gen (FONT, new String[] { SIZE, "-1" },
			    h.gen (BLOCKQUOTE,
				   "BioMoby registry: " + registry.getRegistryEndpoint() + "<br>" +
				   "Last re-read: " + (lastRead <= 0 ? "unknown" : new Date (lastRead).toString()))));
	out.println (h.gen (P, "Specify below what kind of graph should be created and what properties it should have. Notice that some properties are shared by all graph types - look at the end of the page."));

	// ----------------------------------------------------
	out.println (h.gen (H3, "Graph of service instances"));
	// ----------------------------------------------------
	out.println (start (BLOCKQUOTE));

	// list of output types
	String[] labels = null;
	String[] values = null;
	synchronized (supportedTypesForServices) {
	    labels = new String [supportedTypesForServices.length];
	    values = new String [supportedTypesForServices.length];
	    for (int i = 0; i < supportedTypesForServices.length; i++) {
		labels[i] = (String)displayNamesForTypes.get (supportedTypesForServices[i]);
		values[i] = supportedTypesForServices[i];
	    }
	}
	Hashtable selected = new Hashtable();
	selected.put (T_PNG, "1");
	out.println ("Select output type: ");
	out.println (h.list (OUTPUT_TYPE_SERVI, labels, values, selected));

	// lists of service names and authorities (as read from moby)
	String checked1 = "document.forms[0]." + FILTER + "[1].checked = '1';";
	String checked2 = "document.forms[0]." + FILTER + "[2].checked = '1';";
	Hashtable attrs = new Hashtable();
	HashSet authorities = new HashSet();
	labels = new String [services.length];
	for (int i = 0; i < services.length; i++) {
	    labels[i] = services[i].getName();
	    String auth = services[i].getAuthority();
	    if (auth != null)
		authorities.add (auth);
	}
	Arrays.sort (labels);
	selected = arr2hash (req.getParameterValues (SEL_SERVI_MULTI));
	attrs.clear();
	attrs.put (NAME, SEL_SERVI_MULTI);
	attrs.put (SIZE, "6");
	attrs.put (MULTIPLE, h.getNullObject());
	attrs.put (ONCHANGE, checked1);
	String selectServices = h.list (attrs, labels, null, selected);

	selected.clear();
	String previouslySelected = getString (req, SEL_SERVI_1);
	if (previouslySelected != null)
	    selected.put (previouslySelected, "1");
	attrs.clear();
	attrs.put (NAME, SEL_SERVI_1);
	attrs.put (ONCHANGE, checked2);
	String selectService1 = h.list (attrs, labels, null, selected);

	selected.clear();
	previouslySelected = getString (req, SEL_SERVI_2);
	if (previouslySelected != null)
	    selected.put (previouslySelected, "1");
	attrs.clear();
	attrs.put (NAME, SEL_SERVI_2);
	attrs.put (ONCHANGE, checked2);
	String selectService2 = h.list (attrs, labels, null, selected);

	labels = new String [authorities.size()];
	int k = -1;
	for (Iterator it = authorities.iterator(); it.hasNext(); )
	    labels [++k] = (String)it.next();
	Arrays.sort (labels);
	selected = arr2hash (req.getParameterValues (SEL_AUTH_MULTI));
	attrs.clear();
	attrs.put (NAME, SEL_AUTH_MULTI);
	attrs.put (SIZE, "6");
	attrs.put (MULTIPLE, h.getNullObject());
	attrs.put (ONCHANGE, checked1);
	String selectAuthorities = h.list (attrs, labels, null, selected);

	// list of depth	
	selected.clear();
	attrs.clear();
	attrs.put (NAME, SEL_DEPTH);
	attrs.put (ONCHANGE, checked1);
	previouslySelected = getString (req, SEL_DEPTH);
	if (previouslySelected == null)
	    selected.put ("1", "1");
	else
	    selected.put (previouslySelected, "1");
	String selectDepth =
	    h.list (attrs,
		    new String[] { "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "MAX" },
		    null, selected);

	// lists of data types (and namespaces) - for selecting a datapath
	String checked3 = "document.forms[0]." + FILTER + "[3].checked = '1';";

	String[] dataLabels = new String [dataTypes.length];
	for (int i = 0; i < dataTypes.length; i++)
	    dataLabels[i] = Utils.pureName (dataTypes[i].getName());
	Arrays.sort (dataLabels);

	selected.clear();
	previouslySelected = getString (req, SEL_DATA_1);
	if (previouslySelected != null)
	    selected.put (previouslySelected, "1");
	attrs.clear();
	attrs.put (NAME, SEL_DATA_1);
	attrs.put (ONCHANGE, checked3);
	String selectData1 = h.list (attrs, dataLabels, null, selected);

	selected.clear();
	previouslySelected = getString (req, SEL_DATA_2);
	if (previouslySelected != null)
	    selected.put (previouslySelected, "1");
	attrs.clear();
	attrs.put (NAME, SEL_DATA_2);
	attrs.put (ONCHANGE, checked3);
	String selectData2 = h.list (attrs, dataLabels, null, selected);

	Vector v = new Vector();
	for (int i = 0; i < namespaces.length; i++) {
	    v.addElement (namespaces[i].getName());
	}
// 	for (Iterator it = namespaces.entrySet().iterator(); it.hasNext(); ) {
// 	    Map.Entry entry = (Map.Entry)it.next();
// 	    v.addElement (entry.getKey());
// 	}
	new Sorter().sort (v);
	v.insertElementAt (WITHOUT_NAMESPACE, 0);
	String[] nsLabels = new String [v.size()];
	v.copyInto (nsLabels);

	selected.clear();
	previouslySelected = getString (req, SEL_NS_1);
	if (previouslySelected != null)
	    selected.put (previouslySelected, "1");
	attrs.clear();
	attrs.put (NAME, SEL_NS_1);
	attrs.put (ONCHANGE, checked3);
	String selectNs1 = h.list (attrs, nsLabels, null, selected);

	selected.clear();
	previouslySelected = getString (req, SEL_NS_2);
	if (previouslySelected != null)
	    selected.put (previouslySelected, "1");
	attrs.clear();
	attrs.put (NAME, SEL_NS_2);
	attrs.put (ONCHANGE, checked3);
	String selectNs2 = h.list (attrs, nsLabels, null, selected);

	// the main radio group (what to do)
	int checked = 3;
	previouslySelected = getString (req, FILTER);
	if (previouslySelected != null) {
	    if (previouslySelected.equals (FILTER_SELECT))
		checked = 1;
	    else if (previouslySelected.equals (FILTER_PATH))
		checked = 2;
	    else if (previouslySelected.equals (FILTER_DATAPATH))
		checked = 3;
	}
	String[] radios = radioGroup (h, FILTER,
				      new String[] { FILTER_FULL, FILTER_SELECT, FILTER_PATH, FILTER_DATAPATH },
				      checked);

	// and put everything together
	out.println
	    (h.gen (TABLE, new String[] { CELLPADDING, "5" },
		    h.gen (TR,
			   h.gen (TD,
				  radios[0]) +
			   h.gen (TD,
				  "Show all services and all connections " + h.gen (EM, "(very long and big)"))) +
		    h.gen (TR,
			   h.gen (TD, new String[] { VALIGN, "top" },
				  radios[1]) +
			   h.gen (TD,
				  "Show only selected authorities and services<BR>" +
				  "Maximum neighbourhood level: " + selectDepth)) +
		    h.gen (TR,
			   h.gen (TD, "&nbsp;") +
			   h.gen (TD,
				  h.gen (TABLE,
					 h.gen (TR,
						h.gen (TD, selectAuthorities) +
						h.gen (TD, h.gen (B, " and ")) +
						h.gen (TD, selectServices))))) +
		    h.gen (TR,
			   h.gen (TD,
				  radios[2]) +
			   h.gen (TD,
				  "Show only pathes between two selected services")) +
		    h.gen (TR,
			   h.gen (TD, "&nbsp;") +
			   h.gen (TD,
				  h.gen (TABLE,
					 h.gen (TR,
						h.gen (TD, selectService1) +
						h.gen (TD, h.gen (B, " <--> ")) +
						h.gen (TD, selectService2))))) +
		    h.gen (TR,
			   h.gen (TD, new String[] { VALIGN, "top" },
				  radios[3]) +
			   h.gen (TD, h.gen (IMG, new String[] { BORDER, "0",
								 ALIGN, "left",
								 SRC, req.getContextPath() + "/images/new.gif" }) +
				  "Show only pathes between two selected data types<br>" +
				  "(e.g. GO/Object --&gt; AminoAcidSequence)")) +
		    h.gen (TR,
			   h.gen (TD, "&nbsp;") +
			   h.gen (TD,
				  h.gen (TABLE,
					 h.gen (TR,
						h.gen (TD, "Namespace: ") +
						h.gen (TD, selectNs1 + " /") +
						h.gen (TD, ROWSPAN, "2", h.gen (B, " --> ")) +
						h.gen (TD, "Namespace: ") +
						h.gen (TD, selectNs2 + " /")) +
					 h.gen (TR,
						h.gen (TD, "Data type: ") +
						h.gen (TD, selectData1) +
						h.gen (TD, "Data type: ") +
						h.gen (TD, selectData2))
					 )
				  )
			   )
			)
	       );

 	out.println (h.gen (P,
			    h.submit (" Create Graph of Service Instances ", ACTION_JOB_SERVI)));
	out.println (h.end (BLOCKQUOTE));

	// ---------------------------------------------
	out.println (h.gen (H3, "Graph of data types"));
	// ---------------------------------------------
	out.println (start (BLOCKQUOTE));

	labels = null;
	values = null;
	synchronized (supportedTypesForData) {
	    labels = new String [supportedTypesForData.length];
	    values = new String [supportedTypesForData.length];
	    for (int i = 0; i < supportedTypesForData.length; i++) {
		labels[i] = (String)displayNamesForTypes.get (supportedTypesForData[i]);
		values[i] = supportedTypesForData[i];
	    }
	}
	selected.clear();
	selected.put (T_PNG, "1");

	out.println ("Select output type: ");
	out.println (h.list (OUTPUT_TYPE_DATA, labels, values, selected));
 	out.println (h.gen (P,
			    h.submit (" Create Graph of Data Types ", ACTION_JOB_DATA)));
	out.println (h.end (BLOCKQUOTE));

	// ---------------------------------------------
	out.println (h.gen (H3, "Graph of service types"));
	// ---------------------------------------------
	out.println (start (BLOCKQUOTE));

	labels = null;
	values = null;
	synchronized (supportedTypesForServiceTypes) {
	    labels = new String [supportedTypesForServiceTypes.length];
	    values = new String [supportedTypesForServiceTypes.length];
	    for (int i = 0; i < supportedTypesForServiceTypes.length; i++) {
		labels[i] = (String)displayNamesForTypes.get (supportedTypesForServiceTypes[i]);
		values[i] = supportedTypesForServiceTypes[i];
	    }
	}
	selected.clear();
	selected.put (T_PNG, "1");

	out.println ("Select output type: ");
	out.println (h.list (OUTPUT_TYPE_SERVT, labels, values, selected));
 	out.println (h.gen (P,
			    h.submit (" Create Graph of Service Types ", ACTION_JOB_SERVT)));
	out.println (h.end (BLOCKQUOTE));

	// -------------------------------------------------------------
	out.println (h.gen (H3, "Additional visualization properties"));
	// -------------------------------------------------------------
	out.println (start (BLOCKQUOTE));
	out.println ("Set direction of graph layout. If sets to left-right, the graph is laid out from left to right, meaning that the directed edges tend to go from left to right. Other value is top-bottom where the graph is laid out from top to bottom:");
	out.println (h.gen (BR));
	out.println (h.list (RANKDIR,
			     new String[] { "Left to right", "Top to bottom"},
			     new String[] { "LR",            "TB"},
			     null));
	out.println (h.end (BLOCKQUOTE));

 	out.print (h.end (FORM));
 	out.print (getSignature (h));
	out.print (h.endHtml());		    
    }

    /*************************************************************************
     * Create and return a graph of Moby service instances.
     *************************************************************************/
    protected void doGraphServices (HttpServletRequest req, HttpServletResponse res)
	throws ServletException, IOException {

	// this says what type of service graph is wanted
	String filter = getString (req, FILTER);

	// check if the output type is supported
	String wantedOutputType = getString (req, OUTPUT_TYPE_SERVI);
	if (wantedOutputType == null)
	    wantedOutputType = T_PNG;
	boolean supported = false;
	for (int i = 0; i < supportedTypesForServices.length; i++) {
	    if (supportedTypesForServices[i].equals (wantedOutputType)) {
		supported = true;
		break;
	    }
	}
	if (! supported) {
	    error (res, HttpServletResponse.SC_SERVICE_UNAVAILABLE,
		   new MobyException ("Unrecognized output type '" + wantedOutputType + "'."));
	    return;
	}
	if (filter.equals (FILTER_DATAPATH) && wantedOutputType.equals (T_RDF)) {
	    error (res, HttpServletResponse.SC_SERVICE_UNAVAILABLE,
		   new MobyException ("Sorry, RDF type for graphs of data paths is not supported."));
	    return;
	}

	// collect visualization properties
	Properties props = new Properties();
	String rankdir = getString (req, RANKDIR);
	if (rankdir != null)
	    props.put (Graphviz.PROP_RANKDIR, rankdir);

	// this is to distinguish also the cached HTML pages
	props.put ("wot", wantedOutputType);

	// add other properties - to be used for caching etc.
	props.put (FILTER, filter);
	String[] authorities = null;
	String[] serviceNames = null;
	int depth = 1;
	String s1 = null;
	String s2 = null;
	String s3 = null;
	String s4 = null;
	if (filter.equals (FILTER_SELECT)) {
	    authorities = req.getParameterValues (SEL_AUTH_MULTI);
	    serviceNames = req.getParameterValues (SEL_SERVI_MULTI);
	    String depthStr = getString (req, SEL_DEPTH);
	    if (depthStr != null) {
		if (depthStr.equals (MAX_DEPTH)) {
		    depth = Integer.MAX_VALUE;
		} else {
		    try {
			depth = Integer.valueOf (depthStr).intValue();
		    } catch (java.lang.NumberFormatException e) {}
		}
	    }
	    if (authorities != null)
		props.put (SEL_AUTH_MULTI, StringUtils.join (authorities, ","));
	    if (serviceNames != null)
		props.put (SEL_SERVI_MULTI, StringUtils.join (serviceNames, ","));
	    props.put (SEL_DEPTH, ""+depth);

	} else if (filter.equals (FILTER_PATH)) {
	    s1 = getString (req, SEL_SERVI_1);
	    s2 = getString (req, SEL_SERVI_2);
	    if (s1 != null && s2 != null) {
		props.put (SEL_SERVI_1, s1);
		props.put (SEL_SERVI_2, s2);
	    }

	} else if (filter.equals (FILTER_DATAPATH)) {
	    s1 = getString (req, SEL_DATA_1);
	    s2 = getString (req, SEL_DATA_2);
	    s3 = getString (req, SEL_NS_1);
	    s4 = getString (req, SEL_NS_2);
	    if (s1 != null && s2 != null && s3 != null && s4 != null) {
		props.put (SEL_DATA_1, s1);
		props.put (SEL_DATA_2, s2);
		props.put (SEL_NS_1, s3);
		props.put (SEL_NS_2, s4);
	    }
	}

	if (filter.equals (FILTER_SELECT)) {
	    if ( (authorities == null || authorities.length == 0) &&
		 (serviceNames == null || serviceNames.length == 0) ) {
		error (res, HttpServletResponse.SC_SERVICE_UNAVAILABLE,
		       new MobyException ("Please, select at least one authority or one service name."));
		return;
	    }
	}

	try {
	    // make sure that we have data from a registry (it is
	    // either already in the registry cache - so it does not
	    // take long to read it, or it is not - so it is good to
	    // refill the cache and to get the new 'lastRead'
	    // timestamp)
	    MobyService[] services = registry.getServices();
	    MobyDataType[] dataTypes = registry.getDataTypes();
	    lastRead = getLastRead();

	    // perhaps we have the same graph in the cache already
	    // (but not for DATAPATH - there it is more complicated)
	    String graphId = cache.createId (registry.getRegistryEndpoint(), GRAPH_SERVI,
					     wantedOutputType, lastRead, props);
	    String pageId = cache.createId (registry.getRegistryEndpoint(), GRAPH_SERVI,
					    "html", lastRead, props);
	    if ( cache.existsInCache (graphId) &&
		 cache.existsInCache (pageId) &&
		 ! filter.equals (FILTER_DATAPATH) ) {
		res.sendRedirect (res.encodeRedirectURL (cache.getURL (pageId)));
		return;
	    }

	    // create all edges between all services
	    log ("Creating a graph of the service instances...\n");
	    ServicesEdge[] edges = ServiceConnections.build (dataTypes, services);

	    // create an HMTL static page (and the graps the page is
	    // linked to)
	    if (filter.equals (FILTER_DATAPATH)) {
		doDataPathResultPage (pageId, wantedOutputType, req, edges, dataTypes, services, props);

	    } else {
		doServicesResultPage (pageId, graphId, wantedOutputType, req, edges,
				      filter, authorities, serviceNames, depth, s1, s2, props);
	    }
	    res.sendRedirect (res.encodeRedirectURL (cache.getURL (pageId)));

	} catch (MobyException e) {
	    error (res, HttpServletResponse.SC_SERVICE_UNAVAILABLE, e);
	    return;
	}
    }

    /*************************************************************************
     * Create a page containing results for a service graph.
     *************************************************************************/
    protected void doServicesResultPage (String pageId, String graphId, String wantedOutputType,
					 HttpServletRequest req,
					 ServicesEdge[] edges,
					 String filter, String[] authorities, String[] serviceNames, int depth,
					 String service1, String service2,
					 Properties props)
	throws ServletException, IOException, MobyException {

	// where (and by whom) to create resulting HTML page
	String filename = cache.getFilename (pageId);
	PrintWriter out = new PrintWriter (new FileOutputStream (filename));
	Html h = new Html (req);

	String title = "Services graph";

	//
	// filter edges according the given parameters
	//
	if (filter.equals (FILTER_SELECT)) {
	    edges = FilterServices.filter (edges, authorities, serviceNames, depth);

	} else if (filter.equals (FILTER_PATH)) {
	    if (service1 != null && service2 != null) {
		edges = FilterServices.pathes (edges, service1, service2);
		if (edges == null) {
		    // no connection found between services service1 and service2
		    out.print (h.startHtml (new String[] {
			TITLE, "Moby Graphs Services Graph page - no path found",
			BGCOLOR, BG_COLOR,
			LINK, LINK_COLOR, VLINK, VLINK_COLOR, TEXT, TEXT_COLOR }));

		    out.println (h.gen (H1, title));
		    out.println ("No connection found between service " +
				 h.gen (B, service1) + " and service " +
				 h.gen (B, service2));
		    out.print (h.endHtml());		    
		    out.close();
		    return;
		}
	    }
	}
	int nos = getNumberOfServices (edges);

	//
	// create a graph (or an RDF representation)
	//
	if (wantedOutputType.equals (T_RDF)) {
	    String graph = RDF.createServicesGraph (edges, props);
	    cache.setContents (graphId, graph.getBytes());

	} else {
	    createGraph (graphId, wantedOutputType,
			 Graphviz.createServicesGraph (edges, props));
	}
	long fsize = new File (cache.getFilename (graphId)).length();

	//
	// create the resulting page
	//
	out.print (h.startHtml (new String[] {
	    TITLE, "Moby Graphs Services page",
	    BGCOLOR, BG_COLOR,
	    LINK, LINK_COLOR, VLINK, VLINK_COLOR, TEXT, TEXT_COLOR }));

	out.println (h.a (cache.getURL (graphId), RESULTWIN, h.gen (H1, title)));
	out.println (h.gen (BLOCKQUOTE,
			    h.gen (FONT, SIZE, "-1",
				   "Click on the header above to get the whole graph. WARNING: Please take file size into account before downloading! Do not try to open too large image in your browser. Save the file to your computer instead.")));

	StringBuffer buf = new StringBuffer();
	if (filter.equals (FILTER_SELECT)) {
	    if (authorities != null && authorities.length > 0) {
		StringBuffer b = new StringBuffer();
		for (int i = 0; i < authorities.length; i++) {
		    b.append (authorities[i] + "<br>");
		}
		buf.append (h.gen (TR,
				   h.gen (TD, VALIGN, "top", h.gen (B, "Selected authorities: ")) +
				   h.gen (TD, new String (b))));
	    }
	    if (serviceNames != null && serviceNames.length > 0) {
		StringBuffer b = new StringBuffer();
		for (int i = 0; i < serviceNames.length; i++) {
		    b.append (serviceNames[i] + "<br>");
		}
		buf.append (h.gen (TR,
				   h.gen (TD, VALIGN, "top", h.gen (B, "Selected services: ")) +
				   h.gen (TD, new String (b))));
	    }
	    buf.append (h.gen (TR,
			       h.gen (TD, h.gen (B, "Maximum neighbourhood level: ")) +
			       h.gen (TD, ""+depth)));

	} else if (filter.equals (FILTER_PATH)) {
	    if (service1 != null && service2 != null) {
		buf.append (h.gen (TR,
				   h.gen (TD, VALIGN, "top", h.gen (B, "Included only paths between services: ")) +
				   h.gen (TD, service1 + "<br>" + service2)));
	    }
	}

	out.println
	    (h.gen (TABLE, new String[] { CELLPADDING, "5" },
		    h.gen (TR,
			   h.gen (TD, h.gen (B, "Number of included services: ")) +
			   h.gen (TD, ""+nos)) +
		    h.gen (TR,
			   h.gen (TD, h.gen (B, "Number of edges between them: ")) +
			   h.gen (TD, ""+edges.length)) +
		    h.gen (TR,
			   h.gen (TD, h.gen (B, "Image type: ")) +
			   h.gen (TD, wantedOutputType)) +
		    h.gen (TR,
			   h.gen (TD, h.gen (B, "Image size: ")) +
			   h.gen (TD, fsize + " B")) +
		    new String (buf)
		    ));

 	out.print (getSignature (h));
	out.print (h.endHtml());		    
	out.close();
    }

    /*************************************************************************
     * Create a page containing results for one data path.
     *************************************************************************/
    protected void doDataPathResultPage (String pageId, String wantedOutputType,
					 HttpServletRequest req,
					 ServicesEdge[] edges,
					 MobyDataType[] dataTypes, MobyService[] services,
					 Properties props)
	throws ServletException, IOException, MobyException {

	// where (and by whom) to create resulting HTML page
	String filename = cache.getFilename (pageId);
	PrintWriter out = new PrintWriter (new FileOutputStream (filename));
	Html h = new Html (req);

	// what we got as input arguments
	String s1 = getString (req, SEL_DATA_1);
	String s2 = getString (req, SEL_DATA_2);
	String s3 = getString (req, SEL_NS_1);
	String s4 = getString (req, SEL_NS_2);

	if (s1 == null) s1 = "Object";
	if (s2 == null) s2 = "Object";
	if (s3 == null || s3.equals (WITHOUT_NAMESPACE)) s3 = "";
	if (s4 == null || s4.equals (WITHOUT_NAMESPACE)) s4 = "";

	String title = "Data path from " + (s3.equals ("") ? "" : (s3 + "/")) + s1 +
	    " to " +  (s4.equals ("") ? "" : (s4 + "/")) + s2;

	//
	// do computing and edges creation
	//
	MobyPrimaryDataSimple sourceData = createSimpleData (s3, s1);
	MobyPrimaryDataSimple targetData = createSimpleData (s4, s2);

	DataServiceEdge[] startingEdges = ServiceConnections.findStartingEdges (sourceData, dataTypes, services);
	DataServiceEdge[] endingEdges = ServiceConnections.findEndingEdges (targetData, dataTypes, services);

	// this creates *all* pathes, but some of them have cycles and inside branches
	ServicesEdge[][] separatePaths = FilterServices.dataPaths (startingEdges, edges, endingEdges);
	if (separatePaths.length == 0) {
	    out.print (h.startHtml (new String[] {
		TITLE, "Moby Graphs Data Path page - no path found",
		BGCOLOR, BG_COLOR,
		LINK, LINK_COLOR, VLINK, VLINK_COLOR, TEXT, TEXT_COLOR }));

	    out.println (h.gen (H1, title));
	    out.println ("No connection found.");
	    out.print (h.endHtml());		    
	    out.close();
	    return;
	}
	ServicesEdge[] allPaths = FilterServices.joinPaths (separatePaths);
	
	// this separate paths to straight paths (no cycles, no branches)
	separatePaths = FilterServices.straightDataPaths (startingEdges, allPaths, endingEdges);

	String theGraphURL = null;

	// create separate graph definitions (plus several paths on a
	// page, plus scufl definition for each individual path)
	Properties tavernaPropsRaw = new Properties();
	tavernaPropsRaw.put (Taverna.PROP_RAWINPUT, "true");
	tavernaPropsRaw.put (Taverna.PROP_RAWOUTPUT, "true");
	int pageSize = 5;
	String[] scufls = new String [separatePaths.length];
	String[] scuflRaws = new String [separatePaths.length];
	String[] scuflURLs = new String [separatePaths.length];
	String[] scuflRawURLs = new String [separatePaths.length];
	String[] pathNames = new String [separatePaths.length];
	String[] scuflNames = new String [separatePaths.length];
	for (int i = 0; i < separatePaths.length; i++) {
	    pathNames[i] = String.format ("Path %.2d", i+1);
	    scuflNames[i] = String.format ("Scufl %.2d", i+1);
	}
	String[] graphs = new String [separatePaths.length];
	String[] graphURLs = new String [separatePaths.length];
	int[] nosInGraphs = new int [separatePaths.length];
	int[] noeInGraphs = new int [separatePaths.length];
	int pageBeginPos = 0;
	int graphIndex = 0;
	int numberOfJointGraphs = 0;
	if (separatePaths.length > 0)
	    numberOfJointGraphs = (separatePaths.length - 1) / pageSize + 1;
	String[] jointGraphs = new String [numberOfJointGraphs];
	String[] jointGraphURLs = new String [numberOfJointGraphs];
	for (int i = 0; i < separatePaths.length; i++) {
	    // a separate graph
	    graphs[i] = Graphviz.createServicesGraph (separatePaths[i], props);
	    nosInGraphs[i] = getNumberOfServices (separatePaths[i]);
	    noeInGraphs[i] = separatePaths[i].length;
	    // a joint graph: have we reached an end of a page?
	    if ( (i+1) % pageSize == 0 || (i+1) == separatePaths.length ) {
		// yes => create a graph containing only paths from 'pageBeginPos' to 'i'
		jointGraphs [graphIndex++] =
		    Graphviz.createServicesGraph (separatePaths, pageBeginPos, i, pathNames, props);
		pageBeginPos = i+1;
	    }
	    // a scufl definition
	    scufls[i] = Taverna.buildWorkflow (separatePaths[i], registry.getRegistryEndpoint(),
					       new Properties());
	    scuflRaws[i] = Taverna.buildWorkflow (separatePaths[i], registry.getRegistryEndpoint(),
						  tavernaPropsRaw);
	}

	// create a definition for the whole graph
	String theGraph = Graphviz.createServicesGraph (allPaths, props);
	int nosTheGraph = getNumberOfServices (allPaths);
	int noeTheGraph = allPaths.length;

	//
	// call 'dot'to produce images from the various sets of edges
	// created abovce
	//

	String fileId;

	// create the whole graph (image)
	long fsizeTheGraph = 0;
	if (theGraph != null) {
	    fileId = cache.createId (registry.getRegistryEndpoint(), GRAPH_SERVI,
				     wantedOutputType, lastRead, props);
	    if (! cache.existsInCache (fileId))
		createGraph (fileId, wantedOutputType, theGraph);
	    theGraphURL = cache.getURL (fileId);
	    fsizeTheGraph = new File (cache.getFilename (fileId)).length();
	}

	// create individual graphs (images); make sure that they have
	// always a unique name because I cannot guarantee that the
	// separated paths will be in the same order (for the same big
	// graph) - it depends on the algoritm used by Hashtable which
	// I do not know
	if (graphs != null && graphs.length > 0) {
	    for (int i = 0; i < graphs.length; i++) {

		props.put ("UNIQUE", new java.rmi.server.UID().toString());
		fileId = cache.createId (registry.getRegistryEndpoint(), GRAPH_SERVI,
					 wantedOutputType, lastRead, props);
		createGraph (fileId, wantedOutputType, graphs[i]);
		graphURLs[i] = cache.getURL (fileId);
	    }
	}
	if (jointGraphs != null && jointGraphs.length > 0) {
	    for (int i = 0; i < jointGraphs.length; i++) {

		props.put ("UNIQUE", new java.rmi.server.UID().toString());
		fileId = cache.createId (registry.getRegistryEndpoint(), GRAPH_SERVI,
					 wantedOutputType, lastRead, props);
		createGraph (fileId, wantedOutputType, jointGraphs[i]);
		jointGraphURLs[i] = cache.getURL (fileId);
	    }
	}
	if (scufls != null && scufls.length > 0) {
	    for (int i = 0; i < scufls.length; i++) {

		props.put ("UNIQUE", new java.rmi.server.UID().toString());
		fileId = cache.createId (registry.getRegistryEndpoint(), GRAPH_SERVI,
					 "scufl", lastRead, props);
		cache.setContents (fileId, scufls[i].getBytes());
		scuflURLs[i] = cache.getURL (fileId);
	    }
	}
	if (scuflRaws != null && scuflRaws.length > 0) {
	    for (int i = 0; i < scuflRaws.length; i++) {

		props.put ("UNIQUE", new java.rmi.server.UID().toString());
		fileId = cache.createId (registry.getRegistryEndpoint(), GRAPH_SERVI,
					 "scufl", lastRead, props);
		cache.setContents (fileId, scuflRaws[i].getBytes());
		scuflRawURLs[i] = cache.getURL (fileId);
	    }
	}
	props.remove ("UNIQUE");

	//
	// put it all together and create the resulting page
	//
	out.print (h.startHtml (new String[] {
	    TITLE, "Moby Graphs Data Path page",
	    BGCOLOR, BG_COLOR,
	    LINK, LINK_COLOR, VLINK, VLINK_COLOR, TEXT, TEXT_COLOR }));

	out.println (h.a (theGraphURL, RESULTWIN, h.gen (H1, title)));
	out.println (h.gen (BLOCKQUOTE,
			    h.gen (FONT, SIZE, "-1",
				   "Click on the header above to get the whole graph. WARNING: Please take image file size into account before downloading! Do not try to open too large image in your browser. Save the file to your computer instead.")));

	out.println
	    (h.gen (TABLE, new String[] { CELLPADDING, "5" },
		    h.gen (TR,
			   h.gen (TD, h.gen (B, "Number of involved services: ")) +
			   h.gen (TD, ""+nosTheGraph)) +
		    h.gen (TR,
			   h.gen (TD, h.gen (B, "Number of edges between them: ")) +
			   h.gen (TD, ""+noeTheGraph)) +
		    h.gen (TR,
			   h.gen (TD, h.gen (B, "Number of acyclic, non-forking pathes: ")) +
			   h.gen (TD, ""+separatePaths.length)) +
		    h.gen (TR,
			   h.gen (TD, h.gen (B, "Image type: ")) +
			   h.gen (TD, wantedOutputType)) +
		    h.gen (TR,
			   h.gen (TD, h.gen (B, "Image size: ")) +
			   h.gen (TD, fsizeTheGraph + " B"))
		    ));

	if (separatePaths.length == 0) {
	    out.print (getSignature (h));
	    out.print (h.endHtml());		    
	    out.close();
	    return;
	}

	out.println (h.gen (H2, "Individual, acyclic, non-forking paths"));
	out.println (h.gen (BLOCKQUOTE,
			    h.gen (FONT, SIZE, "-1",
				   "Column <em>Workflow</em> contains a workflow definition for the given path. See ") +
			    h.a (req.getContextPath() + "/Taverna_howto.html", RESULTWIN, "here") +
			    " details how to use it with " +
			    h.a ("http://taverna.sf.net", RESULTWIN, "Taverna") + " workflow GUI and engine."));
	out.println (h.gen (BLOCKQUOTE,
			    h.gen (FONT, SIZE, "-1",
				   "Column <em>Workflow Raw</em> contains the same workflow definition but without components for creating Moby input data object, and extracting from Moby output data object. Use this if the service has more complex data type.")));


	StringBuffer b = new StringBuffer();
	for (int i = 0; i < graphURLs.length; i++) {
	    b.append (h.gen (TR,
			     h.gen (TD, ALIGN, "right", ""+(i+1)) +
			     h.gen (TD, ALIGN, "right", ""+nosInGraphs[i]) +
			     h.gen (TD, ALIGN, "right", ""+noeInGraphs[i]) +
			     h.gen (TD, h.a (graphURLs[i], RESULTWIN, " Graph ")) +
			     h.gen (TD, h.a (scuflURLs[i], RESULTWIN, " Workflow ")) +
			     h.gen (TD, h.a (scuflRawURLs[i], RESULTWIN, " Workflow "))));
	    b.append ("\n");
	}
	out.println
	    (h.gen (TABLE, new String[] { CELLPADDING, "5", BORDER, "2" },
		    h.gen (TR,
			   h.gen (TH, BGCOLOR, COL_HEADER, "&nbsp;") +
			   h.gen (TH, BGCOLOR, COL_HEADER, "# services") +
			   h.gen (TH, BGCOLOR, COL_HEADER, "# edges") +
			   h.gen (TH, BGCOLOR, COL_HEADER, "Graph image") +
			   h.gen (TH, BGCOLOR, COL_HEADER, "Workflow") +
			   h.gen (TH, BGCOLOR, COL_HEADER, "Workflow Raw")) +
		    new String (b)));

	out.println (h.gen (H2, "Joint, but still individual, acyclic, non-forking paths"));
	out.println (h.gen (BLOCKQUOTE,
			    h.gen (FONT, SIZE, "-1",
				   "These graphs are for convenience. They contain the same individual paths as above but collect more of them together.")));

	b = new StringBuffer();
	for (int i = 0; i < jointGraphURLs.length; i++) {
	    if (i == jointGraphURLs.length - 1) {
		b.append (h.gen (TR,
				 h.gen (TD, (i*pageSize+1) + " - " + (separatePaths.length+1)) +
				 h.gen (TD, h.a (jointGraphURLs[i], RESULTWIN, " Graph "))));
	    } else {
		b.append (h.gen (TR,
				 h.gen (TD, (i*pageSize+1) + " - " + (i*pageSize+pageSize)) +
				 h.gen (TD, h.a (jointGraphURLs[i], RESULTWIN, " Graph "))));
	    }
	    b.append ("\n");
	}
	out.println
	    (h.gen (TABLE, new String[] { CELLPADDING, "5", BORDER, "2" },
		    h.gen (TR,
			   h.gen (TH, BGCOLOR, COL_HEADER, "Paths") +
			   h.gen (TH, BGCOLOR, COL_HEADER, "Graph image")) +
		    new String (b)));

 	out.print (getSignature (h));
	out.print (h.endHtml());		    
	out.close();
    }

    // create a simple data type from (potentiobally empty) namesdpace
    // 'ns' and a data type
    static MobyPrimaryDataSimple createSimpleData (String nsName, String dtName) {
	MobyPrimaryDataSimple data = new MobyPrimaryDataSimple ("dummy_name_for " + dtName);
	data.setDataType (new MobyDataType (dtName));
	if (nsName != null && !nsName.equals (WITHOUT_NAMESPACE) && !nsName.equals (""))
	    data.addNamespace (new MobyNamespace (nsName));
	return data;
    }

    // return a number of services involved in 'edges'
    static int getNumberOfServices (ServicesEdge[] edges) {
	HashSet services = new HashSet();
	for (int i = 0; i < edges.length; i++) {
	    MobyService service = edges[i].getSourceService();
	    if (service != null)
		services.add (service.getName());
	    service = edges[i].getTargetService();
	    if (service != null)
		services.add (service.getName());
	}
	return services.size();
    }

    /*************************************************************************
     * Create and return a graph of Moby data types.
     *************************************************************************/
    protected void doGraphDataTypes (HttpServletRequest req, HttpServletResponse res)
	throws ServletException, IOException {

	// check if the output type is supported
	String wantedOutputType = getString (req, OUTPUT_TYPE_DATA);
	if (wantedOutputType == null)
	    wantedOutputType = T_PNG;
	boolean supported = false;
	for (int i = 0; i < supportedTypesForData.length; i++) {
	    if (supportedTypesForData[i].equals (wantedOutputType)) {
		supported = true;
		break;
	    }
	}
	if (! supported) {
	    error (res, HttpServletResponse.SC_SERVICE_UNAVAILABLE,
		   new MobyException ("Unrecognized output type '" + wantedOutputType + "'."));
	    return;
	}

	// collect visualization properties
	Properties props = new Properties();
	String rankdir = getString (req, RANKDIR);
	if (rankdir != null)
	    props.put (Graphviz.PROP_RANKDIR, rankdir);

	// this is to distinguish also the cached HTML pages
	props.put ("wot", wantedOutputType);

	try {
	    // make sure that we have data from a registry (it is
	    // either already in the registry cache - so it does not
	    // take long to read it, or it is not - so it is good to
	    // refill the cache and to get the new 'lastRead'
	    // timestamp)
	    MobyDataType[] dataTypes = registry.getDataTypes();
	    lastRead = getLastRead();

	    // perhaps we have the same graph in the cache already
	    String graphId = cache.createId (registry.getRegistryEndpoint(), GRAPH_DATA,
					     wantedOutputType, lastRead, props);
	    String pageId = cache.createId (registry.getRegistryEndpoint(), GRAPH_DATA,
					    "html", lastRead, props);
	    if ( !cache.existsInCache (graphId) || !cache.existsInCache (pageId) ) {
		// create an HMTL static page (and the graph the page has a link to)
		doDataTypesResultPage (pageId, graphId, wantedOutputType, req, dataTypes, props);
	    }
	    res.sendRedirect (res.encodeRedirectURL (cache.getURL (pageId)));

	} catch (MobyException e) {
	    error (res, HttpServletResponse.SC_SERVICE_UNAVAILABLE, e);
	    return;
	}
    }

    /*************************************************************************
     * Create a page containing results for a data types graph.
     *************************************************************************/
    protected void doDataTypesResultPage (String pageId, String graphId,
					  String wantedOutputType,
					  HttpServletRequest req,
					  MobyDataType[] dataTypes,
					  Properties props)
	throws ServletException, IOException, MobyException {

	// where (and by whom) to create resulting HTML page
	String filename = cache.getFilename (pageId);
	PrintWriter out = new PrintWriter (new FileOutputStream (filename));
	Html h = new Html (req);

	String title = "Data types graph";

	//
	// create a graph
	//
	createGraph (graphId, wantedOutputType,
		     Graphviz.createDataTypesGraph (dataTypes, props));
	long fsize = new File (cache.getFilename (graphId)).length();

	//
	// create the resulting page
	//
	out.print (h.startHtml (new String[] {
	    TITLE, "Moby Graphs Data Types page",
	    BGCOLOR, BG_COLOR,
	    LINK, LINK_COLOR, VLINK, VLINK_COLOR, TEXT, TEXT_COLOR }));

	out.println (h.a (cache.getURL (graphId), RESULTWIN, h.gen (H1, title)));
	out.println (h.gen (BLOCKQUOTE,
			    h.gen (FONT, SIZE, "-1",
				   "Click on the header above to get the whole graph. WARNING: Please take image file size into account before downloading! Do not try to open too large image in your browser. Save the file to your computer instead.")));

	out.println
	    (h.gen (TABLE, new String[] { CELLPADDING, "5" },
		    h.gen (TR,
			   h.gen (TD, h.gen (B, "Number of included data types: ")) +
			   h.gen (TD, ""+dataTypes.length)) +
		    h.gen (TR,
			   h.gen (TD, h.gen (B, "Image type: ")) +
			   h.gen (TD, wantedOutputType)) +
		    h.gen (TR,
			   h.gen (TD, h.gen (B, "Image size: ")) +
			   h.gen (TD, fsize + " B"))
		    ));

 	out.print (getSignature (h));
	out.print (h.endHtml());		    
	out.close();
    }

    /*************************************************************************
     * Create and return a graph of Moby service types.
     *************************************************************************/
    protected void doGraphServiceTypes (HttpServletRequest req, HttpServletResponse res)
	throws ServletException, IOException {

	// check if the output type is supported
	String wantedOutputType = getString (req, OUTPUT_TYPE_SERVT);
	if (wantedOutputType == null)
	    wantedOutputType = T_PNG;
	boolean supported = false;
	for (int i = 0; i < supportedTypesForServiceTypes.length; i++) {
	    if (supportedTypesForServiceTypes[i].equals (wantedOutputType)) {
		supported = true;
		break;
	    }
	}
	if (! supported) {
	    error (res, HttpServletResponse.SC_SERVICE_UNAVAILABLE,
		   new MobyException ("Unrecognized output type '" + wantedOutputType + "'."));
	    return;
	}

	// collect visualization properties
	Properties props = new Properties();
	String rankdir = getString (req, RANKDIR);
	if (rankdir != null)
	    props.put (Graphviz.PROP_RANKDIR, rankdir);

	try {
	    // perhaps we have the same graph in the cache already
	    String id = cache.createId (registry.getRegistryEndpoint(), GRAPH_SERVT,
					wantedOutputType, lastRead, props);
	    if (! cache.existsInCache (id)) {
		// create a dot definition of the graph
		log ("Creating a graph of the services types...\n");
		MobyServiceType[] serviceTypes = registry.getFullServiceTypes();
		lastRead = getLastRead();
		createGraph (id, wantedOutputType,
			     Graphviz.createServiceTypesGraph (serviceTypes, props));
	    }
	    res.sendRedirect (res.encodeRedirectURL (cache.getURL (id)));
	
	} catch (MobyException e) {
	    error (res, HttpServletResponse.SC_SERVICE_UNAVAILABLE, e);
	    return;
	}
    }

    /********************************************************************
     *
     ********************************************************************/
    protected void createGraph (String id,
				String wantedOutputType,
				String graph)
	throws MobyException, IOException {

	// where is the 'dot' program
	String dotProg = "dot";
	String dotPath = (String)initParams.get (DOT_PATH);
	if ( ! UUtils.isEmpty (dotPath) && ! "\"\"".equals (dotPath) )
	    dotProg = dotPath + System.getProperty ("file.separator") + dotProg;

	// depending on the cache implementation we may ask
	// 'dot' to produce output to its standard output, or
	// to write to a file
// 	if (cache.supportsFilenames()) {

	    // note that this filename represents a not-yet-existing file
	    String filename = cache.getFilename (id);

	    // call 'dot' to create a real graph in a 'filename'
	    executeDot (dotProg, graph, wantedOutputType, filename);

// 	} else {

// 	    // call 'dot' to return a real graph as byte array
// 	    byte[] graphBytes = executeDot (dotProg, graph, wantedOutputType);
// 	    if (graphBytes == null || graphBytes.length == 0)
// 		throw new MobyException ("An empty graph. Strange.");
// 	    cache.setContents (id, graphBytes);
// 	}
    }

    /********************************************************************
     * Return an age of the cache (if known).
     ********************************************************************/
    protected long getLastRead() {
	// note that cache age is supported by CentralDigetsCachedImpl
	// class but not by the CentralAll interface so we have to
	// cast it - but let's check first if we have just the right
	// class for it
	if (registry instanceof CentralDigestCachedImpl)
	    return ((CentralDigestCachedImpl)registry).getCacheAge();
	return -1;
    }

    /********************************************************************
     * If the registry cache is too old, it is removed (the method
     * name is just historical).
     ********************************************************************/
    protected void readRegistryIfNeeded()
	throws MobyException {

	// remember how old is the current cache
	lastRead = System.currentTimeMillis();

	// do this only the first time
	if (refreshInterval == -1) {
	    long interval = DEFAULT_REFRESH_IN_MINUTES;
	    String intervalStr = (String)initParams.get (REFRESH_INTERVAL);
	    if (! UUtils.isEmpty (intervalStr)) {
		try {
		    interval = Integer.valueOf (intervalStr).intValue();
		} catch (java.lang.NumberFormatException e) {}
	    }
	    refreshInterval = interval * 60 * 1000;
	}

	if (System.currentTimeMillis() - lastRead > refreshInterval)
	    ((CentralDigestImpl)registry).removeFromCache (null);
    }

    /********************************************************************
     * Execute 'dotProg' program with the given graph description in
     * 'dotGraph' in order to create a graph in 'outputType'
     * format. The graph is created in the given file 'filename'.
     ********************************************************************/
    protected void executeDot (String dotProg, String dotGraph,
			       String outputType, String filename)
	throws MobyException {

	if (outputType.equals ("txt"))
	    outputType = "plain-ext";

	String[] cmdLine = new String[] { dotProg, "-T" + outputType, "-o" + filename };
        String[] envArr = new String[] {};   // no environment
	try {
	    // start an external process
	    Executor executor = new Executor (cmdLine, envArr, dotGraph);

	    // wait here untill the external process ends
	    int exitCode = executor.waitFor();

	    // any errors?
	    if (exitCode != 0)
		throw new MobyException ("External 'dot' program failed with exit code " + exitCode + ".\n" +
					 executor.getStderr());

	} catch (GException e) {
	    throw new MobyException ("Error by executing 'dot': " + e.getMessage());

	} catch (InterruptedException e) {}
    }

    /********************************************************************
     * Execute 'dotProg' program with the given graph description in
     * 'dotGraph' in order to create a graph in 'outputType'
     * format. The created graph is returned back as a byte array.
     ********************************************************************/
    protected byte[] executeDot (String dotProg, String dotGraph, String outputType)
	throws MobyException {

	if (outputType.equals ("txt"))
	    outputType = "plain-ext";

	String[] cmdLine = new String[] { dotProg, "-T" + outputType };
        String[] envArr = new String[] {};   // no environment
	try {
	    // start an external process
	    Executor executor = new Executor (cmdLine, envArr, dotGraph);

	    // reads its standard output by our own thread
 	    GetBinaryData stdoutProcessor =
 		new GetBinaryData (executor.getStdoutStream());
 	    Thread stdoutThread = new Thread (stdoutProcessor);
 	    stdoutThread.start();

	    // wait here untill the external process ends
	    int exitCode = executor.waitFor();

	    // any errors?
	    if (exitCode != 0)
		throw new MobyException ("External 'dot' program failed with exit code " + exitCode + ".\n" +
					 executor.getStderr());

	    // return created graph
 	    return stdoutProcessor.getBinaryData();

	} catch (GException e) {
	    throw new MobyException ("Error by executing 'dot': " + e.getMessage());

	} catch (InterruptedException e) {}
	return new byte[] {};
    }

    class GetBinaryData extends CatchOutputDefaultImpl {

	// output of the external process
	byte[] result = new byte[] {};

	// from here we read data into 'result'
	InputStream stream;

        public GetBinaryData (InputStream stream) {
	    this.stream = stream;
        }

 	static final int BUF_SIZE = 8192;
        public void run() {
	    byte[] myBuf = new byte [BUF_SIZE];
            int readBytes;
            try {
		while ((readBytes = stream.read (myBuf)) != -1) {
		    byte[] tmp = new byte [result.length + readBytes];
		    System.arraycopy (result, 0, tmp, 0, result.length);
		    System.arraycopy (myBuf, 0, tmp, result.length, readBytes);
		    result = new byte [tmp.length];
		    System.arraycopy (tmp, 0, result, 0, tmp.length);
 		}

            } catch (IOException e) {
                errorMessage = "Error by reading 'dot' output: " + e.toString();
            }
        }

	public byte[] getBinaryData()
	    throws MobyException {
	    if (errorMessage != null)
		throw new MobyException (errorMessage);
	    return result;
	}
    }

    /********************************************************************
     *
     ********************************************************************/
    public void log (String message) {
	if (verbose) {
	    super.log (message);
// 	    ServletContext context = getServletContext();
// 	    context.log (context.getServletContextName() + ":: " + message);
	}
    }

    /********************************************************************
     *
     ********************************************************************/
    protected void setVerbose (boolean value) {
	verbose = value;
    }

    /********************************************************************
     *
     ********************************************************************/
    protected void setDebug (boolean value) {
	debug = value;
    }

    /*************************************************************************
     * Take some instance members and put them into hidden form
     * element - so that they are passed to the naxt invocation
     * (usage) of this same instance. The instance members were set in
     * the creation of this instance and they do not change during the
     * life-cycle of this instance.
     *************************************************************************/
    protected String getFieldsAsHidden (Html h) {
	StringBuffer buf = new StringBuffer();
	buf.append (h.hidden (REGISTRY_URL, registry.getRegistryEndpoint(), true));   buf.append ("\n");
	buf.append (h.hidden (REGISTRY_URI, registry.getRegistryNamespace(), true));  buf.append ("\n");
	if (verbose) {
	    buf.append (h.hidden (VERBOSE, "1", true));
	    buf.append ("\n");
	}
	if (debug) {
	    buf.append (h.hidden (DEBUG, "1", true));
	    buf.append ("\n");
	}
	return new String (buf);
    }

    /*************************************************************************
     * Create a signature. It uses 'h' for generating the output.
     *************************************************************************/
    protected String getSignature (Html h) {
	String mName = (String)initParams.get (MASTER_NAME);
	String mEmail = (String)initParams.get (MASTER_EMAIL);
	String contact;
	if (UUtils.isEmpty (mName) || UUtils.isEmpty (mEmail))
	    contact = "";
	else
	    contact = h.gen (A, HREF, "mailto:" + mEmail, mName);

	return
	    h.gen (HR) + h.gen (DIV, ALIGN, "right",
				h.gen (FONT, SIZE, "-2",
				       h.gen (ADDRESS, contact) +
				       "Page created: " + UUtils.formatDate()));
    }

    /*************************************************************************
     * Send an error message.
     * The error was caused by exception 'e' and has an error code 'sc'.
     *************************************************************************/
    protected static void error (HttpServletResponse res, int sc, Throwable e) {
	String msg;
	if (e instanceof MobyException)
	    msg = e.getMessage();
	else
	    msg = e.toString();
	try {
	    res.sendError (sc, msg);
	} catch (IOException ex) {
	    System.err.println ("Failed to deliver error message: " +
				"(" + sc + ") " + msg);
	}
    }

    /********************************************************************
     * Initialize cache (for created graphs). It is separated here for
     * inheriting classes which may wish to use a different cache
     * implementation.
     *
     * All caches instantiated by this method are filesystem-based
     * caches.  If there is an init parameter CACHE_DIR we store
     * cached files starting from CACHE_DIR directory, otherwise we
     * store them inside this servlet context on the 'contextPath'.
     ********************************************************************/
    protected SimpleFileCache initCache (ServletContext sContext,
					 String contextPath) {
	String cacheDir = (String)initParams.get (CACHE_DIR);
	if (UUtils.isEmpty (cacheDir)) {
	    return new ServletFileCache (sContext, contextPath);
	} else {
	    String cacheURL = (String)initParams.get (CACHE_URL);
	    return new FileCache (cacheDir, cacheURL);
	}
    }

    /********************************************************************
     * Put all elements from 'arr' into a hashtable and return it.
     ********************************************************************/
    static protected Hashtable arr2hash (String[] arr) {
	Hashtable result = new Hashtable();
	if (arr == null) return result;
	for (int i = 0; i < arr.length; i++) {
	    if (arr[i] != null)
		result.put (arr[i], "1");
	}
	return result;
    }

    /********************************************************************
     *
     *  Methods candidateing to be included in tools... (TBD)
     *
     ********************************************************************/

    /********************************************************************
     * Return value of a named parameter as a string (trimmed etc.).
     *
     * @param req is the HTTP request having all parameters
     * @param name is the name whose value is asked for
     * @return value of requested parameter or null if the parameter
     * is either empty or does not exists at all; value is trimmed
     ********************************************************************/
    static public String getString (HttpServletRequest req, String name) {
	String value = req.getParameter (name);
	if (value == null) return null;
	value = value.trim();
	if (value.equals ("")) return null;
	return value;
    }

    // TBD:
// 	    // the 'image buttons' do not deliver 'action' directly but in their name
// 	    // (together with .x and .y suffix which must be removed first)
// 	    if (action == null) {
// 		String name;
// 		for (Enumeration en = req.getParameterNames(); en.hasMoreElements(); ) {
// 		    name = (String)en.nextElement();
// 		    if (name.endsWith (".x"))
// 			action = name.substring (0, name.length() - 2);
// 		}
// 	    }

    /********************************************************************
     * Return value of a named parameter as boolean. The values
     * considered to be TRUE are: 'on', '1', 'true', 'yes', '+' (case
     * insensitive), and an empty value. Everything else is considered
     * to be FALSE.
     *
     * @param req is the HTTP request having all parameters
     * @param name is the name whose value is asked for
     * @return boolean value of requested parameter, or FALSE if the
     * parameter does not exist at all
     ********************************************************************/
    static public boolean getBoolean (HttpServletRequest req, String name) {
	String value = req.getParameter (name);
	if (value == null) return false;
	value = value.trim().toLowerCase();
	return ( value.equals ("")     ||
		 value.equals ("on")   ||
		 value.equals ("true") ||
		 value.equals ("yes")  ||
		 value.equals ("+")    ||
		 value.equals ("1") );
    }

    /********************************************************************
     * Return TRUE if the named parameter exists in given 'req'
     * request.
     *
     * @param req is the HTTP request having all parameters
     * @param name is the name whose existence is tested
     * @return TRUE if the named parameter exists
     ********************************************************************/
    static public boolean exists (HttpServletRequest req, String name) {
	return (req.getParameter (name) != null);
    }

    /********************************************************************
     *
     ********************************************************************/
    public String[] radioGroup (Html h, String name, String[] values, int checked) {
	String[] result = new String [values.length];
	Hashtable ht = new Hashtable();
	ht.put (TYPE, "radio");
	ht.put (NAME, name);
	for (int i = 0; i < values.length; i++) {
	    ht.put (VALUE, values[i]);
	    if (i == checked)
		ht.put (CHECKED, h.getNullObject());
	    else
		ht.remove (CHECKED);
	    result[i] = h.gen (INPUT, ht);
	}
	return result;
    }

    public String start (String tag) {
	return ("<" + tag + ">");
    }

    public static final String ONCHANGE = "onChange";

}
