// ServicesGenerator.java
//
//    Created: August 2005
//
// This file is a component of the BioMoby project.
// Copyright Martin Senger (martin.senger@gmail.com).
//

package org.biomoby.service.generator;

import org.biomoby.shared.MobyException;
import org.biomoby.shared.MobyService;
import org.biomoby.shared.MobyDataType;
import org.biomoby.shared.MobyData;
import org.biomoby.shared.MobyNamespace;
import org.biomoby.shared.MobyPrimaryDataSimple;
import org.biomoby.shared.MobyPrimaryDataSet;
import org.biomoby.shared.Utils;
import org.biomoby.shared.CentralCached;
import org.biomoby.shared.parser.MobyParser;
import org.biomoby.shared.datatypes.MapDataTypesIfc;

import org.biomoby.client.ServiceConnections;
import org.biomoby.client.ServicesEdge;
import org.biomoby.client.FilterServices;
import org.biomoby.client.Graphviz;

import org.tulsoft.tools.servlets.Html;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.BooleanUtils;

import java.util.Properties;
import java.util.Iterator;
import java.util.Map;
import java.util.HashMap;
import java.util.Set;
import java.util.HashSet;
import java.util.Map;
import java.util.Vector;
import java.util.Date;
import java.util.regex.Pattern;
import java.io.File;

/**
 * A generator generating skeleton classes for Biomoby services. Such
 * skeletons can be extended by a service-provider and she or he just
 * fills there "business logic" (the real work of a service) without
 * worrying about how to parse and create data in a Biomoby specific
 * XML format. <p>
 *
 * The generated classes uses Biomoby data types that were also
 * generated (see {@link
 * org.biomoby.service.generator.DataTypesGenerator}). <p>
 *
 * Both services and data types are generated using information stored
 * in a Biomoby registry. In other words, generating is possible only
 * for entities that had been registered. <p>
 *
 * The best way how to find what it does for you is to use
 * command-line client <tt>MosesGenerators</tt> and let it generate
 * some classes, browse them and try to extend them. The examples and
 * information which methods are available, and which methods must be
 * implemented are in {@link org.biomoby.service.BaseService} where
 * all generated skeletons inherit from. <p>
 *
 * Start <tt>MosesGenerators</tt> either from a command-line using a
 * script:
 *
 * <pre>
 * build/run/run-generator -h
 * </pre>
 *
 * or using Ant:
 *
 * <pre>
 * ant -Dmoses.service=MyService moses-services
 * </pre>
 *
 * The name <tt>MyService</tt> is the name under a service was
 * registered in a Biomoby registry. <p>
 *
 * @author <A HREF="mailto:martin.senger@gmail.com">Martin Senger</A>
 * @version $Id: ServicesGenerator.java,v 1.8 2008/03/03 11:34:17 senger Exp $
 */

