/*
maintenance: mkraegeloh@sulis.de

purpose: delivers various static utility methods for windchill environment; main method allows to set state (and lifecycle) of objects.

This program is free software; you can redistribute it and/or modify it under the terms of the
GNU Lesser General Public License as published by the Free Software Foundation;
see http://www.gnu.org/licenses/lgpl.html (und deutsch: http://www.gnu.de/lgpl-ger.html)

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU Lesser General Public License for more details.

*/
package ext.tools;

import com.infoengine.SAK.Task;
import com.infoengine.SAK.Webject;
import com.infoengine.object.IeDatum;
import com.infoengine.object.factory.Att;
import com.infoengine.object.factory.Element;
import com.infoengine.object.factory.Group;
import com.infoengine.util.*;
import com.ptc.core.meta.common.*;
import wt.session.SessionHelper;
import wt.enterprise.*;
import wt.util.*;
import wt.fc.collections.*;
import wt.fc.*;
import wt.inf.container.*;
import wt.lifecycle.*;
import wt.method.*;
import wt.part.*;
import wt.pdmlink.*;
import java.io.*;
import java.lang.reflect.*;
import java.util.*;
import java.util.regex.*;
import wt.vc.wip.*;
import wt.vc.*;
import wt.inf.container.*;
import org.apache.log4j.*;

// 20060403 mkr; add history, version
// 20060406 mkr; added logElement method
// 20060703 mkr; shortObid prefixes OR: in 2nd guess, add objFromObid and wrkObjFromObid
// 20060801 mkr; add deser and deserex, fix usage
// 20060809 mkr; add latestVersionWrkObjFromObid
// 20060822 mkr; latestVersionWrkObjFromObid deals with masters, logException(), getOR()
// 20060922 mkr; shortObid accepts Object.
// 20061010 mkr; prettyPrint Object
// 20061026 mkr; getOR(Object)
// 20061102 mkr; prettyPrint(Object, boolean) to transform newlines into ' '

/**
  * collection of a few useful methods to help with windchill/IE
  *
  * @author mkraegeloh
  */

public class ToolUtils implements RemoteAccess {

   public static final String CVSVersion = "$Header: /usr/local/cvsroot/SULIS/src/ext/tools/ToolUtils.java,v 1.13 2007/01/31 17:00:39 mkr Exp $";
   private static final String myVersion = "1.10";
   static Logger logger=Logger.getLogger("ext.tools.ToolUtils");
   private static Pattern shortObid,esc,exc;
   private static Hashtable linkRoles=new Hashtable();
   private static String[] mapXML=new String[63];
   private static ReferenceFactory RF=new ReferenceFactory();
   static{
	try{
	shortObid = Pattern.compile("([OV]R:[^:]+:\\d+)");
	esc=Pattern.compile("(.*?)%([0-9A-Fa-f]{2})(.*)");
	exc=Pattern.compile(".*<exception-object CONTENT-TYPE.*>(.*)<.*");
	for(int i=0;i<mapXML.length;i++) mapXML[i]=null;
	mapXML['&']="&amp;";
	mapXML['\'']="&apos;";
	mapXML['\"']="&quot;";
	mapXML['<']="&lt;";
	mapXML['>']="&gt;";
	mapXML[10]="&#10;";
	mapXML[9]="&#9;";
	}catch(Exception kommtned){throw new RuntimeException(kommtned);}
   }
   /**
     * main to invoke setting of state or test isRoleMaster().
     *
     * @param args	
     * @exception Exception	
     * @return void	
   */
   public static void main(String[] args) throws Exception{
	if(args.length == 0 || args[0].toLowerCase().indexOf("-h") != -1){
	   System.out.println("ToolUtils version "+myVersion);
	   System.out.println("usage: ToolUtils [-l lifecycle] -s state obid [obid2 ...]");
	   System.out.println("or:    ToolUtils -m link-class (tells if rolea/roleb is master)");
	   System.out.println("or:    ToolUtils -d java-serialized-obj (prettyPrints to stdout)");
	   System.out.println("or:    ToolUtils -e base64-encoded-execption (stacktrace to stdout)");
	   System.out.println("or:    ToolUtils -lwo obid -> latest version obj incl. wrk");
	   return;
	}
	fixEscapes(args);
	if(args[0].equals("-m")){
	   System.out.println("link role 0 is:"+isRoleMaster(args[1],0));
	   System.out.println("link role 1 is:"+isRoleMaster(args[1],1));
	   return;
	}

	if(args[0].equals("-s") || args[0].equals("-l")){
	   setStates(args);
	   return;
	}
	if(args[0].equals("-e")){
	   deserex(args);
	   return;
	}
	if(args[0].equals("-d")){
	   deser(args);
	   return;
	}
	if(args[0].equals("-lwo")){
	   System.out.println("-> "+latestVersionWrkObjFromObid(args[1]));
	   return;
	}
   }
   /**
     * set the state of obid[s] passed in; if args[0] is "-l" then args[1] has lifecycle to assign to objs
     *
     * @param args	
     * @exception Exception	
     * @return void	
   */
   public static void setStates(String[] args) throws Exception{
	Persistable p;
	int offset=0;
	boolean needLC=false;
	String lc=null;
	if(args[0].equals("-l")){
	   needLC=true;
	   lc=args[1];
	   offset+=2;
	}
      State newState = State.toState(args[++offset]);
	while(++offset<args.length){
	   p = RF.getReference(shortObid(args[offset])).getObject();
	   if(p instanceof RevisionControlled && ! ((RevisionControlled)p).getLifeCycleState().toString().equals(newState.toString())){
		if(needLC){
		   setLC(lc, (RevisionControlled)p);
		   p=(RevisionControlled)PersistenceHelper.manager.refresh(p);
		}
		try{
		   LifeCycleHelper.service.setLifeCycleState((LifeCycleManaged)p, newState, true);
		   logger.debug("set state of "+p+" to "+newState+" ok.");
		}
		catch(Exception err){
		   System.out.println("skip "+p+" err="+err);
		}
	   }
	}
   }

   public static void setLC(String lcname, RevisionControlled r) throws Exception{
	WTContainerRef cr=((WTContained)r).getContainerReference();
	LifeCycleTemplateReference lcr=LifeCycleHelper.service.getLifeCycleTemplateReference(lcname,cr);
	if(lcr == null){
	   System.out.println("cannot determine LifeCycleTemplateReference "+lcname+" in container "+cr);
	   return;
	}
	LifeCycleHelper.service.reassign((LifeCycleManaged)r, lcr, cr);
   }

