2000-12-29 17:57:21 +00:00
|
|
|
|
// Node.java
|
|
|
|
|
// Copyright (c) Hannes Walln<6C>fer 1997-2000
|
|
|
|
|
|
|
|
|
|
package helma.objectmodel.db;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import java.util.*;
|
|
|
|
|
import java.io.*;
|
|
|
|
|
import java.sql.Types;
|
|
|
|
|
import helma.objectmodel.*;
|
|
|
|
|
import helma.util.*;
|
|
|
|
|
import com.workingdogs.village.*;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* An implementation of INode that can be stored in the internal database or
|
|
|
|
|
* an external relational database.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
public class Node implements INode, Serializable {
|
|
|
|
|
|
|
|
|
|
// The ID of this node's parent node
|
|
|
|
|
protected String parentID;
|
|
|
|
|
// Ordered list of subnodes of this node
|
|
|
|
|
private Vector subnodes;
|
|
|
|
|
// Named subnodes (properties) of this node
|
|
|
|
|
private Hashtable propMap;
|
|
|
|
|
// Other nodes that link to this node. Used for reference counting/checking
|
|
|
|
|
private Vector links;
|
|
|
|
|
// Other nodes that refer to this node as property. Used for reference counting/checking
|
|
|
|
|
private Vector proplinks;
|
|
|
|
|
|
|
|
|
|
// the name of the (Hop) prototype - this is stored as standard property instead.
|
|
|
|
|
// private String prototype;
|
|
|
|
|
|
|
|
|
|
private String contentType;
|
|
|
|
|
private byte content[];
|
|
|
|
|
|
|
|
|
|
private long created;
|
|
|
|
|
private long lastmodified;
|
|
|
|
|
|
|
|
|
|
private String id, name;
|
|
|
|
|
// is this node's main identity as a named property or an
|
|
|
|
|
// anonymous node in a subnode collection?
|
|
|
|
|
protected boolean anonymous = false;
|
|
|
|
|
|
|
|
|
|
private void readObject (ObjectInputStream in) throws IOException {
|
|
|
|
|
try {
|
2001-01-09 21:40:09 +00:00
|
|
|
|
// as a general rule of thumb, if a string can bu null use read/writeObject,
|
|
|
|
|
// if not it's save to use read/writeUTF.
|
|
|
|
|
// version indicates the serialization version
|
2000-12-29 17:57:21 +00:00
|
|
|
|
int version = in.readShort ();
|
|
|
|
|
id = in.readUTF ();
|
|
|
|
|
name = in.readUTF ();
|
|
|
|
|
parentID = (String) in.readObject ();
|
|
|
|
|
created = in.readLong ();
|
|
|
|
|
lastmodified = in.readLong ();
|
|
|
|
|
content = (byte[]) in.readObject ();
|
|
|
|
|
contentType = (String) in.readObject ();
|
|
|
|
|
subnodes = (Vector) in.readObject ();
|
|
|
|
|
links = (Vector) in.readObject ();
|
|
|
|
|
proplinks = (Vector) in.readObject ();
|
|
|
|
|
propMap = (Hashtable) in.readObject ();
|
|
|
|
|
anonymous = in.readBoolean ();
|
2001-01-09 21:40:09 +00:00
|
|
|
|
if (version == 2)
|
2001-01-05 19:26:55 +00:00
|
|
|
|
prototype = in.readUTF ();
|
2001-01-09 21:40:09 +00:00
|
|
|
|
else if (version == 3)
|
|
|
|
|
prototype = (String) in.readObject ();
|
2000-12-29 17:57:21 +00:00
|
|
|
|
} catch (ClassNotFoundException x) {
|
|
|
|
|
throw new IOException (x.toString ());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void writeObject (ObjectOutputStream out) throws IOException {
|
2001-01-09 21:40:09 +00:00
|
|
|
|
out.writeShort (3); // serialization version
|
2000-12-29 17:57:21 +00:00
|
|
|
|
out.writeUTF (id);
|
|
|
|
|
out.writeUTF (name);
|
|
|
|
|
out.writeObject (parentID);
|
|
|
|
|
out.writeLong (created);
|
|
|
|
|
out.writeLong (lastmodified);
|
|
|
|
|
out.writeObject (content);
|
|
|
|
|
out.writeObject (contentType);
|
|
|
|
|
DbMapping smap = dbmap == null ? null : dbmap.getSubnodeMapping ();
|
|
|
|
|
if (smap != null && smap.isRelational ())
|
|
|
|
|
out.writeObject (null);
|
|
|
|
|
else
|
|
|
|
|
out.writeObject (subnodes);
|
|
|
|
|
out.writeObject (links);
|
|
|
|
|
out.writeObject (proplinks);
|
|
|
|
|
out.writeObject (propMap);
|
|
|
|
|
out.writeBoolean (anonymous);
|
2001-01-09 21:40:09 +00:00
|
|
|
|
out.writeObject (prototype);
|
2000-12-29 17:57:21 +00:00
|
|
|
|
}
|
|
|
|
|
|
2001-01-05 15:17:38 +00:00
|
|
|
|
transient String prototype;
|
2000-12-29 17:57:21 +00:00
|
|
|
|
transient INode cacheNode;
|
|
|
|
|
transient WrappedNodeManager nmgr;
|
|
|
|
|
transient DbMapping dbmap;
|
2001-01-18 14:49:13 +00:00
|
|
|
|
transient Key primaryKey = null;
|
2000-12-29 17:57:21 +00:00
|
|
|
|
transient DbMapping parentmap;
|
|
|
|
|
transient String subnodeRelation = null;
|
|
|
|
|
transient long lastSubnodeFetch = 0;
|
|
|
|
|
transient long lastSubnodeChange = 0;
|
|
|
|
|
|
|
|
|
|
transient long lastSubnodeCount = 0; // these two are only used
|
|
|
|
|
transient int subnodeCount = -1; // for aggressive loading relational subnodes
|
|
|
|
|
|
|
|
|
|
transient private volatile Transactor lock;
|
|
|
|
|
transient private int state;
|
|
|
|
|
|
|
|
|
|
// transient String nameProp; // the name of the property which defines the name of this node.
|
|
|
|
|
transient boolean adoptName = true; // little helper to know if this node is being converted
|
|
|
|
|
|
|
|
|
|
static final long serialVersionUID = -3740339688506633675L;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Constructor used for virtual nodes.
|
|
|
|
|
*/
|
|
|
|
|
public Node (Node home, String propname, WrappedNodeManager nmgr, String prototype) {
|
|
|
|
|
this.nmgr = nmgr;
|
|
|
|
|
setParent (home);
|
|
|
|
|
// this.dbmap = null;
|
|
|
|
|
// generate a key for the virtual node that can't be mistaken for a JDBC-URL
|
2001-01-18 18:10:49 +00:00
|
|
|
|
this.id = Key.makeVirtualID (parentmap, parentID, propname);
|
2000-12-29 17:57:21 +00:00
|
|
|
|
this.name = propname;
|
|
|
|
|
this.anonymous = false;
|
2001-01-28 21:18:07 +00:00
|
|
|
|
setPrototype (prototype);
|
2000-12-29 17:57:21 +00:00
|
|
|
|
this.state = VIRTUAL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Constructor used for nodes being stored in a relational database table.
|
|
|
|
|
*/
|
|
|
|
|
public Node (DbMapping dbmap, Record rec, WrappedNodeManager nmgr) throws DataSetException {
|
|
|
|
|
this.nmgr = nmgr;
|
|
|
|
|
this.dbmap = dbmap;
|
|
|
|
|
id = rec.getValue (dbmap.getIDField ()).asString ();
|
|
|
|
|
checkWriteLock ();
|
|
|
|
|
String nameField = dbmap.getNameField ();
|
|
|
|
|
name = nameField == null ? id : rec.getValue (nameField).asString ();
|
2001-01-11 19:25:38 +00:00
|
|
|
|
if (name == null || name.length() == 0)
|
|
|
|
|
name = id;
|
2001-01-05 15:17:38 +00:00
|
|
|
|
setPrototype (dbmap.getTypeName ());
|
2000-12-29 17:57:21 +00:00
|
|
|
|
for (Enumeration e=dbmap.db2prop.elements (); e.hasMoreElements(); ) {
|
|
|
|
|
|
|
|
|
|
Relation rel = (Relation) e.nextElement ();
|
|
|
|
|
// NOTE: this should never be the case, since we're just looping through
|
|
|
|
|
// mappnigs with a local db column
|
|
|
|
|
if (rel.direction != Relation.PRIMITIVE && rel.direction != Relation.FORWARD)
|
|
|
|
|
continue;
|
|
|
|
|
|
2001-01-25 20:58:18 +00:00
|
|
|
|
Value val = rec.getValue (rel.getDbField ());
|
2000-12-29 17:57:21 +00:00
|
|
|
|
|
|
|
|
|
if (val.isNull ())
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
Property newprop = new Property (rel.propname, this);
|
|
|
|
|
|
|
|
|
|
switch (val.type ()) {
|
|
|
|
|
|
|
|
|
|
case Types.BIT:
|
|
|
|
|
newprop.setBooleanValue (val.asBoolean());
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case Types.TINYINT:
|
|
|
|
|
case Types.BIGINT:
|
|
|
|
|
case Types.SMALLINT:
|
|
|
|
|
case Types.INTEGER:
|
|
|
|
|
newprop.setIntegerValue (val.asLong());
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case Types.REAL:
|
|
|
|
|
case Types.FLOAT:
|
|
|
|
|
case Types.DOUBLE:
|
|
|
|
|
newprop.setFloatValue (val.asDouble());
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case Types.DECIMAL:
|
|
|
|
|
case Types.NUMERIC:
|
|
|
|
|
java.math.BigDecimal num = val.asBigDecimal ();
|
|
|
|
|
if (num.scale() > 0)
|
|
|
|
|
newprop.setFloatValue (val.asDouble());
|
|
|
|
|
else
|
|
|
|
|
newprop.setIntegerValue (val.asLong());
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case Types.LONGVARBINARY:
|
|
|
|
|
case Types.VARBINARY:
|
|
|
|
|
case Types.BINARY:
|
|
|
|
|
newprop.setStringValue (val.asString());
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case Types.LONGVARCHAR:
|
|
|
|
|
case Types.CHAR:
|
|
|
|
|
case Types.VARCHAR:
|
|
|
|
|
case Types.OTHER:
|
|
|
|
|
newprop.setStringValue (val.asString());
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case Types.DATE:
|
|
|
|
|
case Types.TIME:
|
|
|
|
|
case Types.TIMESTAMP:
|
|
|
|
|
newprop.setDateValue (val.asTimestamp());
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case Types.NULL:
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
newprop.setStringValue (val.asString());
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(propMap == null)
|
|
|
|
|
propMap = new Hashtable ();
|
|
|
|
|
propMap.put (rel.propname.toLowerCase(), newprop);
|
|
|
|
|
// mark property as clean, since it's fresh from the db
|
|
|
|
|
newprop.dirty = false;
|
|
|
|
|
|
|
|
|
|
// if the property is a pointer to another node, change the property type to NODE
|
|
|
|
|
if (rel.direction == Relation.FORWARD) {
|
|
|
|
|
newprop.nvalueID = newprop.getStringValue ();
|
|
|
|
|
newprop.type = IProperty.NODE;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
markAs (CLEAN);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Creates a new Node with the given name.
|
|
|
|
|
*/
|
|
|
|
|
public Node (String n, WrappedNodeManager nmgr) {
|
|
|
|
|
this.nmgr = nmgr;
|
|
|
|
|
id = nmgr.generateID (dbmap);
|
|
|
|
|
checkWriteLock ();
|
|
|
|
|
this.name = n == null || "".equals (n) ? id : n;
|
|
|
|
|
created = lastmodified = System.currentTimeMillis ();
|
|
|
|
|
adoptName = true;
|
|
|
|
|
markAs (TRANSIENT);
|
|
|
|
|
// nmgr.registerNode (this);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Creates a new Node with the given name. Only used by NodeManager for "root nodes" and
|
|
|
|
|
* not in a Transaction context, which is why we can immediately mark it as CLEAN.
|
|
|
|
|
*/
|
|
|
|
|
protected Node (String name, String id, String prototype, WrappedNodeManager nmgr) {
|
|
|
|
|
this.nmgr = nmgr;
|
|
|
|
|
this.id = id;
|
|
|
|
|
this.name = name == null || "".equals (name) ? id : name;
|
2001-01-05 15:17:38 +00:00
|
|
|
|
if (prototype != null)
|
|
|
|
|
setPrototype (prototype);
|
2000-12-29 17:57:21 +00:00
|
|
|
|
created = lastmodified = System.currentTimeMillis ();
|
|
|
|
|
markAs (CLEAN);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Creates a new instance of Node, transforming from another implementation of
|
|
|
|
|
* interface INode. This Constructor is used when a transient
|
|
|
|
|
* node is converted into a persistent-capable one, hence the status is set to NEW.
|
|
|
|
|
*/
|
|
|
|
|
private Node (INode node, Hashtable ntable, boolean conversionRoot, WrappedNodeManager nmgr) {
|
|
|
|
|
this.nmgr = nmgr;
|
|
|
|
|
this.dbmap = node.getDbMapping ();
|
|
|
|
|
this.id = nmgr.generateID (dbmap);
|
|
|
|
|
checkWriteLock ();
|
|
|
|
|
this.name = node.getName ();
|
2001-01-05 19:26:55 +00:00
|
|
|
|
this.prototype = node.getPrototype ();
|
2000-12-29 17:57:21 +00:00
|
|
|
|
created = lastmodified = System.currentTimeMillis ();
|
|
|
|
|
setContent (node.getContent (), node.getContentType ());
|
|
|
|
|
created = lastmodified = System.currentTimeMillis ();
|
|
|
|
|
ntable.put (node, this);
|
|
|
|
|
// only take over name from property if this is not the root of the current node conversion
|
|
|
|
|
adoptName = !conversionRoot;
|
|
|
|
|
for (Enumeration e = node.getSubnodes (); e.hasMoreElements (); ) {
|
|
|
|
|
INode next = (INode) e.nextElement ();
|
|
|
|
|
Node nextc = (next instanceof Node) ? (Node) next : (Node) ntable.get (next); // is this a Node already?
|
|
|
|
|
if (nextc == null)
|
|
|
|
|
nextc = new Node (next, ntable, true, nmgr);
|
|
|
|
|
if (nextc.state == INVALID)
|
|
|
|
|
nextc = nmgr.getNode (nextc.getID (), nextc.getDbMapping ());
|
|
|
|
|
addNode (nextc);
|
|
|
|
|
}
|
|
|
|
|
for (Enumeration e = node.properties (); e.hasMoreElements (); ) {
|
2001-01-28 21:18:07 +00:00
|
|
|
|
IProperty next = node.get ((String) e.nextElement (), false);
|
|
|
|
|
if (next == null)
|
|
|
|
|
continue;
|
2000-12-29 17:57:21 +00:00
|
|
|
|
int t = next.getType ();
|
|
|
|
|
if (t == IProperty.NODE) {
|
|
|
|
|
INode n = next.getNodeValue ();
|
|
|
|
|
Node nextc = (n instanceof Node) ? (Node) n : (Node) ntable.get (n); // is this a Node already?
|
|
|
|
|
if (nextc == null)
|
|
|
|
|
nextc = new Node (n, ntable, true, nmgr);
|
|
|
|
|
if (nextc.state == INVALID)
|
|
|
|
|
nextc = nmgr.getNode (nextc.getID (), nextc.getDbMapping ());
|
|
|
|
|
setNode (next.getName (), nextc);
|
|
|
|
|
} else if (t == IProperty.STRING) {
|
|
|
|
|
setString (next.getName (), next.getStringValue ());
|
|
|
|
|
} else if (t == IProperty.INTEGER) {
|
|
|
|
|
setInteger (next.getName (), next.getIntegerValue ());
|
|
|
|
|
} else if (t == IProperty.FLOAT) {
|
|
|
|
|
setFloat (next.getName (), next.getFloatValue ());
|
|
|
|
|
} else if (t == IProperty.BOOLEAN) {
|
|
|
|
|
setBoolean (next.getName (), next.getBooleanValue ());
|
|
|
|
|
} else if (t == IProperty.DATE) {
|
|
|
|
|
setDate (next.getName (), next.getDateValue ());
|
|
|
|
|
} else if (t == IProperty.JAVAOBJECT) {
|
|
|
|
|
setJavaObject (next.getName (), next.getJavaObjectValue ());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
adoptName = true; // switch back to normal name adoption behaviour
|
|
|
|
|
markAs (NEW);
|
|
|
|
|
// nmgr.registerNode (this);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected synchronized void checkWriteLock () {
|
|
|
|
|
// System.err.println ("registering writelock for "+this.getName ()+" ("+lock+") to "+Thread.currentThread ());
|
|
|
|
|
if (state == TRANSIENT)
|
|
|
|
|
return; // no need to lock transient node
|
|
|
|
|
Transactor current = (Transactor) Thread.currentThread ();
|
|
|
|
|
if (!current.isActive ())
|
|
|
|
|
throw new helma.framework.TimeoutException ();
|
|
|
|
|
if (state == INVALID) {
|
|
|
|
|
IServer.getLogger().log ("Got Invalid Node: "+this);
|
|
|
|
|
Thread.dumpStack ();
|
|
|
|
|
throw new ConcurrencyException ("Node "+this+" was invalidated by another thread.");
|
|
|
|
|
}
|
|
|
|
|
if (lock != null && lock != current && lock.isAlive () && lock.isActive ()) {
|
|
|
|
|
IServer.getLogger().log ("Concurrency conflict for "+this+", lock held by "+lock);
|
|
|
|
|
throw new ConcurrencyException ("Tried to modify "+this+" from two threads at the same time.");
|
|
|
|
|
}
|
|
|
|
|
current.visitNode (this);
|
|
|
|
|
lock = current;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected synchronized void clearWriteLock () {
|
|
|
|
|
lock = null;
|
|
|
|
|
// check if the subnodes are relational.
|
|
|
|
|
// If so, clear the subnode vector.
|
|
|
|
|
// DbMapping smap = dbmap == null ? null : dbmap.getSubnodeMapping ();
|
|
|
|
|
// if (smap != null && smap.isRelational ())
|
|
|
|
|
// subnodes = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected void markAs (int s) {
|
|
|
|
|
if (state == INVALID || state == VIRTUAL || state == TRANSIENT)
|
|
|
|
|
return;
|
|
|
|
|
state = s;
|
|
|
|
|
if (Thread.currentThread () instanceof Transactor) {
|
|
|
|
|
Transactor tx = (Transactor) Thread.currentThread ();
|
|
|
|
|
if (s == CLEAN) {
|
|
|
|
|
clearWriteLock ();
|
|
|
|
|
tx.dropNode (this);
|
|
|
|
|
} else {
|
|
|
|
|
tx.visitNode (this);
|
|
|
|
|
if (s == NEW) {
|
|
|
|
|
clearWriteLock ();
|
|
|
|
|
tx.visitCleanNode (this);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public int getState () {
|
|
|
|
|
return state;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void setState (int s) {
|
|
|
|
|
this.state = s;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Mark node as invalid so it is re-fetched from the database */
|
|
|
|
|
public void invalidate () {
|
|
|
|
|
checkWriteLock ();
|
|
|
|
|
nmgr.evictNode (this);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* navigation-related
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
public String getID () {
|
|
|
|
|
return id;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected void setID (String id) {
|
|
|
|
|
this.id = id;
|
|
|
|
|
((Transactor) Thread.currentThread()).visitCleanNode (this);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public boolean isAnonymous () {
|
|
|
|
|
return anonymous;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public String getName () {
|
|
|
|
|
return name;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get something to identify this node within a URL. This is the ID for anonymous nodes
|
|
|
|
|
* and a property value for named properties.
|
|
|
|
|
*/
|
|
|
|
|
public String getNameOrID () {
|
|
|
|
|
// if subnodes are also mounted as properties, try to get the "nice" prop value
|
|
|
|
|
// instead of the id by turning the anonymous flag off.
|
|
|
|
|
if (anonymous && parentmap != null) {
|
|
|
|
|
Relation prel = parentmap.getPropertyRelation();
|
2001-01-25 20:58:18 +00:00
|
|
|
|
if (prel != null && prel.subnodesAreProperties && !prel.usesPrimaryKey ()) try {
|
|
|
|
|
Relation localrel = dbmap.columnNameToProperty (prel.getRemoteField ());
|
2000-12-29 17:57:21 +00:00
|
|
|
|
String propvalue = getString (localrel.propname, false);
|
|
|
|
|
if (propvalue != null && propvalue.length() > 0) {
|
|
|
|
|
setName (propvalue);
|
|
|
|
|
anonymous = false;
|
|
|
|
|
// nameProp = localrel.propname;
|
|
|
|
|
}
|
|
|
|
|
} catch (Exception ignore) {} // just fall back to ID
|
|
|
|
|
}
|
2001-01-11 19:25:38 +00:00
|
|
|
|
return anonymous || name == null || name.length() == 0 ? id : name;
|
2000-12-29 17:57:21 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public String getFullName () {
|
|
|
|
|
return getFullName (null);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public String getFullName (INode root) {
|
|
|
|
|
String fullname = "";
|
|
|
|
|
String divider = null;
|
|
|
|
|
StringBuffer b = new StringBuffer ();
|
|
|
|
|
INode p = this;
|
|
|
|
|
int loopWatch = 0;
|
|
|
|
|
|
|
|
|
|
while (p != null && p.getParent () != null && p != root) {
|
|
|
|
|
if (divider != null)
|
|
|
|
|
b.insert (0, divider);
|
|
|
|
|
else
|
|
|
|
|
divider = "/";
|
|
|
|
|
b.insert (0, p.getNameOrID ());
|
|
|
|
|
p = p.getParent ();
|
|
|
|
|
|
|
|
|
|
loopWatch++;
|
|
|
|
|
if (loopWatch > 10) {
|
|
|
|
|
b.insert (0, "...");
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return b.toString ();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public INode[] getPath () {
|
|
|
|
|
int pathSize = 1;
|
|
|
|
|
INode p = getParent ();
|
|
|
|
|
|
|
|
|
|
while (p != null) {
|
|
|
|
|
pathSize +=1;
|
|
|
|
|
p = p.getParent ();
|
|
|
|
|
if (pathSize > 100) // sanity check
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
INode path[] = new INode[pathSize];
|
|
|
|
|
p = this;
|
|
|
|
|
for (int i = pathSize-1; i>=0; i--) {
|
|
|
|
|
path[i] = p;
|
|
|
|
|
p = p.getParent ();
|
|
|
|
|
}
|
|
|
|
|
return path;
|
|
|
|
|
}
|
|
|
|
|
|
2001-01-05 15:17:38 +00:00
|
|
|
|
public String getPrototype () {
|
|
|
|
|
if (prototype == null && propMap != null) {
|
|
|
|
|
// retrieve prototype name from properties
|
|
|
|
|
Property pp = (Property) propMap.get ("prototype");
|
|
|
|
|
if (pp != null)
|
|
|
|
|
prototype = pp.getStringValue ();
|
|
|
|
|
}
|
|
|
|
|
return prototype;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void setPrototype (String proto) {
|
|
|
|
|
this.prototype = proto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2000-12-29 17:57:21 +00:00
|
|
|
|
public void setDbMapping (DbMapping dbmap) {
|
|
|
|
|
if (this.dbmap != dbmap) {
|
|
|
|
|
this.dbmap = dbmap;
|
2001-01-18 14:49:13 +00:00
|
|
|
|
primaryKey = null;
|
2000-12-29 17:57:21 +00:00
|
|
|
|
((Transactor) Thread.currentThread()).visitCleanNode (this);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public DbMapping getDbMapping () {
|
|
|
|
|
return dbmap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Key getKey () {
|
2001-01-18 14:49:13 +00:00
|
|
|
|
if (primaryKey == null)
|
|
|
|
|
primaryKey = new Key (dbmap, id);
|
|
|
|
|
return primaryKey;
|
2000-12-29 17:57:21 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void setSubnodeRelation (String rel) {
|
|
|
|
|
if ((rel == null && this.subnodeRelation == null)
|
|
|
|
|
|| (rel != null && rel.equalsIgnoreCase (this.subnodeRelation)))
|
|
|
|
|
return;
|
|
|
|
|
checkWriteLock ();
|
|
|
|
|
this.subnodeRelation = rel;
|
|
|
|
|
DbMapping smap = dbmap == null ? null : dbmap.getSubnodeMapping ();
|
|
|
|
|
if (smap != null && smap.isRelational ()) {
|
|
|
|
|
subnodes = null;
|
|
|
|
|
subnodeCount = -1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public String getSubnodeRelation () {
|
|
|
|
|
return subnodeRelation;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void setName (String name) {
|
|
|
|
|
// "/" is used as delimiter, so it's not a legal char
|
|
|
|
|
if (name.indexOf('/') > -1)
|
|
|
|
|
throw new RuntimeException ("The name of the node must not contain \"/\".");
|
|
|
|
|
if (name == null || name.trim().length() == 0)
|
|
|
|
|
this.name = id; // use id as name
|
|
|
|
|
else
|
|
|
|
|
this.name = name;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Register a node as parent of the present node. We can't refer to the node directly, so we use
|
|
|
|
|
* the ID + DB map combo.
|
|
|
|
|
*/
|
|
|
|
|
public void setParent (Node parent) {
|
|
|
|
|
this.parentID = parent == null ? null : parent.getID();
|
|
|
|
|
this.parentmap = parent == null ? null : parent.getDbMapping();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* This version of setParent additionally marks the node as anonymous or non-anonymous,
|
|
|
|
|
* depending on the string argument.
|
|
|
|
|
*/
|
|
|
|
|
public void setParent (Node parent, String propertyName) {
|
|
|
|
|
this.parentID = parent == null ? null : parent.getID();
|
|
|
|
|
this.parentmap = parent == null ? null : parent.getDbMapping();
|
|
|
|
|
if (propertyName == null) {
|
|
|
|
|
this.anonymous = true;
|
|
|
|
|
} else {
|
|
|
|
|
this.anonymous = false;
|
|
|
|
|
this.name = propertyName;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public INode getParent () {
|
2001-01-05 23:11:44 +00:00
|
|
|
|
|
|
|
|
|
// check what's specified in the type.properties for this node.
|
|
|
|
|
String[] parentProps = null;
|
|
|
|
|
if (dbmap != null && dbmap.isRelational ())
|
|
|
|
|
parentProps = dbmap.getParentPropNames ();
|
|
|
|
|
|
|
|
|
|
// check if current parent candidate matches presciption, if not, try to get it
|
|
|
|
|
if (parentProps != null) {
|
|
|
|
|
for (int i=0; i<parentProps.length; i++) {
|
|
|
|
|
INode pn = getNode (parentProps[i], false);
|
|
|
|
|
if (pn != null) {
|
|
|
|
|
// see if dbmapping specifies anonymity for this node
|
|
|
|
|
Boolean[] ano = dbmap.getAnonymous ();
|
|
|
|
|
if (ano != null && ano.length > i)
|
|
|
|
|
anonymous = ano[i].booleanValue();
|
|
|
|
|
return pn;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// fall back to heuristic parent (the node that fetched this one from db)
|
2000-12-29 17:57:21 +00:00
|
|
|
|
if (parentID == null)
|
|
|
|
|
return null;
|
2001-01-05 23:11:44 +00:00
|
|
|
|
return nmgr.getNode (parentID, parentmap);
|
2000-12-29 17:57:21 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* INode-related
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
public INode addNode (INode elem) {
|
|
|
|
|
return addNode (elem, numberOfNodes ());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public INode addNode (INode elem, int where) {
|
|
|
|
|
|
|
|
|
|
Node node = null;
|
|
|
|
|
if (elem instanceof Node)
|
|
|
|
|
node = (Node) elem;
|
|
|
|
|
else
|
|
|
|
|
node = convert (elem);
|
|
|
|
|
// if the new node is marked as TRANSIENT and this node is not, mark new node as NEW
|
|
|
|
|
if (state != TRANSIENT && node.state == TRANSIENT)
|
|
|
|
|
node.makePersistentCapable ();
|
|
|
|
|
|
|
|
|
|
String n = node.getName();
|
|
|
|
|
if (n.indexOf('/') > -1)
|
|
|
|
|
throw new RuntimeException ("\"/\" found in Node name.");
|
|
|
|
|
|
|
|
|
|
// only lock node if it has to be modified for a change in subnodes
|
|
|
|
|
if (!ignoreSubnodeChange ())
|
|
|
|
|
checkWriteLock ();
|
|
|
|
|
node.checkWriteLock ();
|
|
|
|
|
|
|
|
|
|
// only mark this node as modified if subnodes are not in relational db
|
|
|
|
|
// pointing to this node.
|
|
|
|
|
if (!ignoreSubnodeChange () && (state == CLEAN || state == DELETED))
|
|
|
|
|
markAs (MODIFIED);
|
|
|
|
|
if (node.state == CLEAN || node.state == DELETED)
|
|
|
|
|
node.markAs (MODIFIED);
|
|
|
|
|
|
|
|
|
|
loadNodes ();
|
|
|
|
|
|
2001-02-06 09:43:50 +00:00
|
|
|
|
// check if this node has a group-by subnode-relation
|
|
|
|
|
if (dbmap != null) {
|
|
|
|
|
Relation srel = dbmap.getSubnodeRelation ();
|
|
|
|
|
if (srel != null && srel.groupby != null) try {
|
|
|
|
|
Relation groupbyRel = (Relation) srel.other.db2prop.get (srel.groupby);
|
|
|
|
|
String groupbyProp = (groupbyRel != null) ?
|
|
|
|
|
groupbyRel.propname : srel.groupby;
|
|
|
|
|
String groupbyValue = node.getString (groupbyProp, false);
|
|
|
|
|
INode groupbyNode = getNode (groupbyValue, false);
|
|
|
|
|
// if group-by node doesn't exist, we'll create it
|
|
|
|
|
if (groupbyNode == null)
|
|
|
|
|
groupbyNode = getGroupbySubnode (groupbyValue, true);
|
|
|
|
|
groupbyNode.addNode (node);
|
|
|
|
|
checkBackLink (node);
|
|
|
|
|
return node;
|
|
|
|
|
} catch (Exception x) {
|
|
|
|
|
System.err.println ("Error adding groupby: "+x);
|
|
|
|
|
// x.printStackTrace ();
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2000-12-29 17:57:21 +00:00
|
|
|
|
if (where < 0 || where > numberOfNodes ())
|
|
|
|
|
where = numberOfNodes ();
|
|
|
|
|
if (node.getParent () != null) {
|
|
|
|
|
// this makes the job of addLink, which means that addLink and addNode
|
|
|
|
|
// are functionally equivalent now.
|
|
|
|
|
if (node.getParent () != this || !node.anonymous) {
|
|
|
|
|
node.registerLink (this);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (subnodes != null && subnodes.contains (node.getID ())) {
|
|
|
|
|
// Node is already subnode of this - just move to new position
|
|
|
|
|
subnodes.removeElement (node.getID ());
|
|
|
|
|
where = Math.min (where, numberOfNodes ());
|
|
|
|
|
subnodes.insertElementAt (node.getID (), where);
|
|
|
|
|
} else {
|
|
|
|
|
if (subnodes == null) subnodes = new ExternalizableVector ();
|
|
|
|
|
subnodes.insertElementAt (node.getID (), where);
|
|
|
|
|
|
|
|
|
|
// check if properties are subnodes (_properties.aresubnodes=true)
|
|
|
|
|
if (dbmap != null && node.dbmap != null) {
|
|
|
|
|
Relation prel = dbmap.getPropertyRelation();
|
2001-01-25 20:58:18 +00:00
|
|
|
|
if (prel != null && prel.subnodesAreProperties && !prel.usesPrimaryKey ()) {
|
|
|
|
|
Relation localrel = node.dbmap.columnNameToProperty (prel.getRemoteField ());
|
2000-12-29 17:57:21 +00:00
|
|
|
|
// if no relation from db column to prop name is found, assume that both are equal
|
2001-01-25 20:58:18 +00:00
|
|
|
|
String propname = localrel == null ? prel.getRemoteField() : localrel.propname;
|
2000-12-29 17:57:21 +00:00
|
|
|
|
String prop = node.getString (propname, false);
|
|
|
|
|
if (prop != null && prop.length() > 0) {
|
|
|
|
|
INode old = getNode (prop, false);
|
|
|
|
|
if (old != null && old != node) {
|
|
|
|
|
unset (prop);
|
|
|
|
|
removeNode (old);
|
|
|
|
|
}
|
|
|
|
|
// throw new RuntimeException ("Property "+prop+" is already defined for "+this);
|
|
|
|
|
setNode (prop, node);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2001-02-01 19:11:39 +00:00
|
|
|
|
if (node.getParent () == null && !"root".equalsIgnoreCase (node.getPrototype ())) {
|
2000-12-29 17:57:21 +00:00
|
|
|
|
node.setParent (this);
|
|
|
|
|
node.anonymous = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2001-02-06 09:43:50 +00:00
|
|
|
|
checkBackLink (node);
|
|
|
|
|
|
|
|
|
|
lastmodified = System.currentTimeMillis ();
|
|
|
|
|
lastSubnodeChange = lastmodified;
|
|
|
|
|
// Server.throwNodeEvent (new NodeEvent (this, NodeEvent.SUBNODE_ADDED, node));
|
|
|
|
|
return node;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void checkBackLink (Node node) {
|
2000-12-29 17:57:21 +00:00
|
|
|
|
// check if the subnode is in relational db and needs to link back to this
|
|
|
|
|
// in order to make it a subnode
|
|
|
|
|
if (dbmap != null) {
|
|
|
|
|
Relation srel = dbmap.getSubnodeRelation ();
|
|
|
|
|
if (srel != null && srel.direction == Relation.BACKWARD) {
|
2001-01-25 20:58:18 +00:00
|
|
|
|
Relation backlink = srel.other.columnNameToProperty (srel.getRemoteField());
|
2000-12-29 17:57:21 +00:00
|
|
|
|
if (backlink != null && backlink.propname != null) {
|
|
|
|
|
if (node.get (backlink.propname, false) == null) {
|
|
|
|
|
if (this.state == VIRTUAL)
|
2001-02-06 09:43:50 +00:00
|
|
|
|
node.setString (backlink.propname, getNonVirtualHomeID());
|
2000-12-29 17:57:21 +00:00
|
|
|
|
else
|
|
|
|
|
node.setString (backlink.propname, getID());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public INode createNode () {
|
|
|
|
|
return createNode (null, numberOfNodes ()); // create new node at end of subnode array
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public INode createNode (int where) {
|
|
|
|
|
return createNode (null, where);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public INode createNode (String nm) {
|
|
|
|
|
// parameter where is ignored if nm != null so we try to avoid calling numberOfNodes()
|
|
|
|
|
return createNode (nm, nm == null ? numberOfNodes () : 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public INode createNode (String nm, int where) {
|
|
|
|
|
checkWriteLock ();
|
|
|
|
|
boolean anon = false;
|
|
|
|
|
if (nm == null || "".equals (nm.trim ()))
|
|
|
|
|
anon = true;
|
|
|
|
|
Node n = new Node (nm, nmgr);
|
|
|
|
|
if (anon)
|
|
|
|
|
addNode (n, where);
|
|
|
|
|
else
|
|
|
|
|
setNode (nm, n);
|
|
|
|
|
return n;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* register a node that links to this node.
|
|
|
|
|
*/
|
|
|
|
|
protected void registerLink (Node from) {
|
|
|
|
|
if (links == null)
|
|
|
|
|
links = new ExternalizableVector ();
|
|
|
|
|
Object fromID = from.getID ();
|
|
|
|
|
if (!links.contains (fromID))
|
|
|
|
|
links.addElement (fromID);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public INode getSubnode (String path) {
|
|
|
|
|
StringTokenizer st = new StringTokenizer (path, "/");
|
|
|
|
|
Node retval = this, runner;
|
2001-01-03 15:15:30 +00:00
|
|
|
|
|
2000-12-29 17:57:21 +00:00
|
|
|
|
while (st.hasMoreTokens () && retval != null) {
|
|
|
|
|
runner = retval;
|
|
|
|
|
String next = st.nextToken().trim().toLowerCase ();
|
2001-01-03 15:15:30 +00:00
|
|
|
|
|
2000-12-29 17:57:21 +00:00
|
|
|
|
if ("".equals (next)) {
|
|
|
|
|
retval = this;
|
|
|
|
|
} else {
|
|
|
|
|
runner.loadNodes ();
|
|
|
|
|
boolean found = runner.subnodes == null ? false : runner.subnodes.contains (next);
|
2001-01-03 15:15:30 +00:00
|
|
|
|
|
|
|
|
|
if (!found)
|
|
|
|
|
retval = null;
|
|
|
|
|
else {
|
|
|
|
|
Relation srel = null;
|
|
|
|
|
DbMapping smap = null;
|
|
|
|
|
if (runner.dbmap != null) {
|
|
|
|
|
srel = runner.dbmap.getSubnodeRelation ();
|
|
|
|
|
smap = runner.dbmap.getSubnodeMapping ();
|
|
|
|
|
}
|
|
|
|
|
// check if there is a group-by relation
|
|
|
|
|
if (srel != null && srel.groupby != null)
|
|
|
|
|
retval = nmgr.getNode (this, next, srel);
|
|
|
|
|
else
|
|
|
|
|
retval = nmgr.getNode (next, smap);
|
|
|
|
|
}
|
|
|
|
|
|
2001-02-01 19:11:39 +00:00
|
|
|
|
if (retval != null && retval.parentID == null && !"root".equalsIgnoreCase (retval.getPrototype ())) {
|
2000-12-29 17:57:21 +00:00
|
|
|
|
retval.setParent (runner);
|
|
|
|
|
retval.anonymous = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (retval == null) {
|
|
|
|
|
retval = (Node) runner.getNode (next, false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return retval;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public INode getSubnodeAt (int index) {
|
|
|
|
|
loadNodes ();
|
|
|
|
|
|
|
|
|
|
if (subnodes == null)
|
|
|
|
|
return null;
|
|
|
|
|
|
|
|
|
|
Relation srel = null;
|
|
|
|
|
DbMapping smap = null;
|
|
|
|
|
if (dbmap != null) {
|
|
|
|
|
srel = dbmap.getSubnodeRelation ();
|
|
|
|
|
smap = dbmap.getSubnodeMapping ();
|
|
|
|
|
}
|
|
|
|
|
Node retval = null;
|
|
|
|
|
if (subnodes.size () > index) {
|
|
|
|
|
// check if there is a group-by relation
|
|
|
|
|
if (srel != null && srel.groupby != null)
|
|
|
|
|
retval = nmgr.getNode (this, (String) subnodes.elementAt (index), srel);
|
|
|
|
|
else
|
|
|
|
|
retval = nmgr.getNode ((String) subnodes.elementAt (index), smap);
|
2001-02-01 19:11:39 +00:00
|
|
|
|
if (retval != null && retval.parentID == null && !"root".equalsIgnoreCase (retval.getPrototype ())) {
|
2000-12-29 17:57:21 +00:00
|
|
|
|
retval.setParent (this);
|
|
|
|
|
retval.anonymous = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return retval;
|
|
|
|
|
}
|
|
|
|
|
|
2001-02-06 09:43:50 +00:00
|
|
|
|
public Node getGroupbySubnode (String sid, boolean create) {
|
2001-01-02 15:28:56 +00:00
|
|
|
|
loadNodes ();
|
2001-02-06 09:43:50 +00:00
|
|
|
|
if (subnodes == null)
|
|
|
|
|
subnodes = new ExternalizableVector ();
|
|
|
|
|
|
|
|
|
|
if (subnodes.contains (sid) || create) try {
|
2001-01-04 12:48:29 +00:00
|
|
|
|
Relation srel = dbmap.getSubnodeRelation ();
|
|
|
|
|
Relation prel = dbmap.getPropertyRelation ();
|
2001-02-06 09:43:50 +00:00
|
|
|
|
boolean relational = srel.other != null && srel.other.isRelational ();
|
|
|
|
|
|
|
|
|
|
if (relational || create) {
|
|
|
|
|
Node node = relational ? new Node (this, sid, nmgr, null) : new Node ("groupby-"+sid, nmgr);
|
|
|
|
|
// set "groupname" property to value of groupby field
|
|
|
|
|
node.setString ("groupname", sid);
|
|
|
|
|
|
|
|
|
|
if (relational) {
|
|
|
|
|
DbMapping dbm = new DbMapping ();
|
|
|
|
|
Relation gsrel = srel.getGroupbySubnodeRelation();
|
|
|
|
|
dbm.setSubnodeMapping (srel.other);
|
|
|
|
|
dbm.setSubnodeRelation (gsrel);
|
|
|
|
|
if (prel != null) {
|
|
|
|
|
dbm.setPropertyMapping (prel.other);
|
|
|
|
|
dbm.setPropertyRelation (prel.getGroupbyPropertyRelation());
|
|
|
|
|
}
|
|
|
|
|
node.setDbMapping (dbm);
|
|
|
|
|
String snrel = "WHERE "+srel.groupby +"='"+sid+"'";
|
|
|
|
|
if (gsrel.direction == Relation.BACKWARD)
|
|
|
|
|
snrel += " AND "+gsrel.getRemoteField()+"='"+getNonVirtualHomeID()+"'";
|
|
|
|
|
if (gsrel.order != null)
|
|
|
|
|
snrel += " ORDER BY "+gsrel.order;
|
|
|
|
|
node.setSubnodeRelation (snrel);
|
|
|
|
|
} else {
|
|
|
|
|
subnodes.addElement (node.getID ());
|
|
|
|
|
setNode (sid, node);
|
|
|
|
|
}
|
|
|
|
|
return node;
|
2001-01-28 21:18:07 +00:00
|
|
|
|
}
|
|
|
|
|
} catch (Exception noluck) {
|
|
|
|
|
IServer.getLogger ().log ("Error creating group-by node for "+sid+": "+noluck);
|
|
|
|
|
}
|
2001-01-02 15:28:56 +00:00
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2000-12-29 17:57:21 +00:00
|
|
|
|
public boolean remove () {
|
|
|
|
|
checkWriteLock ();
|
|
|
|
|
if (anonymous)
|
|
|
|
|
getParent ().unset (name);
|
|
|
|
|
else
|
|
|
|
|
getParent ().removeNode (this);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public void removeNode (INode node) {
|
|
|
|
|
IServer.getLogger().log ("removing: "+ node);
|
|
|
|
|
Node n = (Node) node;
|
|
|
|
|
checkWriteLock ();
|
|
|
|
|
n.checkWriteLock ();
|
|
|
|
|
|
|
|
|
|
releaseNode (n);
|
|
|
|
|
if (n.getParent () == this) {
|
|
|
|
|
n.deepRemoveNode ();
|
|
|
|
|
} else {
|
|
|
|
|
// removed just a link, not the main node.
|
|
|
|
|
if (n.links != null) {
|
|
|
|
|
n.links.removeElement (this.id);
|
|
|
|
|
if (n.state == CLEAN) n.markAs (MODIFIED);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* "Locally" remove a subnode from the subnodes table.
|
|
|
|
|
* The logical stuff necessary for keeping data consistent is done in removeNode().
|
|
|
|
|
*/
|
|
|
|
|
protected void releaseNode (Node node) {
|
|
|
|
|
if (subnodes != null)
|
|
|
|
|
subnodes.removeElement (node.getID ());
|
|
|
|
|
|
|
|
|
|
lastSubnodeChange = System.currentTimeMillis ();
|
|
|
|
|
|
|
|
|
|
// check if the subnode is in relational db and has a link back to this
|
|
|
|
|
// which needs to be unset
|
|
|
|
|
if (dbmap != null) {
|
|
|
|
|
Relation srel = dbmap.getSubnodeRelation ();
|
|
|
|
|
if (srel != null && srel.direction == Relation.BACKWARD) {
|
2001-01-25 20:58:18 +00:00
|
|
|
|
Relation backlink = srel.other.columnNameToProperty (srel.getRemoteField ());
|
2000-12-29 17:57:21 +00:00
|
|
|
|
if (backlink != null && id.equals (node.getString (backlink.propname, false)))
|
|
|
|
|
node.unset (backlink.propname);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// check if subnodes are handled as virtual fs
|
|
|
|
|
if (dbmap != null && node.dbmap != null) {
|
|
|
|
|
Relation prel = dbmap.getPropertyRelation();
|
2001-01-25 20:58:18 +00:00
|
|
|
|
if (prel != null && prel.subnodesAreProperties && !prel.usesPrimaryKey ()) {
|
|
|
|
|
Relation localrel = node.dbmap.columnNameToProperty (prel.getRemoteField());
|
2000-12-29 17:57:21 +00:00
|
|
|
|
// if no relation from db column to prop name is found, assume that both are equal
|
2001-01-25 20:58:18 +00:00
|
|
|
|
String propname = localrel == null ? prel.getRemoteField () : localrel.propname;
|
2000-12-29 17:57:21 +00:00
|
|
|
|
String prop = node.getString (propname, false);
|
|
|
|
|
if (prop != null && getNode (prop, false) == node)
|
|
|
|
|
unset (prop);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If subnodes are relational no need to mark this node as modified
|
|
|
|
|
if (ignoreSubnodeChange ())
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
// Server.throwNodeEvent (new NodeEvent (node, NodeEvent.NODE_REMOVED));
|
|
|
|
|
// Server.throwNodeEvent (new NodeEvent (this, NodeEvent.SUBNODE_REMOVED, node));
|
|
|
|
|
lastmodified = System.currentTimeMillis ();
|
|
|
|
|
// IServer.getLogger().log ("released node "+node +" from "+this+" oldobj = "+what);
|
|
|
|
|
if (state == CLEAN) markAs (MODIFIED);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Delete the node from the db. This mainly tries to notify all nodes referring to this that
|
|
|
|
|
* it's going away. For nodes from the embedded db it also does a cascading delete, since
|
|
|
|
|
* it can tell which nodes are actual children and which are just linked in.
|
|
|
|
|
*/
|
|
|
|
|
protected void deepRemoveNode () {
|
|
|
|
|
|
|
|
|
|
// notify nodes that link to this node that it is being deleted.
|
|
|
|
|
int l = links == null ? 0 : links.size ();
|
|
|
|
|
for (int i = 0; i < l; i++) {
|
|
|
|
|
// TODO: solve dbmap problem
|
|
|
|
|
Node link = nmgr.getNode ((String) links.elementAt (i), null);
|
|
|
|
|
if (link != null) link.releaseNode (this);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// clean up all nodes that use n as a property
|
|
|
|
|
if (proplinks != null) {
|
|
|
|
|
for (Enumeration e1 = proplinks.elements (); e1.hasMoreElements (); ) try {
|
|
|
|
|
String pid = (String) e1.nextElement ();
|
|
|
|
|
Node pnode = nmgr.getNode (pid, null);
|
|
|
|
|
if (pnode != null) {
|
|
|
|
|
IServer.getLogger().log("Warning: Can't unset node property of "+pnode.getFullName ());
|
|
|
|
|
}
|
|
|
|
|
} catch (Exception ignore) {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// tell all nodes that are properties of n that they are no longer used as such
|
|
|
|
|
if (propMap != null) {
|
|
|
|
|
for (Enumeration e2 = propMap.elements (); e2.hasMoreElements (); ) {
|
|
|
|
|
Property p = (Property) e2.nextElement ();
|
|
|
|
|
if (p != null && p.type == Property.NODE)
|
|
|
|
|
p.unregisterNode ();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// cascading delete of all subnodes. This is never done for relational subnodes, because
|
|
|
|
|
// the parent info is not 100% accurate for them.
|
|
|
|
|
if (subnodes != null) {
|
|
|
|
|
Vector v = new Vector ();
|
|
|
|
|
// removeElement modifies the Vector we are enumerating, so we are extra careful.
|
|
|
|
|
for (Enumeration e3 = getSubnodes (); e3.hasMoreElements(); ) {
|
|
|
|
|
v.addElement (e3.nextElement());
|
|
|
|
|
}
|
|
|
|
|
int m = v.size ();
|
|
|
|
|
for (int i=0; i<m; i++) {
|
|
|
|
|
// getParent() is heuristical/implicit for relational nodes, so we don't base
|
|
|
|
|
// a cascading delete on that criterium for relational nodes.
|
|
|
|
|
Node n = (Node) v.elementAt (i);
|
|
|
|
|
if (n.dbmap == null || !n.dbmap.isRelational())
|
|
|
|
|
removeNode (n);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// mark the node as deleted
|
|
|
|
|
setParent (null);
|
|
|
|
|
markAs (DELETED);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public int contains (INode n) {
|
|
|
|
|
if (n == null)
|
|
|
|
|
return -1;
|
|
|
|
|
loadNodes ();
|
|
|
|
|
if (subnodes == null)
|
|
|
|
|
return -1;
|
|
|
|
|
return subnodes.indexOf (n.getID ());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Count the subnodes of this node. If they're stored in a relational data source, we
|
|
|
|
|
* may actually load their IDs in order to do this.
|
|
|
|
|
*/
|
|
|
|
|
public int numberOfNodes () {
|
|
|
|
|
// If the subnodes are loaded aggressively, we really just
|
|
|
|
|
// do a count statement, otherwise we just return the size of the id index.
|
|
|
|
|
// (after loading it, if it's coming from a relational data source).
|
|
|
|
|
DbMapping smap = dbmap == null ? null : dbmap.getSubnodeMapping ();
|
|
|
|
|
if (smap != null && smap.isRelational ()) {
|
|
|
|
|
// check if subnodes need to be rechecked
|
|
|
|
|
Relation subRel = dbmap.getSubnodeRelation ();
|
|
|
|
|
if (subRel.aggressiveLoading) {
|
|
|
|
|
// we don't want to load *all* nodes if we just want to count them
|
|
|
|
|
long lastChange = subRel.aggressiveCaching ? lastSubnodeChange : smap.lastDataChange;
|
|
|
|
|
// also reload if the type mapping has changed.
|
|
|
|
|
lastChange = Math.max (lastChange, dbmap.getLastTypeChange ());
|
|
|
|
|
if (lastChange < lastSubnodeFetch && subnodes != null) {
|
|
|
|
|
// we can use the nodes vector to determine number of subnodes
|
|
|
|
|
subnodeCount = subnodes.size();
|
|
|
|
|
lastSubnodeCount = System.currentTimeMillis ();
|
|
|
|
|
} else if (lastChange >= lastSubnodeCount || subnodeCount < 0) {
|
|
|
|
|
// count nodes in db without fetching anything
|
|
|
|
|
subnodeCount = nmgr.countNodes (this, subRel);
|
|
|
|
|
lastSubnodeCount = System.currentTimeMillis ();
|
|
|
|
|
}
|
|
|
|
|
return subnodeCount;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
loadNodes ();
|
|
|
|
|
return subnodes == null ? 0 : subnodes.size ();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Make sure the subnode index is loaded for subnodes stored in a relational data source.
|
|
|
|
|
* Depending on the subnode.loadmode specified in the type.properties, we'll load just the
|
|
|
|
|
* ID index or the actual nodes.
|
|
|
|
|
*/
|
|
|
|
|
protected void loadNodes () {
|
|
|
|
|
DbMapping smap = dbmap == null ? null : dbmap.getSubnodeMapping ();
|
|
|
|
|
if (smap != null && smap.isRelational ()) {
|
|
|
|
|
// check if subnodes need to be reloaded
|
|
|
|
|
Relation subRel = dbmap.getSubnodeRelation ();
|
|
|
|
|
synchronized (this) {
|
|
|
|
|
long lastChange = subRel.aggressiveCaching ? lastSubnodeChange : smap.lastDataChange;
|
|
|
|
|
// also reload if the type mapping has changed.
|
|
|
|
|
lastChange = Math.max (lastChange, dbmap.getLastTypeChange ());
|
|
|
|
|
if (lastChange >= lastSubnodeFetch || subnodes == null) {
|
|
|
|
|
if (subRel.aggressiveLoading)
|
|
|
|
|
subnodes = nmgr.getNodes (this, dbmap.getSubnodeRelation());
|
|
|
|
|
else
|
|
|
|
|
subnodes = nmgr.getNodeIDs (this, dbmap.getSubnodeRelation());
|
|
|
|
|
lastSubnodeFetch = System.currentTimeMillis ();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Enumeration getSubnodes () {
|
|
|
|
|
loadNodes ();
|
|
|
|
|
class Enum implements Enumeration {
|
|
|
|
|
int count = 0;
|
|
|
|
|
public boolean hasMoreElements () {
|
|
|
|
|
return count < numberOfNodes ();
|
|
|
|
|
}
|
|
|
|
|
public Object nextElement () {
|
|
|
|
|
return getSubnodeAt (count++);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return new Enum ();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private boolean ignoreSubnodeChange () {
|
|
|
|
|
// return true if a change in subnodes can be ignored because it is
|
|
|
|
|
// stored in the subnodes themselves.
|
|
|
|
|
Relation rel = dbmap == null ? null : dbmap.getSubnodeRelation();
|
2001-02-06 09:43:50 +00:00
|
|
|
|
return (rel != null && rel.other != null && rel.other.isRelational() && rel.direction == Relation.BACKWARD);
|
2000-12-29 17:57:21 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get all properties of this node.
|
|
|
|
|
*/
|
|
|
|
|
public Enumeration properties () {
|
|
|
|
|
|
2001-01-28 21:18:07 +00:00
|
|
|
|
/* final Relation prel = dbmap == null ? null : dbmap.getPropertyRelation ();
|
2000-12-29 17:57:21 +00:00
|
|
|
|
final DbMapping pmap = prel == null ? null : prel.other;
|
|
|
|
|
if (pmap != null && pmap.isRelational ()) {
|
|
|
|
|
class Enum implements Enumeration {
|
|
|
|
|
// get relational property nodes from db
|
|
|
|
|
Vector propnodes = nmgr.getNodes (Node.this, prel.subnodesAreProperties ?
|
|
|
|
|
dbmap.getSubnodeRelation () : prel);
|
|
|
|
|
// add non-relational properties from propMap
|
|
|
|
|
Enumeration penum = propMap == null ? null : propMap.elements ();
|
|
|
|
|
int size = propnodes.size ();
|
|
|
|
|
int psize = propMap == null ? 0 : propMap.size();
|
|
|
|
|
int count = 0;
|
|
|
|
|
public boolean hasMoreElements () {
|
|
|
|
|
return count < size+psize;
|
|
|
|
|
}
|
|
|
|
|
public Object nextElement () {
|
|
|
|
|
// first deliver local, non-relational properties ....
|
|
|
|
|
if (penum != null && penum.hasMoreElements ()) {
|
|
|
|
|
count++;
|
|
|
|
|
return penum.nextElement ();
|
|
|
|
|
}
|
|
|
|
|
// .... then the relational ones.
|
|
|
|
|
Node n = nmgr.getNode ((String) propnodes.elementAt (count - psize), pmap);
|
|
|
|
|
Property prop = new Property (n.getName (), Node.this, n);
|
|
|
|
|
count++;
|
|
|
|
|
return prop;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return new Enum ();
|
2001-01-28 21:18:07 +00:00
|
|
|
|
} */
|
|
|
|
|
if (dbmap != null && dbmap.prop2db.size() > 0)
|
|
|
|
|
return dbmap.prop2db.keys();
|
|
|
|
|
else
|
|
|
|
|
return propMap == null ? new EmptyEnumeration () : propMap.keys ();
|
2000-12-29 17:57:21 +00:00
|
|
|
|
|
|
|
|
|
// NOTE: we don't enumerate node properties here
|
|
|
|
|
// return propMap == null ? new Vector ().elements () : propMap.elements ();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public IProperty get (String propname, boolean inherit) {
|
|
|
|
|
return getProperty (propname, inherit);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public String getParentInfo () {
|
|
|
|
|
return "anonymous:"+anonymous+",parentID:"+parentID+",parentmap:"+parentmap+",parent:"+getParent();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected Property getProperty (String propname, boolean inherit) {
|
|
|
|
|
// IServer.getLogger().log ("GETTING PROPERTY: "+propname);
|
|
|
|
|
if (propname == null)
|
|
|
|
|
return null;
|
|
|
|
|
Property prop = propMap == null ? null : (Property) propMap.get (propname.toLowerCase ());
|
|
|
|
|
|
|
|
|
|
// See if this could be a relationally linked node which still doesn't know
|
|
|
|
|
// (i.e, still thinks it's just the key as a string)
|
|
|
|
|
DbMapping pmap = dbmap == null ? null : dbmap.getExactPropertyMapping (propname);
|
|
|
|
|
if (pmap != null && prop != null && prop.type != IProperty.NODE) {
|
2001-02-01 19:11:39 +00:00
|
|
|
|
// this is a relational node stored by id but we still think it's just a string. fix it
|
2000-12-29 17:57:21 +00:00
|
|
|
|
prop.nvalueID = prop.getStringValue ();
|
|
|
|
|
prop.type = IProperty.NODE;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (prop == null && dbmap != null) {
|
|
|
|
|
Relation prel = dbmap.getPropertyRelation (propname);
|
|
|
|
|
if (prel == null)
|
|
|
|
|
prel = dbmap.getPropertyRelation ();
|
|
|
|
|
if (prel != null && (prel.direction == Relation.DIRECT || prel.virtual)) {
|
|
|
|
|
// this *may* be a relational node stored by property name
|
|
|
|
|
try {
|
|
|
|
|
Node pn = nmgr.getNode (this, propname, prel);
|
|
|
|
|
if (pn != null) {
|
2001-02-01 19:11:39 +00:00
|
|
|
|
if (pn.parentID == null && !"root".equalsIgnoreCase (pn.getPrototype ())) {
|
2000-12-29 17:57:21 +00:00
|
|
|
|
pn.setParent (this);
|
|
|
|
|
pn.name = propname;
|
|
|
|
|
pn.anonymous = false;
|
|
|
|
|
}
|
|
|
|
|
prop = new Property (propname, this, pn);
|
|
|
|
|
}
|
|
|
|
|
} catch (RuntimeException nonode) {
|
|
|
|
|
// wasn't a node after all
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (prop == null && inherit && getParent () != null) {
|
|
|
|
|
prop = ((Node) getParent ()).getProperty (propname, inherit);
|
|
|
|
|
}
|
|
|
|
|
return prop;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public String getString (String propname, String defaultValue, boolean inherit) {
|
|
|
|
|
String propValue = getString (propname, inherit);
|
|
|
|
|
return propValue == null ? defaultValue : propValue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public String getString (String propname, boolean inherit) {
|
|
|
|
|
propname = propname.toLowerCase ();
|
|
|
|
|
Property prop = getProperty (propname, inherit);
|
|
|
|
|
try {
|
|
|
|
|
return prop.getStringValue ();
|
|
|
|
|
} catch (Exception ignore) {}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public long getInteger (String propname, boolean inherit) {
|
|
|
|
|
propname = propname.toLowerCase ();
|
|
|
|
|
Property prop = getProperty (propname, inherit);
|
|
|
|
|
try {
|
|
|
|
|
return prop.getIntegerValue ();
|
|
|
|
|
} catch (Exception ignore) {}
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public double getFloat (String propname, boolean inherit) {
|
|
|
|
|
propname = propname.toLowerCase ();
|
|
|
|
|
Property prop = getProperty (propname, inherit);
|
|
|
|
|
try {
|
|
|
|
|
return prop.getFloatValue ();
|
|
|
|
|
} catch (Exception ignore) {}
|
|
|
|
|
return 0.0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Date getDate (String propname, boolean inherit) {
|
|
|
|
|
propname = propname.toLowerCase ();
|
|
|
|
|
Property prop = getProperty (propname, inherit);
|
|
|
|
|
try {
|
|
|
|
|
return prop.getDateValue ();
|
|
|
|
|
} catch (Exception ignore) {}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public boolean getBoolean (String propname, boolean inherit) {
|
|
|
|
|
propname = propname.toLowerCase ();
|
|
|
|
|
Property prop = getProperty (propname, inherit);
|
|
|
|
|
try {
|
|
|
|
|
return prop.getBooleanValue ();
|
|
|
|
|
} catch (Exception ignore) {}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public INode getNode (String propname, boolean inherit) {
|
|
|
|
|
propname = propname.toLowerCase ();
|
|
|
|
|
Property prop = getProperty (propname, inherit);
|
|
|
|
|
try {
|
|
|
|
|
return prop.getNodeValue ();
|
|
|
|
|
} catch (Exception ignore) {}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Object getJavaObject (String propname, boolean inherit) {
|
|
|
|
|
propname = propname.toLowerCase ();
|
|
|
|
|
Property prop = getProperty (propname, inherit);
|
|
|
|
|
try {
|
|
|
|
|
return prop.getJavaObjectValue ();
|
|
|
|
|
} catch (Exception ignore) {}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void setString (String propname, String value) {
|
|
|
|
|
// IServer.getLogger().log ("setting String prop");
|
|
|
|
|
checkWriteLock ();
|
|
|
|
|
|
|
|
|
|
if (propMap == null)
|
|
|
|
|
propMap = new Hashtable ();
|
|
|
|
|
|
|
|
|
|
propname = propname.trim ();
|
|
|
|
|
String p2 = propname.toLowerCase ();
|
|
|
|
|
|
|
|
|
|
Property prop = (Property) propMap.get (p2);
|
|
|
|
|
String oldvalue = null;
|
|
|
|
|
|
|
|
|
|
if (prop != null) {
|
|
|
|
|
oldvalue = prop.getStringValue ();
|
|
|
|
|
// check if the value has changed
|
|
|
|
|
if (value != null && value.equals (oldvalue))
|
|
|
|
|
return;
|
|
|
|
|
prop.setStringValue (value);
|
|
|
|
|
} else {
|
|
|
|
|
prop = new Property (propname, this);
|
|
|
|
|
prop.setStringValue (value);
|
|
|
|
|
propMap.put (p2, prop);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// check if this may have an effect on the node's URL when using subnodesAreProperties
|
|
|
|
|
INode parent = getParent ();
|
|
|
|
|
|
|
|
|
|
if (parent != null && parent.getDbMapping() != null) {
|
|
|
|
|
// check if this node is already registered with the old name; if so, remove it.
|
|
|
|
|
// then set parent's property to this node for the new name value
|
|
|
|
|
parentmap = parent.getDbMapping ();
|
|
|
|
|
Relation prel = parentmap.getPropertyRelation ();
|
|
|
|
|
|
2001-01-25 20:58:18 +00:00
|
|
|
|
if (prel != null && prel.subnodesAreProperties && propname.equals (prel.getRemoteField())) {
|
2000-12-29 17:57:21 +00:00
|
|
|
|
INode n = parent.getNode (value, false);
|
|
|
|
|
if (n != null && n != this) {
|
|
|
|
|
parent.unset (value);
|
|
|
|
|
parent.removeNode (n);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (oldvalue != null) {
|
|
|
|
|
n = parent.getNode (oldvalue, false);
|
|
|
|
|
if (n == this) {
|
|
|
|
|
parent.unset (oldvalue);
|
|
|
|
|
parent.addNode (this);
|
|
|
|
|
// let the node cache know this key's not for this node anymore.
|
2001-01-18 14:49:13 +00:00
|
|
|
|
nmgr.evictKey (new Key (prel.other, prel.getKeyID (parent, oldvalue)));
|
2000-12-29 17:57:21 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
parent.setNode (value, this);
|
|
|
|
|
setName (value);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Server.throwNodeEvent (new NodeEvent (this, NodeEvent.PROPERTIES_CHANGED));
|
|
|
|
|
lastmodified = System.currentTimeMillis ();
|
|
|
|
|
if (state == CLEAN) markAs (MODIFIED);
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void setInteger (String propname, long value) {
|
|
|
|
|
// IServer.getLogger().log ("setting bool prop");
|
|
|
|
|
checkWriteLock ();
|
|
|
|
|
|
|
|
|
|
if (propMap == null)
|
|
|
|
|
propMap = new Hashtable ();
|
|
|
|
|
propname = propname.trim ();
|
|
|
|
|
String p2 = propname.toLowerCase ();
|
|
|
|
|
|
|
|
|
|
Property prop = (Property) propMap.get (p2);
|
|
|
|
|
if (prop != null) {
|
|
|
|
|
prop.setIntegerValue (value);
|
|
|
|
|
} else {
|
|
|
|
|
prop = new Property (propname, this);
|
|
|
|
|
prop.setIntegerValue (value);
|
|
|
|
|
propMap.put (p2, prop);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Server.throwNodeEvent (new NodeEvent (this, NodeEvent.PROPERTIES_CHANGED));
|
|
|
|
|
lastmodified = System.currentTimeMillis ();
|
|
|
|
|
if (state == CLEAN) markAs (MODIFIED);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void setFloat (String propname, double value) {
|
|
|
|
|
// IServer.getLogger().log ("setting bool prop");
|
|
|
|
|
checkWriteLock ();
|
|
|
|
|
|
|
|
|
|
if (propMap == null)
|
|
|
|
|
propMap = new Hashtable ();
|
|
|
|
|
propname = propname.trim ();
|
|
|
|
|
String p2 = propname.toLowerCase ();
|
|
|
|
|
|
|
|
|
|
Property prop = (Property) propMap.get (p2);
|
|
|
|
|
if (prop != null) {
|
|
|
|
|
prop.setFloatValue (value);
|
|
|
|
|
} else {
|
|
|
|
|
prop = new Property (propname, this);
|
|
|
|
|
prop.setFloatValue (value);
|
|
|
|
|
propMap.put (p2, prop);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Server.throwNodeEvent (new NodeEvent (this, NodeEvent.PROPERTIES_CHANGED));
|
|
|
|
|
lastmodified = System.currentTimeMillis ();
|
|
|
|
|
if (state == CLEAN) markAs (MODIFIED);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void setBoolean (String propname, boolean value) {
|
|
|
|
|
// IServer.getLogger().log ("setting bool prop");
|
|
|
|
|
checkWriteLock ();
|
|
|
|
|
|
|
|
|
|
if (propMap == null)
|
|
|
|
|
propMap = new Hashtable ();
|
|
|
|
|
propname = propname.trim ();
|
|
|
|
|
String p2 = propname.toLowerCase ();
|
|
|
|
|
|
|
|
|
|
Property prop = (Property) propMap.get (p2);
|
|
|
|
|
if (prop != null) {
|
|
|
|
|
prop.setBooleanValue (value);
|
|
|
|
|
} else {
|
|
|
|
|
prop = new Property (propname, this);
|
|
|
|
|
prop.setBooleanValue (value);
|
|
|
|
|
propMap.put (p2, prop);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Server.throwNodeEvent (new NodeEvent (this, NodeEvent.PROPERTIES_CHANGED));
|
|
|
|
|
lastmodified = System.currentTimeMillis ();
|
|
|
|
|
if (state == CLEAN) markAs (MODIFIED);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public void setDate (String propname, Date value) {
|
|
|
|
|
// IServer.getLogger().log ("setting date prop");
|
|
|
|
|
checkWriteLock ();
|
|
|
|
|
|
|
|
|
|
if (propMap == null)
|
|
|
|
|
propMap = new Hashtable ();
|
|
|
|
|
propname = propname.trim ();
|
|
|
|
|
String p2 = propname.toLowerCase ();
|
|
|
|
|
|
|
|
|
|
Property prop = (Property) propMap.get (p2);
|
|
|
|
|
if (prop != null) {
|
|
|
|
|
prop.setDateValue (value);
|
|
|
|
|
} else {
|
|
|
|
|
prop = new Property (propname, this);
|
|
|
|
|
prop.setDateValue (value);
|
|
|
|
|
propMap.put (p2, prop);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Server.throwNodeEvent (new NodeEvent (this, NodeEvent.PROPERTIES_CHANGED));
|
|
|
|
|
lastmodified = System.currentTimeMillis ();
|
|
|
|
|
if (state == CLEAN) markAs (MODIFIED);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void setJavaObject (String propname, Object value) {
|
|
|
|
|
// IServer.getLogger().log ("setting jobject prop");
|
|
|
|
|
checkWriteLock ();
|
|
|
|
|
|
|
|
|
|
if (propMap == null)
|
|
|
|
|
propMap = new Hashtable ();
|
|
|
|
|
propname = propname.trim ();
|
|
|
|
|
String p2 = propname.toLowerCase ();
|
|
|
|
|
|
|
|
|
|
Property prop = (Property) propMap.get (p2);
|
|
|
|
|
if (prop != null) {
|
|
|
|
|
prop.setJavaObjectValue (value);
|
|
|
|
|
} else {
|
|
|
|
|
prop = new Property (propname, this);
|
|
|
|
|
prop.setJavaObjectValue (value);
|
|
|
|
|
propMap.put (p2, prop);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Server.throwNodeEvent (new NodeEvent (this, NodeEvent.PROPERTIES_CHANGED));
|
|
|
|
|
lastmodified = System.currentTimeMillis ();
|
|
|
|
|
if (state == CLEAN) markAs (MODIFIED);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void setNode (String propname, INode value) {
|
|
|
|
|
// IServer.getLogger().log ("setting node prop");
|
|
|
|
|
checkWriteLock ();
|
|
|
|
|
|
|
|
|
|
Node n = null;
|
|
|
|
|
if (value instanceof Node)
|
|
|
|
|
n = (Node) value;
|
|
|
|
|
else
|
|
|
|
|
n = convert (value);
|
|
|
|
|
// if the new node is marked as TRANSIENT and this node is not, mark new node as NEW
|
|
|
|
|
if (state != TRANSIENT && n.state == TRANSIENT)
|
|
|
|
|
n.makePersistentCapable ();
|
|
|
|
|
|
|
|
|
|
n.checkWriteLock ();
|
|
|
|
|
|
|
|
|
|
// check if the main identity of this node is as a named property
|
|
|
|
|
// or as an anonymous node in a collection
|
2001-02-01 19:11:39 +00:00
|
|
|
|
if (n.getParent () == null && n.adoptName && !"root".equalsIgnoreCase (n.getPrototype ())) {
|
2000-12-29 17:57:21 +00:00
|
|
|
|
n.setParent (this);
|
|
|
|
|
n.name = propname;
|
|
|
|
|
n.anonymous = false;
|
|
|
|
|
// IServer.getLogger().log ("adopted named node: "+n.getFullName ());
|
|
|
|
|
} // else IServer.getLogger().log ("not adopted: "+n.getFullName ());
|
|
|
|
|
|
|
|
|
|
if (propMap == null)
|
|
|
|
|
propMap = new Hashtable ();
|
|
|
|
|
propname = propname.trim ();
|
|
|
|
|
String p2 = propname.toLowerCase ();
|
|
|
|
|
|
|
|
|
|
DbMapping nmap = dbmap == null ? null : dbmap.getPropertyMapping (propname);
|
|
|
|
|
if (nmap != null && nmap != n.getDbMapping()) {
|
|
|
|
|
n.setDbMapping (nmap);
|
2001-01-05 15:17:38 +00:00
|
|
|
|
n.setPrototype (nmap.getTypeName ());
|
2000-12-29 17:57:21 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Property prop = (Property) propMap.get (p2);
|
|
|
|
|
if (prop != null) {
|
|
|
|
|
if (prop.type == IProperty.NODE && n.getID ().equals (prop.nvalueID)) {
|
|
|
|
|
// nothing to do, just clean up locks and return
|
|
|
|
|
if (state == CLEAN) clearWriteLock ();
|
|
|
|
|
if (n.state == CLEAN) n.clearWriteLock ();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
prop = new Property (propname, this);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
prop.setNodeValue (n);
|
|
|
|
|
Relation rel = dbmap == null ? null : dbmap.getPropertyRelation (propname);
|
|
|
|
|
if (rel == null || rel.direction == Relation.FORWARD || rel.virtual || rel.other == null || !rel.other.isRelational()) {
|
|
|
|
|
// the node must be stored as explicit property
|
|
|
|
|
propMap.put (p2, prop);
|
|
|
|
|
}
|
|
|
|
|
String nID = n.getID();
|
2001-01-31 19:17:05 +00:00
|
|
|
|
|
|
|
|
|
// check node in with transactor cache
|
|
|
|
|
Transactor tx = (Transactor) Thread.currentThread ();
|
|
|
|
|
tx.visitCleanNode (new Key (nmap, nID), n);
|
2000-12-29 17:57:21 +00:00
|
|
|
|
// if the field is not the primary key of the property, also register it
|
|
|
|
|
if (rel != null && !rel.getKeyID(this, p2).equals (nID))
|
2001-01-31 19:17:05 +00:00
|
|
|
|
tx.visitCleanNode (new Key (rel.other, rel.getKeyID(this, p2)), n);
|
2000-12-29 17:57:21 +00:00
|
|
|
|
|
|
|
|
|
// Server.throwNodeEvent (new NodeEvent (this, NodeEvent.SUBNODE_ADDED, n));
|
|
|
|
|
// Server.throwNodeEvent (new NodeEvent (this, NodeEvent.PROPERTIES_CHANGED));
|
|
|
|
|
lastmodified = System.currentTimeMillis ();
|
|
|
|
|
if (state == CLEAN) markAs (MODIFIED);
|
|
|
|
|
if (n.state == DELETED) n.markAs (MODIFIED);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Remove a property. Note that this works only for explicitly set properties, not for those
|
|
|
|
|
* specified via property relation.
|
|
|
|
|
*/
|
|
|
|
|
public void unset (String propname) {
|
|
|
|
|
if (propMap == null)
|
|
|
|
|
return;
|
|
|
|
|
try {
|
|
|
|
|
Property p = (Property) propMap.remove (propname.toLowerCase ());
|
|
|
|
|
if (p != null) {
|
|
|
|
|
checkWriteLock ();
|
|
|
|
|
if (p.type == Property.NODE)
|
|
|
|
|
p.unregisterNode ();
|
|
|
|
|
// Server.throwNodeEvent (new NodeEvent (this, NodeEvent.PROPERTIES_CHANGED));
|
|
|
|
|
lastmodified = System.currentTimeMillis ();
|
|
|
|
|
if (state == CLEAN)
|
|
|
|
|
markAs (MODIFIED);
|
|
|
|
|
}
|
|
|
|
|
} catch (Exception ignore) {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected void registerPropLink (INode n) {
|
|
|
|
|
if (proplinks == null)
|
|
|
|
|
proplinks = new ExternalizableVector ();
|
|
|
|
|
String plid = n.getID ();
|
|
|
|
|
if (!proplinks.contains (plid))
|
|
|
|
|
proplinks.addElement (n.getID ());
|
|
|
|
|
if (state == CLEAN || state == DELETED)
|
|
|
|
|
markAs (MODIFIED);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected void unregisterPropLink (INode n) {
|
|
|
|
|
if (proplinks != null)
|
|
|
|
|
proplinks.removeElement (n.getID ());
|
|
|
|
|
// Server.throwNodeEvent (new NodeEvent (this, NodeEvent.NODE_REMOVED));
|
|
|
|
|
// Server.throwNodeEvent (new NodeEvent (n, NodeEvent.SUBNODE_REMOVED, this));
|
|
|
|
|
if (state == CLEAN)
|
|
|
|
|
markAs (MODIFIED);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public void sanityCheck () {
|
|
|
|
|
checkSubnodes ();
|
|
|
|
|
checkProperties ();
|
|
|
|
|
checkLinks ();
|
|
|
|
|
checkPropLinks ();
|
|
|
|
|
if (getParent () == null && !"root".equals (name)) {
|
|
|
|
|
System.out.println ("*** parent: "+parentID+": "+this.getName ());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private void checkLinks () {
|
|
|
|
|
Vector v = links == null ? null : (Vector) links.clone ();
|
|
|
|
|
int l = v == null ? 0 : v.size ();
|
|
|
|
|
for (int i = 0; i < l; i++) {
|
|
|
|
|
String k = (String) v.elementAt (i);
|
|
|
|
|
Node link = nmgr.getNode (k, null);
|
|
|
|
|
if (link == null) {
|
|
|
|
|
links.removeElement (k);
|
|
|
|
|
System.out.println ("**** link "+k+": "+this.getFullName ());
|
|
|
|
|
markAs (MODIFIED);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void checkPropLinks () {
|
|
|
|
|
Vector v = proplinks == null ? null : (Vector) proplinks.clone ();
|
|
|
|
|
int l = v == null ? 0 : v.size ();
|
|
|
|
|
for (int i = 0; i < l; i++) {
|
|
|
|
|
String k = (String) v.elementAt (i);
|
|
|
|
|
Node link = nmgr.getNode (k, null);
|
|
|
|
|
if (link == null) {
|
|
|
|
|
proplinks.removeElement (k);
|
|
|
|
|
System.out.println ("**** proplink "+k+": "+this.getFullName ());
|
|
|
|
|
markAs (MODIFIED);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void checkSubnodes () {
|
|
|
|
|
Vector v = subnodes == null ? null : (Vector) subnodes.clone ();
|
|
|
|
|
int l = v == null ? 0 : v.size ();
|
|
|
|
|
for (int i = 0; i < l; i++) {
|
|
|
|
|
String k = (String) v.elementAt (i);
|
|
|
|
|
Node link = nmgr.getNode (k, null);
|
|
|
|
|
if (link == null) {
|
|
|
|
|
subnodes.removeElement (k);
|
|
|
|
|
System.out.println ("**** subnode "+k+": "+this.getFullName ());
|
|
|
|
|
markAs (MODIFIED);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void checkProperties () {
|
|
|
|
|
Hashtable ht = propMap == null ? new Hashtable () : (Hashtable) propMap.clone ();
|
|
|
|
|
for (Enumeration ps = ht.elements (); ps.hasMoreElements (); ) {
|
|
|
|
|
Property p = (Property) ps.nextElement ();
|
|
|
|
|
if (p.getType () == IProperty.NODE && p.getNodeValue () == null) {
|
|
|
|
|
System.out.println ("**** property "+p.propname+"->"+p.nvalueID+": "+this.getFullName ());
|
|
|
|
|
// INode par = getParent ();
|
|
|
|
|
// if (par != null)
|
|
|
|
|
// par.removeNode (this);
|
|
|
|
|
// markAs (DELETED);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* content-related
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
public boolean isText () throws IOException {
|
|
|
|
|
return getContentType().indexOf ("text/") == 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public boolean isBinary () throws IOException {
|
|
|
|
|
return getContentType().indexOf ("text/") != 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public String getContentType () {
|
|
|
|
|
if (contentType == null)
|
|
|
|
|
return "text/plain";
|
|
|
|
|
return contentType;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void setContentType (String type) {
|
|
|
|
|
checkWriteLock ();
|
|
|
|
|
contentType = type;
|
|
|
|
|
lastmodified = System.currentTimeMillis ();
|
|
|
|
|
if (state == CLEAN) markAs (MODIFIED);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public int getContentLength () {
|
|
|
|
|
if (content == null)
|
|
|
|
|
return 0;
|
|
|
|
|
return content.length;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void setContent (byte cnt[], String type) {
|
|
|
|
|
checkWriteLock ();
|
|
|
|
|
|
|
|
|
|
if (type != null)
|
|
|
|
|
contentType = type;
|
|
|
|
|
content = cnt;
|
|
|
|
|
lastmodified = System.currentTimeMillis ();
|
|
|
|
|
// Server.throwNodeEvent (new NodeEvent (this, NodeEvent.CONTENT_CHANGED));
|
|
|
|
|
if (state == CLEAN) markAs (MODIFIED);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public void setContent (String cstr) {
|
|
|
|
|
checkWriteLock ();
|
|
|
|
|
|
|
|
|
|
content = cstr.getBytes ();
|
|
|
|
|
lastmodified = System.currentTimeMillis ();
|
|
|
|
|
// Server.throwNodeEvent (new NodeEvent (this, NodeEvent.CONTENT_CHANGED));
|
|
|
|
|
if (state == CLEAN) markAs (MODIFIED);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public byte[] getContent () {
|
|
|
|
|
|
|
|
|
|
if (content == null || content.length == 0)
|
|
|
|
|
return "".getBytes ();
|
|
|
|
|
|
|
|
|
|
byte retval[] = new byte[content.length];
|
|
|
|
|
System.arraycopy (content, 0, retval, 0, content.length);
|
|
|
|
|
// IServer.getLogger().log ("copied "+retval.length+ " bytes");
|
|
|
|
|
return retval;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public String getText () {
|
|
|
|
|
if (content != null) {
|
|
|
|
|
if (getContentType ().startsWith ("text/")) {
|
|
|
|
|
return new String (content);
|
|
|
|
|
} else {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public String getHref (INode root, INode userroot, String tmpname, String prefix) {
|
|
|
|
|
return prefix + getUrl (root, userroot, tmpname);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the path to eiter the general data-root or the user root, depending on
|
|
|
|
|
* where this node is located.
|
|
|
|
|
*/
|
|
|
|
|
public String getUrl (INode root, INode userroot, String tmpname) {
|
|
|
|
|
// String fullname = "";
|
|
|
|
|
String divider = "/";
|
|
|
|
|
StringBuffer b = new StringBuffer ();
|
|
|
|
|
INode p = this;
|
|
|
|
|
int loopWatch = 0;
|
|
|
|
|
while (p != null && p.getParent () != null && p != root && p != userroot) {
|
|
|
|
|
b.insert (0, divider);
|
|
|
|
|
b.insert (0, UrlEncoder.encode (p.getNameOrID ()));
|
|
|
|
|
|
|
|
|
|
if (p.getParent () == userroot)
|
|
|
|
|
b.insert (0, "users"+divider);
|
|
|
|
|
p = p.getParent ();
|
|
|
|
|
|
|
|
|
|
if (loopWatch++ > 10)
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
return b.toString()+UrlEncoder.encode (tmpname);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public long lastModified () {
|
|
|
|
|
return lastmodified;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public long created () {
|
|
|
|
|
return created;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public String toString () {
|
|
|
|
|
return "Node " + name;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Recursively convert other implementations of INode into helma.objectmodel.db.Node.
|
|
|
|
|
*/
|
|
|
|
|
protected Node convert (INode n) {
|
|
|
|
|
Hashtable ntable = new Hashtable ();
|
|
|
|
|
Node converted = new Node (n, ntable, false, nmgr);
|
|
|
|
|
return converted;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Recursively turn node status from TRANSIENT to NEW so that the Transactor will
|
|
|
|
|
* know it has to insert this node.
|
|
|
|
|
*/
|
|
|
|
|
protected void makePersistentCapable () {
|
|
|
|
|
for (Enumeration e = getSubnodes (); e.hasMoreElements (); ) {
|
|
|
|
|
Node n = (Node) e.nextElement ();
|
|
|
|
|
if (n.state == TRANSIENT)
|
|
|
|
|
n.makePersistentCapable ();
|
|
|
|
|
}
|
|
|
|
|
for (Enumeration e = properties (); e.hasMoreElements (); ) {
|
2001-01-28 21:18:07 +00:00
|
|
|
|
IProperty next = get ((String) e.nextElement (), false);
|
|
|
|
|
if (next != null && next.getType () == IProperty.NODE) {
|
2000-12-29 17:57:21 +00:00
|
|
|
|
Node n = (Node) next.getNodeValue ();
|
|
|
|
|
if (n != null && n.state == TRANSIENT)
|
|
|
|
|
n.makePersistentCapable ();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
state = NEW;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the cache node for this node. This can be used to store transient cache data per node from Javascript.
|
|
|
|
|
*/
|
|
|
|
|
public synchronized INode getCacheNode () {
|
|
|
|
|
if (cacheNode == null)
|
|
|
|
|
cacheNode = new helma.objectmodel.Node();
|
|
|
|
|
return cacheNode;
|
|
|
|
|
}
|
|
|
|
|
|
2001-01-02 15:28:56 +00:00
|
|
|
|
// walk down node path to the first non-virtual node and return its id.
|
|
|
|
|
// limit max depth to 3, since there shouldn't be more then 2 layers of virtual nodes.
|
|
|
|
|
public String getNonVirtualHomeID () {
|
|
|
|
|
INode node = this;
|
|
|
|
|
for (int i=0; i<3; i++) {
|
|
|
|
|
if (node == null) break;
|
|
|
|
|
if (node.getState() != Node.VIRTUAL)
|
|
|
|
|
return node.getID ();
|
|
|
|
|
node = node.getParent ();
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void dumpSubnodes () {
|
|
|
|
|
System.err.println (subnodes);
|
|
|
|
|
}
|
|
|
|
|
|
2000-12-29 17:57:21 +00:00
|
|
|
|
}
|
|
|
|
|
|