public class ServicesGenerator
    extends Generator {

    protected static final String DEFAULT_PACKAGE = "org.biomoby.service.skeleton";

    // hashmap keys of template filenames
    protected static final String TN_SERVICE = "tn20";
    protected static final String TN_PARAMETER = "tn21";
    protected static final String TN_PISET = "tn22";
    protected static final String TN_PISIMPLE = "tn23";
    protected static final String TN_POSET = "tn24";
    protected static final String TN_POSIMPLE = "tn25";

    // mutable integer
    class IntValue { int n; }

    /**************************************************************************
     *
     *************************************************************************/
    public ServicesGenerator() {
	super();
    }

    /**************************************************************************
     *
     *************************************************************************/
    public ServicesGenerator (String cacheDir)
	throws MobyException {
	super (cacheDir);
    }

    /**************************************************************************
     *
     *************************************************************************/
    public ServicesGenerator (String registryEndpoint,
			       String registryURI,
			       String cacheDir)
	throws MobyException {
	super (registryEndpoint, registryURI, cacheDir);
    }

    /**************************************************************************
     *
     *************************************************************************/
    public ServicesGenerator (final CentralCached worker) {
	super (worker);
    }

    /*************************************************************************
     *
     *************************************************************************/
    public Map<String,File> getTemplateNames() {
	Map<String,File> tNames = new HashMap<String,File>();
	File dir = new File ("templates");
	tNames.put (TN_SERVICE, new File (dir, "ServiceSkeletonTemplate.java"));
	tNames.put (TN_PARAMETER, new File (dir, "ServiceParameterTemplate.java"));
	tNames.put (TN_PISET, new File (dir, "ServicePISetTemplate.java"));
	tNames.put (TN_PISIMPLE, new File (dir, "ServicePISimpleTemplate.java"));
	tNames.put (TN_POSET, new File (dir, "ServicePOSetTemplate.java"));
	tNames.put (TN_POSIMPLE, new File (dir, "ServicePOSimpleTemplate.java"));
	return tNames;
    }

    //
    // fill these only the first time - they are all immutable so a
    // concurrent access to them is possible and safe
    //
    String serviceTemplate;
    String serviceParameterTemplate;
    String servicePISetTemplate;
    String servicePISimpleTemplate;
    String servicePOSetTemplate;
    String servicePOSimpleTemplate;

    // additional patterns that will be found in templates and
    // replaced - compile them only once (again, they are immutable,
    // therefore thread-safe)
    static final Pattern P_PACKAGE_NAME  = Pattern.compile ("@PACKAGE_NAME@");
    static final Pattern P_SERVICE_NAME  = Pattern.compile ("@SERVICE_NAME@");
    static final Pattern P_SERVICE_TYPE  = Pattern.compile ("@SERVICE_TYPE@");
    static final Pattern P_SERVICE_URL   = Pattern.compile ("@SERVICE_URL@");
    static final Pattern P_SERVICE_RDF   = Pattern.compile ("@SERVICE_RDF@");
    static final Pattern P_IOTABLE       = Pattern.compile ("@IOTABLE@");
    static final Pattern P_FALLBACK_CODE = Pattern.compile ("@FALLBACK_CODE@");

    // dynamically created (org.biomoby.shared.datatypes.MapDataTypes)
    // (we need it for generating set/get methods for input/output
    // that do not have their own article name (which is a usual case)
    // - so we will use their class names instead (and this mapping
    // interface will give them to us)
    MapDataTypesIfc mapDataTypes;

    /**************************************************************************
     * Generate classes for service(s) from a given Biomoby
     * registry. This is the main method. <p>
     *
     * @see #GPROP_SERVICE property GPROP_SERVICE
     * @see #GPROP_AUTH property GPROP_AUTH
     * @see #GPROP_OUTDIR property GPROP_OUTDIR
     * @see #GPROP_VERBOSE property GPROP_VERBOSE
     * @see #GPROP_NOGRAPHS property GPROP_NOGRAPHS
     * @see #GPROP_DOTLOCATION property GPROP_DOTLOCATION
     *
     * @param props are properties influencing the generated results
     *
     * @throws MobyException if anything goes wrong
     *************************************************************************/
    public void generate (Properties props)
	throws MobyException {

	verbose = BooleanUtils.toBoolean (props.getProperty (GPROP_VERBOSE));

	// compile patterns for filtering by service and authority names
	Pattern wantedService = null;
	String filter = props.getProperty (GPROP_SERVICE);
	if (StringUtils.isNotBlank (filter))
	    wantedService = Pattern.compile (filter);
	Pattern wantedAuthority = null;
	filter = props.getProperty (GPROP_AUTH);
	if (StringUtils.isNotBlank (filter))
	    wantedAuthority = Pattern.compile (filter);

	// load all templates (from files)
	Map<String,File> templates = getTemplateNames();
	serviceTemplate          = loadTemplate (templates.get (TN_SERVICE));
	serviceParameterTemplate = loadTemplate (templates.get (TN_PARAMETER));
	servicePISetTemplate     = loadTemplate (templates.get (TN_PISET));
	servicePISimpleTemplate  = loadTemplate (templates.get (TN_PISIMPLE));
	servicePOSetTemplate     = loadTemplate (templates.get (TN_POSET));
	servicePOSimpleTemplate  = loadTemplate (templates.get (TN_POSIMPLE));
	templates = null;   // not needed any more

	// load a mapping rules between biomoby names and Java classes
	mapDataTypes = MobyParser.loadB2JMapping();

 	// read all authority and service names (from a registry)
	if (verbose)
	    System.out.println ("Reading service names...");
 	Map authorities = worker.getServiceNamesByAuthority();

	// conditionally, read all data types (also from a registry)
	MobyDataType[] allDataTypes = null;
	MobyService[] allServices = null;
	if (worker.isUsingCache() ||
	    ! BooleanUtils.toBoolean (props.getProperty (GPROP_NOGRAPHS))) {
	    if (verbose)
		System.out.println ("Reading data type definitions...");
	    allDataTypes = worker.getDataTypes();
	    if (verbose)
		System.out.println ("Reading service definitions...");
	    allServices = worker.getServices();
	}

	// filter by name - and if found, generate for it
	IntValue serviceCount = new IntValue();
	for (Iterator it = authorities.entrySet().iterator(); it.hasNext(); ) {
	    Map.Entry entry = (Map.Entry)it.next();
	    String authority = (String)entry.getKey();
	    if (filteredIn (authority, wantedAuthority)) {
		String[] names = (String[])entry.getValue();
		for (int i = 0; i < names.length; i++) {
		    if (filteredIn (names[i], wantedService)) {
			MobyService patternService = new MobyService (names[i]);
			patternService.setAuthority (authority);
			patternService.setCategory ("");
			MobyService[] services = findService (patternService, worker, allServices);
			for (int s = 0; s < services.length; s++)
			    generateService (services[s], props, allServices, allDataTypes, serviceCount);
		    }
		}
	    }
	}
	if (verbose)
	    System.out.println ("Generated " + serviceCount.n + " service skeletons.");
    }

    /**************************************************************************
     * Generate skeleton for a 'service'.
     *
     * TBD:? Perhaps I should rather consider to steal from
     * org.apache.tools.ant.filters.ReplaceToken, and not to use regex...
     *************************************************************************/
    protected void generateService (MobyService service,
				    Properties props,
				    MobyService[] allServices,
				    MobyDataType[] allDataTypes,
				    IntValue serviceCount)
	throws MobyException {

 	String serviceName = service.getName();
 	if (! serviceName.equals (Utils.javaEscape (serviceName)) ) {
	    System.err.println ("Ignoring service '" + serviceName +
				"'. Sorry, in Java, its name is problematic.");
	    return;
	}

	if (BooleanUtils.toBoolean (props.getProperty (GPROP_NOGEN))) {
	    System.out.println (service.toShortString());
	    return;
	}
	if (verbose)
	    System.out.println (service.toShortString());

	String packageName = authority2package (service.getAuthority());

	// prepare output dirs
  	File outputDir = makeDirs (props.getProperty (GPROP_OUTDIR),
				   packageName);
	makeDirForGraphs (outputDir, props);

	// graphs?
	boolean graphCreated = createGraph (serviceName, outputDir, props, allServices, allDataTypes);

	// fallback type - its type depends on the number of primary inputs
	StringBuilder fallbackCode = new StringBuilder();
	MobyData[] pis = service.getPrimaryInputs();
	if (pis.length == 0) {
	    fallbackCode.append ("String fallback = null;");
	} else if (pis.length == 1) {
	    String dataTypeName = mapDataType2class (pis[0]);
	    if (dataTypeName != null) {
		fallbackCode.append ("String fallback = \"");
		fallbackCode.append (dataTypeName);
		fallbackCode.append ("\";");
	    } else {
		fallbackCode.append ("String fallback = null;");
	    }
	} else if (pis.length > 1) {
	    fallbackCode.append
		("java.util.Map<String,String> fallback = new java.util.HashMap<String,String>(); ");
	    for (MobyData p: pis) {
		String dataTypeName = mapDataType2class (p);
		if (dataTypeName != null) {
		    String articleName = p.getName();
		    if (StringUtils.isNotBlank (articleName)) {
			fallbackCode.append ("fallback.put (\"");
			fallbackCode.append (articleName);
			fallbackCode.append ("\",\"");
			fallbackCode.append (dataTypeName);
			fallbackCode.append ("\"); ");
		    }
		}
	    }
	}
	fallbackCode.append ("\n");

	// replace tokens in templates
	String result = P_SERVICE_NAME.matcher (serviceTemplate).replaceAll (serviceName);
	result = P_PACKAGE_NAME.matcher (result).replaceAll (packageName);
	result = P_SERVICE_TYPE.matcher (result).replaceAll (service.getType());
	result = P_SERVICE_URL.matcher (result).replaceAll (service.getURL());
	result = P_SERVICE_RDF.matcher (result).replaceAll (service.getSignatureURL());
	result = P_DATE.matcher (result).replaceAll (new Date().toString());
	result = P_USER_OS.matcher (result).replaceAll (getSignature());
	result = P_DESCRIPTION.matcher (result).replaceAll (htmlEscape (service.getDescription()));
	result = P_AUTHORITY.matcher (result).replaceAll (htmlEscape (service.getAuthority()));
	result = P_EMAIL_CONTACT.matcher (result).replaceAll (htmlEscape (service.getEmailContact()));
	result = P_IMAGESTART.matcher (result).replaceAll (graphCreated ? "" : "<!-- ");
	result = P_IMAGEEND.matcher (result).replaceAll (graphCreated ? "" : " -->");
	result = P_FALLBACK_CODE.matcher (result).replaceAll (fallbackCode.toString());

	// 
 	// now apply templates for set/get methods (put the result in
 	// a buffer that will be later added to the main result)
	StringBuffer bufMethods = new StringBuffer (1000);

	// ...primary inputs
	if (verifyNames (pis, serviceName)) {
	    for (int i = 0; i < pis.length; i++) {
		String methods;
		if (pis[i] instanceof MobyPrimaryDataSimple)
		    methods = servicePISimpleTemplate;
		else
		    methods = servicePISetTemplate;

		methods = replace (pis[i], methods, serviceName);
		if (methods == null)
		    return;  // ignoring this service
		bufMethods.append (methods);
	    }
	}

	// ...primary outputs
	MobyData[] pos = service.getPrimaryOutputs();
	if (verifyNames (pos, serviceName)) {
	    for (int i = 0; i < pos.length; i++) {
		String methods;
		if (pos[i] instanceof MobyPrimaryDataSimple)
		    methods = servicePOSimpleTemplate;
		else
		    methods = servicePOSetTemplate;

		methods = replace (pos[i], methods, serviceName);
		if (methods == null)
		    return;  // ignoring this service
		bufMethods.append (methods);
	    }
	}

	// ...secondary inputs
	MobyData[] sis = service.getSecondaryInputs();
	for (int i = 0; i < sis.length; i++) {
	    String methods = serviceParameterTemplate;

	    String articleName = sis[i].getName();
	    if (StringUtils.isBlank (articleName)) {
		System.err.println (MSG_SECONDARY_PBL (serviceName, sis[i]));
		continue;
	    }
	    String escArticleName = Utils.mobyEscape (Utils.javaEscape (articleName));

	    methods = P_ARTICLE_NAME.matcher (methods).replaceAll (articleName);
	    methods = P_ESC_ARTICLE_NAME.matcher (methods).replaceAll (escArticleName);
	    bufMethods.append (methods);
	}

	// incorporate the method's templates into the main template
 	result = P_METHODS.matcher (result).replaceAll (new String (bufMethods));

	// add an input/output HTML table
	MyHtml htmlWorker = new MyHtml();
 	result = P_IOTABLE.matcher (result).replaceAll (htmlWorker.makeIOTable (pis, pos, sis));

 	// output it
 	Utils.createFile (new File (outputDir, serviceName + "Skel.java"), result);
	serviceCount.n++;
    }
	
    //
    // shared by primary inputs and primary outputs
    //
    private String replace (MobyData data, String methods, String serviceName) {

	// don't worry about a null in articleName: it was already
	// checked by verifyNames() - such case will not come here at all
	String articleName = getOrCreateArticleName (data);
	String escArticleName = Utils.mobyEscape (Utils.javaEscape (articleName));

	// here you need to worry about a null because an input (or
	// output) can have a good article name - so it passes through
	// verifyNames() but its data type was not generated, yet - in
	// such case we ignore the whole service (by returning null)
	String dataTypeName = mapDataType2class (data);
	if (dataTypeName == null) {
	    System.err.println (MSG_DATATYPE_PBL (serviceName, data));
	    return null;
	}

	methods = P_ARTICLE_NAME.matcher (methods).replaceAll (articleName);
	methods = P_ESC_ARTICLE_NAME.matcher (methods).replaceAll (escArticleName);
	methods = P_DATATYPE.matcher (methods).replaceAll (dataTypeName);

	return methods;
    }

    /*************************************************************************
     * The inputs and outputs in service descriptions should have
     * article names. But often they don't. It still does not need to
     * harm us here as long as the number of missing article names is
     * small.
     *
     * For example, if a Simple is the only input to a service, the
     * article name can be substituted by the data type carried by
     * this Simple. But if there are two Simples without article
     * names, and both carry the same data type, we have a
     * problem. The same for collections (a data type of its first
     * Simple is used).
     *
     * If there are two inputs (or outputs) without article names but
     * one is a Simple and one is a Collection then it is not
     * considered a problem because for both a different method will
     * be genrated (with "Set" at the end for collections).
     *
     * Another problem is if a service has two inputs with the same
     * article name.
     *
     * Last but not least, a service may use a data type that we do
     * not know (that was not generated, yet).
     *
     * If this method finds any of these problems it reports it
     * (that's why it gets a 'serviceName') and returns false.
     *************************************************************************/
    protected boolean verifyNames (MobyData[] data, String serviceName) {
	Set<String> names = new HashSet<String>();
	for (int i = 0; i < data.length; i++) {
	    String name = getOrCreateArticleName (data[i]);

	    // probably a missing data type
	    if (name == null) {
		System.err.println (MSG_DATATYPE_PBL (serviceName, data[i]));
		return false;
	    }

	    // do we have already the same name? (escape it first
	    // because that's how it would be used in generated method
	    // names)
	    String escName = Utils.mobyEscape (Utils.javaEscape (name));
	    if (data[i] instanceof MobyPrimaryDataSet)
		escName += "Set";
	    if (names.contains (escName)) {
		System.err.println (MSG_DUPLICATE_PBL (serviceName, data[i]));
		return false;
	    }
	    names.add (escName);
	}
	return true;	
    }

    /*************************************************************************
     * Create a package name from 'authority'. If authority is empty,
     * use a default package name. If authority has bad charcters,
     * ignore them or replace them.
     *************************************************************************/
    protected String authority2package (String authority) {
	if (StringUtils.isBlank (authority))
	    return DEFAULT_PACKAGE;
 	StringBuffer buf = new StringBuffer (100);
	String[] parts = authority.split ("\\.");
	int len = parts.length;
	for (int i = len-1; i >= 0; i--) {
	    if (i < len-1) buf.append ('.');
	    buf.append (Utils.javaEscape (parts[i]));
	}
	return new String (buf);
    }
    
    /*************************************************************************
     * Decide if the 'name' (which is either a service name or an
     * authority name but it really does not matter here) should be
     * included in the generated output.
     *
     * Then return true either if there is no filtering 'pattern', or
     * if the name of 'service' matches the 'pattern'.
     *************************************************************************/
    protected static boolean filteredIn (String name, Pattern pattern) {
 	if (pattern == null)
 	    return true;
 	return pattern.matcher (name).find();
    }

    /*************************************************************************
     * Extract an article name from given 'data'. If there is none,
     * next step depends what kind of object 'data' is:
     *
     * If 'data' is a secondary input (a parameter) then ignore it and
     * return null (a parameter should have always an article name)
     * (but, actually, I do not send here the secondary inputs at all
     * - because for them it does not make sense to try to use a
     * datatype, it's always a string).
     *
     * If it is a PrimaryInput or PrimaryOutput (which means a Simple)
     * then return a (simple, un-qualified) class name that will
     * represent this data type when the real data come (we have a
     * mapping class for it already loaded).
     *
     * If it is a PrimaryInputSet or PrimaryOutputSet (which means a
     * Collection) then use its first element and return the same as
     * described above for PrimaryInput/Output.
     *************************************************************************/
    protected String getOrCreateArticleName (MobyData data) {
	String name = data.getName();
	if (StringUtils.isNotBlank (name))
	    return name;
	return mapDataType2class (data);
    }

    protected String mapDataType2class (MobyData data) {
	if (data instanceof MobyPrimaryDataSimple) {
	    MobyDataType dt = ((MobyPrimaryDataSimple)data).getDataType();
	    return mapSimple (dt);
	} else if (data instanceof MobyPrimaryDataSet) {
	    MobyPrimaryDataSimple[] elems = ((MobyPrimaryDataSet)data).getElements();
	    if (elems.length == 0) return null;
	    MobyDataType dt = elems[0].getDataType();
	    return mapSimple (dt);
	}
	return null;
    }

    private String mapSimple (MobyDataType dt) {
	String className = mapDataTypes.getClassName (dt.getName());
	if (className == null) return null;
	return Utils.simpleClassName (className);
    }

    /**************************************************************************
     * Return services that match 'patternService'. You can take an
     * advantage of already collected all services in 'allServices' if
     * it is not null (it is null if a local cache is not used, or if
     * graphs are not required). If it is null, however, do a normal
     * way: go to a registry using given 'worker'.
     *
     * Note that 'patternService' has filled only service and
     * authority name (and both are guaranteed not to be null), so
     * there is no need to make matching too fancy.
     *************************************************************************/
    protected MobyService[] findService (MobyService patternService,
					 CentralCached worker,
					 MobyService[] allServices)
	throws MobyException {
 	if (allServices == null)
 	    return worker.findService (patternService);
	Vector<MobyService> v = new Vector<MobyService>();
	String name = patternService.getName();
	String auth = patternService.getAuthority();
	for (int i = 0; i < allServices.length; i++) {
	    if ( name.equals (allServices[i].getName()) &&
		 auth.equals (allServices[i].getAuthority()) )
		v.addElement (allServices[i]);
	}
	MobyService[] result = new MobyService [v.size()];
	v.copyInto (result);
	return result;
    }

    /*************************************************************************
     * Conditionally, create a graph for the given service showing its
     * neighbours.
     *
     * Be sensible about errors (a graph is not a primary concern,
     * that's why it does not throw any exception if it fails).
     *
     * Return true if an image file was successfull created (so other
     * parts can generate links to it).
     *************************************************************************/
    protected boolean createGraph (String serviceName,
				   File outputDir,
				   Properties props,
				   MobyService[] allServices,
				   MobyDataType[] allDataTypes) {

	if (BooleanUtils.toBoolean (props.getProperty (GPROP_NOGRAPHS)))
	    return false;
	if (allServices == null || allDataTypes == null)
	    return false;

	ServicesEdge[] edges
	    = ServiceConnections.build (allDataTypes, allServices);
	edges = FilterServices.filter (edges, null, new String[] { serviceName }, 1);

	Properties graphProps = new Properties();
	graphProps.put (Graphviz.PROP_HIGHLIGHT, serviceName);
	String dot = Graphviz.createServicesGraph (edges, graphProps);
	// to debug: Utils.createFile (new File ("dt.dot"), dot);

	// 'dot' program converts our 'dot' definition to an image
	return executeDot (dot, outputDir, serviceName, props, null);
    }

    /**************************************************************************
     *
     *************************************************************************/
    private String MSG_DUPLICATE_PBL (String serviceName, MobyData data) {
	String name =
	    ("".equals (data.getName()) ? "" : "'" + data.getName() + "' ");
	return ("Ignoring some input or output data in '" + serviceName +
		"'. The data " + name + "are ambiguously defined.");
    }

    /**************************************************************************
     *
     *************************************************************************/
    private String MSG_DATATYPE_PBL (String serviceName, MobyData data) {
	String name = data.getName(); // but an article name is not good enough here
	if (data instanceof MobyPrimaryDataSimple) {
	    MobyDataType dataType = ((MobyPrimaryDataSimple)data).getDataType();
	    if (dataType != null)
		name = dataType.getName();
	}
	return ("Ignoring service '" + serviceName +
		"'. I am probably missing data type '" + name + "'.\n" +
		"Try to generate data types again, without using any cache. For example:\n" +
		"   ant -Dregistry.cache.dir=\"\" moses-datatypes");
    }

    /**************************************************************************
     *
     *************************************************************************/
    private String MSG_SECONDARY_PBL (String serviceName, MobyData data) {
	return ("Ignoring secondary parameter in service '" + serviceName +
		"' because it does not have an article name.");
    }

    /**************************************************************************
     * Generate a piece of HTML here...
     *************************************************************************/
    class MyHtml extends Html {

	static final String NBSP = "&nbsp;";
	String nbsp (String value) {
	    if (StringUtils.isBlank (value)) return NBSP;
	    else return value;
	}
	MobyPrimaryDataSimple getOneSimple (MobyData data) {
	    if (data instanceof MobyPrimaryDataSimple)
		return ((MobyPrimaryDataSimple)data);
	    if (data instanceof MobyPrimaryDataSet) {
		MobyPrimaryDataSimple[] components =
		    ((MobyPrimaryDataSet)data).getElements();
		if (components.length > 0)
		    return components[0];
	    }
	    return null;
	}
	String getDataType (MobyData data) {
	    MobyPrimaryDataSimple simple = getOneSimple (data);
	    if (simple == null) return NBSP;
	    return simple.getDataType().getName();
	}
	String getNamespaces (MobyData data) {
	    MobyPrimaryDataSimple simple = getOneSimple (data);
	    if (simple == null) return NBSP;
	    StringBuffer buf = new StringBuffer();
	    MobyNamespace[] ns = simple.getNamespaces();
	    for (int i = 0; i < ns.length; i++) {
		if (i > 0) buf.append ("<BR>");
		buf.append (ns[i].getName());
	    }
	    return new String (buf);
	}

	//
	// fill buffer 'b' from 'data'
	//
	public String makeIOTable (MobyData[] pis, MobyData[] pos, MobyData[] sis) {

	    StringBuffer b = new StringBuffer (1000);
	    b.append ("<table border=1 cellpadding=5>");
	    b.append (gen (TR,
			     gen (TH, NBSP) +
			     gen (TH, "Article name") +
			     gen (TH, "Data type") +
			     gen (TH, "Namespace(s)")));
	    for (int i = 0; i < pis.length; i++) {
		String articleName = nbsp (pis[i].getName());
		String dataTypeName = nbsp (getDataType (pis[i]));
		if (pis[i] instanceof MobyPrimaryDataSet)
		    dataTypeName = gen (EM, "Collection of<br>") + dataTypeName;
		String namespaces = nbsp (getNamespaces (pis[i]));
		b.append (gen (TR,
				 (i == 0 ? gen (TD, new String[] { ROWSPAN, "" + pis.length },
						  gen (B, "Primary Inputs")) : "") +
				 gen (TD, articleName) +
				 gen (TD, dataTypeName) +
				 gen (TD, namespaces) ));
	
	    }
	    for (int i = 0; i < sis.length; i++) {
		String articleName = nbsp (sis[i].getName());
		b.append (gen (TR,
				 (i == 0 ? gen (TD, new String[] { ROWSPAN, "" + sis.length },
						  gen (B, "Secondary Inputs")) : "") +
				 gen (TD, articleName) +
				 gen (TD, NBSP) +
				 gen (TD, NBSP) ));
	    }
	    for (int i = 0; i < pos.length; i++) {
		String articleName = nbsp (pos[i].getName());
		String dataTypeName = nbsp (getDataType (pos[i]));
		if (pos[i] instanceof MobyPrimaryDataSet)
		    dataTypeName = gen (EM, "Collection of<br>") + dataTypeName;
		String namespaces = nbsp (getNamespaces (pos[i]));
		b.append (gen (TR,
				 (i == 0 ? gen (TD, new String[] { ROWSPAN, "" + pos.length },
						  gen (B, "Primary Outputs")) : "") +
				 gen (TD, articleName) +
				 gen (TD, dataTypeName) +
				 gen (TD, namespaces) ));
	    }
	    b.append (end (TABLE));
	    return new String (b);
	}
    }

}