   /**
    * rotate group -> cols get rows, new col headers are row0 row1 etc.
    * rot is done within group -> group gets altered!
    *
    * @param in group to rotate
    */
   public static void rotGroup(Group in){
	   logger.debug("rotGroup starting");
	   Vector v = new Vector();
	   Enumeration rows = null;
	   Enumeration atts = null;
	   Enumeration vals = null;
	   IeDatum val = null;
	   Att a;
	   String name;
	   int nElements = in.getElementCount();
	   Element row;
	   Element newRow;
	   Hashtable names=new Hashtable();
	   Object[] oarr;

	   // find available names.
	   if((row = in.getElementAt(0)) == null)
		return;
	   for (atts = row.getAtts(); atts.hasMoreElements(); )
		names.put(((Att) atts.nextElement()).getName().toLowerCase(),"");
	   oarr=names.keySet().toArray();
	   Arrays.sort(oarr);
	   logger.debug("found and sorted "+oarr.length+" names");

	   // prepare rows with initial att
	   for (int j=0;j<oarr.length;j++){
		v.add(newRow = new Element());
		newRow.addAtt(new Att("name", oarr[j]));
	   }
	   for (int i = 0; i < nElements; i++) {
		row = in.getElementAt(i);
		for (int j=0;j<oarr.length;j++){
		   a = ((Att) row.getAtt((String)oarr[j]));
		   if(a==null)
		      a=new Att("tmp","");
		   else
			a.getAtt().removeAllMeta();
		   a.setName("row_" + i);
		   ((Element) v.get(j)).addAtt(a);
		}
	   }
	   in.removeElements();
	   nElements = v.size();
	   for (int i = 0; i < nElements; i++)
		in.addElement((Element) v.get(i));
	}

	/**
	 * return a string that will set the width for a display webject for all
	 * cols of a group to the max num of chars contained in each col&#46;<br><b>
	 *
	 * NOTE: as a sideeffect it checks and makes sure first element(=line) has all attributes occurring in elements 2..n, so display-table will work properly</b>
	 *
	 * @param in group to determine the widths for
	 *
	 * @return String    e.g., 10,30,23,24
	 */
	public static String getWidths(Group in) {
	   StringBuffer wb = new StringBuffer();

	   logger.debug("getWidths, in="+in);
	   if(in==null) return "999";
	   Enumeration rows = null;
	   Enumeration atts = null;
	   Enumeration vals = null;
	   IeDatum val = null;
	   Att a;
	   String name;
	   String s;
	   boolean firstpass = true;
	   int n = 0;
	   int arraySize=0;
	   Integer l;
	   int l1;
	   int nElements = in.getElementCount();
	   if (nElements == 0)
		return "";

	   Hashtable widths=new Hashtable();
	   Element row;
	   for (int i = 0; i < nElements; i++) {
		row = in.getElementAt(i);
		n = 0;
		for (atts = row.getAtts(); atts.hasMoreElements(); n++){
		   a = (Att) atts.nextElement();
		   name = a.getName();
		   if((l=(Integer)widths.get(name)) == null)
			widths.put(name,l=new Integer(name.length())); // first time seen, make sure name fits
		   l1 = nn(a.getValue()).length();
		   if (l.intValue()<l1)
			widths.put(name, new Integer(l1));
		}
	   }
	   String comma = "";

	   row=in.getElementAt(0);
	   for(Enumeration e=widths.keys(); e.hasMoreElements();){
		name=(String)e.nextElement();
		if(row.getAtt(name)==null)
		   row.addAtt(new Att(name,""));
	   }
	   for (atts = row.getAtts(); atts.hasMoreElements(); n++){
		name =((Att)atts.nextElement()).getName();
		wb.append(comma).append(widths.get(name).toString());
		comma = ",";
	   }
	   s = wb.toString();
	   logger.debug("getWidths " + s);
	   return s;
	}

	/**
	 * class to call dump of first element of group passed in. passes call to
	 * dumpGroup(group, 1)
	 *
	 * @param wt_group group to ask dump for
	 */
	public static void dumpGroup(Group wt_group)
	{
	   dumpGroup(wt_group, 1);
	}

	/**
	 * class to dump a number of elements of group passed in.
	 *
	 * @param g input group to dump
	 * @param many number of elements to dump
	 */
	public static void dumpGroup( Group g, int many) {
	   if (g==null){
		System.err.println("dumpGroup: null group.");
		return;
	   }
	   else
		System.err.println("->dumpGroup: "+g.getName()+" ok.");

	   int n=g.getElementCount();
	   System.err.println("dumpGroup: n="+n+", success="+g.getSuccess());
	   if (n == 0){
		System.err.println("dumpGroup: group " + g.getName() + " is empty.");
		return;
	   }
	   if (many > n)
		many = n;
	   Element e = g.getElementAt(0);
	   ByteArrayOutputStream out = new ByteArrayOutputStream();
	   PrintWriter pts = new PrintWriter(out);
	   e.toXML(pts, true);
	   for(int i=1; i<many; i++)
		if ((e=g.getElementAt(i)) != null)
		   e.toXML(pts, true);

	   if(many==1)
		System.err.println("1st row of group " + g.getName() + " (total="+n+ ") is:");
	   else
		System.err.println(""+many+" rows of group " + g.getName() + " (total="+n+ "):");
	   System.err.println(out.toString());
	}


	/**
	 * make obid atts in group contain only the short obid for wt so we loop and
	 * change all values of atts named obid or masterReference
	 *
	 * @param in group to make subst in
	 *
	 * @return void
	 *
	 * @throws Exception 
	 */
	public static Group fixObid(Group in) throws Exception{
	   return fixObid(in,new String[]{"obid","masterReference"}, true);
	}
	/**
	 * make obid atts in group contain only the short obid for wt so we loop and
	 * change all values of atts in String[] fixNames
	 *
	 * @param in group to make subst in
	 * @param fixNames names of atts to fix.
	 *
	 * @return void
	 *
	 * @throws Exception 
	 */
	public static Group fixObid(Group in, String[] fixNames, boolean complain) throws Exception {
	   Hashtable lookup=new Hashtable();
	   for(int hi=0;hi<fixNames.length;hi++)
	      lookup.put(fixNames[hi],"");
	   Enumeration rows = null;
	   Enumeration atts = null;
	   Enumeration vals = null;
	   IeDatum val = null;
	   Att a = null;
	   String shortObid = null;

	   for (rows = in.getElements(); rows.hasMoreElements();) {
		for (atts = ((Element) rows.nextElement()).getAtts(); atts.hasMoreElements();) {
		   a = (Att) atts.nextElement();
		   if (lookup.get(a.getName())!=null){
			for (vals = a.getAtt().getDatums(); vals.hasMoreElements();) {
			   val = (IeDatum) vals.nextElement();
			   if (val.rawValueType().equals("String")) {
				val.setValue(shortObid((String) val.rawValueOf(), complain));
			   } // end val is string
			} // end loop values
		   } // end att is obid
		} // end loop atts
	   } // end loop elements
	   return in;
	}


