/*
* 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.router;
import org.griphyn.vdl.classes.*;
import org.griphyn.vdl.util.*;
import org.griphyn.vdl.dax.*;
// java.util.List clashes with org.griphyn.vdl.classes.List
import java.util.HashMap;
import java.util.Vector;
import java.util.HashSet;
import java.util.Collection;
import java.util.Set;
import java.util.TreeSet;
import java.util.Iterator;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Random;
import java.io.*;
/**
* This class stores state when constructing the DAG for output. It
* is expected that for each DAG generation, one instance of this
* class performs the state tracking. The class is tightly coupled
* to the class that performs the routing.
*
* @author Jens-S. Vöckler
* @author Yong Zhao
* @version $Revision$
* @see Route
*/
public class BookKeeper
{
/**
* This contains the set of visited derivations.
*/
private HashMap m_visited;
/**
* This map tracks the set of parents for each child derivation.
* Each value item in the map is a set.
*/
private HashMap m_parent;
/**
* The content of the abstract DAG for XML (DAX) is created after
* the alternatives are found. Maybe in a later version, it will
* be created in parallel with adding derivations?
*/
private ADAG m_dax;
/**
* Unfortunately, for this release, we need to map the shortID of
* a derivation to some number that is acceptable to NMTOKEN, ID and
* IDREF.
*/
private SequenceMapping m_mapping;
/**
* The <code>Profile</code> elements in nested compound TR can
* be equally nested.
*/
private ListStack m_profileStack;
/**
* The names of compound transformations that we got through. This
* data will also be added to the job description once a job is added
* to the book-keeper.
*/
private ArrayList m_transformations;
/**
* The temporary files must be unique on a per-workflow basis.
*/
private HashSet m_tempfiles;
/**
* To create temporary filenames, we need an entropy source.
*/
private Random m_rng;
/**
* Default ctor.
*/
public BookKeeper()
{
this.m_parent = new HashMap();
this.m_visited = new HashMap();
this.m_dax = new ADAG();
this.m_mapping = new SequenceMapping();
this.m_profileStack = new ListStack();
this.m_transformations = new ArrayList();
this.m_tempfiles = new HashSet();
this.m_rng = new Random();
}
/**
* Accessor: push a vector of profiles down the profile stack.
* @param profiles is a list of profiles, may be empty.
* @see #popProfile()
*/
public void pushProfile( java.util.List profiles )
{
this.m_profileStack.push(profiles);
}
/**
* Accessor: pop a vector of profiles from the profile stack.
* @return the last active vector of profiles, may be empty.
* @see #pushProfile( java.util.List )
*/
public java.util.List popProfile()
{
return this.m_profileStack.pop();
}
/**
* Accessor: obtains all profiles on the profile stack.
* @return all stacked profiles in one structure, may be empty.
*/
public java.util.List getAllProfiles()
{
return this.m_profileStack.flatten();
}
/**
* Accessor: push the FQDI of a transformation onto a stack.
* @param fqdi is the fully-qualified definition identifier.
* @see #popTransformation()
*/
public void pushTransformation( String fqdi )
{
this.m_transformations.add(fqdi);
}
/**
* Accessor: pop an FQDI from the stack of transformations.
* @return the last FQDI pushed, may be empty for an empty stack.
* @see #pushTransformation( String )
*/
public String popTransformation()
{
int pos = this.m_transformations.size()-1;
if ( pos >= 0 ) return (String) this.m_transformations.remove(pos);
else return null;
}
/**
* Accessor: obtains all transformations that were pushed as space
* separated string.
*
* @return all transformation names, may be empty.
* @see #addJob( Job )
*/
public String getAllTransformations()
{
StringBuffer result = new StringBuffer();
for ( Iterator i=this.m_transformations.iterator(); i.hasNext(); ) {
result.append( (String) i.next() );
if ( i.hasNext() ) result.append(' ');
}
return result.toString();
}
/**
* Accessor: Returns the constructed internal DAX keeper.
*
* @return the abstract DAX description. Note that this may be empty!
*/
public ADAG getDAX( String name )
{
this.m_dax.setName(name);
return this.m_dax;
}
/**
* Accessor: Appends a job definition to the DAX structure.
*/
public boolean addJob( Job job )
{
return this.m_dax.addJob(job);
}
/**
* This method updates the "cursor" position with a new derivation.
* The cursor helps tracking derivations while a DAG is being produced.
*
* @param dv is the new current derivation that the cursor will be set to.
* @see #getCurrent()
*/
public void setCurrent( HasPass dv )
{
// assign a new cursor
Logging.instance().log( "state", 2,
"NOOP: setting cursor to " + dv.identify() );
}
/**
* Obtains the current cursor position.
* @return the derivation that the cursor is located at.
* @see #setCurrent( HasPass )
*/
public HasPass getCurrent()
{
throw new RuntimeException( "method not implemented" );
}
/**
* Maps a DV identification to a name that can be put into the XML
* datatypes NMTOKEN, ID and IDREF. The identification used to be
* the DV's short ID, but recent changes use the full ID.
*
* @param id is the derivation identifier
* @return an XML-compatible job id
*/
public String mapJob( String id )
{
String s = this.m_mapping.forward(id);
Logging.instance().log( "state", 3, "mapJob(" + id + ") = " + s );
return s;
}
/**
* Obtains an existing mapping of a DV indentification to a job id.
* The identification used to be the DV's short ID, but recent changes
* use the full ID.
*
* @param id is the derivation identifier
* @return a job identifier, or null if no such mapping exists.
*/
public String jobOf( String id )
{
String s = this.m_mapping.get(id);
Logging.instance().log( "state", 3, "jobOf(" + id + ") = " +
(s == null ? "(null)" : s) );
return s;
}
/**
* Accessor: Obtains the level of a given job from the DAX structure.
* @param jobid is the job identifier
* @return the level of the job, or -1 in case of error (no such job).
*/
private int getJobLevel( String jobid )
{
Job j = this.m_dax.getJob(jobid);
return ( j == null ? -1 : j.getLevel() );
}
/**
* Accessor: Recursively adjusts job level backwards.
*
* @see org.griphyn.vdl.dax.ADAG#adjustLevels( String, int )
*/
private int adjustJobLevels( String startjob, int distance )
{
return this.m_dax.adjustLevels( startjob, distance );
}
/**
* Adds a parent set for the given element to the state
* @param current is the current derivation to add parents for
* @param parents are the parents, any number, to add for the given element
* @return true, if the parent was not know before or the currelem is empty,
* false, if the parent was added previously.
*/
public boolean addParent( HasPass current, Set parents )
{
String id = current.identify();
if ( parents.size() == 0 ) {
// nothing to do
Logging.instance().log( "state", 4, "no parents for " + id );
return false;
}
if ( ! this.m_parent.containsKey(id) ) {
Logging.instance().log( "state", 4, "adding new parent space for " + id );
this.m_parent.put( id, new TreeSet() );
}
// check for modifications
boolean modified = ((Set) this.m_parent.get(id)).addAll( parents );
if ( modified ) {
Logging.instance().log( "state", 4, "CHILD " + id + " PARENT " +
this.m_parent.get(id).toString() );
// add to parent/child DAX relationship // OLD
// NEW: adjust job level count as necessary
String jobid = mapJob(id); // OLD
int level = getJobLevel(jobid);
for ( Iterator i=parents.iterator(); i.hasNext(); ) { // OLD
String pid = mapJob((String) i.next()); // OLD
this.m_dax.addChild( jobid, pid ); // OLD
int plevel = getJobLevel(pid);
if ( level != -1 && plevel != -1 && level >= plevel ) {
int adjustment = level - plevel + 1;
Logging.instance().log( "state", 3, "adjusting levels starting at " +
pid + " by " + adjustment );
adjustJobLevels( pid, adjustment );
}
}
}
return modified;
}
/**
* This method marks a derivation as visited in the set of visited nodes.
*
* @param current is the caller to add to the visited node set.
* @param real is the value to add for the given caller.
* @return <code>true</code>, if the caller was previously unknown
* to the visited set.
*/
public boolean addVisited( HasPass current, Set real )
{
String id = current.identify();
boolean exists = this.m_visited.containsKey(id);
if ( exists ) {
Set temp = (Set) this.m_visited.get(id);
Logging.instance().log( "state", 2, "already visited node " + id +
" keeping " + temp );
} else {
this.m_visited.put( id, real );
Logging.instance().log( "state", 2, "adding visited node " + id +
" with " + real );
}
return exists;
}
/**
* Checks if a node was previously visited. The visited nodes have to
* be tracked in any kind of breadth first and depth first search.
*
* @param dv is the derivation to check, if it was visited before.
* @return true, if the derivation was already visited previously.
*/
public boolean wasVisited( HasPass dv )
{
String id = dv.identify();
boolean result = this.m_visited.containsKey(id);
Logging.instance().log( "state", 5, "wasVisited(" + id + ") = " + result );
return result;
}
public Set getVisited( HasPass dv )
{
String id = dv.identify();
Set result = (Set) this.m_visited.get(id);
if ( result == null ) result = new TreeSet();
Logging.instance().log( "state", 5, "getVisited(" + id + ") = " + result );
return result;
}
/**
* This method helps to track the input and output files. From this
* information, the input, intermediary and output files of the
* complete DAG can be constructed. This method does allow for inout.
*
* @param lfnset is a set of LFN instances, which encapsulate their
* respective linkage.
* @see java.util.AbstractCollection#addAll( java.util.Collection )
*/
public void addFilenames( Collection lfnset )
{
if ( lfnset == null )
return; // nothing to do
for ( Iterator i=lfnset.iterator(); i.hasNext(); ) {
LFN lfn = (LFN) i.next();
String name = lfn.getFilename();
if ( lfn.getLink() == LFN.INPUT || lfn.getLink() == LFN.INOUT ) {
this.m_dax.addFilename( name, true , lfn.getTemporary(),
lfn.getDontRegister(), lfn.getDontTransfer() );
Logging.instance().log( "state", 5, "adding input filename " + name );
}
if ( lfn.getLink() == LFN.OUTPUT || lfn.getLink() == LFN.INOUT ) {
this.m_dax.addFilename( name, false, lfn.getTemporary(),
lfn.getDontRegister(), lfn.getDontTransfer() );
Logging.instance().log( "state", 5, "adding output filename " + name );
}
}
}
/**
* String constant describing the six X.
*/
static final String XXXXXX = "XXXXXX";
/**
* Where to draw filename characters from.
*/
private static final String filenamechars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz";
/**
* Creates a unique temporary filename. The new name is registered
* locally to ensure uniqueness. A string of multiple capitol X, at
* least six, is replaced with some random factor.
*
* @param hint is a filename hint.
* @param suffix is the suffix for the filename.
* @return a somewhat unique filename - for this workflow only.
*/
public String createTempName( String hint, String suffix )
{
String result = null;
// sanity checks
if ( hint == null || hint.length() == 0 ) hint = XXXXXX;
if ( suffix == null ) suffix = ".tmp";
// where are the X
int where = hint.lastIndexOf( XXXXXX );
if ( where == -1 ) {
hint += XXXXXX;
where = hint.lastIndexOf( XXXXXX );
}
// create a changable buffer
StringBuffer sb = new StringBuffer( hint );
// find end of substitution area
int last = where;
while ( last < sb.length() && sb.charAt(last) == 'X' ) ++last;
// substitute until done (FIXME: may be a forever in rare cases)
for (;;) {
// create and substitute
for ( int i=where; i<last; ++i ) {
sb.setCharAt(i, filenamechars.charAt( m_rng.nextInt( filenamechars.length() ) ) );
}
// check uniqueness
if ( suffix.length() > 0 )
result = sb.toString() + suffix;
if ( ! m_tempfiles.contains(result) ) {
m_tempfiles.add(result);
break;
}
}
return result;
}
/**
* Detects valid results in the ADAG as opposed to an empty shell.
*
* @return true, if the ADAG is an empty shell.
*/
public boolean isEmpty()
{
Iterator i = this.m_dax.iterateJob();
return ( ! i.hasNext() );
}
/**
* This method is a possibly more memory efficient version of
* constructing a DAG.
* @param stream is a generic stream to put textual results onto.
*/
public void toString( Writer stream )
throws IOException
{ this.m_dax.toString(stream); }
/**
* dumps the state of this object into human readable format.
*/
public String toString()
{ return this.m_dax.toString(); }
/**
* This method is a possibly more memory efficient version of
* constructing a DAX.
* @param stream is a generic stream to put XML results onto.
* @param indent is the initial indentation level.
* @param namespace is an optional XML namespace.
*/
public void toXML( Writer stream, String indent, String namespace )
throws IOException
{ this.m_dax.toXML(stream,indent,namespace); }
/**
* This method is a possibly more memory efficient version of
* constructing a DAX.
* @param stream is a generic stream to put XML results onto.
* @param indent is the initial indentation level.
*/
public void toXML( Writer stream, String indent )
throws IOException
{ this.m_dax.toXML(stream,indent,null); }
/**
* Dumps the state of this object into machine readable XML.
* @param indent is the initial indentation level.
* @param namespace is an optional XML namespace.
*/
public String toXML( String indent, String namespace )
{ return this.m_dax.toXML(indent,namespace); }
}