Java tutorial
/* * 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: NodeManager.java,v $ * $Author: hannes $ * $Revision: 1.146 $ * $Date: 2006/03/21 16:52:46 $ */ /* * Modified by: * * Axiom Software Inc., 11480 Commerce Park Drive, Third Floor, Reston, VA 20191 USA * email: info@axiomsoftwareinc.com */ package axiom.objectmodel.db; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.Reader; import java.io.StringReader; import java.io.File; import java.math.BigDecimal; import java.sql.*; import java.util.*; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import axiom.framework.ErrorReporter; import axiom.framework.core.Application; import axiom.framework.core.RequestEvaluator; import axiom.objectmodel.*; import axiom.objectmodel.dom.LuceneDatabase; import axiom.scripting.rhino.AxiomObject; import axiom.util.ResourceProperties; /** * The NodeManager is responsible for fetching Nodes from the internal or * external data sources, caching them in a least-recently-used Hashtable, * and writing changes back to the databases. */ public final class NodeManager { protected Application app; private ObjectCache cache; protected HashMap<String, IDatabase> dbs; protected IDatabase defaultDb; protected IDGenerator idgen; private boolean logSql; private Log sqlLog = null; protected boolean logReplication; private ArrayList listeners = new ArrayList(); public static final String DEFAULT_DB = "axiom.objectmodel.dom.LuceneDatabase"; // a wrapper that catches some Exceptions while accessing this NM public final WrappedNodeManager safe; /** * Create a new NodeManager for Application app. */ public NodeManager(Application app) { this.app = app; safe = new WrappedNodeManager(this); } /** * Initialize the NodeManager for the given dbHome and * application properties. An embedded database will be * created in dbHome if one doesn't already exist. */ public void init(File dbHome, Properties props) throws DatabaseException, ClassNotFoundException, IllegalAccessException, InstantiationException { String cacheImpl = props.getProperty("cacheImpl", "axiom.util.EhCacheMap"); this.cache = (ObjectCache) Class.forName(cacheImpl).newInstance(); this.cache.init(app); String idgenImpl = props.getProperty("idGeneratorImpl"); if (idgenImpl != null) { idgen = (IDGenerator) Class.forName(idgenImpl).newInstance(); idgen.init(app); } logSql = "true".equalsIgnoreCase(props.getProperty("logsql")); logReplication = "true".equalsIgnoreCase(props.getProperty("logReplication")); this.setupDbs(dbHome); } private void setupDbs(File dbHome) { this.dbs = new HashMap<String, IDatabase>(); try { IDatabase db = (IDatabase) Class.forName(DEFAULT_DB).newInstance(); db.init(dbHome, app); this.dbs.put(DEFAULT_DB, db); this.defaultDb = db; } catch (Exception ex) { ex.printStackTrace(); throw new DatabaseException( "Could not initialize the default database " + DEFAULT_DB + ": " + ex.getMessage()); } ResourceProperties dbProps = app.getDbProperties(); Enumeration e = dbProps.keys(); while (e.hasMoreElements()) { String key = (String) e.nextElement(); if (key.endsWith(".class")) { String value = dbProps.getProperty(key); try { IDatabase idb = (IDatabase) Class.forName(value).newInstance(); idb.init(dbHome, this.app); this.dbs.put(value, idb); } catch (Exception ex) { } } } } /** * Gets the application's root node. */ public Node getRootNode() throws Exception { DbMapping rootMapping = app.getRootMapping(); DbKey key = new DbKey(rootMapping, app.getRootId(), DbKey.LIVE_LAYER); Node node = getNode(key); if (node != null && rootMapping != null) { node.setDbMapping(rootMapping); node.setPrototype(rootMapping.getTypeName()); } return node; } /** * Checks if the given node is the application's root node. */ public boolean isRootNode(Node node) { return app.getRootId().equals(node.getID()) && DbMapping.areStorageCompatible(app.getRootMapping(), node.getDbMapping()); } /** * app.properties file has been updated. Reread some settings. */ public void updateProperties(Properties props) { // notify the cache about the properties update logSql = "true".equalsIgnoreCase(props.getProperty("logsql")); logReplication = "true".equalsIgnoreCase(props.getProperty("logReplication")); } /** * Shut down this node manager. This is called when the application * using this node manager is stopped. */ public void shutdown() throws DatabaseException { Iterator<IDatabase> idbs = this.dbs.values().iterator(); while (idbs.hasNext()) { idbs.next().shutdown(); } if (cache != null) { cache.shutdown(); cache = null; } if (idgen != null) { idgen.shutdown(); } } /** * Delete a node from the database. */ public void deleteNode(Node node) throws Exception { if (node != null) { synchronized (this) { Transactor tx = (Transactor) Thread.currentThread(); node.setState(Node.INVALID); IDatabase db = this.getDatabaseForMapping(node.dbmap); deleteNode(db, tx.getTransaction(db), node); } } } public Node getNode(Key key) throws Exception { return this.getNode(key, true); } /** * Get a node by key. This is called from a node that already holds * a reference to another node via a NodeHandle/Key. */ public Node getNode(Key key, boolean multiLayers) throws Exception { Transactor tx = null; try { tx = (Transactor) Thread.currentThread(); } catch (ClassCastException ccex) { tx = null; } final boolean isValidInCache = isValidInCache(key); // See if Transactor has already come across this node Node node = null; if (isValidInCache) { node = (tx != null) ? tx.getVisitedNode(key) : null; if ((node != null) && (node.getState() != Node.INVALID)) { return node; } // try to get the node from the shared cache node = this.getNodeFromCache(key); } if ((node == null) || (node.getState() == Node.INVALID)) { // The requested node isn't in the shared cache. if (key instanceof SyntheticKey) { Node parent = getNode(key.getParentKey()); Relation rel = parent.dbmap.getPropertyRelation(key.getID()); if (rel != null) { //!!!!!!!!! //return getNode(parent, key.getID(), rel); node = getNode(parent, key.getID(), rel); } else { node = null; } } else if (key instanceof DbKey) { ITransaction txn = null; if (tx != null) { txn = tx.getTransaction( this.getDatabaseForMapping(this.app.getDbMapping(key.getStorageName()))); } node = getNodeByKey(txn, (DbKey) key, multiLayers); if ((node != null) && (node.getTypeDirty())) { node = getNodeByKey(txn, (DbKey) key, node.get(node.dbmap.getPrototypeField()).getStringValue(), multiLayers); } } if (node != null && isValidInCache) { // synchronize with cache synchronized (cache) { Node oldnode = (Node) cache.put(key, node); if ((oldnode != null) && !oldnode.isNullNode() && (oldnode.getState() != Node.INVALID)) { cache.put(key, oldnode); node = oldnode; } } // end of cache-synchronized section } } if (node != null && tx != null) { tx.visitCleanNode(key, node); } return node; } /** * Get a node by relation, using the home node, the relation and a key to apply. * In contrast to getNode (Key key), this is usually called when we don't yet know * whether such a node exists. */ public Node getNode(Node home, String kstr, Relation rel) throws Exception { if (kstr == null) { return null; } Transactor tx = (Transactor) Thread.currentThread(); Key key; // check what kind of object we're looking for and make an apropriate key if (rel.isComplexReference()) { // a key for a complex reference key = new MultiKey(rel.otherType, rel.getKeyParts(home)); } else if (rel.createOnDemand()) { // a key for a virtually defined object that's never actually stored in the db // or a key for an object that represents subobjects grouped by some property, // generated on the fly key = new SyntheticKey(home.getKey(), kstr); } else { // Not a relation we use can use getNodeByRelation() for return null; } // See if Transactor has already come across this node Node node = tx.getVisitedNode(key); if ((node != null) && (node.getState() != Node.INVALID)) { // we used to refresh the node in the main cache here to avoid the primary key // entry being flushed from cache before the secondary one // (risking duplicate nodes in cache) but we don't need to since we fetched // the node from the threadlocal transactor cache and didn't refresh it in the // main cache. return node; } // try to get the node from the shared cache node = this.getNodeFromCache(key); // check if we can use the cached node without further checks. // we need further checks for subnodes fetched by name if the subnodes were changed. if ((node != null) && (node.getState() != Node.INVALID)) { // check if node is null node (cached null) if (node.isNullNode()) { if ((node.created < rel.otherType.getLastDataChange()) || (node.created < rel.ownType.getLastTypeChange())) { node = null; // cached null not valid anymore } } else if (!rel.virtual) { // apply different consistency checks for groupby nodes and database nodes: // for group nodes, check if they're contained if (rel.groupby != null) { if (home.contains(node)) { node = null; } // for database nodes, check if constraints are fulfilled } else if (!rel.usesPrimaryKey()) { if (!rel.checkConstraints(home, node)) { node = null; } } } } if ((node == null) || (node.getState() == Node.INVALID)) { // The requested node isn't in the shared cache. // Synchronize with key to make sure only one version is fetched // from the database. ITransaction txn = null; if (tx != null) { txn = tx.getTransaction(this.getDatabaseForMapping(rel != null ? rel.otherType : null)); } node = getNodeByRelation(txn, home, kstr, rel); if ((node != null) && (node.getTypeDirty())) { node = getNodeByRelation(txn, home, kstr, rel, node.get("type").getStringValue()); } if (node != null) { Key primKey = node.getKey(); boolean keyIsPrimary = primKey.equals(key); synchronized (cache) { // check if node is already in cache with primary key Node oldnode = (Node) cache.put(primKey, node); // no need to check for oldnode != node because we fetched a new node from db if ((oldnode != null) && !oldnode.isNullNode() && (oldnode.getState() != Node.INVALID)) { // reset create time of old node, otherwise Relation.checkConstraints // will reject it under certain circumstances. oldnode.created = oldnode.lastmodified; cache.put(primKey, oldnode); if (!keyIsPrimary) { cache.put(key, oldnode); } //node = oldnode; } else if (!keyIsPrimary) { // cache node with secondary key cache.put(key, node); } } // synchronized } else { // node fetched from db is null, cache result using nullNode synchronized (cache) { cache.put(key, new Node()); // we ignore the case that onother thread has created the node in the meantime return null; } } } else if (node.isNullNode()) { // the nullNode caches a null value, i.e. an object that doesn't exist return null; } else { // update primary key in cache to keep it from being flushed, see above if (!rel.usesPrimaryKey()) { synchronized (cache) { Node oldnode = (Node) cache.put(node.getKey(), node); if ((oldnode != node) && (oldnode != null) && (oldnode.getState() != Node.INVALID)) { cache.put(node.getKey(), oldnode); cache.put(key, oldnode); node = oldnode; } } } } if (node != null) { tx.visitCleanNode(key, node); } // tx.timer.endEvent ("getNode "+kstr); return node; } public Key getChildKey(String field, String key, String parentid, int mode) throws Exception { Iterator<IDatabase> iter = this.dbs.values().iterator(); while (iter.hasNext()) { IDatabase db = iter.next(); try { Key k = db.getChildKey(field, key, parentid, mode); if (k != null) { return k; } } catch (Exception ignore) { } } return null; } /** * Register a node in the node cache. */ public void registerNode(Node node) { cache.put(node.getKey(), node); } /** * Register a node in the node cache using the key argument. */ protected void registerNode(Node node, Key key) { cache.put(key, node); } /** * Remove a node from the node cache. If at a later time it is accessed again, * it will be refetched from the database. */ public void evictNode(Node node) { node.setState(INode.INVALID); cache.remove(node.getKey()); } /** * Remove a node from the node cache. If at a later time it is accessed again, * it will be refetched from the database. */ public void evictNodeByKey(Key key) { Node n = (Node) cache.remove(key); if (n != null) { n.setState(INode.INVALID); if (!(key instanceof DbKey)) { cache.remove(n.getKey()); } } } /** * Used when a key stops being valid for a node. The cached node itself * remains valid, if it is present in the cache by other keys. */ public void evictKey(Key key) { cache.remove(key); } //////////////////////////////////////////////////////////////////////// // methods to do the actual db work //////////////////////////////////////////////////////////////////////// /** * Insert a new node in the embedded database or a relational database table, * depending on its db mapping. */ public void insertNode(IDatabase db, ITransaction txn, Node node) throws IOException, SQLException, ClassNotFoundException { // Transactor tx = (Transactor) Thread.currentThread (); // tx.timer.beginEvent ("insertNode "+node); invokeOnPersist(node); DbMapping dbm = node.getDbMapping(); if ((dbm == null) || !dbm.isRelational()) { String className = dbm.getClassName(); IDatabase idb = null; if (className != null) { idb = (IDatabase) this.dbs.get(className); } if (idb == null) { idb = db; } idb.insertNode(txn, node.getID(), node); } else { insertRelationalNode(node, dbm, dbm.getConnection()); } } /** * Insert a node into a different (relational) database than its default one. */ public void exportNode(Node node, DbSource dbs) throws IOException, SQLException, ClassNotFoundException { if (node == null) { throw new IllegalArgumentException("Node can't be null in exportNode"); } DbMapping dbm = node.getDbMapping(); if (dbs == null) { throw new IllegalArgumentException("DbSource can't be null in exportNode"); } else if ((dbm == null) || !dbm.isRelational()) { throw new IllegalArgumentException("Can't export into non-relational database"); } else { insertRelationalNode(node, dbm, dbs.getConnection()); } } /** * Insert a node into a different (relational) database than its default one. */ public void exportNode(Node node, DbMapping dbm) throws IOException, SQLException, ClassNotFoundException { if (node == null) { throw new IllegalArgumentException("Node can't be null in exportNode"); } if (dbm == null) { throw new IllegalArgumentException("DbMapping can't be null in exportNode"); } else if (!dbm.isRelational()) { throw new IllegalArgumentException("Can't export into non-relational database"); } else { insertRelationalNode(node, dbm, dbm.getConnection()); } } protected void insertRelationalNode(Node node, DbMapping dbm, Connection con) throws ClassNotFoundException, SQLException { if (dbm.getTableCount() > 1) { int c = dbm.getTableCount(); for (int i = 0; i < c; i++) { insertRelationalNodeInto(node, dbm, con, i); } } else { insertRelationalNodeInto(node, dbm, con, -1); } } /** * Insert a node into a relational database. */ protected void insertRelationalNodeInto(Node node, DbMapping dbm, Connection con, int tableNumber) throws ClassNotFoundException, SQLException { if (con == null) { throw new NullPointerException("Error inserting relational node: Connection is null"); } // set connection to write mode if (con.isReadOnly()) con.setReadOnly(false); String insertString = dbm.getInsert(tableNumber); PreparedStatement stmt = con.prepareStatement(insertString); DbColumn[] columns = dbm.getColumns(tableNumber); String nameField = dbm.getNameField(); String prototypeField = dbm.getPrototypeField(); String idField = dbm.getTableKey(tableNumber); long logTimeStart = logSql ? System.currentTimeMillis() : 0; try { int stmtNumber = 1; // first column of insert statement is always the primary key // Changed to get the primary key value from the db mapping, // in the case where a particular table's primary key is not the primary key // of the main table in the db mapping. stmt.setString(stmtNumber, dbm.getPrimaryKeyValue(node, tableNumber)); //node.getID()); Hashtable propMap = node.getPropMap(); for (int i = 0; i < columns.length; i++) { Relation rel = columns[i].getRelation(); Property p = null; if (rel != null && propMap != null && (rel.isPrimitive() || rel.isReference())) { p = (Property) propMap.get(rel.getPropName()); } String name = columns[i].getName(); if (name.equals(idField)) { continue; } //Ensure the id is not repeated. if (!((rel != null) && (rel.isPrimitive() || rel.isReference())) && !name.equalsIgnoreCase(nameField) && !name.equalsIgnoreCase(prototypeField)) { continue; } stmtNumber++; if (p != null) { this.setStatementValues(stmt, stmtNumber, p, columns[i].getType()); } else if (name.equalsIgnoreCase(nameField)) { stmt.setString(stmtNumber, node.getName()); } else if (name.equalsIgnoreCase(prototypeField)) { stmt.setString(stmtNumber, node.getPrototype()); } else { stmt.setNull(stmtNumber, columns[i].getType()); } } stmt.executeUpdate(); // node.setInteger("id", new Integer(node.getID()).longValue()); } finally { if (logSql) { long logTimeStop = java.lang.System.currentTimeMillis(); logSqlStatement("SQL INSERT", dbm.getTableName(), logTimeStart, logTimeStop, insertString); } if (stmt != null) { try { stmt.close(); } catch (Exception ex) { app.logError(ErrorReporter.errorMsg(this.getClass(), "insertRelationalNodeInto"), ex); } } } } /** * calls onPersist function for the AxiomObject */ private void invokeOnPersist(Node node) { try { // We need to reach deap into axiom.framework.core to invoke onPersist(), // but the functionality is really worth it. RequestEvaluator reval = this.app.getCurrentRequestEvaluator(); if (reval != null) { reval.invokeDirectFunction(node, "onPersist", reval.EMPTY_ARGS); } } catch (Exception x) { app.logError("Error invoking onPersist()", x); } } /** * Updates a modified node in the embedded db or an external relational database, depending * on its database mapping. * * @return true if the DbMapping of the updated Node is to be marked as updated via * DbMapping.setLastDataChange */ public boolean updateNode(IDatabase db, ITransaction txn, Node node) throws IOException, SQLException, ClassNotFoundException { // Transactor tx = (Transactor) Thread.currentThread (); // tx.timer.beginEvent ("updateNode "+node); invokeOnPersist(node); DbMapping dbm = node.getDbMapping(); boolean markMappingAsUpdated = false; if ((dbm == null) || !dbm.isRelational()) { String className = dbm.getClassName(); IDatabase idb = null; if (className != null) { idb = (IDatabase) this.dbs.get(className); } if (idb == null) { idb = db; } idb.updateNode(txn, node.getID(), node); } else { Hashtable propMap = node.getPropMap(); Property[] props; if (propMap == null) { props = new Property[0]; } else { props = new Property[propMap.size()]; propMap.values().toArray(props); } // make sure table meta info is loaded by dbmapping dbm.getColumns(); StringBuffer b = dbm.getUpdate(); // comma flag set after the first dirty column, also tells as // if there are dirty columns at all boolean comma = false; for (int i = 0; i < props.length; i++) { // skip clean properties if ((props[i] == null) || !props[i].dirty) { // null out clean property so we don't consider it later props[i] = null; continue; } Relation rel = dbm.propertyToRelation(props[i].getName()); // skip readonly, virtual and collection relations if ((rel == null) || rel.readonly || rel.virtual || ((rel.reftype != Relation.REFERENCE) && (rel.reftype != Relation.PRIMITIVE))) { // null out property so we don't consider it later props[i] = null; continue; } if (comma) { b.append(", "); } else { comma = true; } b.append(rel.getDbField()); b.append(" = ?"); } // if no columns were updated, return false if (!comma) { return false; } b.append(" WHERE "); //b.append(dbm.getTableName(0)); //b.append("."); b.append(dbm.getIDField()); b.append(" = "); if (dbm.needsQuotes(dbm.getIDField())) { b.append("'"); b.append(escape(node.getID())); b.append("'"); } else { b.append(node.getID()); } b.append(dbm.getTableJoinClause(0)); Connection con = dbm.getConnection(); // set connection to write mode if (con.isReadOnly()) con.setReadOnly(false); PreparedStatement stmt = con.prepareStatement(b.toString()); int stmtNumber = 0; long logTimeStart = logSql ? System.currentTimeMillis() : 0; try { for (int i = 0; i < props.length; i++) { Property p = props[i]; if (p == null) { continue; } Relation rel = dbm.propertyToRelation(p.getName()); stmtNumber++; this.setStatementValues(stmt, stmtNumber, p, rel.getColumnType()); p.dirty = false; if (!rel.isPrivate()) { markMappingAsUpdated = true; } } stmt.executeUpdate(); } finally { if (logSql) { long logTimeStop = System.currentTimeMillis(); logSqlStatement("SQL UPDATE", dbm.getTableName(), logTimeStart, logTimeStop, b.toString()); } if (stmt != null) { try { stmt.close(); } catch (Exception ignore) { app.logEvent(ignore.getMessage()); } } } } // update may cause changes in the node's parent subnode array // TODO: is this really needed anymore? if (markMappingAsUpdated && node.isAnonymous()) { Node parent = node.getCachedParent(); if (parent != null) { parent.setLastSubnodeChange(System.currentTimeMillis()); } } return markMappingAsUpdated; } /** * Performs the actual deletion of a node from either the embedded or an external * SQL database. */ public void deleteNode(IDatabase db, ITransaction txn, Node node) throws Exception { DbMapping dbm = node.getDbMapping(); if ((dbm == null) || !dbm.isRelational()) { String className = dbm.getClassName(); IDatabase idb = null; if (className != null) { idb = (IDatabase) this.dbs.get(className); } if (idb == null) { idb = db; } try { idb.deleteNode(txn, node.getID(), ((DbKey) node.getKey()).getLayer()); String proto = node.getPrototype(); if ("File".equals(proto) || "Image".equals(proto)) { LuceneDatabase ldb = (LuceneDatabase) idb; ldb.getLuceneManager().deleteFromStorage(node); } } catch (Exception ex) { idb.deleteNode(txn, node.getID()); } } else { Statement st = null; long logTimeStart = logSql ? System.currentTimeMillis() : 0; String idstring = node.getID(); if (dbm.needsQuotes(dbm.getIDField())) { idstring = "'" + escape(idstring) + "'"; } String str = new StringBuffer("DELETE " + dbm.getTableDeleteProperties() + "FROM ") .append(dbm.getTableName()).append(" WHERE ") //.append(dbm.getTableName(0)) //.append(".") .append(dbm.getIDField()).append(" = ").append(idstring).append(dbm.getTableJoinClause(0)) .toString(); try { Connection con = dbm.getConnection(); // set connection to write mode if (con.isReadOnly()) con.setReadOnly(false); st = con.createStatement(); st.executeUpdate(str); } finally { if (logSql) { long logTimeStop = System.currentTimeMillis(); logSqlStatement("SQL DELETE", dbm.getTableName(), logTimeStart, logTimeStop, str); } if (st != null) { try { st.close(); } catch (Exception ignore) { app.logEvent(ignore.getMessage()); } } } } // node may still be cached via non-primary keys. mark as invalid node.setState(Node.INVALID); } /** * Generate a new ID for a given type, delegating to our IDGenerator if set. */ public String generateID(DbMapping map) throws Exception { if (idgen != null) { // use our custom IDGenerator return idgen.generateID(map); } else { return doGenerateID(map); } } /** * Actually generates an ID, using a method matching the given DbMapping. */ public String doGenerateID(DbMapping map) throws Exception { if ((map == null) || !map.isRelational()) { // use embedded db id generator return generateEmbeddedID(map); } String idMethod = map.getIDgen(); if (idMethod == null || "[max]".equalsIgnoreCase(idMethod)) { // use select max as id generator return generateMaxID(map); } else if ("[axiom]".equalsIgnoreCase(idMethod)) { // use embedded db id generator return generateEmbeddedID(map); } else { // use db sequence as id generator return generateSequenceID(map); } } /** * Gererates an ID for use with the embedded database. */ synchronized String generateEmbeddedID(DbMapping map) throws Exception { return defaultDb.nextID(); } public synchronized void setEmbeddedID(String _id) { ((LuceneDatabase) this.defaultDb).setEmbeddedID(_id); } /** * Generates an ID for the table by finding out the maximum current value */ synchronized String generateMaxID(DbMapping map) throws Exception { // Transactor tx = (Transactor) Thread.currentThread (); // tx.timer.beginEvent ("generateID "+map); String retval = null; Statement stmt = null; long logTimeStart = logSql ? System.currentTimeMillis() : 0; String q = new StringBuffer("SELECT MAX(") /*.append(map.getTableName(0)).append(".")*/.append(map.getIDField()).append(") FROM ") .append(map.getTableName(0)).toString(); try { Connection con = map.getConnection(); // set connection to read-only mode if (!con.isReadOnly()) con.setReadOnly(true); stmt = con.createStatement(); ResultSet rs = stmt.executeQuery(q); // check for empty table if (!rs.next()) { long currMax = map.getNewID(0); retval = Long.toString(currMax); } else { long currMax = rs.getLong(1); currMax = map.getNewID(currMax); retval = Long.toString(currMax); } } finally { if (logSql) { long logTimeStop = System.currentTimeMillis(); logSqlStatement("SQL SELECT_MAX", map.getTableName(), logTimeStart, logTimeStop, q); } if (stmt != null) { try { stmt.close(); } catch (Exception ignore) { app.logEvent(ignore.getMessage()); } } } return retval; } String generateSequenceID(DbMapping map) throws Exception { // Transactor tx = (Transactor) Thread.currentThread (); // tx.timer.beginEvent ("generateID "+map); Statement stmt = null; String retval = null; long logTimeStart = logSql ? System.currentTimeMillis() : 0; String q = new StringBuffer("SELECT ").append(map.getIDgen()).append(".nextval FROM dual").toString(); try { Connection con = map.getConnection(); // TODO is it necessary to set connection to write mode here? if (con.isReadOnly()) con.setReadOnly(false); stmt = con.createStatement(); ResultSet rs = stmt.executeQuery(q); if (!rs.next()) { throw new SQLException("Error creating ID from Sequence: empty recordset"); } retval = rs.getString(1); } finally { if (logSql) { long logTimeStop = System.currentTimeMillis(); logSqlStatement("SQL SELECT_NEXTVAL", map.getTableName(), logTimeStart, logTimeStop, q); } if (stmt != null) { try { stmt.close(); } catch (Exception ignore) { app.logEvent(ignore.getMessage()); } } } return retval; } /** * Loades subnodes via subnode relation. Only the ID index is loaded, the nodes are * loaded later on demand. */ public Collection<NodeHandle> getNodeIDs(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.getNodeIDs called for non-relational node " + home); } else { Collection<NodeHandle> 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 String idfield = (rel.groupby == null) ? rel.otherType.getIDField() : rel.groupby; Connection con = rel.otherType.getConnection(); // set connection to read-only mode if (!con.isReadOnly()) con.setReadOnly(true); String table = rel.otherType.getTableName(); Statement stmt = null; long logTimeStart = logSql ? System.currentTimeMillis() : 0; String query = null; try { StringBuffer b = new StringBuffer("SELECT "); if (rel.queryHints != null) { b.append(rel.queryHints).append(" "); } /*if (idfield.indexOf('(') == -1 && idfield.indexOf('.') == -1) { b.append(table).append('.'); }*/ b/*.append(rel.otherType.getTableName(0)).append(".")*/.append(idfield).append(" FROM ") .append(table); rel.appendAdditionalTables(b); if (home.getSubnodeRelation() != null) { // subnode relation was explicitly set query = b.append(" ").append(home.getSubnodeRelation()).toString(); } else { // let relation object build the query query = b.append(rel.buildQuery(home, home.getNonVirtualParent(), null, " WHERE ", true)) .toString(); } stmt = con.createStatement(); int primary = 1; if (query.indexOf("WHERE") > -1) { primary = 0; } query += rel.otherType.getTableJoinClause(primary); if (rel.maxSize > 0) { stmt.setMaxRows(rel.maxSize); } ResultSet result = stmt.executeQuery(query); // problem: how do we derive a SyntheticKey from a not-yet-persistent Node? Key k = (rel.groupby != null) ? home.getKey() : null; 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 = (rel.groupby == null) ? (Key) new DbKey(rel.otherType, kstr, this.app.getCurrentRequestEvaluator().getLayer()) : (Key) new SyntheticKey(k, kstr); if (retval instanceof SubnodeList) { ((SubnodeList) retval).addSorted(new NodeHandle(key)); } else { retval.add(new NodeHandle(key)); } // if these are groupby nodes, evict nullNode keys if (rel.groupby != null) { Node n = this.getNodeFromCache(key); if ((n != null) && n.isNullNode()) { evictKey(key); } } } } finally { if (logSql) { long logTimeStop = System.currentTimeMillis(); logSqlStatement("SQL SELECT_IDS", table, logTimeStart, logTimeStop, query); } if (stmt != null) { try { stmt.close(); } catch (Exception ignore) { } } } return retval; } } /** * Loades subnodes via subnode relation. This is similar to getNodeIDs, but it * 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 Collection<NodeHandle> 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); } // Transactor tx = (Transactor) Thread.currentThread (); // tx.timer.beginEvent ("getNodes "+home); if ((rel == null) || (rel.otherType == null) || !rel.otherType.isRelational()) { // this should never be called for embedded nodes throw new RuntimeException("NodeMgr.getNodes called for non-relational node " + home); } else { Collection<NodeHandle> retval = home.createSubnodeList(); DbMapping dbm = rel.otherType; Connection con = dbm.getConnection(); // set connection to read-only mode if (!con.isReadOnly()) con.setReadOnly(true); Statement stmt = con.createStatement(); DbColumn[] columns = dbm.getColumns(); Relation[] joins = dbm.getJoins(); String query = null; long logTimeStart = logSql ? System.currentTimeMillis() : 0; try { StringBuffer b = dbm.getSelect(rel); if (home.getSubnodeRelation() != null) { b.append(home.getSubnodeRelation()); } else { // let relation object build the query b.append(rel.buildQuery(home, home.getNonVirtualParent(), null, " WHERE ", true)); } query = b.toString(); if (rel.maxSize > 0) { stmt.setMaxRows(rel.maxSize); } ResultSet rs = stmt.executeQuery(query); while (rs.next()) { // create new Nodes. Node node = createNode(rel.otherType, rs, columns, 0); if (node == null) { continue; } Key primKey = node.getKey(); if (retval instanceof SubnodeList) { ((SubnodeList) retval).addSorted(new NodeHandle(primKey)); } else { retval.add(new NodeHandle(primKey)); } // do we need to synchronize on primKey here? synchronized (cache) { Node oldnode = (Node) cache.put(primKey, node); if ((oldnode != null) && (oldnode.getState() != INode.INVALID)) { cache.put(primKey, oldnode); } } fetchJoinedNodes(rs, joins, columns.length); } } finally { if (logSql) { long logTimeStop = System.currentTimeMillis(); logSqlStatement("SQL SELECT_ALL", dbm.getTableName(), logTimeStart, logTimeStop, query); } if (stmt != null) { try { stmt.close(); } catch (Exception ignore) { app.logEvent(ignore.getMessage()); } } } 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 { Collection<NodeHandle> 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(); synchronized (cache) { Node oldnode = (Node) cache.put(key, node); if ((oldnode != null) && (oldnode.getState() != INode.INVALID)) { cache.put(key, oldnode); } } } else { key = new DbKey(rel.otherType, kstr, this.app.getCurrentRequestEvaluator().getLayer()); } newNodes.add(new NodeHandle(key)); // if these are groupby nodes, evict nullNode keys if (rel.groupby != null) { Node n = this.getNodeFromCache(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) { app.logEvent(ignore.getMessage()); } } } } } /** * */ public void prefetchNodes(Node home, Relation rel, Key[] keys) throws Exception { DbMapping dbm = rel.otherType; if ((dbm == null) || !dbm.isRelational()) { // this does nothing for objects in the embedded database return; } else { boolean missing = missingFromCache(keys); if (missing) { Connection con = dbm.getConnection(); // set connection to read-only mode if (!con.isReadOnly()) con.setReadOnly(true); Statement stmt = con.createStatement(); DbColumn[] columns = dbm.getColumns(); Relation[] joins = dbm.getJoins(); String query = null; long logTimeStart = logSql ? System.currentTimeMillis() : 0; try { StringBuffer b = dbm.getSelect(rel); String idfield = (rel.groupby != null) ? rel.groupby : dbm.getIDField(); boolean needsQuotes = dbm.needsQuotes(idfield); b.append(" WHERE "); //b.append(dbm.getTableName()); //b.append("."); b.append(idfield); b.append(" IN ("); boolean first = true; for (int i = 0; i < keys.length; i++) { if (keys[i] != null) { if (!first) { b.append(','); } else { first = false; } if (needsQuotes) { b.append("'"); b.append(escape(keys[i].getID())); b.append("'"); } else { b.append(keys[i].getID()); } } } b.append(") "); dbm.addJoinConstraints(b, " AND "); if (rel.groupby != null) { rel.renderConstraints(b, home, home.getNonVirtualParent(), " AND "); if (rel.order != null) { b.append(" ORDER BY "); b.append(rel.order); } } query = b.toString(); ResultSet rs = stmt.executeQuery(query); String groupbyProp = null; HashMap groupbySubnodes = null; if (rel.groupby != null) { groupbyProp = dbm.columnNameToProperty(rel.groupby); groupbySubnodes = new HashMap(); } String accessProp = null; if ((rel.accessName != null) && !rel.usesPrimaryKey()) { accessProp = dbm.columnNameToProperty(rel.accessName); } while (rs.next()) { // create new Nodes. Node node = createNode(dbm, rs, columns, 0); if (node == null) { continue; } Key primKey = node.getKey(); // for grouped nodes, collect subnode lists for the intermediary // group nodes. String groupName = null; if (groupbyProp != null) { groupName = node.getString(groupbyProp); SubnodeList sn = (SubnodeList) groupbySubnodes.get(groupName); if (sn == null) { sn = new SubnodeList(safe, rel); groupbySubnodes.put(groupName, sn); } sn.addSorted(new NodeHandle(primKey)); } // if relation doesn't use primary key as accessName, get accessName value String accessName = null; if (accessProp != null) { accessName = node.getString(accessProp); } // register new nodes with the cache. If an up-to-date copy // existed in the cache, use that. synchronized (cache) { Node oldnode = (Node) cache.put(primKey, node); if ((oldnode != null) && (oldnode.getState() != INode.INVALID)) { // found an ok version in the cache, use it. cache.put(primKey, oldnode); } else if (accessName != null) { // put the node into cache with its secondary key if (groupName != null) { cache.put(new SyntheticKey(new SyntheticKey(home.getKey(), groupName), accessName), node); } else { cache.put(new SyntheticKey(home.getKey(), accessName), node); } } } fetchJoinedNodes(rs, joins, columns.length); } // If these are grouped nodes, build the intermediary group nodes // with the subnod lists we created if (groupbyProp != null) { for (Iterator i = groupbySubnodes.keySet().iterator(); i.hasNext();) { String groupname = (String) i.next(); if (groupname == null) { continue; } Node groupnode = home.getGroupbySubnode(groupname, true); groupnode.setSubnodes((SubnodeList) groupbySubnodes.get(groupname)); groupnode.lastSubnodeFetch = System.currentTimeMillis(); } } } catch (Exception x) { System.err.println("Error in prefetchNodes(): " + x); } finally { if (logSql) { long logTimeStop = System.currentTimeMillis(); logSqlStatement("SQL SELECT_PREFETCH", dbm.getTableName(), logTimeStart, logTimeStop, query); } if (stmt != null) { try { stmt.close(); } catch (Exception ignore) { app.logEvent(ignore.getMessage()); } } } } } } private boolean missingFromCache(Key[] keys) { final int count = this.cache.containsKeys(keys); return count != keys.length; } /** * Count the nodes contained in the child collection of the home node * which is defined by Relation rel. */ public int countNodes(Node home, Relation rel) throws Exception { // Transactor tx = (Transactor) Thread.currentThread (); // tx.timer.beginEvent ("countNodes "+home); if ((rel == null) || (rel.otherType == null) || !rel.otherType.isRelational()) { // this should never be called for embedded nodes throw new RuntimeException("NodeMgr.countNodes called for non-relational node " + home); } else { int retval = 0; Connection con = rel.otherType.getConnection(); // set connection to read-only mode if (!con.isReadOnly()) con.setReadOnly(true); String table = rel.otherType.getTableName(); Statement stmt = null; long logTimeStart = logSql ? System.currentTimeMillis() : 0; String query = null; try { StringBuffer tables = new StringBuffer(table); rel.appendAdditionalTables(tables); // NOTE: we explicitly convert tables StringBuffer to a String // before appending to be compatible with JDK 1.3 StringBuffer b = new StringBuffer("SELECT count(*) FROM ").append(tables.toString()) .append(rel.otherType.getTableJoinClause(1)); if (home.getSubnodeRelation() != null) { // use the manually set subnoderelation of the home node query = b.append(" ").append(home.getSubnodeRelation()).toString(); } else { // let relation object build the query query = b.append(rel.buildQuery(home, home.getNonVirtualParent(), null, " WHERE ", false)) .toString(); } stmt = con.createStatement(); ResultSet rs = stmt.executeQuery(query); if (!rs.next()) { retval = 0; } else { retval = rs.getInt(1); } } finally { if (logSql) { long logTimeStop = System.currentTimeMillis(); logSqlStatement("SQL SELECT_COUNT", table, logTimeStart, logTimeStop, query); } if (stmt != null) { try { stmt.close(); } catch (Exception ex) { app.logError(ErrorReporter.errorMsg(this.getClass(), "countNodes"), ex); } } } return (rel.maxSize > 0) ? Math.min(rel.maxSize, retval) : retval; } } /** * Similar to getNodeIDs, but returns a Vector that return's the nodes property names instead of IDs */ public Vector getPropertyNames(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.getPropertyNames called for non-relational node " + home); } else { Vector retval = new Vector(); // if we do a groupby query (creating an intermediate layer of groupby nodes), // retrieve the value of that field instead of the primary key String namefield = (rel.groupby == null) ? rel.accessName : rel.groupby; Connection con = rel.otherType.getConnection(); // set connection to read-only mode if (!con.isReadOnly()) con.setReadOnly(true); String table = rel.otherType.getTableName(); StringBuffer tables = new StringBuffer(table); rel.appendAdditionalTables(tables); Statement stmt = null; long logTimeStart = logSql ? System.currentTimeMillis() : 0; String query = null; try { // NOTE: we explicitly convert tables StringBuffer to a String // before appending to be compatible with JDK 1.3 StringBuffer b = new StringBuffer("SELECT ").append(namefield).append(" FROM ") .append(tables.toString()); if (home.getSubnodeRelation() != null) { b.append(" ").append(home.getSubnodeRelation()); } else { // let relation object build the query b.append(rel.buildQuery(home, home.getNonVirtualParent(), null, " WHERE ", true)); } stmt = con.createStatement(); query = b.toString(); ResultSet rs = stmt.executeQuery(query); while (rs.next()) { String n = rs.getString(1); if (n != null) { retval.addElement(n); } } } finally { if (logSql) { long logTimeStop = System.currentTimeMillis(); logSqlStatement("SQL SELECT_ACCESSNAMES", table, logTimeStart, logTimeStop, query); } if (stmt != null) { try { stmt.close(); } catch (Exception ex) { app.logError(ErrorReporter.errorMsg(this.getClass(), "getPropertyNames"), ex); } } } return retval; } } /////////////////////////////////////////////////////////////////////////////////////// // private getNode methods /////////////////////////////////////////////////////////////////////////////////////// private Node getNodeByKey(ITransaction txn, DbKey key, boolean multiLayers) throws Exception { return this.getNodeByKey(txn, key, null, multiLayers); } private Node getNodeByKey(ITransaction txn, DbKey key, String type, boolean multiLayers) throws Exception { // Note: Key must be a DbKey, otherwise will not work for relational objects Node node = null; if (type == null) { type = key.getStorageName(); } DbMapping dbm = app.getDbMapping(type); String kstr = key.getID(); if ((dbm == null) || !dbm.isRelational()) { IDatabase db = this.getDatabaseForMapping(dbm); try { final int mode; if (key instanceof DbKey) { // andy maybe i don't know /*if(this.app.getCurrentRequestEvaluator().getMode() == DbKey.DRAFT_MODE){ mode = this.app.getCurrentRequestEvaluator().getMode(); } else{*/ mode = ((DbKey) key).getLayer(); //} } else { mode = this.app.getCurrentRequestEvaluator().getLayer(); } node = (Node) db.getNode(txn, kstr, mode, multiLayers); } catch (Exception ex) { if (multiLayers) { node = (Node) db.getNode(txn, kstr); } } if (node != null) { node.nmgr = safe; } if ((node != null) && (dbm != null)) { node.setDbMapping(dbm); } } else { String idfield = dbm.getIDField(); Statement stmt = null; String query = null; long logTimeStart = logSql ? System.currentTimeMillis() : 0; try { Connection con = dbm.getConnection(); // set connection to read-only mode if (!con.isReadOnly()) con.setReadOnly(true); stmt = con.createStatement(); DbColumn[] columns = dbm.getColumns(); Relation[] joins = dbm.getJoins(); StringBuffer b = dbm.getSelect(null).append("WHERE ") //.append(dbm.getTableName(0)) //.append(".") .append(idfield).append(" = "); if (dbm.needsQuotes(idfield)) { b.append("'"); b.append(escape(kstr)); b.append("'"); } else { b.append(kstr); } dbm.addJoinConstraints(b, " AND "); query = b.toString(); query += dbm.getTableJoinClause(0); ResultSet rs = stmt.executeQuery(query); if (!rs.next()) { return null; } node = createNode(dbm, rs, columns, 0); fetchJoinedNodes(rs, joins, columns.length); if (rs.next()) { app.logError(ErrorReporter.errorMsg(this.getClass(), "getNodeByKey") + "More than one value returned for query " + query); } } finally { if (logSql) { long logTimeStop = System.currentTimeMillis(); logSqlStatement("SQL SELECT_BYKEY", dbm.getTableName(), logTimeStart, logTimeStop, query); } if (stmt != null) { try { stmt.close(); } catch (Exception ex) { app.logError(ErrorReporter.errorMsg(this.getClass(), "getNodeByKey"), ex); } } } } return node; } private Node getNodeByRelation(ITransaction txn, Node home, String kstr, Relation rel) throws Exception { return getNodeByRelation(txn, home, kstr, rel, null); } private Node getNodeByRelation(ITransaction txn, Node home, String kstr, Relation rel, String type) throws Exception { Node node = null; boolean updateTypeDirty = false; if ((rel != null) && rel.virtual) { if (rel.needsPersistence()) { node = (Node) home.createNode(kstr); } else { node = new Node(home, kstr, safe, rel.prototype); } // set prototype and dbmapping on the newly created virtual/collection node node.setPrototype(rel.prototype); node.setDbMapping(rel.getVirtualMapping()); } else if ((rel != null) && (rel.groupby != null)) { node = home.getGroupbySubnode(kstr, false); if ((node == null) && ((rel.otherType == null) || !rel.otherType.isRelational())) { IDatabase db = this.getDatabaseForMapping(rel.otherType); try { node = (Node) db.getNode(txn, kstr, this.app.getCurrentRequestEvaluator().getLayer()); } catch (Exception ex) { node = (Node) db.getNode(txn, kstr); } node.nmgr = safe; } return node; } else if ((rel == null) || (rel.otherType == null) || !rel.otherType.isRelational()) { IDatabase db = (rel == null) ? this.defaultDb : this.getDatabaseForMapping(rel.otherType); try { node = (Node) ((LuceneDatabase) db).getNode(txn, kstr, this.app.getCurrentRequestEvaluator().getLayer()); } catch (Exception ex) { node = (Node) db.getNode(txn, kstr); } node.nmgr = safe; node.setDbMapping(rel.otherType); return node; } else { //DbMapping dbm = rel.otherType; DbMapping dbm = rel.otherType; if (type != null) { dbm = this.getDbMapping(type); //set the DbMapping later? updateTypeDirty = true; } Statement stmt = null; String query = null; long logTimeStart = logSql ? System.currentTimeMillis() : 0; try { Connection con = dbm.getConnection(); // set connection to read-only mode if (!con.isReadOnly()) con.setReadOnly(true); DbColumn[] columns = dbm.getColumns(); Relation[] joins = dbm.getJoins(); StringBuffer b = dbm.getSelect(rel); if (home.getSubnodeRelation() != null && !rel.isComplexReference()) { // combine our key with the constraints in the manually set subnode relation b.append(" WHERE "); if (rel.accessName.indexOf('(') == -1 && rel.accessName.indexOf('.') == -1) { b.append(dbm.getTableName(0)); b.append("."); } b.append(rel.accessName); b.append(" = '"); b.append(escape(kstr)); b.append("'"); // add join contraints in case this is an old oracle style join dbm.addJoinConstraints(b, " AND "); // add potential constraints from manually set subnodeRelation String subrel = home.getSubnodeRelation().trim(); if (subrel.length() > 5) { b.append(" AND ("); b.append(subrel.substring(5).trim()); b.append(")"); } } else { b.append(rel.buildQuery(home, home.getNonVirtualParent(), kstr, " WHERE ", false)); } b.append(dbm.getTableJoinClause(0)); stmt = con.createStatement(); query = b.toString(); ResultSet rs = stmt.executeQuery(query); if (!rs.next()) { return null; } //node = createNode(rel.otherType, rs, columns, 0); node = createNode(dbm, rs, columns, 0); fetchJoinedNodes(rs, joins, columns.length); if (rs.next()) { app.logError(ErrorReporter.errorMsg(this.getClass(), "getNodeByRelation") + "More than one value returned for query " + query); } // Check if node is already cached with primary Key. if (!rel.usesPrimaryKey() && node != null) { Key pk = node.getKey(); Node existing = this.getNodeFromCache(pk); if ((existing != null) && (existing.getState() != Node.INVALID) && (!updateTypeDirty)) { //Check for dirty bit! node = existing; } } } finally { if (logSql) { long logTimeStop = System.currentTimeMillis(); logSqlStatement("SQL SELECT_BYRELATION", dbm.getTableName(), logTimeStart, logTimeStop, query); } if (stmt != null) { try { stmt.close(); } catch (Exception ex) { app.logError(ErrorReporter.errorMsg(this.getClass(), "getNodeByRelation"), ex); } } } } if ((node != null) && (updateTypeDirty)) { node.setTypeDirty(false); } return node; } /** * Create a new Node from a ResultSet. */ public Node createNode(DbMapping dbm, ResultSet rs, DbColumn[] columns, int offset) throws SQLException, IOException, ClassNotFoundException { HashMap propBuffer = new HashMap(); String id = null; String name = null; String protoName = dbm.getTypeName(); DbMapping dbmap = dbm; Node node = new Node(); for (int i = 0; i < columns.length; i++) { // set prototype? if (columns[i].isPrototypeField()) { protoName = rs.getString(i + 1 + offset); if (protoName != null) { dbmap = getDbMapping(protoName); if (dbmap == null) { // invalid prototype name! app.logError(ErrorReporter.errorMsg(this.getClass(), "createNode") + "Invalid prototype name: " + protoName + " - using default"); dbmap = dbm; protoName = dbmap.getTypeName(); } } } // set id? if (columns[i].isIdField()) { id = rs.getString(i + 1 + offset); // if id == null, the object doesn't actually exist - return null if (id == null) { return null; } } // set name? if (columns[i].isNameField()) { name = rs.getString(i + 1 + offset); } Property newprop = new Property(node); switch (columns[i].getType()) { case Types.BIT: newprop.setBooleanValue(rs.getBoolean(i + 1 + offset)); break; case Types.TINYINT: case Types.BIGINT: case Types.SMALLINT: case Types.INTEGER: newprop.setIntegerValue(rs.getLong(i + 1 + offset)); break; case Types.REAL: case Types.FLOAT: case Types.DOUBLE: newprop.setFloatValue(rs.getDouble(i + 1 + offset)); break; case Types.DECIMAL: case Types.NUMERIC: BigDecimal num = rs.getBigDecimal(i + 1 + offset); if (num == null) { break; } if (num.scale() > 0) { newprop.setFloatValue(num.doubleValue()); } else { newprop.setIntegerValue(num.longValue()); } break; case Types.VARBINARY: case Types.BINARY: // newprop.setStringValue(rs.getString(i+1+offset)); newprop.setJavaObjectValue(rs.getBytes(i + 1 + offset)); break; case Types.LONGVARBINARY: { InputStream in = rs.getBinaryStream(i + 1 + offset); if (in == null) { break; } ByteArrayOutputStream bout = new ByteArrayOutputStream(); byte[] buffer = new byte[2048]; int read; while ((read = in.read(buffer)) > -1) { bout.write(buffer, 0, read); } newprop.setJavaObjectValue(bout.toByteArray()); } break; case Types.LONGVARCHAR: try { newprop.setStringValue(rs.getString(i + 1 + offset)); } catch (SQLException x) { Reader in = rs.getCharacterStream(i + 1 + offset); char[] buffer = new char[2048]; int read = 0; int r; while ((r = in.read(buffer, read, buffer.length - read)) > -1) { read += r; if (read == buffer.length) { // grow input buffer char[] newBuffer = new char[buffer.length * 2]; System.arraycopy(buffer, 0, newBuffer, 0, buffer.length); buffer = newBuffer; } } newprop.setStringValue(new String(buffer, 0, read)); } break; case Types.CHAR: case Types.VARCHAR: case Types.OTHER: newprop.setStringValue(rs.getString(i + 1 + offset)); break; case Types.DATE: case Types.TIME: case Types.TIMESTAMP: newprop.setDateValue(rs.getTimestamp(i + 1 + offset)); break; case Types.NULL: newprop.setStringValue(null); break; case Types.CLOB: Clob cl = rs.getClob(i + 1 + offset); if (cl == null) { newprop.setStringValue(null); break; } char[] c = new char[(int) cl.length()]; Reader isr = cl.getCharacterStream(); isr.read(c); newprop.setStringValue(String.copyValueOf(c)); break; default: newprop.setStringValue(rs.getString(i + 1 + offset)); break; } if (rs.wasNull()) { newprop.setStringValue(null); } propBuffer.put(columns[i].getName(), newprop); // mark property as clean, since it's fresh from the db newprop.dirty = false; } if (id == null) { return null; } Hashtable propMap = new Hashtable(); DbColumn[] columns2 = dbmap.getColumns(); for (int i = 0; i < columns2.length; i++) { Relation rel = columns2[i].getRelation(); if (rel != null && (rel.reftype == Relation.PRIMITIVE || rel.reftype == Relation.REFERENCE)) { Property prop = (Property) propBuffer.get(columns2[i].getName()); if (prop == null) { continue; } prop.setName(rel.propName); // if the property is a pointer to another node, change the property type to NODE if ((rel.reftype == Relation.REFERENCE) && rel.usesPrimaryKey()) { // FIXME: References to anything other than the primary key are not supported prop.convertToNodeReference(rel.otherType, this.app.getCurrentRequestEvaluator().getLayer()); } propMap.put(rel.propName.toLowerCase(), prop); } } node.init(dbmap, id, name, protoName, propMap, safe); return node; } /** * Fetch nodes that are fetched additionally to another node via join. */ public void fetchJoinedNodes(ResultSet rs, Relation[] joins, int offset) throws ClassNotFoundException, SQLException, IOException { int resultSetOffset = offset; // create joined objects for (int i = 0; i < joins.length; i++) { DbMapping jdbm = joins[i].otherType; Node node = createNode(jdbm, rs, jdbm.getColumns(), resultSetOffset); if (node != null) { Key primKey = node.getKey(); // register new nodes with the cache. If an up-to-date copy // existed in the cache, use that. synchronized (cache) { Node oldnode = (Node) cache.put(primKey, node); if ((oldnode != null) && (oldnode.getState() != INode.INVALID)) { // found an ok version in the cache, use it. cache.put(primKey, oldnode); } } } resultSetOffset += jdbm.getColumns().length; } } /** * Get a DbMapping for a given prototype name. This is just a proxy * method to the app's getDbMapping() method. */ public DbMapping getDbMapping(String protoname) { return app.getDbMapping(protoname); } // a utility method to escape single quotes private String escape(String str) { if (str == null) { return null; } if (str.indexOf("'") < 0) { return str; } int l = str.length(); StringBuffer sbuf = new StringBuffer(l + 10); for (int i = 0; i < l; i++) { char c = str.charAt(i); if (c == '\'') { sbuf.append('\''); } sbuf.append(c); } return sbuf.toString(); } /** * Get an array of the the keys currently held in the object cache */ public Object[] getCacheEntries() { return null; } /** * Get the number of elements in the object cache */ public int countCacheEntries() { return cache.size(); } /** * Clear the object cache, causing all objects to be recreated. */ public void clearCache() { synchronized (cache) { cache.clear(); } } /** * Add a listener that is notified each time a transaction commits * that adds, modifies or deletes any Nodes. */ public void addNodeChangeListener(NodeChangeListener listener) { listeners.add(listener); } /** * Remove a previously added NodeChangeListener. */ public void removeNodeChangeListener(NodeChangeListener listener) { listeners.remove(listener); } /** * Let transactors know if they should collect and fire NodeChangeListener * events */ protected boolean hasNodeChangeListeners() { return listeners.size() > 0; } /** * Called by transactors after committing. */ protected void fireNodeChangeEvent(List inserted, List updated, List deleted, List parents) { int l = listeners.size(); for (int i = 0; i < l; i++) { try { ((NodeChangeListener) listeners.get(i)).nodesChanged(inserted, updated, deleted, parents); } catch (Error e) { e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } } } private void setStatementValues(PreparedStatement stmt, int stmtNumber, Property p, int columnType) throws SQLException { if (p.getValue() == null) { stmt.setNull(stmtNumber, columnType); } else { switch (columnType) { case Types.BIT: case Types.TINYINT: case Types.BIGINT: case Types.SMALLINT: case Types.INTEGER: stmt.setLong(stmtNumber, p.getIntegerValue()); break; case Types.REAL: case Types.FLOAT: case Types.DOUBLE: case Types.NUMERIC: case Types.DECIMAL: stmt.setDouble(stmtNumber, p.getFloatValue()); break; case Types.VARBINARY: case Types.BINARY: case Types.BLOB: stmt.setString(stmtNumber, p.getStringValue()); break; case Types.LONGVARBINARY: case Types.LONGVARCHAR: try { stmt.setString(stmtNumber, p.getStringValue()); } catch (SQLException x) { String str = p.getStringValue(); Reader r = new StringReader(str); stmt.setCharacterStream(stmtNumber, r, str.length()); } break; case Types.CLOB: String val = p.getStringValue(); Reader isr = new StringReader(val); stmt.setCharacterStream(stmtNumber, isr, val.length()); break; case Types.CHAR: case Types.VARCHAR: case Types.OTHER: stmt.setString(stmtNumber, p.getStringValue()); break; case Types.DATE: case Types.TIME: case Types.TIMESTAMP: stmt.setTimestamp(stmtNumber, p.getTimestampValue()); break; case Types.NULL: stmt.setNull(stmtNumber, 0); break; default: stmt.setString(stmtNumber, p.getStringValue()); break; } } } private void logSqlStatement(String type, String table, long logTimeStart, long logTimeStop, String statement) { // init sql-log if necessary if (sqlLog == null) { String sqlLogName = app.getProperty("sqlLog", "axiom." + app.getName() + ".sql"); sqlLog = LogFactory.getLog(sqlLogName); } sqlLog.info(new StringBuffer().append(type).append(" ").append(table).append(" ") .append((logTimeStop - logTimeStart)).append(": ").append(statement).toString()); } private boolean isValidInCache(Key key) { final int layer = key.getLayer(); if (layer > DbKey.LIVE_LAYER) { RequestEvaluator reqeval = this.app.getCurrentRequestEvaluator(); if (reqeval != null) { Object[] domains = this.app.getDomainsForLayer(layer); final String id = key.getID(); for (int i = 0; i < domains.length; i++) { if (!reqeval.getSession().isDraftIdOn(id, (String) domains[i], layer)) { return false; } } } } return true; } public Node getNodeFromTransaction(Key key) { Transactor tx = (Transactor) Thread.currentThread(); Node node = tx.getVisitedNode(key); if ((node != null) && (node.getState() != Node.INVALID)) { return node; } return null; } public Node getNodeFromCache(Key key) { Node node = (Node) cache.get(key); long now = System.currentTimeMillis(); if (node != null && node.dbmap != null && node.dbmap.timeout != -1L && node.dbmap.timeout < (now - node.created)) { cache.remove(node.getKey()); return null; } if ((node != null) && (node.getState() != Node.INVALID)) { return node; } return null; } public Node conditionalCacheUpdate(Node node) { if (node != null) { // synchronize with cache synchronized (cache) { Key k = node.getKey(); Node oldnode = (Node) cache.put(k, node); if ((oldnode != null) && !oldnode.isNullNode() && (oldnode.getState() != Node.INVALID)) { cache.put(k, oldnode); node = oldnode; } } // end of cache-synchronized section } return node; } public void conditionalNodeVisit(Key key, Node node) { Transactor tx = (Transactor) Thread.currentThread(); if (node != null) { tx.visitCleanNode(key, node); } } public int getCurrentCacheSize() { return this.cache.size(); } public Node getNodeInLayer(Node node, final int mode) throws Exception { if (app.debug()) app.logEvent("NodeManager.getNodeInLayer() for " + node.logString() + " on layer " + mode); DbKey dkey = new DbKey(node.getDbMapping(), node.getID(), mode); Node nnode = null; try { nnode = getNode(dkey, false); } catch (Exception ex) { nnode = null; } Node parent = (Node) node.getParent(); if (parent != null) { Node nparent = getNodeInLayerDontCreate(parent, mode); if (nparent != null) { parent = nparent; } } if (nnode != null) { if (app.debug()) app.logEvent("NodeManager.getNodeInLayer(), getting node " + nnode.logString() + ", hashcode = " + nnode.hashCode() + ", state = " + nnode.getState() + ", id = " + nnode.getString("id") + ", layer = " + nnode.getLayer() + ", layerinStorage = " + nnode.getLayerInStorage() + " by thread " + Thread.currentThread()); final int state = nnode.getState(); if (state == Node.DELETED || (state == Node.MODIFIED && nnode.getParent() == null)) { nnode.checkWriteLock(); nnode.markAs(Node.MODIFIED); nnode.setParent(parent); nnode.cloneProperties(node); } return nnode; } // draft node in the requested layer does not already exist, so create one // and return it nnode = new Node(node.getName(), node.getID(), node.getPrototype(), safe, node.created, node.lastmodified); nnode.getKey(mode); nnode.setLayer(mode); nnode.setLayerInStorage(mode); nnode.setParent(parent); nnode.setSubnodes(node.getSubnodeList()); nnode.cloneProperties(node); nnode.markAs(Node.NEW); if (app.debug()) app.logEvent("NodeManager.getNodeInLayer(), created new node " + nnode.logString() + ", hashcode = " + nnode.hashCode() + ", id = " + nnode.getString("id") + " by thread " + Thread.currentThread()); // synchronize with cache synchronized (cache) { Node oldnode = (Node) cache.put(nnode.getKey(), nnode); if ((oldnode != null) && !oldnode.isNullNode() && (oldnode.getState() != Node.INVALID)) { cache.put(oldnode.getKey(), oldnode); nnode = oldnode; } } // end of cache-synchronized section return nnode; } protected Node getNodeInLayerDontCreate(Node node, final int mode) { DbKey dkey = new DbKey(node.getDbMapping(), node.getID(), mode); Node nnode = null; try { nnode = getNode(dkey, false); } catch (Exception ex) { nnode = null; } return nnode; } public void deleteNodeInLayer(Node node, final int mode) { if (app.debug()) app.logEvent("NodeManager.deleteNodeInLayer() for " + node.logString() + " on layer " + mode); DbKey dkey = new DbKey(node.getDbMapping(), node.getID(), mode); Node nnode = null; try { nnode = getNode(dkey, false); } catch (Exception ex) { nnode = null; } if (nnode != null) { nnode.checkWriteLock(); if (app.debug()) app.logEvent("NodeManager.deleteNodeInLayer(), setting delete status on " + nnode.logString() + ", hashcode = " + nnode.hashCode() + ", state = " + nnode.getState() + ", id = " + nnode.getString("id") + " by thread " + Thread.currentThread()); nnode.markAs(Node.DELETED); INode parent = nnode.getParent(); if (parent != null) { parent.removeNode(nnode); } nnode.setParentHandle(null); } if (app.debug()) app.logEvent("NodeManager.deleteNodeInLayer(), evict key = " + dkey); this.evictKey(dkey); } public void saveNodeInLayer(Node node, final int layer) { Key oldkey = node.getKey(); Transactor tx = (Transactor) Thread.currentThread(); IDatabase db = this.getDatabaseForMapping(node.dbmap); ITransaction txn = tx.getTransaction(db); DbKey dkey = new DbKey(node.dbmap, node.getID(), layer); node.setKey(dkey); node.updateLayersOnReferences(layer); tx.visitCleanNode(dkey, node); Node dbNode = null; try { if (db instanceof LuceneDatabase) { dbNode = ((LuceneDatabase) db).getLuceneManager().retrieveFromIndexFixedMode(dkey.getID(), dkey.getLayer(), true); } String newPath = AxiomObject.getPath(node, node.getPrototype(), this.app); String oldPath = null; if (dbNode != null) { oldPath = AxiomObject.getPath(dbNode, dbNode.getPrototype(), this.app); } if (newPath != null && oldPath != null && !newPath.equals(oldPath)) { node.hasPathChanged = true; if (this.app.debug()) this.app.logEvent("Node.saveNodeInLayer(): Path has changed on " + node.logString()); } } catch (Exception ignore) { // object not found in current layer, ignore } try { INode oldnode = null; try { oldnode = ((LuceneDatabase) db).getLuceneManager().retrieveFromIndexFixedMode(oldkey.getID(), oldkey.getLayer(), true); } catch (Exception ex) { oldnode = null; } if (oldnode != null) { db.deleteNode(txn, oldkey.getID(), oldkey.getLayer()); tx.deleteFromPathIndices(oldkey); } } catch (Exception ex) { this.app.logError( ErrorReporter.errorMsg(this.getClass(), "saveNodeInLayer") + ": Could not delete " + oldkey, ex); } node.checkWriteLock(); if (dbNode != null) { node.markAs(Node.MODIFIED); } else { node.markAs(Node.NEW); } // synchronize with cache synchronized (cache) { this.evictNodeByKey(oldkey); Node oldnode = (Node) cache.put(node.getKey(), node); if ((oldnode != null) && !oldnode.isNullNode() && (oldnode.getState() != Node.INVALID)) { cache.put(oldnode.getKey(), oldnode); } } // end of cache-synchronized section } public IDatabase getDatabaseForMapping(DbMapping dbm) { IDatabase db = null; if (dbm != null) { String className = dbm.getClassName(); if (className != null) { db = this.dbs.get(className); } } if (db == null) { db = this.defaultDb; } return db; } public IDatabase getDefaultDb() { return this.defaultDb; } public ArrayList<IDatabase> getDatabases() { return new ArrayList<IDatabase>(this.dbs.values()); } public void evictKeys(HashSet<Key> keyset) { synchronized (cache) { Iterator<Key> keys = keyset.iterator(); while (keys.hasNext()) { this.evictNodeByKey(keys.next()); } } } }