	/**
	 * return part of a group (for paging? )
	 *
	 * @param g group with all records
	 * @param skip elements to skip
	 * @param n number of elements to return
	 *
	 * @return Group    newly created smaller group
	 */
	public static Group subGroup( Group g, int skip, int n) {
	   int max = g.getElementCount();

	   if (max < n) return g;
	   max = ((skip + n) > max) ? max : (skip + n);
	   Group gn = new Group(g.getName());

	   for (int i = skip; i < max; i++)
		gn.addElement(g.getElementAt(i));
	   return gn;
	}

	/**
	   * @deprecated Method shortObid(String) is deprecated, replaced by shortObid(Object). 
	   */
	public static String shortObid(String longObid) throws WTException {
	   return shortObid((Object) longObid, true);
	}
	/**
	   * @deprecated Method shortObid(String ,boolean) is deprecated, replaced by shortObid(Object, boolean).
	   */
	public static String shortObid(String longObid, boolean throwException ) throws WTException {
	     return shortObid((Object) longObid, throwException);
	}
	
	/**
	   * extract the short ([OV]R:class.name:seqnr) obid from a ufid
	   *
	   * @param longObid
	   *
	   * @return String
	   *
	   * @exception Exception
	   */
	public static String shortObid(Object longObid) throws WTException {
	   return shortObid(longObid, true);
	}

	/**
	   * extract the short ([OV]R:class.name:seqnr) obid from a ufid
	   *
	   * @param longObid
	   * @param throwException 
	   *
	   * @return String
	   *
	   * @exception Exception
	   */
	public static String shortObid(Object longObid, boolean throwException ) throws WTException {
	     Matcher r;

	     if ((r = shortObid.matcher(longObid.toString())).find())
		    return r.group(1);
	     // retry with OR:
	     if ((r = shortObid.matcher("OR:"+longObid)).find())
		    return r.group(1);
	     if (throwException) throw new WTException("cannot find a short obid in " + longObid);
	     return longObid.toString();
	}

	/**
	 * make csv data from group
	 *
	 * @param in group to make data from
	 *
	 * @return data
	 */
	public static String group2csv(Group in, String fdelim, String rdelim, boolean includeHeaders) {
	   StringBuffer b = new StringBuffer();
	   StringBuffer b1 = new StringBuffer();

	   String s=getWidths(in);
	   if(s.equals("")) return rdelim;

	   int nElements = in.getElementCount();
	   int n = 0;
	   Element row = in.getElementAt(0);
	   Vector v=new Vector();
	   Enumeration atts = null;
	   String name;
	   String delim="";
	   int nAtts;
	   Att a;
	   Object ied=null;
	   for (atts = row.getAtts(); atts.hasMoreElements(); n++){
		a = (Att) atts.nextElement();
		v.add(name = a.getName());
		if(includeHeaders)
		   b.append(delim).append(name);
		b1.append(delim).append(a.getValue().toString());
		delim=fdelim;
	   }
	   if(includeHeaders)
		b.append(rdelim);
	   b.append(b1.toString());
	   b.append(rdelim);

	   nAtts=v.size();
	   for(n=1;n<nElements;n++){
		delim="";
		row = in.getElementAt(n);
		for(int m=0;m<nAtts;m++){
		   a = (Att) row.getAtt((String)v.get(m));
		   b.append(delim).append(a==null ? "" : (ied=a.getValue()) == null ? "" : ied.toString());
		   delim=fdelim;
		}
		b.append(rdelim);
	   }

	   return b.toString();
	}

	/**
	 * Make an infoengine group from a hash table. Creates one row.
	 *
	 * @param h input hash table with name/value pairs
	 * @param groupName name to give to newly created group
	 *
	 * @return Constructed group
	 */
	public static Group hash2Group( Hashtable h, String groupName){
	   Group g = new Group(groupName);

	   if (h == null)
		return g;
	   Element el = new Element();
	   String k = null;
	   Object o = null;
	   Vector v;
	   int n;
	   Att a;

	   for (Enumeration e = h.keys(); e.hasMoreElements();){
		k = (String) e.nextElement();
		o = h.get(k);
		if (o instanceof Att)
		   el.addAtt((Att) o);
		else if (o instanceof Vector){
		   v = (Vector) o;
		   n = v.size();
		   a=new Att(k);
		   for (int i = 0; i < n; i++)
			a.addValue(v.get(i));
		   el.addAtt(a);
		}
		else
		   el.addAtt(new Att(k, o));
	   }
	   g.addElement(el);
	   return g;
	}
	
	/**
	 * Make an infoengine group from a hash table, each key/value pair givng an element having a key and value att. Creates as many rows as necessary.
	 *
	 * @param h input hash table with key/value pairs
	 * @param groupName name to give to newly created group
	 * @param key Attribute name for the key column
	 * @param value Attribute name for the value column
	 */
	public static Group hash2Group(Hashtable h, String groupName, String key, String value) {
	   Group g = new Group(groupName);

	   if (h == null)
		return g;
	   String k = null;
	   Object o = null;

	   for (Enumeration e = h.keys(); e.hasMoreElements();){
		Element el = new Element();
		k = (String) e.nextElement();
		o = h.get(k);
		el.addAtt(new Att(key,k));
		if (o instanceof Att)
		   el.addAtt((Att) o);
		else
		   el.addAtt(new Att(value, o));
	   g.addElement(el);
	   }
	   return g;
	}

   /**
     * adds normal Att for requested meta data
     *
     * @param in	
     * @param meta	
     * @param attName	
     * @return void	
    */

	public static void meta2att(Group in, String meta, String attName){
	   if (in != null){
		int nElements = in.getElementCount();
		Element row;
		Att a;
		for (int i = 0; i < nElements; i++){
		   row = in.getElementAt(i);
		   row.addAtt(new Att(attName, nn(row.getMeta(meta),"NOT_SET")));
		}
	   }
	}

