/* * This file or a portion of this file is licensed under the terms of * the Globus Toolkit Public License, found in file ../GTPL, or at * http://www.globus.org/toolkit/download/license.html. This notice must * appear in redistributions of this file, with or without modification. * * Redistributions of this Software, with or without modification, must * reproduce the GTPL in: (1) the Software, or (2) the Documentation or * some other similar material which is provided with the Software (if * any). * * Copyright 1999-2004 University of Chicago and The University of * Southern California. All rights reserved. */ package org.griphyn.vdl.dax; import edu.isi.pegasus.common.util.Currently; import org.griphyn.vdl.dax.*; import org.griphyn.vdl.classes.LFN; import org.griphyn.vdl.classes.Derivation; import java.util.*; import java.io.Writer; import java.io.IOException; /** * This class is the container for an abstract DAG description. It consists * of three parts.<p> * * <ol> * <li> {@link Filename} deals with the filenames that are used in the * picture of the DAG - does a file go into the DAG, come out of the * DAG, or is it an intermediary file. There are multiple instances * stored in a DAX. * * <li> {@link Job} deals with the description of all jobs in a DAG. * Each job has a logical transformation, commandline argument, possible * stdio redirection, and a potential set of <code>Profile</code> * settings. There are multiple instance stored in a DAX. * * <li> {@link Child} deals with the dependency in a two-level fashion. * It contains a list of child to parent(s) relationships. The children * and parents refer to jobs from the previous section. There are * multiple instances stored in a DAX. * </ol> * * @author Jens-S. Vöckler * @author Yong Zhao * @version $Revision$ */ public class ADAG extends DAX implements Cloneable { /** * The "official" namespace URI of the DAX schema. */ public static final String SCHEMA_NAMESPACE = "http://pegasus.isi.edu/schema/DAX"; /** * The "not-so-official" location URL of the DAX schema definition. */ public static final String SCHEMA_LOCATION = "http://pegasus.isi.edu/schema/dax-2.1.xsd"; /** * The version to report. */ public static final String SCHEMA_VERSION = "2.1"; /** * list of all filenames in terms of Filename * @see Filename */ private TreeMap m_fileMap; /** * list of all jobs * @see Job */ private TreeMap m_jobMap; /** * list of all child nodes to construct DAG. * @see Child */ private TreeMap m_childMap; /** * list of replacements for node collapsion from compounds. */ private TreeMap m_replace; private boolean m_dirty; /** * optional name of this document. */ private String m_name = null; /** * When generating alternatives, this is the total number of alternatives. */ private int m_size; /** * When generating alternatives, this is the zero-based count. */ private int m_index; /** * The version to report back, or alternatively to the version that * the DAX file had when reading. */ private String m_version = SCHEMA_VERSION; /** * Creates and returns a copy of this object. * @return a new instance, deep copy of elements */ public Object clone() { ADAG result = new ADAG( this.m_size, this.m_index, this.m_name ); // maybe unsafe? result.setVersion( this.m_version ); for ( Iterator i=this.m_fileMap.values().iterator(); i.hasNext(); ) { result.addFilename( (Filename) ((Filename) i.next()).clone() ); } for ( Iterator i=this.m_jobMap.values().iterator(); i.hasNext(); ) { result.addJob( (Job) ((Job) i.next()).clone() ); } for ( Iterator i=this.m_childMap.values().iterator(); i.hasNext(); ) { result.addChild( (Child) ((Child) i.next()).clone() ); } for ( Iterator i=this.m_replace.keySet().iterator(); i.hasNext(); ) { String key = (String) i.next(); result.replaceParent( key, (String) this.m_replace.get(key) ); } return result; } /** * Default ctor: construct a hollow shell to add data later. */ public ADAG() { m_size = 1; m_index = 0; m_jobMap = new TreeMap(); m_fileMap = new TreeMap(); m_childMap = new TreeMap(); m_replace = new TreeMap(); } /** * Ctor: Construct a hollow shell with the required arguments. * * @param size is the total number of DAXes that will be constructed. * @param index is the zero-based number in the total number of DAXes. */ public ADAG( int size, int index ) { m_size = size; m_index = index; m_jobMap = new TreeMap(); m_fileMap = new TreeMap(); m_childMap = new TreeMap(); m_replace = new TreeMap(); } /** * Ctor: Construct a hollow shell with all element attributes * * @param size is the total number of DAXes that will be constructed. * @param index is the zero-based number in the total number of DAXes. * @param name is an optional name to use for the DAX. In later versions * this might be useful, if several DAXes are interleaved on the same * connection. */ public ADAG( int size, int index, String name ) { m_name = name; m_size = size; m_index = index; m_jobMap = new TreeMap(); m_fileMap = new TreeMap(); m_childMap = new TreeMap(); m_replace = new TreeMap(); } /** * Adds a logical filename string with input or output notion to the * list of maintained filenames. If the filename does not exist * previously, a new entry is added. If the filename does exist, the * io state will be checked. A filename that was previously an input, * and is now an output, will become inout. If a filename was ever * declared not-transfer or not-register, it will maintain these * attributes. Each filename is only added once. * * @param lfn is the logical filename string * @param isInput is a predicate with true to signal an input filename. * @param temporary is a temp file hint, currently unused. * @param dontRegister a true value will be propagated (mono-flop) * @param dontTransfer any non-mandatory value will be propagated */ public void addFilename( String lfn, boolean isInput, String temporary, boolean dontRegister, int dontTransfer ) { Filename f = (Filename) this.m_fileMap.get(lfn); if ( f != null ) { // found! check link status if ( (f.getLink() == LFN.INPUT && !isInput) || (f.getLink() == LFN.OUTPUT && isInput) ) { // need to change linkage f.setLink( LFN.INOUT ); } // set file hint if ( temporary != null ) f.setTemporary(temporary); if ( dontRegister ) f.setRegister( !dontRegister ); if ( dontTransfer != LFN.XFER_MANDATORY ) f.setTransfer(dontTransfer); } else { // file is not in list, add it // PS: and it is most likely not a stdio filename? this.m_fileMap.put( lfn, new Filename( lfn, isInput ? LFN.INPUT : LFN.OUTPUT, temporary, dontRegister, dontTransfer, null ) ); } } /** * Adds a completely constructed {@link Filename} structure to the * map of filenames. The structure must be assembled outside. This * method is primarily a convenience for the {@link #clone()} method. * * @param lfn is the Filename instance. * @return true, if the bag did not contain an identical Filename already. */ protected boolean addFilename( Filename lfn ) { String id = lfn.getFilename(); boolean result = ! this.m_fileMap.containsKey(id); this.m_fileMap.put( id, lfn ); return result; } /** * Adds a completely constructed {@link Job} structure to the * map of jobs. The structure must be assembled outside, using * the related classes. The job ID will be taken as unique key. * If a job with this ID already exists in the DAX, it will be * replaced with the new job. * * @param job is the new job to add * @return true, if the bag did not contain this job already. */ public boolean addJob( Job job ) { String id = job.getID(); boolean result = ! this.m_jobMap.containsKey(id); this.m_jobMap.put( id, job ); return result; } /** * Adds a child node which was constructed elsewhere to the list of * known children. If the child already exists, nothing is done. * * @param child is the new {@link Child} instance to put into the bag. * @return true if the bag did not already contain the specified element. */ public boolean addChild( Child child ) { if ( this.m_childMap.containsKey(child.getChild()) ) { return false; } else { this.m_childMap.put( child.getChild(), child ); return true; } } /** * Adds a child node without any parent relationship to the list of * known children. If the child already exists, nothing is done. * * @param child_id is the new child to put into the bag. * @return true if the bag did not already contain the specified element. */ public boolean addChild( String child_id ) { if ( this.m_childMap.containsKey(child_id) ) { return false; } else { this.m_childMap.put( child_id, new Child(child_id) ); return true; } } /** * Adds a child node with a parent relationship to the list of known * children. If the child already exists, but the parent relationship * is not known, it will be added to the child's list of parents. If * the child already exists and the parent relationship is known, * nothing is done. * * @param child_id is the id of the child for which to modify a parent * @param parent_id is the new parent to add to the specified child. * @return true if the bag did not already contain the relationship. */ public boolean addChild( String child_id, String parent_id ) { Child current = (Child) this.m_childMap.get( child_id ); if ( current == null ) { // unknown child, add to bag this.m_childMap.put( child_id, new Child(child_id,parent_id) ); return true; } else { // child is know, check the parent if ( ! current.getParent(parent_id) ) { // parent is unknown, add to child current.addParent(parent_id); return true; } else { // parent is already known return false; } } } /** * Registers a job node collapsion as a replacement. */ public String replaceParent( String oldid, String newid ) { String old = (String) this.m_replace.put( oldid, newid ); this.m_dirty = true; return old; } /** * Accessor: Provides an iterator for the bag of filenames. * @return the iterator for <code>Filename</code> elements. * @see Filename * @deprecated Use the new Collection based interfaces */ public Enumeration enumerateFilename() { return Collections.enumeration(this.m_fileMap.values()); } /** * Accessor: Provides an iterator for the bag of jobs. * @return the iterator for <code>Job</code> elements. * @see Job * @deprecated Use the new Collection based interfaces */ public Enumeration enumerateJob() { return Collections.enumeration(this.m_jobMap.values()); } /** * Accessor: Provides an iterator for the bag of relationships. * @return the iterator for <code>Child</code> elements. * @see Child * @deprecated Use the new Collection based interfaces */ public Enumeration enumerateChild() { if ( this.m_dirty ) updateChildren(); return Collections.enumeration(this.m_childMap.values()); } /** * Accessor: Obtains a <code>Filename</code> from its string. * * @param lfn is the logical filename string to look it up with. * @return the filename instance at the specified place. * @see #addFilename( Filename ) * @see #setFilename( Filename ) */ public Filename getFilename( String lfn ) { return (Filename) this.m_fileMap.get(lfn); } /** * Accessor: Obtains the index of filename instances. * * @return the number of arguments in the filename list. * @see Filename */ public int getFilenameCount() { return this.m_fileMap.size(); } /** * Accessor: Counts the number of jobs in the abstract DAG. * * @return the number of jobs. */ public int getJobCount() { return this.m_jobMap.size(); } /** * Access: Counts the number of dependencies in the DAG. * * @return dependency count, which may be zilch. */ public int getChildCount() { if ( this.m_dirty ) updateChildren(); return this.m_childMap.size(); } /** * Accessor: Obtains the zero-based index. * * @return a number in the interval [0,size-1]. * @see #setIndex( int ) */ public int getIndex() { return this.m_index; } /** * Accessor: Obtains a job by its id from the job list. * * @return a job or null, if not found. * @see #addJob( Job ) */ public Job getJob( String jobID ) { return (Job) this.m_jobMap.get( jobID ); } /** * Accessor: Obtains the name of the DAX. * * @return the name of this DAX, or <code>null</code>, if no name * was specified. * @see #setName( String ) */ public String getName() { return this.m_name; } /** * Accessor: Obtains the total number of alternatives. This is the * number of DAXes generatable from alternatives. * @return a positive natural integer. * @see #setSize( int ) */ public int getSize() { return this.m_size; } /** * Accessor: Obtains the version that will be reported in the DAX. * @return the version as a string. * @see #setVersion( String ) * @since 1.7 */ public String getVersion() { return this.m_version; } /** * Accessor: Provides an iterator for the bag of filenames. * @return the iterator for <code>Filename</code> elements. * @see Filename */ public Iterator iterateFilename() { return this.m_fileMap.values().iterator(); } /** * Accessor: Provides an iterator for the bag of jobs. * @return the iterator for <code>Job</code> elements. * @see Job */ public Iterator iterateJob() { return this.m_jobMap.values().iterator(); } /** * Accessor: Provides an iterator for the bag of relationships. * @return the iterator for <code>Child</code> elements. * @see Child */ public Iterator iterateChild() { if ( this.m_dirty ) updateChildren(); return this.m_childMap.values().iterator(); } /** * Accessor: Removes all filename instances. * @see Filename */ public void removeAllFilename() { this.m_fileMap.clear(); } /** * Accessor: Removes a specific logical filename instance from the bag. * * @param lfn is the logical filename string to refer to a filename. * @return the {@link Filename} instance to which this lfn had been mapped * in this hashtable, or <code>null</code> if the lfn did not have a mapping. */ public Filename removeFilename( String lfn ) { return (Filename) this.m_fileMap.remove(lfn); } /** * Accessor: Overwrites an filename instance with a new one. * * @param vFilename is the new filename instance, which contains all * necessary information. */ public void setFilename( Filename vFilename ) { this.m_fileMap.put( vFilename.getFilename(), vFilename ); } /** * Accessor: Replace this filename instance list with a new list. * * @param fileArray is the new list of Filename instances * @see Filename * @deprecated Use the new Collection based interfaces */ public void setFilename(Filename[] fileArray) { this.m_fileMap.clear(); for (int i = 0; i < fileArray.length; i++) { this.m_fileMap.put( fileArray[i].getFilename(), fileArray[i] ); } } /** * Accessor: Replace this filename instance list with a new list. * * @param files is the new collection of Filename instances * @see Filename */ public void setFilename(java.util.Collection files) { this.m_fileMap.clear(); for (Iterator i=files.iterator(); i.hasNext(); ) { Filename lfn = (Filename) i.next(); this.m_fileMap.put( lfn.getFilename(), lfn ); } } /** * Accessor: Replace this filename instance list with a map. * * @param files is the new map of Filename instances * @see Filename */ public void setFilename(java.util.Map files) { this.m_fileMap.clear(); this.m_fileMap.putAll(files); } /** * Acessor: Sets a new zero-based index for this document. The index * is used in conjunction with the total number of documents count. * * @param index is the new zero-based index of this element. * @see #getIndex() */ public void setIndex( int index ) { this.m_index = index; } /** * Acessor: Sets a new optional name for this document. * * @param name is the new name. * @see #getName() */ public void setName( String name ) { this.m_name = name; } /** * Acessor: Sets a new total document count in this document. The count * is used in conjunction with the zero-based document index. * * @param size is the new total document count. * @see #getSize() */ public void setSize( int size ) { this.m_size = size; } /** * Acessor: Sets a new version number for this document. The version * number is taken by the abstract planner to support a range of valid * DAX documents. * * @param version is the new version number as string composed of two * integers separted by a period. * @see #getVersion() * @since 1.7 */ public void setVersion( String version ) { this.m_version = version; } private void updateChildren() { // find all child nodes in need of replacement TreeMap temp = new TreeMap(); for ( Iterator i=this.m_childMap.values().iterator(); i.hasNext(); ) { Child newchild = ((Child) i.next()).updateChild(this.m_replace); if ( temp.containsKey( newchild.getChild() ) ) { // need to merge two definitions Child oldchild = (Child) temp.get( newchild.getChild() ); for ( Iterator j=oldchild.iterateParent(); j.hasNext(); ) { newchild.addParent( (String) j.next() ); } } // plain insertion temp.put( newchild.getChild(), newchild ); } this.m_childMap = temp; this.m_dirty = false; } /** * Adjusts all job levels along the search path. Given a starting point, * this method will re-iterate the search-tree, and adjust the level of * each known job by the specified distance. * * @param id is the job id to start * @param distance is the increment (or decrement for negative). * @return number of jobs adjusted? */ public int adjustLevels( String id, int distance ) { int result = 0; if ( m_jobMap.containsKey(id) ) { Job job = (Job) m_jobMap.get(id); job.setLevel( job.getLevel() + distance ); result++; // also recursively adjust all known parents of this job if ( m_childMap.containsKey(id) ) { Child c = (Child) m_childMap.get(id); for ( Iterator i=c.iterateParent(); i.hasNext(); ) { result += adjustLevels( (String) i.next(), distance ); } } } // done return result; } /** * Converts the active state into something meant for human consumption. * The method will be called when recursively traversing the instance * tree. * * @param stream is a stream opened and ready for writing. This can also * be a string stream for efficient output. */ public void toString( Writer stream ) throws IOException { String newline = System.getProperty( "line.separator", "\r\n" ); // FIXME: default name of a DAX w/o name is "test" String daxname = this.m_name != null ? this.m_name : "test"; stream.write( "adag " ); stream.write( escape(daxname) ); stream.write( " {" ); stream.write( newline ); stream.write( " count=" ); stream.write( (new Integer(this.m_size)).toString() ); stream.write( ';' ); stream.write( newline ); stream.write( " index=" ); stream.write( (new Integer(this.m_index)).toString() ); stream.write( ';' ); stream.write( newline ); // part 1: filelist stream.write( " files {" ); stream.write( newline ); for ( Iterator i=this.m_fileMap.values().iterator(); i.hasNext(); ) { stream.write( " " ); ((Filename) i.next()).toString(stream); stream.write(newline); } stream.write( " }" ); stream.write(newline); // part 2: job list stream.write( " jobs {" ); stream.write( newline ); for ( Iterator i=this.m_jobMap.values().iterator(); i.hasNext(); ) { ((Job) i.next()).toString(stream); } stream.write( " }" ); stream.write(newline); // part 3: dependencies stream.write( " dependencies {" ); stream.write( newline ); if ( this.m_dirty ) updateChildren(); for ( Iterator i=this.m_childMap.values().iterator(); i.hasNext(); ) { ((Child) i.next()).toString(stream); } stream.write( " }" ); stream.write( newline ); stream.write( '}' ); stream.write( newline ); stream.flush(); } /** * Writes the header of the XML output. The output contains the special * strings to start an XML document, some comments, and the root element. * The latter points to the XML schema via XML Instances. * * @param stream is a stream opened and ready for writing. This can also * be a string stream for efficient output. * @param indent is a <code>String</code> of spaces used for pretty * printing. The initial amount of spaces should be an empty string. * The parameter is used internally for the recursive traversal. * @param namespace is the XML schema namespace prefix. If neither * empty nor null, each element will be prefixed with this prefix, * and the root element will map the XML namespace. * @exception IOException if something fishy happens to the stream. */ public void writeXMLHeader( Writer stream, String indent, String namespace ) throws IOException { String newline = System.getProperty( "line.separator", "\r\n" ); // FIXME: default name of a DAX w/o name is "test" String daxname = this.m_name != null ? this.m_name : "test"; // intro if ( indent != null && indent.length() > 0 ) stream.write( indent ); stream.write( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" ); stream.write( newline ); // when was this document generated if ( indent != null && indent.length() > 0 ) stream.write( indent ); stream.write( "<!-- generated: " ); stream.write( Currently.iso8601(false) ); stream.write( " -->" ); stream.write( newline ); // who generated this document if ( indent != null && indent.length() > 0 ) stream.write( indent ); stream.write( "<!-- generated by: " ); stream.write( System.getProperties().getProperty("user.name", "unknown") ); stream.write( " [" ); stream.write( System.getProperties().getProperty("user.region","??") ); stream.write( "] -->" ); stream.write( newline ); // root element with elementary attributes if ( indent != null && indent.length() > 0 ) stream.write( indent ); stream.write( '<' ); if ( namespace != null && namespace.length() > 0 ) { stream.write( namespace ); stream.write( ':' ); } stream.write( "adag xmlns" ); if ( namespace != null && namespace.length() > 0 ) { stream.write( ':' ); stream.write( namespace ); } stream.write( "=\""); stream.write( SCHEMA_NAMESPACE ); stream.write( "\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"" ); stream.write( SCHEMA_NAMESPACE ); stream.write( ' ' ); stream.write( SCHEMA_LOCATION ); stream.write( '"' ); writeAttribute( stream, " version=\"", SCHEMA_VERSION ); writeAttribute( stream, " count=\"", Integer.toString(this.m_size) ); writeAttribute( stream, " index=\"", Integer.toString(this.m_index) ); writeAttribute( stream, " name=\"", daxname ); // added with dax-1.9 writeAttribute( stream, " jobCount=\"", Integer.toString(this.m_jobMap.size()) ); writeAttribute( stream, " fileCount=\"", Integer.toString(this.m_fileMap.size()) ); writeAttribute( stream, " childCount=\"", Integer.toString(this.m_childMap.size()) ); stream.write( '>' ); if ( indent != null ) stream.write( newline ); } /** * Dump the state of the current element as XML output. This function * traverses all sibling classes as necessary, and converts the data * into pretty-printed XML output. The stream interface should be able * to handle large output efficiently. * * @param stream is a stream opened and ready for writing. This can also * be a string stream for efficient output. * @param indent is a <code>String</code> of spaces used for pretty * printing. The initial amount of spaces should be an empty string. * The parameter is used internally for the recursive traversal. * @param namespace is the XML schema namespace prefix. If neither * empty nor null, each element will be prefixed with this prefix, * and the root element will map the XML namespace. * @exception IOException if something fishy happens to the stream. */ public void toXML( Writer stream, String indent, String namespace ) throws IOException { String newline = System.getProperty( "line.separator", "\r\n" ); String newindent = indent==null ? null : indent+" "; // write prefix writeXMLHeader( stream, indent, namespace ); // part 1: filelist stream.write( "<!-- part 1: list of all referenced files (may be empty) -->" ); if ( indent != null ) stream.write(newline); for ( Iterator i=this.m_fileMap.values().iterator(); i.hasNext(); ) { if ( indent != null ) stream.write(newindent); ((Filename) i.next()).shortXML( stream, newindent, namespace, 0x03 ); if ( indent != null ) stream.write(newline); } // part 2: job list stream.write( "<!-- part 2: definition of all jobs (at least one) -->" ); if ( indent != null ) stream.write(newline); for ( Iterator i=this.m_jobMap.values().iterator(); i.hasNext(); ) { ((Job) i.next()).toXML( stream, newindent, namespace ); } // part 3: dependencies if ( this.m_dirty ) updateChildren(); stream.write( "<!-- part 3: list of control-flow dependencies (may be empty) -->" ); if ( indent != null ) stream.write(newline); for ( Iterator i=this.m_childMap.values().iterator(); i.hasNext(); ) { ((Child) i.next()).toXML( stream, newindent, namespace ); } // close tag if ( indent != null && indent.length() > 0 ) stream.write( indent ); stream.write( "</" ); if ( namespace != null && namespace.length() > 0 ) { stream.write( namespace ); stream.write( ':' ); } stream.write( "adag>" ); stream.write( newline ); stream.flush(); } }