diff --git a/src/helma/objectmodel/db/ExternalizableVector.java b/src/helma/objectmodel/db/ExternalizableVector.java deleted file mode 100644 index 45ace410..00000000 --- a/src/helma/objectmodel/db/ExternalizableVector.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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$ - * $Author$ - * $Revision$ - * $Date$ - */ - -package helma.objectmodel.db; - -import java.io.Externalizable; -import java.io.IOException; -import java.io.ObjectInput; -import java.io.ObjectOutput; -import java.util.ArrayList; - -/** - * A subclass of Vector that implements the Externalizable interface in order - * to be able to control how it is serialized and deserialized. - */ -public class ExternalizableVector extends ArrayList implements Externalizable { - static final long serialVersionUID = 2316243615310540423L; - - /** - * Reads an externalized representation of the list from a stream. - * - * @param in the input stream to read from - * - * @throws IOException ... - */ - public synchronized void readExternal(ObjectInput in) - throws IOException { - try { - int size = in.readInt(); - - for (int i = 0; i < size; i++) - add(in.readObject()); - } catch (ClassNotFoundException x) { - throw new IOException(x.toString()); - } - } - - /** - * Writes an externalized representation of the list to the stream. - * - * @param out the output stream to write to - * - * @throws IOException ... - */ - public synchronized void writeExternal(ObjectOutput out) - throws IOException { - int size = size(); - - out.writeInt(size); - - for (int i = 0; i < size; i++) - out.writeObject(get(i)); - } -} diff --git a/src/helma/objectmodel/db/Node.java b/src/helma/objectmodel/db/Node.java index 5ec7c878..0d1e0ae4 100644 --- a/src/helma/objectmodel/db/Node.java +++ b/src/helma/objectmodel/db/Node.java @@ -41,7 +41,7 @@ public final class Node implements INode, Serializable { protected NodeHandle parentHandle; // Ordered list of subnodes of this node - private List subnodes; + private SubnodeList subnodes; // Named subnodes (properties) of this node private Hashtable propMap; @@ -201,7 +201,7 @@ public final class Node implements INode, Serializable { created = in.readLong(); lastmodified = in.readLong(); - subnodes = (ExternalizableVector) in.readObject(); + subnodes = (SubnodeList) in.readObject(); // left-over from links vector in.readObject(); propMap = (Hashtable) in.readObject(); @@ -250,7 +250,7 @@ public final class Node implements INode, Serializable { /** * used by Xml deserialization */ - public synchronized void setSubnodes(List subnodes) { + public synchronized void setSubnodes(SubnodeList subnodes) { this.subnodes = subnodes; } @@ -586,7 +586,7 @@ public final class Node implements INode, Serializable { /** * - * + * * @param nmgr */ public void setWrappedNodeManager(WrappedNodeManager nmgr) { @@ -845,12 +845,13 @@ public final class Node implements INode, Serializable { } /** + * 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; @@ -932,7 +933,7 @@ public final class Node implements INode, Serializable { } else { // create subnode list if necessary if (subnodes == null) { - subnodes = new ExternalizableVector(); + subnodes = createSubnodeList(); } // check if subnode accessname is set. If so, check if another node @@ -1097,7 +1098,7 @@ public final class Node implements INode, Serializable { if (state != TRANSIENT && rel.otherType != null && rel.otherType.isRelational()) { return nmgr.getNode(this, name, rel); } else { - // Do what we have to do: loop through subnodes and + // Do what we have to do: loop through subnodes and // check if any one matches String propname = rel.groupby != null ? "groupname" : rel.accessName; INode node = null; @@ -1243,7 +1244,7 @@ public final class Node implements INode, Serializable { loadNodes(); if (subnodes == null) { - subnodes = new ExternalizableVector(); + subnodes = new SubnodeList(); } if (create || subnodes.contains(new NodeHandle(new SyntheticKey(getKey(), sid)))) { @@ -1537,7 +1538,7 @@ public final class Node implements INode, Serializable { * Depending on the subnode.loadmode specified in the type.properties, we'll load just the * ID index or the actual nodes. */ - protected void loadNodes() { + public void 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; @@ -1556,11 +1557,15 @@ public final class Node implements INode, Serializable { // 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()); + if ((lastChange >= lastSubnodeFetch && !subRel.autoSorted) || + (subnodes == null)) { + if (subRel.updateCriteria!=null) { + // updateSubnodeList is setting the subnodes directly returning an integer + nmgr.updateSubnodeList(this, subRel); + } else if (subRel.aggressiveLoading) { + subnodes = nmgr.getNodes(this, subRel); } else { - subnodes = nmgr.getNodeIDs(this, dbmap.getSubnodeRelation()); + subnodes = nmgr.getNodeIDs(this, subRel); } lastSubnodeFetch = System.currentTimeMillis(); @@ -1569,6 +1574,23 @@ public final class Node implements INode, Serializable { } } + /** + * 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 SubnodeList createSubnodeList() { + Relation rel = this.dbmap == null ? null : this.dbmap.getSubnodeRelation(); + if (rel != null && rel.updateCriteria != null) { + subnodes = new UpdateableSubnodeList(rel); + } else if (rel != null && rel.autoSorted) { + subnodes = new OrderedSubnodeList(rel); + } else { + subnodes = new SubnodeList(); + } + return subnodes; + } + /** * * @@ -1609,6 +1631,10 @@ public final class Node implements INode, Serializable { keys[i] = ((NodeHandle) subnodes.get(i + startIndex)).getKey(); } + prefetchChildren (keys); + } + + public void prefetchChildren (Key[] keys) throws Exception { nmgr.nmgr.prefetchNodes(this, dbmap.getSubnodeRelation(), keys); } @@ -2412,7 +2438,7 @@ public final class Node implements INode, Serializable { if (relational) { p.setStringValue(null); - notifyPropertyChange(propname); + notifyPropertyChange(propname); } lastmodified = System.currentTimeMillis(); @@ -2627,4 +2653,28 @@ public final class Node implements INode, Serializable { System.err.println("properties: " + propMap); } + /** + * 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 int updateSubnodes () { + // 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 diff --git a/src/helma/objectmodel/db/NodeManager.java b/src/helma/objectmodel/db/NodeManager.java index 0ee31b4f..c35030a2 100644 --- a/src/helma/objectmodel/db/NodeManager.java +++ b/src/helma/objectmodel/db/NodeManager.java @@ -226,7 +226,7 @@ public final class NodeManager { Transactor tx = (Transactor) Thread.currentThread(); - Key key = null; + Key key; // check what kind of object we're looking for and make an apropriate key if (rel.isComplexReference()) { @@ -864,7 +864,7 @@ public final class NodeManager { * Loades subnodes via subnode relation. Only the ID index is loaded, the nodes are * loaded later on demand. */ - public List getNodeIDs(Node home, Relation rel) throws Exception { + public SubnodeList getNodeIDs(Node home, Relation rel) throws Exception { // Transactor tx = (Transactor) Thread.currentThread (); // tx.timer.beginEvent ("getNodeIDs "+home); @@ -873,7 +873,7 @@ public final class NodeManager { throw new RuntimeException("NodeMgr.getNodeIDs called for non-relational node " + home); } else { - List retval = new ArrayList(); + SubnodeList retval = home.createSubnodeList(); // if we do a groupby query (creating an intermediate layer of groupby nodes), // retrieve the value of that field instead of the primary key @@ -890,7 +890,6 @@ public final class NodeManager { String query = null; try { - StringBuffer b = new StringBuffer("SELECT "); if (rel.queryHints != null) { @@ -941,7 +940,7 @@ public final class NodeManager { ? (Key) new DbKey(rel.otherType, kstr) : (Key) new SyntheticKey(k, kstr); - retval.add(new NodeHandle(key)); + retval.addSorted(new NodeHandle(key)); // if these are groupby nodes, evict nullNode keys if (rel.groupby != null) { @@ -975,7 +974,7 @@ public final class NodeManager { * actually loades all nodes in one go, which is better for small node collections. * This method is used when xxx.loadmode=aggressive is specified. */ - public List getNodes(Node home, Relation rel) throws Exception { + public SubnodeList getNodes(Node home, Relation rel) throws Exception { // This does not apply for groupby nodes - use getNodeIDs instead if (rel.groupby != null) { return getNodeIDs(home, rel); @@ -988,7 +987,7 @@ public final class NodeManager { throw new RuntimeException("NodeMgr.getNodes called for non-relational node " + home); } else { - List retval = new ArrayList(); + SubnodeList retval = home.createSubnodeList(); DbMapping dbm = rel.otherType; Connection con = dbm.getConnection(); @@ -1031,7 +1030,7 @@ public final class NodeManager { } Key primKey = node.getKey(); - retval.add(new NodeHandle(primKey)); + retval.addSorted(new NodeHandle(primKey)); // do we need to synchronize on primKey here? synchronized (cache) { @@ -1062,6 +1061,159 @@ public final class NodeManager { return retval; } } + + /** + * Update a UpdateableSubnodeList retrieving all values having + * higher Values according to the updateCriteria'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 int updateSubnodeList(Node home, Relation rel) throws Exception { + 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 list = home.getSubnodeList(); + if (list == null) + list = home.createSubnodeList(); + + if (!(list instanceof UpdateableSubnodeList)) + throw new RuntimeException ("unable to update SubnodeList not marked as updateable (" + rel.propName + ")"); + + UpdateableSubnodeList sublist = (UpdateableSubnodeList) list; + + // 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); + + rel.appendAdditionalTables(b); + } + String updateCriteria = sublist.getUpdateCriteria(); + if (home.getSubnodeRelation() != null) { + if (updateCriteria != null) { + b.append (" WHERE "); + b.append (sublist.getUpdateCriteria()); + b.append (" AND "); + b.append (home.getSubnodeRelation()); + } else { + b.append (" WHERE "); + b.append (home.getSubnodeRelation()); + } + } else { + if (updateCriteria != null) { + b.append (" WHERE "); + b.append (updateCriteria); + b.append (rel.buildQuery(home, + home.getNonVirtualParent(), + null, + " AND ", + true)); + } else { + b.append (rel.buildQuery(home, + home.getNonVirtualParent(), + null, + " WHERE ", + true)); + } + q = b.toString(); + } + + long logTimeStart = logSql ? System.currentTimeMillis() : 0; + + stmt = con.createStatement(); + + if (rel.maxSize > 0) { + stmt.setMaxRows(rel.maxSize); + } + + ResultSet result = stmt.executeQuery(q); + + if (logSql) { + long logTimeStop = System.currentTimeMillis(); + logSqlStatement("SQL SELECT_UPDATE_SUBNODE_LIST", table, + logTimeStart, logTimeStop, 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 = 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); + } + } + } + // System.err.println("GOT NEW NODES: " + newNodes); + if (!newNodes.isEmpty()) + sublist.addAll(newNodes); + return newNodes.size(); + } finally { + if (stmt != null) { + try { + stmt.close(); + } catch (Exception ignore) { + } + } + } + } + } /** * @@ -1165,14 +1317,14 @@ public final class NodeManager { if (groupbyProp != null) { groupName = node.getString(groupbyProp); - List sn = (List) groupbySubnodes.get(groupName); + SubnodeList sn = (SubnodeList) groupbySubnodes.get(groupName); if (sn == null) { - sn = new ExternalizableVector(); + sn = new SubnodeList(); groupbySubnodes.put(groupName, sn); } - sn.add(new NodeHandle(primKey)); + sn.addSorted(new NodeHandle(primKey)); } // if relation doesn't use primary key as accessName, get accessName value @@ -1220,7 +1372,7 @@ public final class NodeManager { Node groupnode = home.getGroupbySubnode(groupname, true); - groupnode.setSubnodes((List) groupbySubnodes.get(groupname)); + groupnode.setSubnodes((SubnodeList) groupbySubnodes.get(groupname)); groupnode.lastSubnodeFetch = System.currentTimeMillis(); } } @@ -1696,7 +1848,7 @@ public final class NodeManager { Reader in = rs.getCharacterStream(i+1+offset); char[] buffer = new char[2048]; int read = 0; - int r = 0; + int r; while ((r = in.read(buffer, read, buffer.length - read)) > -1) { read += r; diff --git a/src/helma/objectmodel/db/OrderedSubnodeList.java b/src/helma/objectmodel/db/OrderedSubnodeList.java new file mode 100644 index 00000000..ff4702be --- /dev/null +++ b/src/helma/objectmodel/db/OrderedSubnodeList.java @@ -0,0 +1,471 @@ +/* + * 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$ + * $Author$ + * $Revision$ + * $Date$ + */ +package helma.objectmodel.db; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; + + +/** + * @author manfred andres + * This subnode-collection may be used to add nodes in an ordered way depending on + * the given order. It is also possible to retrieve views of this list in a different + * order. These views will be cached and automatically updated if this List's add- + * or remove-methods are called. + */ +public class OrderedSubnodeList extends SubnodeList { + HashMap views = null; + private final OrderedSubnodeList origin; + + // an array containing the order-fields + private final String orderProperties[]; + // an array containing the direction for ordering + private final boolean orderIsDesc[]; + + // the relation which is the basis for this collection + final Relation rel; + + /** + * Construct a new OrderedSubnodeList. The Relation is needed + * to get the information about the ORDERING + */ + public OrderedSubnodeList (Relation rel) { + this.rel = rel; + this.origin = null; + // 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]; + orderIsDesc = new boolean[singleOrders.length]; + DbMapping dbm = rel.otherType; + 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]); + } + System.err.println("ORDER PROP " + i + " IS " + orderProperties[i] + " FROM " +currOrder[0]); + if (currOrder.length < 2 + || "ASC".equalsIgnoreCase(currOrder[1])) + orderIsDesc[i]=false; + else + orderIsDesc[i]=true; + } + } + } + + /** + * This constructor is used to create a view for the OrderedSubnodeList origin. + * @param origin the origin-list which holds the elements + * @param expr the new order for this view + * @param rel the relation given for the origin-list + */ + public OrderedSubnodeList (OrderedSubnodeList origin, String expr, Relation rel) { + this.origin = origin; + this.rel = rel; + if (expr==null) { + orderProperties=null; + orderIsDesc=null; + } else { + String singleOrders[] = expr.split(","); + orderProperties = new String[singleOrders.length]; + orderIsDesc = new boolean[singleOrders.length]; + DbMapping dbm = rel.otherType; + for (int i = 0; i0 && this.size() >= rel.maxSize) + super.remove(0); + // escape sorting for presorted adds and grouped nodes + if (sorted || rel.groupby != null) { + super.add(obj); + } else { + sortIn(obj); + } + return true; + } + + /** + * add a new node honoring the Nodes SQL-Order + * @param obj the object to add + */ + public boolean sortIn(Object obj) { + // no order, just add + if (this.orderProperties==null) + return super.add(obj); + vAdd(obj); + long start = System.currentTimeMillis(); + try { + int idx = this.determineNodePosition((NodeHandle) obj, 0); + System.err.println("Position: " + idx); + if (idx<0) + return super.add(obj); + else + super.add(idx, obj); + return true; + } finally { + System.out.println("Sortmillis: " + (System.currentTimeMillis() - start)); + } + } + + private void vAdd (Object obj) { + if (views==null || origin!=null || views.size()<1) + return; + for (Iterator i = views.values().iterator(); i.hasNext(); ) { + OrderedSubnodeList osl = (OrderedSubnodeList) i.next(); + osl.sortIn(obj); + } + } + + public boolean addAll(Collection col) { + return sortIn(col, true) > 0; + } + + private void vAddAll (Collection col) { + if (views==null || origin!=null || views.size()<1) + return; + for (Iterator i = views.values().iterator(); i.hasNext(); ) { + OrderedSubnodeList osl = (OrderedSubnodeList) i.next(); + osl.addAll(col); + } + } + + public void add(int idx, Object obj) { + if (this.orderProperties!=null) + throw new RuntimeException ("Indexed add isn't alowed for ordered subnodes"); + super.add(idx, obj); + vAdd(obj); + } + + /** + * 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 but doesn't prevent adding of unordered Collections. + * Ordered Collections will be sorted in more efficient than unordered ones. + * @param col the collection containing all elements to add in the order returned by the select-statement + * @param colHasDefaultOrder true if the given collection does have the default-order defined by the relation + */ + public int sortIn (Collection col, boolean colHasDefaultOrder) { + vAddAll(col); + int cntr=0; + // there is no order specified, add on top + if (orderProperties==null) { + for (Iterator i = col.iterator(); i.hasNext(); ) { + super.add(cntr, i.next()); + cntr++; + } + if (rel.maxSize > 0) { + int diff = this.size() - rel.maxSize; + if (diff > 0) + super.removeRange(this.size()-1-diff, this.size()-1); + } + } else if (!colHasDefaultOrder || origin!=null) { + // this collection is a view or the given collection doesn't have the + // default order + for (Iterator i = col.iterator(); i.hasNext(); ) { + Object obj = i.next(); + if (obj==null) + continue; + sortIn (obj); + cntr++; + } + } else { + NodeHandle[] nhArr = (NodeHandle[]) col.toArray (new NodeHandle[0]); + int locIdx=determineNodePosition(nhArr[0], 0); // determine start-point + if (locIdx==-1) + locIdx=this.size(); + // int interval=Math.max(1, this.size()/2); + int addIdx=0; + for (; addIdx < nhArr.length; addIdx++) { + while (locIdx < this.size() && compareNodes(nhArr[addIdx], (NodeHandle) this.get(locIdx)) >= 0) + locIdx++; + if (locIdx >= this.size()) + break; + this.add(locIdx, nhArr[addIdx]); + cntr++; + } + for (; addIdx < nhArr.length; addIdx++) { + this.add(nhArr[addIdx]); + cntr++; + } + } + return cntr; + } + + /** + * remove the object specified by the given index-position + * @param idx the index-position of the NodeHandle to remove + */ + public Object remove (int idx) { + vRemove(idx); + return super.remove(idx); + } + + private void vRemove(int idx) { + if (views==null || origin!=null || views.size()<1) + return; + for (Iterator i = views.values().iterator(); i.hasNext(); ) { + OrderedSubnodeList osl = (OrderedSubnodeList) i.next(); + osl.remove(idx); + } + } + + /** + * remove the given Object from this List + * @param obj the NodeHandle to remove + */ + public boolean remove (Object obj) { + vRemove(obj); + return super.remove(obj); + } + + private void vRemove(Object obj) { + if (views==null || origin!=null || views.size()<1) + return; + for (Iterator i = views.values().iterator(); i.hasNext(); ) { + OrderedSubnodeList osl = (OrderedSubnodeList) i.next(); + osl.remove(obj); + } + } + + /** + * remove all elements conteined inside the specified collection + * from this List + * @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) { + vRemoveAll(c); + return super.removeAll(c); + } + + private void vRemoveAll(Collection c) { + if (views==null || origin!=null || views.size()<1) + return; + for (Iterator i = views.values().iterator(); i.hasNext(); ) { + OrderedSubnodeList osl = (OrderedSubnodeList) i.next(); + osl.removeAll(c); + } + } + + /** + * remove all elements from this List, which are NOT specified + * inside the specified Collecion + * @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) { + vRetainAll(c); + return super.retainAll(c); + } + + private void vRetainAll(Collection c) { + if (views==null || origin!=null || views.size()<1) + return; + for (Iterator i = views.values().iterator(); i.hasNext(); ) { + OrderedSubnodeList osl = (OrderedSubnodeList) i.next(); + osl.retainAll (c); + } + } + + private int determineNodePosition (NodeHandle nh, int startIdx) { + int size = this.size(); + int interval = Math.max(1, (size-startIdx)/2); + boolean dirUp=true; + int cntr = 0; + for (int i = 0; i < size + && (i < rel.maxSize || rel.maxSize <= 0) + && cntr<(size*2); cntr++) { // cntr is used to avoid endless-loops which shouldn't happen + NodeHandle curr = (NodeHandle) this.get(i); + int comp = compareNodes(nh, curr); + // current NodeHandle is below the given NodeHandle + // interval has to be 1 and + // idx must be zero or the node before the current node must be higher or equal + // all conditions must be met to determine the correct position of a node + if (comp < 0 && interval==1 && (i==0 || compareNodes(nh, (NodeHandle) this.get(i-1)) >= 0)) { + return i; + } else if (comp < 0) { + dirUp=false; + } else if (comp > 0) { + dirUp=true; + } else if (comp == 0) { + dirUp=true; + interval=1; + } + if (dirUp) { + i=i+interval; + if (i >= this.size()) { + if (compareNodes(nh, (NodeHandle) this.get(size-1)) >= 0) + break; + interval = Math.max(1, (i - size-1)/2); + i = this.size()-1; + } else { + interval = Math.max(1,interval/2); + } + } else { + i=i-interval; + if (i < 0) { // shouldn't happen i think + interval=Math.max(1,(interval+i)/2); + i=0; + } + } + + } + if (cntr >= size*2 && size>1) { + System.err.println("determineNodePosition needed more than the allowed iterations" + this.rel.prototype); + } + return -1; + } + + /** + * Compare two nodes depending on the specified ORDER for this collection. + * @param nh1 the first NodeHandle + * @param 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=null; + for (int i = 0; i < orderProperties.length; i++) { + if (orderProperties[i]==null) { + // we have the id as order-criteria-> 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; + } + System.err.println("CHECKING PROPERTY: " + orderProperties[i] + " / " + orderIsDesc[i]); + if (wnmgr == null) + wnmgr = rel.otherType.getWrappedNodeManager(); + Property p1 = nh1.getNode(wnmgr).getProperty(orderProperties[i]); + Property p2 = nh2.getNode(wnmgr).getProperty(orderProperties[i]); + System.out.println ("*** Comparing " + p1 + " - " + p2); + int j; + if (p1==null && p2==null) + continue; + else if (p1==null) + j = -1; + else + j = p1.compareTo(p2); + if (j == 0) + continue; + if (orderIsDesc[i]) + j = j * -1; + return j; + } + return -1; + } + + /** + * Compare two strings containing numbers depending on their numeric values + * instead of doing a lexicographical comparison. + * @param a the first String + * @param b the second String + */ + public static 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; + } + + public List getOrderedView (String order) { + if (origin != null) + return origin.getOrderedView(order); + String key = order.trim().toLowerCase(); + if (key.equalsIgnoreCase(rel.order)) + return this; + long start = System.currentTimeMillis(); + if (views == null) + views = new HashMap(); + OrderedSubnodeList osl = (OrderedSubnodeList) views.get(key); + if (osl == null) { + osl = new OrderedSubnodeList (this, order, rel); + views.put(key, osl); + System.out.println("getting view cost me " + (System.currentTimeMillis()-start) + " millis"); + } else + System.out.println("getting cached view cost me " + (System.currentTimeMillis()-start) + " millis"); + return osl; + } +} diff --git a/src/helma/objectmodel/db/Property.java b/src/helma/objectmodel/db/Property.java index b9f0efff..bd31282e 100644 --- a/src/helma/objectmodel/db/Property.java +++ b/src/helma/objectmodel/db/Property.java @@ -31,7 +31,7 @@ import java.util.Date; * 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; @@ -253,7 +253,12 @@ public final class Property implements IProperty, Serializable, Cloneable { */ public void setDateValue(Date date) { type = DATE; - value = date; + // normalize from java.sql.* Date subclasses + if (date != null && date.getClass() != Date.class) { + value = new Date(date.getTime()); + } else { + value = date; + } dirty = true; } @@ -483,4 +488,56 @@ public final class Property implements IProperty, Serializable, Cloneable { return null; } -} + + /** + * @see java.lang.Comparable#compareTo(java.lang.Object) + * + * The following cases throw a ClassCastException + * - Properties of a different type + * - Properties of boolean or node type + */ + public int compareTo(Object obj) { + Property p = (Property) obj; + int ptype = p.getType(); + Object pvalue = p.getValue(); + + if (type==NODE || ptype==NODE || + type == BOOLEAN || ptype == BOOLEAN) { + throw new ClassCastException("uncomparable values " + this + "(" + type + ") : " + p + "(" + ptype + ")"); + } + if (value==null && pvalue == null) { + return 0; + } else if (value == null) { + return -1; + } if (pvalue == null) { + return 1; + } + if (type != ptype) { + throw new ClassCastException("uncomparable values " + this + "(" + type + ") : " + p + "(" + ptype + ")"); + + } + if (!(value instanceof Comparable)) { + throw new ClassCastException("uncomparable value " + value + "(" + value.getClass() + ")"); + } + // System.err.println("COMPARING: " + value.getClass() + " TO " + pvalue.getClass()); + return ((Comparable) value).compareTo(pvalue); + } + + /** + * Return true if object o is equal to this property. + * + * @param obj the object to compare to + * @return true if this equals obj + * @see java.lang.Object#equals(java.lang.Object) + */ + public boolean equals(Object obj) { + if (obj == this) + return true; + if (obj == null) + return false; + if (!(obj instanceof Property)) + return false; + Property p = (Property) obj; + return value == null ? p.value == null : value.equals(p.value); + } +} \ No newline at end of file diff --git a/src/helma/objectmodel/db/Relation.java b/src/helma/objectmodel/db/Relation.java index 40f4686c..ad721185 100644 --- a/src/helma/objectmodel/db/Relation.java +++ b/src/helma/objectmodel/db/Relation.java @@ -87,8 +87,10 @@ public final class Relation { boolean aggressiveCaching; boolean isPrivate = false; boolean referencesPrimaryKey = false; + String updateCriteria; String accessName; // db column used to access objects through this relation String order; + boolean autoSorted = false; String groupbyOrder; String groupby; String prototype; @@ -127,6 +129,8 @@ public final class Relation { this.logicalOperator = rel.logicalOperator; this.aggressiveLoading = rel.aggressiveLoading; this.aggressiveCaching = rel.aggressiveCaching; + this.updateCriteria = rel.updateCriteria; + this.autoSorted = rel.autoSorted; } /** @@ -270,6 +274,12 @@ public final class Relation { order = null; } + // get the criteria(s) for updating this collection + updateCriteria = config.getProperty("updatecriteria"); + + // get the autosorting flag + autoSorted = "auto".equalsIgnoreCase(config.getProperty("sortmode")); + // get additional filter property filter = config.getProperty("filter"); diff --git a/src/helma/objectmodel/db/SubnodeList.java b/src/helma/objectmodel/db/SubnodeList.java new file mode 100644 index 00000000..12dbffcc --- /dev/null +++ b/src/helma/objectmodel/db/SubnodeList.java @@ -0,0 +1,36 @@ +/* + * 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$ + * $Author$ + * $Revision$ + * $Date$ + */ + +package helma.objectmodel.db; + +import java.util.ArrayList; + +/** + * A subclass of ArrayList that adds an addSorted(Object) method to + */ +public class SubnodeList extends ArrayList { + + /** + * Inserts the specified element at the specified position in this + * list without performing custom ordering + * + * @param obj element to be inserted. + */ + public boolean addSorted(Object obj) { + return add(obj); + } + +} diff --git a/src/helma/objectmodel/db/UpdateableSubnodeList.java b/src/helma/objectmodel/db/UpdateableSubnodeList.java new file mode 100644 index 00000000..b4fbd00c --- /dev/null +++ b/src/helma/objectmodel/db/UpdateableSubnodeList.java @@ -0,0 +1,399 @@ +/* + * 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$ + * $Author$ + * $Revision$ + * $Date$ + */ +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 OrderedSubnodeList { + + // the update-criteria-fields + private final String updateCriteria[]; + // the corresponding property to this update-criteria + 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-criteria + private Object highestValues[]=null; + private Object lowestValues[]=null; + + /** + * Construct a new UpdateableSubnodeList. The Relation is needed + * to get the information about the ORDERING and the UPDATECriteriaS + */ + public UpdateableSubnodeList (Relation rel) { + super(rel); + // check the update-criterias for updating this collection + if (rel.updateCriteria == null) { + // criteria-field muss vom criteria-operant getrennt werden + // damit das update-criteria-rendering gut funktioniert + updateCriteria = new String[1]; + updateCriteria[0]=rel.otherType.getIDField(); + updateProperty=null; + updateTypeDesc = new boolean[1]; + updateTypeDesc[0] = false; + highestValues = new Object[1]; + lowestValues = new Object[1]; + } else { + String singleCriterias[] = rel.updateCriteria.split(","); + updateCriteria = new String[singleCriterias.length]; + updateProperty = new String[singleCriterias.length]; + updateTypeDesc = new boolean[singleCriterias.length]; + highestValues = new Object[singleCriterias.length]; + lowestValues = new Object[singleCriterias.length]; + for (int i = 0; i < singleCriterias.length; i++) { + parseCriteria (i, singleCriterias[i]); + } + } + } + + /** + * Utility-method for parsing criterias for updating this collection. + */ + private void parseCriteria (int idx, String criteria) { + String criteriaParts[] = criteria.trim().split(" "); + updateCriteria[idx]=criteriaParts[0].trim(); + updateProperty[idx] = rel.otherType.columnNameToProperty(updateCriteria[idx]); + if (updateProperty[idx] == null + && !updateCriteria[idx].equalsIgnoreCase(rel.otherType.getIDField())) { + throw new RuntimeException("updateCriteria has to be mapped as Property of this Prototype (" + updateCriteria[idx] + ")"); + } + if (criteriaParts.length < 2) { + // default to INCREASING or to BIDIRECTIONAL? + updateTypeDesc[idx]=false; + return; + } + String direction = criteriaParts[1].trim().toLowerCase(); + if ("desc".equals(direction)) { + updateTypeDesc[idx]=true; + } else { + updateTypeDesc[idx]=false; + } + } + + /** + * render the criterias for fetching new nodes, which have been added to the + * relational db. + * @return the sql-criteria 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 getUpdateCriteria() throws SQLException, ClassNotFoundException { + StringBuffer criteria = new StringBuffer (); + for (int i = 0; i < updateCriteria.length; i++) { + // if we don't know the borders ignore this criteria + if (!updateTypeDesc[i] && highestValues[i]==null) + continue; + if (updateTypeDesc[i] && lowestValues[i]==null) + continue; + if (criteria.length() > 0) { + criteria.append (" OR "); + } + renderUpdateCriteria(i, criteria); + } + if (criteria.length() < 1) + return null; + criteria.insert(0, "("); + criteria.append(")"); + return criteria.toString(); + } + + /** + * Render the current updatecriteria specified by idx and add the result to the given + * StringBuffer (sb). + * @param idx index of the current updatecriteria + * @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 renderUpdateCriteria(int idx, StringBuffer sb) throws SQLException, ClassNotFoundException { + if (!updateTypeDesc[idx]) { + sb.append(updateCriteria[idx]); + sb.append(" > "); + renderValue(idx, highestValues, sb); + } else { + sb.append(updateCriteria[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(updateCriteria[idx]); + if (rel.otherType.getIDField().equalsIgnoreCase(updateCriteria[idx])) { + if (rel.otherType.needsQuotes(updateCriteria[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(updateCriteria[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 updateCriterias 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; + updateBorders(nh); + return super.add(nh); + } + + /** + * add a new node at the given index position and check if the + * borders for the updateCriterias 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); + } + + /** + * Check if the currently added node changes the borers for the updateCriterias and + * update them if neccessary. + * @param nh the added Node + */ + private void updateBorders(NodeHandle nh) { + Node node=null; + for (int i = 0; i < updateCriteria.length; i++) { + node=updateBorder(i, nh, node); + } + } + + /** + * Check if the given NodeHandle's node is outside of the criteria'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 = updateCriteria[idx]; + if (rel.otherType.getIDField().equals(cret)) { + String nid = nh.getID(); + if (updateTypeDesc[idx] + && OrderedSubnodeList.compareNumericString(nid, (String) lowestValues[idx]) < 0) { + lowestValues[idx]=nid; + } else if (!updateTypeDesc[idx] + && OrderedSubnodeList.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[updateCriteria.length]; + Node node = nh.getNode(rel.otherType.getWrappedNodeManager()); + for (int i = 0; i < updateCriteria.length; i++) { + String cret = updateCriteria[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 < updateCriteria.length; i++) { + if (!check[i]) + continue; + rebuildBorder(i); + } + } + + /** + * Rebuild all borders for all the updateCriterias + */ + public void rebuildBorders() { + for (int i = 0; i < updateCriteria.length; i++) { + rebuildBorder(i); + } + } + + /** + * Only rebuild the border for the update-criteria specified by the given + * index-position (idx). + * @param idx the index-position of the updateCriteria 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; + } + + /** + * checks if the borders have to be rebuilt because of the removed or + * the added NodeHandle. + */ + public Object set(int idx, Object obj) { + Object prevObj = super.set(idx, obj); + rebuildBorders((NodeHandle) prevObj); + updateBorders((NodeHandle) obj); + return prevObj; + } + + /** + * if the wrapped List is an instance of OrderedSubnodeList, + * the sortIn-method will be used. + */ + public boolean addAll(Collection col) { + return sortIn(col, true) > 0; + } +} diff --git a/src/helma/objectmodel/db/WrappedNodeManager.java b/src/helma/objectmodel/db/WrappedNodeManager.java index 513432b6..e038f5b6 100644 --- a/src/helma/objectmodel/db/WrappedNodeManager.java +++ b/src/helma/objectmodel/db/WrappedNodeManager.java @@ -104,7 +104,7 @@ public final class WrappedNodeManager { * @param rel * @return */ - public List getNodes(Node home, Relation rel) { + public SubnodeList getNodes(Node home, Relation rel) { try { return nmgr.getNodes(home, rel); } catch (Exception x) { @@ -124,7 +124,7 @@ public final class WrappedNodeManager { * @param rel * @return */ - public List getNodeIDs(Node home, Relation rel) { + public SubnodeList getNodeIDs(Node home, Relation rel) { try { return nmgr.getNodeIDs(home, rel); } catch (Exception x) { @@ -136,6 +136,21 @@ public final class WrappedNodeManager { } } + /** + * @see helma.objectmodel.db.NodeManager#updateSubnodeList(Node, Relation) + */ + public int updateSubnodeList (Node home, Relation rel) { + try { + return nmgr.updateSubnodeList(home, rel); + } catch (Exception x) { + if (nmgr.app.debug()) { + x.printStackTrace(); + } + + throw new RuntimeException("Error retrieving NodeIDs: ", x); + } + } + /** * Count the nodes contained in the given Node's collection * specified by the given Relation. diff --git a/src/helma/objectmodel/dom/XmlDatabase.java b/src/helma/objectmodel/dom/XmlDatabase.java index 53751829..d630832b 100644 --- a/src/helma/objectmodel/dom/XmlDatabase.java +++ b/src/helma/objectmodel/dom/XmlDatabase.java @@ -21,9 +21,12 @@ import helma.objectmodel.db.NodeManager; import helma.objectmodel.db.Node; import helma.framework.core.Application; +import javax.xml.parsers.ParserConfigurationException; import java.io.*; import java.util.ArrayList; +import org.xml.sax.SAXException; + /** * A simple XML-database */ @@ -125,7 +128,7 @@ public final class XmlDatabase implements IDatabase { throw (new DatabaseException("Error initializing db")); } } - + /** * Try to copy style sheet for XML files to database directory */ @@ -134,7 +137,7 @@ public final class XmlDatabase implements IDatabase { FileOutputStream out = null; byte[] buffer = new byte[1024]; int read; - + try { in = getClass().getResourceAsStream("helma.xsl"); out = new FileOutputStream(destination); @@ -145,9 +148,9 @@ public final class XmlDatabase implements IDatabase { System.err.println("Error copying db style sheet: "+iox); } finally { try { - if (out != null) + if (out != null) out.close(); - if (in != null) + if (in != null) in.close(); } catch (IOException ignore) { } @@ -181,7 +184,7 @@ public final class XmlDatabase implements IDatabase { if (idgen.dirty) { try { saveIDGenerator(txn); - idgen.dirty = false; + idgen.dirty = false; } catch (IOException x) { throw new DatabaseException(x.toString()); } @@ -266,12 +269,15 @@ public final class XmlDatabase implements IDatabase { throw new ObjectNotFoundException("Object not found for key " + kstr); } - try { + try { XmlDatabaseReader reader = new XmlDatabaseReader(nmgr); Node node = reader.read(f); return node; - } catch (Exception x) { + } catch (ParserConfigurationException x) { + app.logError("Error reading " +f, x); + throw new IOException(x.toString()); + } catch (SAXException x) { app.logError("Error reading " +f, x); throw new IOException(x.toString()); } diff --git a/src/helma/objectmodel/dom/XmlDatabaseReader.java b/src/helma/objectmodel/dom/XmlDatabaseReader.java index 43bb0b40..3e9b9f41 100644 --- a/src/helma/objectmodel/dom/XmlDatabaseReader.java +++ b/src/helma/objectmodel/dom/XmlDatabaseReader.java @@ -43,7 +43,7 @@ public final class XmlDatabaseReader extends DefaultHandler implements XmlConsta private String elementName = null; private StringBuffer charBuffer = null; Hashtable propMap = null; - List subnodes = null; + SubnodeList subnodes = null; /** * Creates a new XmlDatabaseReader object. @@ -121,11 +121,10 @@ public final class XmlDatabaseReader extends DefaultHandler implements XmlConsta if ("hop:child".equals(qName)) { if (subnodes == null) { - subnodes = new ExternalizableVector(); - currentNode.setSubnodes(subnodes); + subnodes = currentNode.createSubnodeList(); } - subnodes.add(handle); + subnodes.addSorted(handle); } else if ("hop:parent".equals(qName)) { currentNode.setParentHandle(handle); } else { diff --git a/src/helma/scripting/rhino/HopObject.java b/src/helma/scripting/rhino/HopObject.java index bacb2f16..26d7de34 100644 --- a/src/helma/scripting/rhino/HopObject.java +++ b/src/helma/scripting/rhino/HopObject.java @@ -32,7 +32,7 @@ import java.io.UnsupportedEncodingException; import java.io.IOException; /** - * + * */ public class HopObject extends ScriptableObject implements Wrapper, PropertyRecorder { static Method hopObjCtor; @@ -573,7 +573,7 @@ public class HopObject extends ScriptableObject implements Wrapper, PropertyReco while ((e != null) && e.hasMoreElements()) { Object obj = e.nextElement(); - if (obj != null) + if (obj != null) a.add(Context.toObject(obj, core.global)); } @@ -1102,4 +1102,46 @@ public class HopObject extends ScriptableObject implements Wrapper, PropertyReco 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 int jsFunction_update() { + if (!(node instanceof helma.objectmodel.db.Node)) + throw new RuntimeException ("update only callabel on persistent HopObjects"); + checkNode(); + helma.objectmodel.db.Node n = (helma.objectmodel.db.Node) node; + return n.updateSubnodes(); + } + + /** + * Retrieve a view having a different order from this Node's subnodelist. + * The underlying OrderedSubnodeList will keep those views and updates them + * if the original collection has been updated. + * @param expr the order (like sql-order using the properties instead) + * @return ListViewWrapper holding the information of the ordered view + */ + public Object jsFunction_getOrderedView(String expr) { + if (!(node instanceof helma.objectmodel.db.Node)) { + throw new RuntimeException ( + "getOrderedView only callable on persistent HopObjects"); + } + helma.objectmodel.db.Node n = (helma.objectmodel.db.Node) node; + n.loadNodes(); + List subnodes = n.getSubnodeList(); + if (subnodes == null) { + throw new RuntimeException ( + "getOrderedView only callable on already existing subnode-collections"); + } + if (subnodes instanceof OrderedSubnodeList) { + return new ListViewWrapper ((((OrderedSubnodeList) subnodes).getOrderedView(expr)), + core, n.getDbMapping().getWrappedNodeManager(), this); + } + throw new RuntimeException ( + "getOrderedView only callable on OrderedSubnodeList"); + } } diff --git a/src/helma/scripting/rhino/ListViewWrapper.java b/src/helma/scripting/rhino/ListViewWrapper.java new file mode 100644 index 00000000..4e890a53 --- /dev/null +++ b/src/helma/scripting/rhino/ListViewWrapper.java @@ -0,0 +1,322 @@ +/* + * 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$ + * $Author$ + * $Revision$ + * $Date$ + */ +package helma.scripting.rhino; + +import helma.objectmodel.INode; +import helma.objectmodel.db.Key; +import helma.objectmodel.db.Node; +import helma.objectmodel.db.NodeHandle; +import helma.objectmodel.db.OrderedSubnodeList; +import helma.objectmodel.db.UpdateableSubnodeList; +import helma.objectmodel.db.WrappedNodeManager; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.List; + +import org.mozilla.javascript.Context; +import org.mozilla.javascript.EvaluatorException; +import org.mozilla.javascript.FunctionObject; +import org.mozilla.javascript.Scriptable; +import org.mozilla.javascript.ScriptableObject; +import org.mozilla.javascript.Undefined; +import org.mozilla.javascript.Wrapper; +import org.mozilla.javascript.ScriptRuntime; + + +public class ListViewWrapper extends ScriptableObject implements Wrapper, Scriptable { + final List list; + final RhinoCore core; + final WrappedNodeManager wnm; + final HopObject hObj; + INode node; + + ListViewWrapper (List list, RhinoCore core, WrappedNodeManager wnm, HopObject hObj) { + if (list == null) { + throw new IllegalArgumentException ("ListWrapper unable to wrap null list."); + } + this.core = core; + this.list = list; + this.wnm = wnm; + this.hObj = hObj; + this.node = hObj.node; + init(); + } + + private void init() { + int attributes = READONLY | DONTENUM | PERMANENT; + + Method[] methods = this.getClass().getDeclaredMethods(); + for (int i=0; i= list.size()) + return; + checkNode(); + int l = Math.min(list.size() - start, length); + if (l < 1) + return; + Key[] keys = new Key[l]; + for (int i = start; i