   /**
     * removes the type prefix in an att's name (e.g., wt.part.WTPartUsageLink.quantity.unit -> quantity.unit)
     *
     * @param in	
     * @return void	
     * @throws Exception if an att creates a duplicate name.
    */

   public static void removeAttNameTypePrefix(Group in, String[] names) throws WTException{
      if (in != null){
	 int nElements = in.getElementCount();
	 Element row;
	 Att a;
	 Matcher m;
	 Hashtable unique;
	 Enumeration atts;
	 String n;
	 AttributeTypeIdentifier at;
	 Hashtable want=new Hashtable();
	 for(int i=0; i<names.length; i++)
	    want.put(names[i],"");
	 for (int i = 0; i < nElements; i++){
	    unique=new Hashtable();
	    atts = (row=in.getElementAt(i)).getAtts();
	    while(atts.hasMoreElements())
	       unique.put(((Att)atts.nextElement()).getName(),"");
	    atts = row.getAtts();
	    while(atts.hasMoreElements()){
	       a=(Att)atts.nextElement();
	       if((at=a.getAttributeTypeIdentifier())==null)
		  continue;
	       if((n=at.getAttributeName()).indexOf("WCTYPE") != -1)
		  continue;
	       // requested?
	       if(want.get(n)==null)
		  continue;
	       if(! n.equals(a.getName()))
		  if(unique.get(n)==null)
		     a.setName(n);
	          else
		     throw new WTException("att "+a.getName()+" hits existing Att "+n);
	    } // loop rows atts
	 } // loop rows
      } // group not null
   }

   /**
     * removes the type prefix in an att's name (e.g., wt.part.WTPartUsageLink.quantity.unit -> quantity.unit)
     *
     * @param in	
     * @return void	
     * @throws Exception if an att creates a duplicate name.
    */

   public static void removeAttNameTypePrefix(Group in, String[] names, String tail) throws WTException{
      if (in != null){
	 int nElements = in.getElementCount();
	 if(nElements < 2)
	    return;
	 Element row;
	 Att a;
	 Matcher m;
	 Hashtable unique;
	 Enumeration atts;
	 String n;
	 AttributeTypeIdentifier at;
	 Hashtable want=new Hashtable();
	 String prefix=null;
	 int prefixLength=0;
	 // find prefix to remove.
	 atts = in.getElementAt(1).getAtts();
	 while(atts.hasMoreElements()){
	    n=((Att)atts.nextElement()).getName();
	    if(n.endsWith(tail)){
	       prefix=n.substring(0,n.length()-tail.length());
		 prefixLength=prefix.length();
		 break;
	    }
	 }
	 if(prefix==null){
	    logger.debug("no prefix found to remove.");
	    return;
	 }
	 // build names we're after
	 for(int i=0; i<names.length; i++)
	    want.put(prefix+names[i],"");
	 for (int i = 0; i < nElements; i++){
	    unique=new Hashtable();
	    atts = (row=in.getElementAt(i)).getAtts();
	    while(atts.hasMoreElements())
	       unique.put(((Att)atts.nextElement()).getName(),"");
	    atts = row.getAtts();
	    while(atts.hasMoreElements()){
	       a=(Att)atts.nextElement();
	       n=a.getName();
	       // requested?
	       if(want.get(n)==null)
		    continue;
	       n=n.substring(prefixLength);
		 if(unique.get(n)==null)
		     a.setName(n);
	       else
		     throw new WTException("att "+a.getName()+" hits existing Att "+n);
	    } // loop rows atts
	 } // loop rows
      } // group not null
   }

	/**
	 * workflow form of callTask
	 *
	 * @param pbo	obid of the workflows primaryBusinessObject
	 * @param myTaskName name of the Task 
	 *
	 * @return the output group of the Task (or new Group with getSuccess()="ERROR" and exception-object and exception-message set if task.invoke() throws an exception e.g. task cannot be compiled)
	 *
	 * @throws Exception if the task setup fails.
	 */
	public static Group callTask( Persistable pbo, String myTaskName) throws Exception {
	   Hashtable formHash=new Hashtable();
	   formHash.put("obid", RF.getReferenceString(pbo));
	   return callTask(formHash,null,myTaskName,null);
	}
	/**
	 * short form of callTask
	 *
	 * @param formHash form data in a hash
	 * @param serverGroup if null we'll create one - else server data to pass on
	 * @param myTaskName name of the Task 
	 *
	 * @return the output group of the Task (or new Group with getSuccess()="ERROR" and exception-object and exception-message set if task.invoke() throws an exception e.g. task cannot be compiled)
	 *
	 * @throws Exception if the task setup fails.
	 */
	public static Group callTask( Hashtable formHash, Group serverGroup, String myTaskName) throws Exception {
	   return callTask(formHash,serverGroup,myTaskName,null);
	}
	/**
	 * Prepare and invoke an InfoEngine Task.
	 *
	 * @param formHash form data in a hash
	 * @param serverGroup if null we'll create one - else server data to pass on
	 * @param myTaskName name of the Task 
	 * @param params hashtable to make form param=name=value entries
	 *
	 * @return the output group of the Task (or new Group with getSuccess()="ERROR" and exception-object and exception-message set if task.invoke() throws an exception e.g. task cannot be compiled)
	 *
	 * @throws Exception if the task setup fails.
	 */
	public static Group callTask( Hashtable formHash, Group serverGroup, String myTaskName, Hashtable params) throws Exception {
	   logger.info("callTask: " + myTaskName);
	   if(logger.isDebugEnabled()) logger.debug("form="+formHash);
	   Group myGroup = null;
	   com.infoengine.SAK.Task myTask = new com.infoengine.SAK.Task();
	   myTask.setFormGroup(ToolUtils.hash2ParamGroup(formHash,params,"FORM"));
	   myTask.setTaskURI(myTaskName);

	   // set remote task processor name from wt.properties
	   Vector v = new Vector();
	   v.addElement(WTProperties.getLocalProperties().getProperty("wt.federation.ie.VMName"));
	   myTask.setProcessors(v);

	   // set user name from Windchill context
	   String user = SessionHelper.manager.getPrincipal().getName();
	   if(serverGroup != null && serverGroup.getElementCount() > 0){
		// check if server group contains authorization, else add 
		if (serverGroup.getAttributeValue(0, "authorization") == null){
		   logger.debug("add auth to existing SERVER group for " + myTaskName + " user = " + user);
		   serverGroup.getElementAt(0).addAtt(new Att("auth-user", user));
		   myTask.setServerGroup(serverGroup);
		}
	   }
	   else
		myTask.setUsername(user);
	   Group go=null;
	   // Invoke task.
	   long strt=System.currentTimeMillis();
	   try{
		myTask.invoke();
		go = myTask.getGroup(null);
		logger.info("callTask done time="+(System.currentTimeMillis()-strt));
	   }
	   catch (Exception iee){
		go=new Group("error");
		go.setSuccess("ERROR");
		Element e=new Element();
		go.addElement(e);
		e.addAtt(new Att("exception-object",iee));
		e.addAtt(new Att("exception-message",iee.toString()));
		logger.info("callTask exception time="+(System.currentTimeMillis()-strt));
		logException(logger,iee);
	   }
	   // check error status ??
	   return go;
	}

