Index: helma/objectmodel/db/Node.java =================================================================== RCS file: /opt/cvs/hop/helma/src/helma/objectmodel/db/Node.java,v retrieving revision 1.153 diff -u -r1.153 Node.java --- helma/objectmodel/db/Node.java 25 Nov 2004 14:17:01 -0000 1.153 +++ helma/objectmodel/db/Node.java 31 Jan 2005 16:27:36 -0000 @@ -804,12 +804,13 @@ } /** + * Add a node to this Node's subnodes, making the added node persistent if it + * hasn't been before and this Node is already persistent. * + * @param elem the node to add to this Nodes subnode-list + * @param where the index-position where this node has to be added * - * @param elem ... - * @param where ... - * - * @return ... + * @return the added node itselve */ public INode addNode(INode elem, int where) { Node node = null; @@ -890,7 +891,7 @@ } } else { if (subnodes == null) { - subnodes = new ExternalizableVector(); + subnodes = getEmptySubnodeList(); } synchronized (subnodes) { // check if index is out of bounds when adding @@ -1489,11 +1490,12 @@ * 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. + * @return true if this subnodelist has been loaded, false if not (this Node is TRANSIENT or no subnodemapping is available or no Data-/Mapping-change occured inside Helma) */ - protected void loadNodes() { + protected boolean loadNodes() { // Don't do this for transient nodes which don't have an explicit subnode relation set if (((state == TRANSIENT) || (state == NEW)) && (subnodeRelation == null)) { - return; + return false; } DbMapping smap = (dbmap == null) ? null : dbmap.getSubnodeMapping(); @@ -1517,9 +1519,11 @@ } lastSubnodeFetch = System.currentTimeMillis(); + return true; } } } + return false; } /** @@ -2551,4 +2555,50 @@ System.err.println("properties: " + propMap); } -} + /** + * Retrieve an empty subnodelist. This empty List is an instance of the Class + * used for this Nodes subnode-list + * @return List an empty List of the type used by this Node + */ + public List getEmptySubnodeList() { + Relation rel = this.dbmap.getSubnodeRelation(); + if (rel != null && rel.updateCreteria!=null) { + this.subnodes = new UpdateableSubnodeList(rel); + } else { + this.subnodes = new ExternalizableVector(); + } + return this.subnodes; + } + + /** + * This method get's called from the JavaScript environment + * (HopObject.updateSubnodes() or HopObject.collection.updateSubnodes())) + * The subnode-collection will be updated with a selectstatement getting all + * Nodes having a higher id than the highest id currently contained within + * this Node's subnoderelation. If this subnodelist has a special order + * all nodes will be loaded honoring this order. + * Example: + * order by somefield1 asc, somefieled2 desc + * gives a where-clausel like the following: + * (somefiled1 > theHighestKnownValue value and somefield2 < theLowestKnownValue) + * @return the number of loaded nodes within this collection update + */ + public Map updateSubnodes () { + // the subnode-list has to be build from scratch + if (loadNodes()) { + Map map = new HashMap(); + map.put ("newNodes", new Integer(this.subnodes.size())); + map.put ("addedNodes", new Integer(this.subnodes.size())); + return map; + } + // FIXME: what do we do if this.dbmap is null + if (this.dbmap == null) { + throw new RuntimeException (this + " doesn't have a DbMapping"); + } + Relation rel = this.dbmap.getSubnodeRelation(); + synchronized (this) { + lastSubnodeFetch = System.currentTimeMillis(); + return this.nmgr.updateSubnodeList(this, rel); + } + } +} \ No newline at end of file Index: helma/objectmodel/db/NodeManager.java =================================================================== RCS file: /opt/cvs/hop/helma/src/helma/objectmodel/db/NodeManager.java,v retrieving revision 1.126 diff -u -r1.126 NodeManager.java --- helma/objectmodel/db/NodeManager.java 25 Nov 2004 14:17:01 -0000 1.126 +++ helma/objectmodel/db/NodeManager.java 31 Jan 2005 16:27:37 -0000 @@ -836,7 +836,7 @@ throw new RuntimeException("NodeMgr.getNodeIDs called for non-relational node " + home); } else { - List retval = new ArrayList(); + List retval = home.getEmptySubnodeList(); // if we do a groupby query (creating an intermediate layer of groupby nodes), // retrieve the value of that field instead of the primary key @@ -948,7 +948,7 @@ throw new RuntimeException("NodeMgr.getNodes called for non-relational node " + home); } else { - List retval = new ArrayList(); + List retval = home.getEmptySubnodeList(); DbMapping dbm = rel.otherType; Connection con = dbm.getConnection(); @@ -1012,6 +1012,161 @@ } return retval; + } + } + + /** + * Update a UpdateableSubnodeList retrieving all values having + * higher Values according to the updateCreteria's set for this Collection's Relation + * The returned Map-Object has two Properties: + * addedNodes = an Integer representing the number of Nodes added to this collection + * newNodes = an Integer representing the number of Records returned by the Select-Statement + * These two values may be different if a max-size is defined for this Collection and a new + * node would be outside of this Border because of the ordering of this collection. + * @param home the home of this subnode-list + * @param rel the relation the home-node has to the nodes contained inside the subnodelist + * @return A map having two properties of type String (newNodes (number of nodes retreived by the select-statment), addedNodes (nodes added to the collection)) + * @throws Exception + */ + public Map updateSubnodeList(Node home, Relation rel) throws Exception { + // Transactor tx = (Transactor) Thread.currentThread (); + // tx.timer.beginEvent ("getNodeIDs "+home); + + if ((rel == null) || (rel.otherType == null) || !rel.otherType.isRelational()) { + // this should never be called for embedded nodes + throw new RuntimeException("NodeMgr.updateSubnodeList called for non-relational node " + + home); + } else { + List sList = home.getSubnodeList(); + if (sList == null) + sList = home.getEmptySubnodeList(); + + if (!(sList instanceof UpdateableSubnodeList)) + throw new RuntimeException ("unable to update SubnodeList not marked as updateable (" + rel.propName + ")"); + + UpdateableSubnodeList retval = (UpdateableSubnodeList) sList; + + // FIXME: grouped subnodes aren't supported yet + if (rel.groupby!=null) + throw new RuntimeException ("update not yet supported on grouped collections"); + + String idfield = rel.otherType.getIDField(); + Connection con = rel.otherType.getConnection(); + String table = rel.otherType.getTableName(); + + Statement stmt = null; + + try { + String q = null; + + StringBuffer b = new StringBuffer(); + if (rel.loadAggressively()) { + b.append (rel.otherType.getSelect(rel)); + } else { + b.append ("SELECT "); + if (rel.queryHints != null) { + b.append(rel.queryHints).append(" "); + } + b.append(table).append('.') + .append(idfield).append(" FROM ") + .append(table); + + if (rel.additionalTables != null) { + b.append(',').append(rel.additionalTables); + } + } + String updateCreteria = retval.getUpdateCreteria(); + if (home.getSubnodeRelation() != null) { + if (updateCreteria != null) { + b.append (" WHERE "); + b.append (retval.getUpdateCreteria()); + b.append (" AND "); + b.append (home.getSubnodeRelation()); + } else { + b.append (" WHERE "); + b.append (home.getSubnodeRelation()); + } + } else { + if (updateCreteria != null) { + b.append (" WHERE "); + b.append (updateCreteria); + b.append (rel.buildQuery(home, + home.getNonVirtualParent(), + null, + " AND ", + true)); + } else { + b.append (rel.buildQuery(home, + home.getNonVirtualParent(), + null, + " WHERE ", + true)); + } + q = b.toString(); + } + + if (logSql) { + app.logEvent("### updateSubnodeList: " + q); + } + + stmt = con.createStatement(); + + if (rel.maxSize > 0) { + stmt.setMaxRows(rel.maxSize); + } + + ResultSet result = stmt.executeQuery(q); + + // problem: how do we derive a SyntheticKey from a not-yet-persistent Node? + Key k = (rel.groupby != null) ? home.getKey() : null; + int cntr = 0; + + DbColumn[] columns = rel.loadAggressively() ? rel.otherType.getColumns() : null; + List newNodes = new ArrayList(rel.maxSize); + while (result.next()) { + String kstr = result.getString(1); + + // jump over null values - this can happen especially when the selected + // column is a group-by column. + if (kstr == null) { + continue; + } + + // make the proper key for the object, either a generic DB key or a groupby key + Key key; + if (rel.loadAggressively()) { + Node node = createNode(rel.otherType, result, columns, 0); + if (node == null) { + continue; + } + key = node.getKey(); + } else { + key = (Key) new DbKey(rel.otherType, kstr); + } + newNodes.add(new NodeHandle(key)); + + // if these are groupby nodes, evict nullNode keys + if (rel.groupby != null) { + Node n = (Node) cache.get(key); + + if ((n != null) && n.isNullNode()) { + evictKey(key); + } + } + } + HashMap map = new HashMap(); + map.put("newNodes", new Integer(newNodes.size())); + map.put("addedNodes", new Integer(retval.sortIn(newNodes))); + return map; + } finally { + // tx.timer.endEvent ("getNodeIDs "+home); + if (stmt != null) { + try { + stmt.close(); + } catch (Exception ignore) { + } + } + } } } Index: helma/objectmodel/db/Property.java =================================================================== RCS file: /opt/cvs/hop/helma/src/helma/objectmodel/db/Property.java,v retrieving revision 1.29 diff -u -r1.29 Property.java --- helma/objectmodel/db/Property.java 31 Jan 2005 15:45:00 -0000 1.29 +++ helma/objectmodel/db/Property.java 31 Jan 2005 16:27:37 -0000 @@ -10,8 +10,8 @@ * * $RCSfile: Property.java,v $ * $Author: hannes $ - * $Revision: 1.29 $ - * $Date: 2005/01/31 15:45:00 $ + * $Revision: 1.28 $ + * $Date: 2004/01/14 16:52:54 $ */ package helma.objectmodel.db; @@ -31,7 +31,7 @@ * A property implementation for Nodes stored inside a database. Basically * the same as for transient nodes, with a few hooks added. */ -public final class Property implements IProperty, Serializable, Cloneable { +public final class Property implements IProperty, Serializable, Cloneable, Comparable { static final long serialVersionUID = -1022221688349192379L; private String propname; private Node node; @@ -482,5 +482,74 @@ } return null; + } + + /** + * @see java.lang.Comparable#compareTo(java.lang.Object) + * FIXME: throw a ClassCastException instead? + * The following cases throw a RuntimeException + * - Properties of a different type + * - Properties of boolean type + */ + public int compareTo(Object obj) { + Property p = (Property) obj; + if (p.getType() != type) + throw new ClassCastException ("uncomparable values " + this + " : " + p); + switch (p.getType()) { + case Property.STRING: + return (this.getStringValue().compareTo(p.getStringValue())); + case Property.INTEGER: + long l = this.getIntegerValue() - p.getIntegerValue(); + // don't know what happens if the result of the subtraction + // has a higher value than the value which may be stored inside + // of an integer. + if (l < 0) + return -1; + if (l > 0) + return 1; + return 0; + case Property.DATE: + return this.getDateValue().compareTo(p.getDateValue()); + case Property.FLOAT: + double d = this.getFloatValue() - p.getFloatValue(); + if (d < 0) + return -1; + if (d > 0) + return 1; + return 0; + case Property.BOOLEAN: + throw new RuntimeException ("unable to compare boolean " + this + " : " + p); + case Property.NODE: + throw new RuntimeException ("unable to compare nodes " + this + " : " + p); + case Property.JAVAOBJECT: + } + throw new RuntimeException ("uncomparable values " + this + " : " + p); + } + + public boolean equals(Object o) { + if (o == this) + return true; + if (o == null) + return false; + if (!(o instanceof Property)) + return false; + Property p = (Property) o; + switch (p.getType()) { + case Property.STRING: + return (this.getStringValue().equals(p.getStringValue())); + case Property.INTEGER: + return this.getIntegerValue() == p.getIntegerValue(); + case Property.DATE: + return this.getDateValue().equals(p.getDateValue()); + case Property.FLOAT: + return this.getFloatValue() == p.getFloatValue(); + case Property.BOOLEAN: + return this.getBooleanValue() == p.getBooleanValue(); + case Property.NODE: + return this.node.equals(p.node); + case Property.JAVAOBJECT: + return this.value.equals(p.value); + } + return false; } } Index: helma/objectmodel/db/Relation.java =================================================================== RCS file: /opt/cvs/hop/helma/src/helma/objectmodel/db/Relation.java,v retrieving revision 1.49 diff -u -r1.49 Relation.java --- helma/objectmodel/db/Relation.java 19 Mar 2004 17:02:18 -0000 1.49 +++ helma/objectmodel/db/Relation.java 31 Jan 2005 16:27:37 -0000 @@ -85,6 +85,7 @@ boolean aggressiveCaching; boolean isPrivate = false; boolean referencesPrimaryKey = false; + String updateCreteria; String accessName; // db column used to access objects through this relation String order; String groupbyOrder; @@ -123,6 +124,7 @@ this.logicalOperator = rel.logicalOperator; this.aggressiveLoading = rel.aggressiveLoading; this.aggressiveCaching = rel.aggressiveCaching; + this.updateCreteria = rel.updateCreteria; } /** @@ -263,6 +265,9 @@ if ((order != null) && (order.trim().length() == 0)) { order = null; } + + // mark this collection as updateable if defined + updateCreteria = props.getProperty(propName + ".updatecreteria"); // get additional filter property filter = props.getProperty(propName + ".filter"); Index: helma/objectmodel/db/WrappedNodeManager.java =================================================================== RCS file: /opt/cvs/hop/helma/src/helma/objectmodel/db/WrappedNodeManager.java,v retrieving revision 1.18 diff -u -r1.18 WrappedNodeManager.java --- helma/objectmodel/db/WrappedNodeManager.java 9 Nov 2004 22:30:26 -0000 1.18 +++ helma/objectmodel/db/WrappedNodeManager.java 31 Jan 2005 16:27:37 -0000 @@ -19,6 +19,7 @@ import helma.objectmodel.ObjectNotFoundException; import java.util.List; +import java.util.Map; import java.util.Vector; /** @@ -130,6 +131,21 @@ public List getNodeIDs(Node home, Relation rel) { try { return nmgr.getNodeIDs(home, rel); + } catch (Exception x) { + if (nmgr.app.debug()) { + x.printStackTrace(); + } + + throw new RuntimeException("Error retrieving NodeIDs: " + x); + } + } + + /** + * @see helma.objectmodel.db.NodeManager#updateSubnodeList(Node, Relation) + */ + public Map updateSubnodeList (Node home, Relation rel) { + try { + return nmgr.updateSubnodeList(home, rel); } catch (Exception x) { if (nmgr.app.debug()) { x.printStackTrace(); Index: helma/scripting/rhino/HopObject.java =================================================================== RCS file: /opt/cvs/hop/helma/src/helma/scripting/rhino/HopObject.java,v retrieving revision 1.52 diff -u -r1.52 HopObject.java --- helma/scripting/rhino/HopObject.java 14 Jan 2005 13:46:53 -0000 1.52 +++ helma/scripting/rhino/HopObject.java 31 Jan 2005 16:27:37 -0000 @@ -1007,4 +1007,18 @@ public void clearChangeSet() { changedProperties = null; } + + /** + * This method represents the Java-Script-exposed function for updating Subnode-Collections. + * The following conditions must be met to make a subnodecollection updateable. + * .) the collection must be specified with collection.updateable=true + * .) the id's of this collection must be in ascending order, meaning, that new records + * do have a higher id than the last record loaded by this collection + */ + public Object jsFunction_updateSubnodes() { + if (!(node instanceof helma.objectmodel.db.Node)) + throw new RuntimeException ("updateSubnodes only callabel on persistent HopObjects"); + helma.objectmodel.db.Node n = (helma.objectmodel.db.Node) node; + return n.updateSubnodes(); + } } Index: src/helma/objectmodel/db/UpdateableSubnodeList.java =================================================================== RCS file: src/helma/objectmodel/db/UpdateableSubnodeList.java diff -N src/helma/objectmodel/db/UpdateableSubnodeList.java --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ src/helma/objectmodel/db/UpdateableSubnodeList.java 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,538 @@ +/* + * Helma License Notice + * + * The contents of this file are subject to the Helma License + * Version 2.0 (the "License"). You may not use this file except in + * compliance with the License. A copy of the License is available at + * http://adele.helma.org/download/helma/license.txt + * + * Copyright 1998-2003 Helma Software. All Rights Reserved. + * + * $RCSfile: UpdateableSubnodeList.java,v $ + * $Author: manfred andres $ + * $Revision: 0.1 $ + * $Date: 2005/01/20 10:26:32 $ + */ +package helma.objectmodel.db; + +import java.sql.SQLException; +import java.sql.Timestamp; +import java.sql.Types; +import java.util.Collection; +import java.util.Iterator; + +public class UpdateableSubnodeList extends ExternalizableVector { + // the update-creteria-fields + private final String updateCreteria[]; + // the corresponding property to this update-creteria + private final String updateProperty[]; + // records to fetch from the db will have a lower value? + private final boolean updateTypeDesc[]; + + // arrays representing the current borders for each update-creteria + private Object highestValues[]=null; + private Object lowestValues[]=null; + + // an array containing the order-fields + private final String orderProperties[]; + // an array containing the direction for ordering + private final boolean orderIsDesc[]; + + private final Relation rel; + + /** + * Construct a new UpdateableSubnodeList. The Relation is needed + * to get the information about the ORDERING and the UPDATECRETERIAS + */ + public UpdateableSubnodeList (Relation rel) { + this.rel = rel; + // check the order of this collection for automatically sorting + // in the values in the correct order + if (rel.order == null) { + orderProperties=null; + orderIsDesc=null; + } else { + String singleOrders[] = rel.order.split(","); + orderProperties = new String[singleOrders.length]; + DbMapping dbm = rel.otherType; + // orderFields = new String[singleOrders.length]; + orderIsDesc = new boolean[singleOrders.length]; + for (int i = 0; i < singleOrders.length; i++) { + String currOrder[] = singleOrders[i].trim().split(" "); + if (currOrder[0].equalsIgnoreCase(rel.otherType.getIDField())) { + orderProperties[i]=null; + } else { + orderProperties[i] = dbm.columnNameToProperty(currOrder[0]); + } + if (currOrder.length < 2 + || "ASC".equalsIgnoreCase(currOrder[1])) + orderIsDesc[i]=false; + else + orderIsDesc[i]=true; + } + } + + // check the updtae-creterias for updating this collection + if (rel.updateCreteria == null) { + // creteria-field muss vom creteria-operant getrennt werden + // damit das update-creteria-rendering gut funktioniert + updateCreteria = new String[1]; + updateCreteria[0]=rel.otherType.getIDField(); + updateProperty=null; + updateTypeDesc = new boolean[1]; + updateTypeDesc[0] = false; + highestValues = new Object[1]; + lowestValues = new Object[1]; + } else { + String singleCreterias[] = rel.updateCreteria.split(","); + updateCreteria = new String[singleCreterias.length]; + updateProperty = new String[singleCreterias.length]; + updateTypeDesc = new boolean[singleCreterias.length]; + highestValues = new Object[singleCreterias.length]; + lowestValues = new Object[singleCreterias.length]; + for (int i = 0; i < singleCreterias.length; i++) + parseCreteria (i, singleCreterias[i]); + } + } + + /** + * Utility-method for parsing creterias for updating this collection. + */ + private void parseCreteria (int idx, String creteria) { + String creteriaParts[] = creteria.trim().split(" "); + updateCreteria[idx]=creteriaParts[0].trim(); + updateProperty[idx] = rel.otherType.columnNameToProperty(updateCreteria[idx]); + if (updateProperty[idx] == null + && !updateCreteria[idx].equalsIgnoreCase(rel.otherType.getIDField())) { + throw new RuntimeException("updateCreteria has to be mapped as Property of this Prototype (" + updateCreteria[idx] + ")"); + } + if (creteriaParts.length < 2) { + // default to INCREASING or to BIDIRECTIONAL? + updateTypeDesc[idx]=false; + return; + } + String direction = creteriaParts[1].trim().toLowerCase(); + if ("desc".equals(direction)) { + updateTypeDesc[idx]=true; + } else { + updateTypeDesc[idx]=false; + } + } + + /** + * render the creterias for fetching new nodes, which have been added to the + * relational db. + * @return the sql-creteria for updating this subnodelist + * @throws ClassNotFoundException @see helma.objectmodel.db.DbMapping#getColumn(String) + * @throws SQLException @see helma.objectmodel.db.DbMapping#getColumn @see helma.objectmodel.db.DbMapping#needsQuotes(String) + */ + public String getUpdateCreteria() throws SQLException, ClassNotFoundException { + StringBuffer creteria = new StringBuffer (); + for (int i = 0; i < updateCreteria.length; i++) { + // if we don't know the borders ignore this creteria + if (!updateTypeDesc[i] && highestValues[i]==null) + continue; + if (updateTypeDesc[i] && lowestValues[i]==null) + continue; + if (creteria.length() > 0) { + creteria.append (" OR "); + } + renderUpdateCreteria(i, creteria); + } + if (creteria.length() < 1) + return null; + creteria.insert(0, "("); + creteria.append(")"); + return creteria.toString(); + } + + /** + * Render the current updatecreteria specified by idx and add the result to the given + * StringBuffer (sb). + * @param idx index of the current updatecreteria + * @param sb the StringBuffer to append to + * @throws ClassNotFoundException @see helma.objectmodel.db.DbMapping#getColumn(String) + * @throws SQLException @see helma.objectmodel.db.DbMapping#getColumn @see helma.objectmodel.db.DbMapping#needsQuotes(String) + */ + private void renderUpdateCreteria(int idx, StringBuffer sb) throws SQLException, ClassNotFoundException { + if (!updateTypeDesc[idx]) { + sb.append(updateCreteria[idx]); + sb.append(" > "); + renderValue(idx, highestValues, sb); + } else { + sb.append(updateCreteria[idx]); + sb.append(" < "); + renderValue(idx, lowestValues, sb); + } + } + + /** + * Renders the value contained inside the given Array (values) at the given + * index (idx) depending on it's SQL-Type and add the result to the given + * StringBuffer (sb). + * @param idx index-position of the value to render + * @param values the values-array to operate on + * @param sb the StringBuffer to append to + * @throws ClassNotFoundException @see helma.objectmodel.db.DbMapping#getColumn(String) + * @throws SQLException @see helma.objectmodel.db.DbMapping#getColumn @see helma.objectmodel.db.DbMapping#needsQuotes(String) + */ + private void renderValue(int idx, Object[] values, StringBuffer sb) throws SQLException, ClassNotFoundException { + DbColumn dbc = rel.otherType.getColumn(updateCreteria[idx]); + if (rel.otherType.getIDField().equalsIgnoreCase(updateCreteria[idx])) { + if (rel.otherType.needsQuotes(updateCreteria[idx])) { + sb.append ("'").append (values[idx]).append("'"); + } else { + sb.append (values[idx]); + } + return; + } + Property p = (Property) values[idx]; + String strgVal = p.getStringValue(); + + switch (dbc.getType()) { + case Types.DATE: + case Types.TIME: + case Types.TIMESTAMP: + // use SQL Escape Sequences for JDBC Timestamps + // (http://www.oreilly.com/catalog/jentnut2/chapter/ch02.html) + Timestamp ts = p.getTimestampValue(); + sb.append ("{ts '"); + sb.append (ts.toString()); + sb.append ("'}"); + return; + + case Types.BIT: + case Types.TINYINT: + case Types.BIGINT: + case Types.SMALLINT: + case Types.INTEGER: + case Types.REAL: + case Types.FLOAT: + case Types.DOUBLE: + case Types.DECIMAL: + case Types.NUMERIC: + case Types.VARBINARY: + case Types.BINARY: + case Types.LONGVARBINARY: + case Types.LONGVARCHAR: + case Types.CHAR: + case Types.VARCHAR: + case Types.OTHER: + case Types.NULL: + case Types.CLOB: + default: + if (rel.otherType.needsQuotes(updateCreteria[idx])) { + sb.append ("'").append (strgVal).append ("'"); + } else { + sb.append (strgVal); + } + } + } + + /** + * add a new node honoring the Nodes SQL-Order and check if the borders for + * the updateCreterias have changed by adding this node + * @param obj the object to add + */ + public boolean add(Object obj) { + // we do not have a SQL-Order and add this node on top of the list + NodeHandle nh = (NodeHandle) obj; + if (this.orderProperties==null) { + while (rel.maxSize>0 && this.size() >= rel.maxSize) + super.remove(this.size()-1); + super.add(0, nh); + updateBorders(nh); + return true; + } + + // sort in the node depending on this collection's SQL-order + for (int i = 0; i < this.size() && (i < rel.maxSize || rel.maxSize <= 0); i++) { + NodeHandle curr = (NodeHandle) this.get(i); + if (compareNodes(nh, curr) < 0) { + super.add(i, nh); + updateBorders(nh); + return true; + } + } + if (this.size() < rel.maxSize || rel.maxSize <= 0) { + super.add(nh); + updateBorders(nh); + return true; + } + return false; + } + + /** + * add a new node at the given index position and check if the + * borders for the updateCreterias have changed + * NOTE: this overrules the ordering (should we disallowe this?) + * @param idx the index-position this node should be added at + * @param obj the NodeHandle of the node which should be added + */ + public void add (int idx, Object obj) { + NodeHandle nh = (NodeHandle) obj; + super.add(idx, nh); + updateBorders(nh); + } + + /** + * Add all nodes contained inside the specified Collection to this + * UpdateableSubnodeList. The order of the added Nodes is asumed to + * be ordered according to the SQL-Order-Clausel given for this Subnodecollection + * @param Collection the collection containing all elements to add in the order returned by the select-statement + */ + public int sortIn (Collection col) { + // there is no order specified, add on top + int cntr=0; + if (orderProperties==null) { + for (Iterator i = col.iterator(); i.hasNext(); ) { + add(cntr, i.next()); + cntr++; + } + if (rel.maxSize > 0) { + int diff = this.size() - rel.maxSize; + if (diff > 0) + this.removeRange(this.size()-1-diff, this.size()-1); + } + return cntr; + } + // sort in the elements contained inside the specified collection + int localIdx=0; // the current position inside current nodelist + for (Iterator i = col.iterator(); i.hasNext(); ) { + NodeHandle currAdded = (NodeHandle) i.next(); + boolean sortedDownwards=false; + while ((localIdx < rel.maxSize || rel.maxSize<=0) + && localIdx < this.size() + && compareNodes((NodeHandle) this.get(localIdx), currAdded) <= 0) { + localIdx++; + sortedDownwards=true; + } + while (!sortedDownwards && localIdx > 0 + && compareNodes((NodeHandle) this.get(localIdx-1), currAdded) > 0) + localIdx--; + // collection's size-limit has been reached (ignore this node) + if (localIdx >= rel.maxSize && rel.maxSize > 0) + continue; + // add the currentAdded-NodeHandle to this collection at the current localIdx + super.add(localIdx, currAdded); + updateBorders(currAdded); + cntr++; + } + return cntr; + } + + /** + * Check if the currently added node changes the borers for the updateCreterias and + * update them if neccessary. + * @param the added Node + */ + private void updateBorders(NodeHandle nh) { + Node node=null; + for (int i = 0; i < updateCreteria.length; i++) { + node=updateBorder(i, nh, node); + } + } + + /** + * Check if the given NodeHandle's node is outside of the creteria's border + * having the given index-position (idx) and update the border if neccessary. + * @param idx the index-position + * @param nh the NodeHandle possible changing the border + * @param node optional the node handled by this NodeHandler + * @return The Node given or the Node retrieved of this NodeHandle + */ + private Node updateBorder(int idx, NodeHandle nh, Node node) { + String cret = updateCreteria[idx]; + if (rel.otherType.getIDField().equals(cret)) { + String nid = nh.getID(); + if (updateTypeDesc[idx] + && compareNumericString(nid, (String) lowestValues[idx]) < 0) { + lowestValues[idx]=nid; + } else if (!updateTypeDesc[idx] + && compareNumericString(nid, (String) highestValues[idx]) > 0) { + highestValues[idx]=nid; + } + } else { + if (node == null) + node = nh.getNode(rel.otherType.getWrappedNodeManager()); + Property np = node.getProperty( + rel.otherType.columnNameToProperty(cret)); + if (updateTypeDesc[idx]) { + Property lp = (Property) lowestValues[idx]; + if (lp==null || np.compareTo(lp)<0) + lowestValues[idx]=np; + } else { + Property hp = (Property) highestValues[idx]; + if (hp==null || np.compareTo(hp)>0) + highestValues[idx]=np; + } + } + return node; + } + + /** + * First check which borders this node will change and rebuild + * these if neccessary. + * @param nh the NodeHandle of the removed Node + */ + private void rebuildBorders(NodeHandle nh) { + boolean[] check = new boolean[updateCreteria.length]; + Node node = nh.getNode(rel.otherType.getWrappedNodeManager()); + for (int i = 0; i < updateCreteria.length; i++) { + String cret = updateCreteria[i]; + if (cret.equals(rel.otherType.getIDField())) { + check[i] = (updateTypeDesc[i] + && nh.getID().equals(lowestValues[i])) + || (!updateTypeDesc[i] + && nh.getID().equals(highestValues[i])); + } else { + Property p = node.getProperty(updateProperty[i]); + check[i] = (updateTypeDesc[i] + && p.equals(lowestValues[i])) + || (!updateTypeDesc[i] + && p.equals(highestValues[i])); + } + } + for (int i = 0; i < updateCreteria.length; i++) { + if (!check[i]) + continue; + rebuildBorder(i); + } + } + + /** + * Rebuild all borders for all the updateCreterias + */ + public void rebuildBorders() { + for (int i = 0; i < updateCreteria.length; i++) { + rebuildBorder(i); + } + } + + /** + * Only rebuild the border for the update-creteria specified by the given + * index-position (idx). + * @param idx the index-position of the updateCreteria to rebuild the border for + */ + private void rebuildBorder(int idx) { + if (updateTypeDesc[idx]) { + lowestValues[idx]=null; + } else { + highestValues[idx]=null; + } + for (int i = 0; i < this.size(); i++) { + updateBorder(idx, (NodeHandle) this.get(i), null); + } + } + + /** + * remove the object specified by the given index-position + * and update the borders if neccesary + * @param idx the index-position of the NodeHandle to remove + */ + public Object remove (int idx) { + Object obj = super.remove(idx); + if (obj == null) + return null; + rebuildBorders((NodeHandle) obj); + return obj; + } + + /** + * remove the given Object from this List and update the borders if neccesary + * @param obj the NodeHandle to remove + */ + public boolean remove (Object obj) { + if (!super.remove(obj)) + return false; + rebuildBorders((NodeHandle) obj); + return true; + } + + /** + * remove all elements conteined inside the specified collection + * from this List and update the borders if neccesary + * @param c the Collection containing all Objects to remove from this List + * @return true if the List has been modified + */ + public boolean removeAll(Collection c) { + if (!super.removeAll(c)) + return false; + for (Iterator i = c.iterator(); i.hasNext(); ) + rebuildBorders((NodeHandle) i.next()); + return true; + } + + /** + * remove all elements from this List, which are NOT specified + * inside the specified Collecion and update the borders if neccesary + * @param c the Collection containing all Objects to keep on the List + * @return true if the List has been modified + */ + public boolean retainAll (Collection c) { + if (!super.retainAll(c)) + return false; + rebuildBorders(); + return true; + } + + /** + * Compare two nodes depending on the specified ORDER for this collection. + * Returns + * @param nh1 the first NodeHandle + * @parem nh2 the second NodeHandle + * @return an integer lesser than zero if nh1 is less than, zero if nh1 is equal to and a value greater than zero if nh1 is bigger than nh2. + */ + private int compareNodes(NodeHandle nh1, NodeHandle nh2) { + WrappedNodeManager wnmgr = rel.otherType.getWrappedNodeManager(); + for (int i = 0; i < orderProperties.length; i++) { + if (orderProperties[i]==null) { + // we have the id as order-creteria-> avoid loading node + // and compare numerically instead of lexicographically + String s1 = nh1.getID(); + String s2 = nh2.getID(); + int j = compareNumericString (s1, s2); + if (j==0) + continue; + if (orderIsDesc[i]) + j=j*-1; + return j; + } + Property p1 = nh1.getNode(wnmgr).getProperty(orderProperties[i]); + Property p2 = nh2.getNode(wnmgr).getProperty(orderProperties[i]); + int j = p1.compareTo(p2); + if (j == 0) + continue; + if (orderIsDesc[i]) + j = j * -1; + return j; + } + return 0; + } + + /** + * Compare two strings containing numbers depending on their numeric values + * instead of an lexicographical order without parsing the number. + * @param a the first String + * @param b the second String + */ + private int compareNumericString(String a, String b) { + if (a == null && b != null) + return -1; + if (a != null && b == null) + return 1; + if (a == null && b == null) + return 0; + if (a.length() < b.length()) + return -1; + if (a.length() > b.length()) + return 1; + for (int i = 0; i < a.length(); i++) { + if (a.charAt(i) < b.charAt(i)) + return -1; + if (a.charAt(i) > b.charAt(i)) + return 1; + } + return 0; + } +}