   /**
     * returns an empty string if passed null or an object's string representation
     
     * @param o	
     * @return String	
    */

	public static String nn(Object o){
	   return o==null ? "" : o.toString();
	}
   /**
     * returns an empty string if passed null or the object
     
     * @param o	
     * @return Object	
    */

	public static Object nno(Object o){
	   return o==null ? "" : o;
	}
   /**
     * returns a default string if passed null or an object's string representation
     *
     * @param o	
     * @return String	
    */

	public static String nn(Object o, String def){
	   return o==null ? def : o.toString();
	}

   /**
     * calls server side update method on a given Persistable, bypassing checkin/checkout etc.
     *
     * @param p	
     * @exception Exception	
     * @return void	
    */

	public static void update(Persistable p) throws Exception{
	   if(RemoteMethodServer.ServerFlag)
		_update(p);
	   else
		RemoteMethodServer.getDefault().invoke("_update","ext.tools.ToolUtils", null, new Class[] {Persistable.class}, new Object[] {p});
	}
   /**
     * server side part of update method.
     *
     * @param p	
     * @exception Exception	
     * @return void	
    */

	public static void _update(Persistable p) throws Exception{
	   wt.fc.PersistenceServerHelper.manager.update(p);
	}

   /**
     * walks a group's elements and trims whitespace from a given Att's value - IeDatum will be String afterwards.
     *
     * @param in	
     * @param attName	
     * @return void	
    */

	public static void trimAtt(Group in, String attName){
	   if (in != null){
		Enumeration atts;
		int nElements = in.getElementCount();
		Element row;
		Att a;
		for (int i = 0; i < nElements; i++){
		   row = in.getElementAt(i);
		   a = row.getAtt(attName);
		   if(a == null)
			row.addAtt(new Att(attName,""));
		   else
			a.setValue(a.getValue().toString().trim());
		}
	   }
	}

   /**
     * call fixEscapes for all strings in an array.
     *
     * @param a	array with strings passed in.
     * @return void	
    */
	public static void fixEscapes(String[] a){
	   StringBuffer b=new StringBuffer();
	   for(int i=0;i<a.length; i++){
		fixEscapes(a[i],b);
		a[i]=b.toString();
	   }
	}

   /**
     * replaces all occurrences of %hh (hh being two hex digits) with the character represented by hex value VR%3Awt.doc.WTDocument%3A423428 -&gt; VR:wt.doc.WTDocument:423428 <br>currently provides no way to escape the % except with %25
     *
     * @param a	 String to make replacements to
     * @param b	 StringBuffer for new data, gets truncated first.
     * @return void
    */
	public static void fixEscapes(String a, StringBuffer b){
	   Matcher m;
	   b.setLength(0);
	   while(true){
		m=esc.matcher(a);
		if(! m.find())
		   break;
		b.append(m.group(1)).append((char)Integer.parseInt(m.group(2),16));
		a=m.group(3);
	   }
	   b.append(a);
	}

   /**
     * return string with occurences of %hh (hh being two hex digits) replaced with the character represented by hex value.
     * if used more than once consider using the method that gets a StringBuffer passed for the newly created String.
     *
     * @param a	
     * @return String	
    */

   public static String fixEscapes(String a){
	StringBuffer b=new StringBuffer();
	fixEscapes(a,b);
	return b.toString();
   }

   /**
    * return a group as an array of Hashtables filled with <em>first</em> att values, including com.infoengine.object.type meta (e.g., wt.part.WTPart)
    *
    * @param in group to unload
    *
    * @return Hashtable[]
    */
   public static Hashtable[] group2HashArray(Group in) {
      return group2HashArray(in, false);
   }

   /**
    * return a group as an array of Hashtables filled with <em>first</em> att values, including com.infoengine.object.type meta (e.g., wt.part.WTPart)
    *
    * @param in group to unload
    * @param toLower flag whether to lowercase att names
    *
    * @return Hashtable[]
    */
   public static Hashtable[] group2HashArray(Group in, boolean toLower) {
	if(in==null) return new Hashtable[0];
	int n;
	Hashtable[] ha=new Hashtable[n=in.getElementCount()];
	Hashtable h;
	Enumeration atts = null;
	Att a;
	Element row;
	Object o;

	for (int i = 0; i < n; i++) {
	   ha[i]=h=new Hashtable();
	   atts = (row=in.getElementAt(i)).getAtts();
	   while(atts.hasMoreElements()){
		a=(Att)atts.nextElement();
		h.put(toLower? a.getName().toLowerCase() : a.getName(),nno(a.getRawValue()));
	   }
	   o=row.getMeta("com.infoengine.object.type");
	   h.put("com.infoengine.object.type",o==null?"":o.toString());
	}
	return ha;
   }
   /**
    * return a lookup hash made from two cols of a group
    *
    * @param in group to unload
    *
    * @return Hashtable
    */
   public static Hashtable lookupFromGroup(Group in, String key, String value) {
	Hashtable h=new Hashtable();
	if(in==null) return h;
	int n=in.getElementCount();

	for (int i = 0; i < n; i++)
	   h.put(ToolUtils.nn(in.getAttributeValue(i,key)),ToolUtils.nn(in.getAttributeValue(i,value)));
	return h;
   }

    /**
      * serialize an infoengine group to a file.
      *
      * @param g	the group
      * @param s	the filename
      * @exception Exception	
      * @return void	
    */
    public static void saveGroup(Group g, String s) throws Exception{
	 g.getGroup().addMeta("saved", new Long((new Date()).getTime()));
	 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(s));
	 oos.writeObject(g);
	 oos.close();
    }

    /**
      * deserialize an infoengine group from a file.
      *
      * @param s	the filename
      * @exception Exception	
      * @return Group	
    */
    public static Group readGroup(String s) throws Exception{
	 ObjectInputStream ois = new ObjectInputStream(new FileInputStream(s));
	 Group g = (Group) ois.readObject();
	 ois.close();
	 return g;
    }

   /**
     * simple override method for fullblown prettyPrint - returns a string of the obj's prettyprinted representation
     * result will (intentionally) resemble perl's Data::Dumper output.
     *
     * @param o	
     * @exception    	
     * @return String	
   */
   public static String prettyPrint(Object o){
	return prettyPrint(o,true);
   }
   /**
     * simple override method for fullblown prettyPrint - returns a string of the obj's prettyprinted representation
     * result will (intentionally) resemble perl's Data::Dumper output. on demand replace \n with ' '
     *
     * @param o	
     * @param wantNL	
     * @exception    	
     * @return String	
   */
   public static String prettyPrint(Object o, boolean wantNL){
	return prettyPrint(o,null,"",wantNL);
   }
   /**
     * print an object in a layout that allows to identify hases, vectors etc. including contents.<br>
     * vector gives ( data data data ...)<br>
     * hashtable gives { key =&gt; value, key =&gt; value ... }<br>
     * enumeration will print it's elements<br>
     * arrays prints [ elem elem ...]<br>
     * wtobjects print displayidentifier<br>
     * else we print obj.toString() (classname)<br>
     *
     * @param o	the object
     * @param b	a stringbuffer to fill
     * @param indent	indentation to use for prettyprintend output
     * @exception    	
     * @return String	
   */
   public static String prettyPrint(Object o, StringBuffer b, String indent, boolean wantNL){
	boolean stringify=b==null;
	String myWhiteSpace=wantNL?"   ":"";
	String myNL=wantNL?"\n":"";
	if(b==null)
	   b=new StringBuffer();
	if(o == null){
	   b.append(indent+o);
	}
	else if(o instanceof Hashtable){
	   Hashtable h=(Hashtable)o;
	   Object key;
	   b.append(indent+"{"+myNL);
	   for(Enumeration e=h.keys(); e.hasMoreElements(); ){
		key=e.nextElement();
		b.append(indent+myWhiteSpace+key+" => ");
		prettyPrint(h.get(key), b, indent+myWhiteSpace,wantNL);
	      if(! wantNL && e.hasMoreElements())
		   b.append(", ");
	   }
	   b.append(indent+"}");
	}
	else if(o instanceof Enumeration){
	   b.append("Enumeration:"+myNL);
	   while(((Enumeration)o).hasMoreElements()){
		prettyPrint(((Enumeration)o).nextElement(), b, indent+myWhiteSpace,wantNL);
	      if(! wantNL && ((Enumeration)o).hasMoreElements())
		   b.append(", ");
	   }
	}
	else if(o instanceof Vector){
	   Vector v=(Vector)o;
	   b.append(indent+"("+myNL);
	   for(int i=0;i<v.size();i++){
		prettyPrint(v.get(i), b, indent+myWhiteSpace,wantNL);
	      if(! wantNL && i<(v.size()-1))
		   b.append(", ");
	   }
	   b.append(indent+")");
	}
	else if(o.getClass().isArray()){
	   b.append(indent+"["+myNL);
	   int length=Array.getLength(o);
	   for(int i=0;i<length;i++){
		prettyPrint(Array.get(o,i), b, indent+myWhiteSpace,wantNL);
	      if(! wantNL && i<(length-1))
		   b.append(", ");
	   }
	   b.append(indent+"]");
	}
	else if(o instanceof Group){
	   ByteArrayOutputStream out = new ByteArrayOutputStream();
	   PrintWriter pts = new PrintWriter(out);
	   ((Group)o).toXML(pts, true, true);
	   b.append(out.toString());
	}
	else if(o instanceof Element){
	   ByteArrayOutputStream out = new ByteArrayOutputStream();
	   PrintWriter pts = new PrintWriter(out);
	   ((Element)o).toXML(pts, true);
	   b.append(out.toString());
	}
	else if(o instanceof WTObject){
	   b.append(indent+((WTObject)o).getDisplayIdentifier());
	   if(o instanceof Iterated) b.append("."+((Iterated)o).getIterationIdentifier().getValue()+ " latest="+VersionControlHelper.isLatestIteration((Iterated)o));
	   try{
	   if(o instanceof Workable){
	      if(WorkInProgressHelper.isCheckedOut((Workable)o))
		   b.append(" "+(WorkInProgressHelper.isWorkingCopy((Workable)o) ? "wrk":"c/o"));
	      else
		   b.append(" c/i");
	   }
	   }
	   catch(Exception wollnmanedsehn_warumauch){ b.append(" ERR: "+wollnmanedsehn_warumauch);}
	   if(o instanceof RevisionControlled)
	      b.append(" state="+((RevisionControlled)o).getLifeCycleState());
	   if(o instanceof Persistable)
		b.append(" OR:"+o.getClass().getName()+":"+PersistenceHelper.getObjectIdentifier((Persistable)o).getId());
	}
	else{
	   b.append(indent+o.toString());
	   if(wantNL)
		b.append(" ("+o.getClass().getName()+")");
	}
	if(wantNL)
	   b.append("\n");
	if(stringify)
	   return b.toString();
	return ""; //lazy
   }
   /**
    * prepare a form group from 2 hashes, one gives "normal" atts, the other
    * makes atts of the form param=name=value
    *
    * @param h
    * @param f
    * @param groupName
    *
    * @return Group
    */
   public static Group hash2ParamGroup( Hashtable h, Hashtable f, String groupName) {
	String k = null;
	Att a = new Att("param");
	Object o;
	Vector v;
	int n;
	boolean objectOk = false;

	if (f != null){
	   for (Enumeration e = f.keys(); e.hasMoreElements();){
		k = (String) e.nextElement();
		o = f.get(k);
		if (o instanceof Vector){
		   v = (Vector) o;
		   n = v.size();
		   for (int i = 0; i < n; i++)
			a.addValue(k + "=" + v.get(i));
		   objectOk = true;
		}
		else{
		   a.addValue(k + "=" + o);
		}
	   }
	   h.put("param", a);
	}
	return hash2Group(h, groupName);
   }

   /**
     * log a group's xml to the log4j logger.
     *
     * @param l	the logger
     * @param g	the group
     * @param reason	string to print in line before groups's xml appears
     * @exception    	
     * @return void	
   */
   public static void logGroup(Logger l, Group g, String reason){
   l.debug("logGroup: "+reason);
   logGroup(l,g);
   }
   /**
     * log a group's xml to the log4j logger.
     *
     * @param l	the logger
     * @param g	the group
     * @exception    	
     * @return void	
   */
   public static void logGroup(Logger l, Group g){
	if(!l.isDebugEnabled())
	   return;
	if(g == null){
	   l.debug("logGroup: g=null");
	   return;
	}
	ByteArrayOutputStream b = new ByteArrayOutputStream();
	PrintWriter p = new PrintWriter(b);
	g.toXML(p,true,true);
	l.debug(b.toString());
   }
   
   /**
     * log a group's xml to the log4j logger.
     *
     * @param l	the logger
     * @param g	the group
     * @exception    	
     * @return void	
   */
   public static void logElement(Logger l, Element e){
	if(!l.isDebugEnabled())
	   return;
	if(e == null){
	   l.debug("logGroup: e=null");
	   return;
	}
	ByteArrayOutputStream b = new ByteArrayOutputStream();
	PrintWriter p = new PrintWriter(b);
	e.toXML(p,true);
	l.debug(b.toString());
   }
   
   /**
     * merge the atts from one group into the rows of a receiving group, using a value from the receiving group as key into group from
     * will throw exception if the lookup key value is not present in "from" group.
     *
     * @param in	receiving group
     * @param key	col name of key into group from
     * @param from	group to merge in
     * @param look	colname for key lookup
     * @param use	array of column names to merge into new group. possibly existing atts are first removed.
     * @exception IEException	thrown for various error conditions
     * @return void	
   */
   public static void mergeSelectedAtts(Group in, String key, Group from, String look, String[] use) throws IEException{
	if(in == null)
	   throw new IEException("group to merge in must not be null");
	if(look == null)
	   throw new IEException("group to merge from must not be null");
	if(key == null || key.equals(""))
	   throw new IEException("lookup key from receiving group must not be null or empty");
	if(look == null || look.equals(""))
	   throw new IEException("lookup key from source group must not be null or empty");
	if(use == null || use.length == 0)
	   throw new IEException("need at least one attribute name to merge");
	for(int i=0;i<use.length; i++)
	   if(use[i] == null || use[i].equals(""))
		throw new IEException("names of atts to merge must not be null or empty");
	// ok, lets do the work. first prepare lookup hashtable "map" so map.get(in.getValue(key)) = row-in-from hashtable
	Hashtable[] ha=group2HashArray(from);
	Hashtable h,map=new Hashtable();
	String sk,sv;
	for(int i=0;i<ha.length; i++){
	   h=ha[i];
	   if((sk=(String)h.get(look))==null)
		throw new IEException("row "+i+" of merge-in group \""+from.getName()+"\" misses key "+look);
	   map.put(sk,h);
	}
	int n=in.getElementCount();
	Att a;
	Element row;

	// now loop input rows, pick corresponding from row and create (or replace) new atts
	for (int i = 0; i < n; i++) {
	   sv = nn((row=in.getElementAt(i)).getValue(key));
	   if((h=(Hashtable)map.get(sv)) == null)
		throw new IEException("row "+(i+1)+": match value \""+sv+"\" in receiving group \""+in.getName()+"\" not found in merge group \""+from.getName()+"\"");
	   for(int j=0;j<use.length; j++){
		if((a=row.getAtt(use[j]))!=null)
		   row.removeAtt(use[j]);
		row.addAtt(new Att(use[j],nn(h.get(use[j]))));
	   }
	}
   }

   /**
     * tell if a group's column contains the same value in all rows (e&#46;g&#46; verify all rows have the same value for att number)
     *
     * @param g	the group
     * @param col	the column name
     * @exception    	
     * @return boolean	
   */
   public static boolean isValUnique(Group g, String col){
	int n=0;
	if(g==null||(n=g.getElementCount()) < 2) 
	   return true;
	String v=nn(g.getAttributeValue(0,col));
	for(int i=1;i<n;i++)
	   if(! nn(g.getAttributeValue(i,col)).equals(v))
		return false;
	return true;
   }

   /**
     * tell caller if a links roleA or roleB object is a Master. according to windchill's design pattern we use introspection to find a links two-argument static constructor method, which happens to be called new&lt;LinkName&gt; e.g., newWTPartUsageLink
     * the first such method is inspected and queried for the object types of it's parameters. the result is cached so we speed up things.
     * as of now all of the links we needed could be inspected that way. in case it happens that a link misses the appropriate constructor,
     * the linkroles hash should be initialized to contain the data for such links.
     *
     * @param link	string name of link class, e.g., wt.part.WTPartUsageLink
     * @param roleoff	0=roleA, 1=roleB
     * @exception WTException	
     * @return boolean	
   */
   public static boolean isRoleMaster(String link, int roleoff) throws WTException{
	boolean[] b=(boolean[])linkRoles.get(link);
	if(b!=null)return b[roleoff]; // do we need to limit roleoff? -> looser!
	try{
	Class class1 = Class.forName(link);
	String methodName="new"+link.replaceFirst(".*\\.","");
	Method[] m=class1.getDeclaredMethods();
	b=new boolean[]{false, false};
	for(int i=0;i<m.length;i++){
	   if(m[i].getName().equals(methodName)){
		//System.out.println("cons found.");
		Class[] c = m[i].getParameterTypes();
		if(c.length == 2){
		   //System.out.println("2 arg cons: "+c[0]+"/"+c[1]);
		   b[0]=(c[0].newInstance() instanceof Mastered);
		   b[1]=(c[1].newInstance() instanceof Mastered);
		   break;
		} // 2 arg cons
	   } // cons method
	} // loop methods
	return b[roleoff];
   }
   catch(Exception err){ throw new WTException(err);}
   }

   /**
     * escape xml (or html) special chars &amp; &gt; &lt; &quot; &apos; with their textual representation e.g. &amp;->&amp;amp;
     *
     * @param o	the object to print (o.toString() or "" if null) 
     * @return String	the escaped string
   */
   public static String escapeXML(Object o){
	char[] ca=nn(o).toCharArray();
	StringBuffer r=new StringBuffer();
	String s;
	for(int i=0;i<ca.length;i++)
	   if(ca[i]<='>' && (s=mapXML[ca[i]])!=null)
		r.append(s);
	   else
		r.append(ca[i]);
	return r.toString();
   }
   /**
     * return a persistable from an object (which via toString() must give an obid)
     *
     * @param obid	
     * @exception WTException	
     * @return Persistable	
   */
   public static Persistable objFromObid(Object obid) throws WTException{
	return RF.getReference(shortObid(obid.toString())).getObject();
   }
   /**
     * return a persistable from an object (which via toString() must give an obid), return the <b>working copy</b> if the object is checked out.
     *
     * @param obid	
     * @exception WTException	
     * @return Persistable	
   */
   public static Workable wrkObjFromObid(Object obid) throws WTException{
      Workable w=(Workable)RF.getReference(shortObid(obid.toString())).getObject();
	if(! WorkInProgressHelper.isCheckedOut(w))
	   return w;
	if(WorkInProgressHelper.isWorkingCopy(w))
	   return w;
	return WorkInProgressHelper.service.workingCopyOf(w);
   }
   /**
     * return the latest version of a persistable from an object (which via toString() must give an obid), return the <b>working copy</b> if the object is checked out.
     *
     * @param obid	
     * @exception WTException	
     * @return Persistable	
   */
   public static Workable latestVersionWrkObjFromObid(Object obid) throws WTException{
      Workable w=null;
	logger.debug("in="+obid);
	if(obid instanceof String && obid.toString().indexOf("Master") != -1)
	   obid=RF.getReference(shortObid(obid.toString())).getObject();
	if(obid instanceof Master){
	   w=(Workable) VersionControlHelper.service.allVersionsOf((Master)obid).nextElement();
	}
	if(obid instanceof String){ // normal obj
	   w=(Workable)RF.getReference(shortObid(obid.toString())).getObject();
	   w=(Workable) VersionControlHelper.service.allVersionsOf((Mastered)((Iterated)w).getMaster()).nextElement();
	}
	logger.debug("latest version="+obid);
	if(! WorkInProgressHelper.isCheckedOut(w))
	   return w;
	if(WorkInProgressHelper.isWorkingCopy(w))
	   return w;
	return WorkInProgressHelper.service.workingCopyOf(w);
   }
   /**
     * deserialize an object from a file and dump it to stdout
     *
     * @param argv	
     * @exception Exception 	
     * @return void	
   */
   public static void deser(String[] argv) throws Exception {
      ObjectInputStream ois = new ObjectInputStream(new FileInputStream(argv[1]));
      Object o = ois.readObject();
      ois.close();
      System.out.println("obj="+o+" type="+o.getClass().getName());
	System.out.println(prettyPrint(o));
   }
   /**
     * deserialize the base64 encoded text given in argv into an object and either print a stacktrace (if it's an exception) or dump it to stdout
     *
     * typical use to get a stacktrace for the exception message in an I*E error group
     * @param argv	
     * @exception Exception 	
     * @return void	
   */
   public static void deserex(String[] argv) throws Exception {
	String s=argv[1];
	for(int i=2;i<argv.length;i++)s+=argv[i];
	if((new File(s)).exists()){
	   FileInputStream f = new FileInputStream(s);
	   BufferedReader b = new BufferedReader(new InputStreamReader(f));
	   String t;
	   s=null;
	   Matcher m;
	   while((t = b.readLine()) != null){ // last ex.
	     if ((m = exc.matcher(t)).matches())
		  s=m.replaceFirst("$1"); // <exception-object CONTENT-TYPE="application/java-serialized-object"> .... </ex
	   }
	}
	if(s==null){
	   System.out.println("no <exception-object CONTENT-TYPE...> found in file!");
	   return;
	}
	ObjectInputStream ois= new ObjectInputStream(new ByteArrayInputStream(Base64.decode(s.getBytes())));
      Object o = ois.readObject();
      ois.close();
	System.out.println("got: " + o);
	if(o instanceof Exception)
	   ((Exception)o).printStackTrace();
	else
	   System.out.println(prettyPrint(o));
   }

   /**
     * log an exceptions stacktrace to the logger
     *
     * @param l  	
     * @param e  	
     * @exception Exception 	
     * @return void	
   */
   public static void logException(Logger l, Throwable e) {
	ByteArrayOutputStream bos=new ByteArrayOutputStream();
	e.printStackTrace(new PrintStream(bos));
	logger.debug("Exception stacktrace is: \n"+bos.toString());
	while((e instanceof WTException) || (e instanceof IEException)){
	   if(e instanceof WTException)
	      e=((WTException)e).getNestedThrowable();
	   else
	      e=((IEException)e).getNestedThrowable();
	   if(e==null)
	      break;
	   bos=new ByteArrayOutputStream();
	   e.printStackTrace(new PrintStream(bos));
	   logger.debug("Nested Exception stacktrace is: \n"+bos.toString());
	}
	if(e!=null){ // even more infos?
	   Throwable t=e.getCause();
	   while(t != null){
		logger.debug("getCause() nested:");
		ToolUtils.logException(logger,t);
		if(t instanceof Exception){
		   e=(Exception)t;
		   t=(Exception)e.getCause();
		}
		else
		   t=null;
	   }
	}
   }

   public static String getOR(Persistable p){
      return getOR((Object)p);
   }
   public static String getOR(Object p){
	if(p == null)
	   return "XX:loser.passes.Null:NNN";
	if((p instanceof WTReference))
	   p=((WTReference)p).getObject();
	if(!(p instanceof Persistable))
	   return "XX:"+p.getClass().getName()+":NNN";
	if(PersistenceHelper.isPersistent(p))
	   return "OR:"+p.getClass().getName()+":"+PersistenceHelper.getObjectIdentifier((Persistable)p).getId();
	return "OR:"+p.getClass().getName()+":???";
   }
   public static boolean rmDashR(String fOrD){
      return rmDashR(new File(fOrD));
   }
   public static boolean rmDashR(File fOrD){
	if(! fOrD.exists())
	   return false;
	if(fOrD.isFile())
	   return fOrD.delete();
	// seems to be directory (or special file on unix?)
	if(! fOrD.isDirectory())
	   return false;
	File[] weg=fOrD.listFiles();
	for(int fi=0;fi<weg.length;fi++)
	   if(! rmDashR(weg[fi]))
	      return false;
      return fOrD.delete();
   }
}
