/********************************************************************************** * $URL: https://source.sakaiproject.org/svn/kernel/trunk/kernel-util/src/main/java/org/sakaiproject/util/BaseDbDoubleStorage.java $ * $Id: BaseDbDoubleStorage.java 66393 2009-09-10 08:19:24Z david.horwitz@uct.ac.za $ *********************************************************************************** * * Copyright (c) 2005, 2006, 2007, 2008 Sakai Foundation * * Licensed under the Educational Community License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.opensource.org/licenses/ECL-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * **********************************************************************************/ package org.sakaiproject.util; import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.util.Hashtable; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Stack; import java.util.Vector; import org.apache.commons.lang.ArrayUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.sakaiproject.db.api.SqlReader; import org.sakaiproject.db.api.SqlReaderFinishedException; import org.sakaiproject.db.api.SqlService; import org.sakaiproject.entity.api.Edit; import org.sakaiproject.entity.api.Entity; import org.sakaiproject.entity.api.ResourceProperties; import org.sakaiproject.event.cover.UsageSessionService; import org.sakaiproject.javax.Filter; import org.sakaiproject.javax.Order; import org.sakaiproject.javax.PagingPosition; import org.sakaiproject.javax.Search; import org.sakaiproject.javax.SearchFilter; import org.sakaiproject.time.api.Time; import org.sakaiproject.time.cover.TimeService; import org.w3c.dom.Document; import org.w3c.dom.Element; /** * <p> * BaseDbDoubleStorage is a class that stores collections of Resources (of some type) in a database, <br /> * provides locked access, and generally implements a services "storage" class. The <br /> * service's storage class can extend this to provide covers to turn Resource and <br /> * Edit into something more type specific to the service. * </p> * <p> * Note: the methods here are all "id" based, with the following assumptions: * <ul> * <li>just the Resource Id field is enough to distinguish one Resource from another (or, for resource, the container's id and the resource id).</li> * <li>a resource's reference is based on no more than the resource id, for containers </li> * <li>and no more than resource and container id for resources</li> * <li>a resource's id and container id cannot change</li> * </ul> * </p> * <br /> * In order to handle Unicode characters properly, the SQL statements executed by this class should not embed Unicode characters into the SQL * statement text; <br /> * rather, Unicode values should be inserted as fields in a PreparedStatement. Databases handle Unicode better in fields. * </p> */ public class BaseDbDoubleStorage { /** Our logger. */ private static Log M_log = LogFactory.getLog(BaseDbDoubleStorage.class); /** Table name for container records. */ protected String m_containerTableName = null; /** The field in the table that holds the container id. */ protected String m_containerTableIdField = null; /** Table name for resource records. */ protected String m_resourceTableName = null; /** The field in the resource table that holds the resource id. */ protected String m_resourceTableIdField = null; /** The field in the resource table that holds the container id. */ protected String m_resourceTableContainerIdField = null; /** The additional field names in the resource table that go between the two ids and the xml. */ protected String[] m_resourceTableOtherFields = null; /** The string searchable field names in the resource table. This must be either null * (i.e. no fields) or this is assumed to be the only fields which participate in search. */ protected String[] m_resourceTableSearchFields = null; /** The field name in the resource table for ordering. */ protected String m_resourceTableOrderField = null; /** The xml tag name for the element holding each actual resource entry. */ protected String m_resourceEntryTagName = null; /** The xml tag name for the element holding each actual container entry. */ protected String m_containerEntryTagName = null; /** The field in the record that has the user id of the resource owner. */ protected String m_resourceTableOwnerField = null; /** The field in the record that has the draft indicator ('0' for no, '1' for yes). */ protected String m_resourceTableDraftField = null; /** The field in the record that has the pubview indicator ('0' for no, '1' for yes). */ protected String m_resourceTablePubViewField = null; /** If true, we do our locks in the remote database. */ protected boolean m_locksAreInDb = true; /** If true, we do our locks in the remove database using a separate locking table. */ protected boolean m_locksAreInTable = true; /** The StorageUser to callback for new Resource and Edit objects. */ protected DoubleStorageUser m_user = null; /** * Locks, keyed by reference, holding Connections (or, if locks are done locally, holding an Edit). Note: keying by reference allows botu * container and resource locks to be stored, the reference distinguishes them. */ protected Hashtable m_locks = null; /** For container, the extra field is (no longer used) NEXT_ID */ protected static final String[] M_containerExtraFields = {"NEXT_ID"}; /** Injected (by constructor) SqlService. */ protected SqlService m_sql = null; /** contains a map of the database dependent handlers. */ protected static Map<String, DoubleStorageSql> databaseBeans; /** The db handler we are using. */ protected DoubleStorageSql doubleStorageSql; public void setDatabaseBeans(Map databaseBeans) { this.databaseBeans = databaseBeans; } /** * sets which bean containing database dependent code should be used depending on the database vendor. */ public void setDoubleStorageSql(String vendor) { this.doubleStorageSql = (databaseBeans.containsKey(vendor) ? databaseBeans.get(vendor) : databaseBeans.get("default")); } // since spring is not used and this class is instatiated directly, we need to "inject" these values ourselves static { databaseBeans = new Hashtable<String, DoubleStorageSql>(); databaseBeans.put("default", new DoubleStorageSqlDefault()); databaseBeans.put("hsqldb", new DoubleStorageSqlHSql()); databaseBeans.put("mysql", new DoubleStorageSqlMySql()); databaseBeans.put("oracle", new DoubleStorageSqlOracle()); } /** * Construct. * * @param containerTableName * Table name for containers. * @param containerTableIdField * The field in the container table that holds the id. * @param resourceTableName * Table name for resources. * @param resourceTableIdField * The field in the resource table that holds the id. * @param resourceTableContainerIdField * The field in the resource table that holds the container id. * @param resourceTableOrderField * The field in the resource table that is used for ordering results. * @param resourceTableOtherFields * The other fields in the resource table (between the two id fields and the xml field). * @param locksInDb * If true, we do our locks in the remote database, otherwise we do them here. * @param containerEntryName * The xml tag name for the element holding each actual container entry. * @param resourceEntryName * The xml tag name for the element holding each actual resource entry. * @param user * The StorageUser class to call back for creation of Resource and Edit objects. * @param sqlService * The SqlService. */ public BaseDbDoubleStorage(String containerTableName, String containerTableIdField, String resourceTableName, String resourceTableIdField, String resourceTableContainerIdField, String resourceTableOrderField, String resourceTableOwnerField, String resourceTableDraftField, String resourceTablePubViewField, String[] resourceTableOtherFields, String[] resourceTableSearchFields, boolean locksInDb, String containerEntryName, String resourceEntryName, DoubleStorageUser user, SqlService sqlService) { m_containerTableName = containerTableName; m_containerTableIdField = containerTableIdField; m_resourceTableName = resourceTableName; m_resourceTableIdField = resourceTableIdField; m_resourceTableContainerIdField = resourceTableContainerIdField; m_resourceTableOrderField = resourceTableOrderField; m_resourceTableOtherFields = resourceTableOtherFields; m_resourceTableSearchFields = resourceTableSearchFields; m_locksAreInDb = locksInDb; m_containerEntryTagName = containerEntryName; m_resourceEntryTagName = resourceEntryName; m_resourceTableOwnerField = resourceTableOwnerField; m_resourceTableDraftField = resourceTableDraftField; m_resourceTablePubViewField = resourceTablePubViewField; m_user = user; m_sql = sqlService; setDoubleStorageSql(m_sql.getVendor()); } /** Backwards compatibility constructor for using DbDouble without search fields */ public BaseDbDoubleStorage(String containerTableName, String containerTableIdField, String resourceTableName, String resourceTableIdField, String resourceTableContainerIdField, String resourceTableOrderField, String resourceTableOwnerField, String resourceTableDraftField, String resourceTablePubViewField, String[] resourceTableOtherFields, // String[] resourceTableSearchFields, boolean locksInDb, String containerEntryName, String resourceEntryName, DoubleStorageUser user, SqlService sqlService) { m_containerTableName = containerTableName; m_containerTableIdField = containerTableIdField; m_resourceTableName = resourceTableName; m_resourceTableIdField = resourceTableIdField; m_resourceTableContainerIdField = resourceTableContainerIdField; m_resourceTableOrderField = resourceTableOrderField; m_resourceTableOtherFields = resourceTableOtherFields; m_resourceTableSearchFields = null; // resourceTableSearchFields; m_locksAreInDb = locksInDb; m_containerEntryTagName = containerEntryName; m_resourceEntryTagName = resourceEntryName; m_resourceTableOwnerField = resourceTableOwnerField; m_resourceTableDraftField = resourceTableDraftField; m_resourceTablePubViewField = resourceTablePubViewField; m_user = user; m_sql = sqlService; setDoubleStorageSql(m_sql.getVendor()); } /** * Open and be ready to read / write. */ public void open() { // setup for locks m_locks = new Hashtable(); } /** * Close. */ public void close() { if (!m_locks.isEmpty()) { M_log.warn("close(): locks remain!"); // %%% } m_locks.clear(); m_locks = null; } /** * Read one Container Resource from xml * * @param xml * An string containing the xml which describes the Container resource. * @return The Container Resource object created from the xml. */ protected Entity readContainer(String xml) { try { if ( m_user instanceof SAXEntityReader ) { SAXEntityReader sm_user = (SAXEntityReader) m_user; DefaultEntityHandler deh = sm_user.getDefaultHandler(sm_user.getServices()); StorageUtils.processString(xml, deh); return deh.getEntity(); } else { // read the xml Document doc = StorageUtils.readDocumentFromString(xml); // verify the root element Element root = doc.getDocumentElement(); if (!root.getTagName().equals(m_containerEntryTagName)) { M_log.warn("readContainer(): not = " + m_containerEntryTagName + " : " + root.getTagName()); return null; } // re-create a resource Entity entry = m_user.newContainer(root); return entry; } } catch (Exception e) { M_log.warn("readContainer(): "+e.getMessage()); M_log.info("readContainer(): ", e); return null; } } /** * Check if a Container by this id exists. * * @param ref * The container reference. * @return true if a Container by this id exists, false if not. */ public boolean checkContainer(String ref) { // just see if the record exists String sql = doubleStorageSql.getSelect1Sql(m_containerTableName, m_containerTableIdField); Object[] fields = new Object[1]; fields[0] = ref; List ids = m_sql.dbRead(sql, fields, null); return (!ids.isEmpty()); } /** * Get the Container with this id, or null if not found. * * @param ref * The container reference. * @return The Container with this id, or null if not found. */ public Entity getContainer(String ref) { Entity entry = null; // get the user from the db String sql = doubleStorageSql.getSelectXml2Sql(m_containerTableName, m_containerTableIdField); Object[] fields = new Object[1]; fields[0] = ref; List xml = m_sql.dbRead(sql, fields, null); if (!xml.isEmpty()) { // create the Resource from the db xml entry = readContainer((String) xml.get(0)); } return entry; } /** * Get all Containers. * * @return The list (Resource) of all Containers. */ public List getAllContainers() { List all = new Vector(); // read all users from the db String sql = doubleStorageSql.getSelectXml1Sql(m_containerTableName); // %%% order by... List xml = m_sql.dbRead(sql); // process all result xml into user objects if (!xml.isEmpty()) { for (int i = 0; i < xml.size(); i++) { Entity entry = readContainer((String) xml.get(i)); if (entry != null) all.add(entry); } } return all; } /** * Add a new Container with this id. * * @param ref * The container reference. * @return The locked Container object with this id, or null if the id is in use. */ public Edit putContainer(String ref) { // create one with just the id Entity entry = m_user.newContainer(ref); // form the XML and SQL for the insert Document doc = StorageUtils.createDocument(); entry.toXml(doc, new Stack()); String xml = StorageUtils.writeDocumentToString(doc); String statement = doubleStorageSql.getInsertSql(m_containerTableName, insertFields(m_containerTableIdField, null, M_containerExtraFields, "XML")); Object[] fields = new Object[2]; fields[0] = entry.getReference(); fields[1] = xml; // process the insert boolean ok = m_sql.dbWrite(statement, fields); // if this failed, assume a key conflict (i.e. id in use) if (!ok) return null; // now get a lock on the record for edit Edit edit = editContainer(ref); if (edit == null) { M_log.warn("putContainer(): didn't get a lock!"); return null; } return edit; } /** * Get a lock on the Container with this id, or null if a lock cannot be gotten. * * @param ref * The container reference. * @return The locked Container with this id, or null if this cannot be locked. */ public Edit editContainer(String ref) { Edit edit = null; if (m_locksAreInDb) { if ("oracle".equals(m_sql.getVendor())) { // read the record and get a lock on it (non blocking) String statement = doubleStorageSql.getSelectXml3Sql(m_containerTableName, m_containerTableIdField, StorageUtils.escapeSql(ref)); StringBuilder result = new StringBuilder(); Connection lock = m_sql.dbReadLock(statement, result); // for missing or already locked... if ((lock == null) || (result.length() == 0)) return null; // make first a Resource, then an Edit Entity entry = readContainer(result.toString()); edit = m_user.newContainerEdit(entry); // store the lock for this object m_locks.put(entry.getReference(), lock); } else { throw new UnsupportedOperationException("Record locking only available when configured with Oracle database"); } } // if the locks are in a separate table in the db else if (m_locksAreInTable) { // get, and return if not found Entity entry = getContainer(ref); if (entry == null) return null; // write a lock to the lock table - if we can do it, we get the lock String statement = doubleStorageSql.getInsertSql2(); // we need session id String sessionId = UsageSessionService.getSessionId(); if (sessionId == null) { sessionId = ""; } // collect the fields Object fields[] = new Object[4]; fields[0] = m_containerTableName; fields[1] = doubleStorageSql.getRecordId(ref); fields[2] = TimeService.newTime(); fields[3] = sessionId; // add the lock - if fails, someone else has the lock boolean ok = m_sql.dbWriteFailQuiet(null, statement, fields); if (!ok) { return null; } // make the edit from the Resource edit = m_user.newContainerEdit(entry); } // otherwise, get the lock locally else { // get, and return if not found Entity entry = getContainer(ref); if (entry == null) return null; // we only sync this getting - someone may release a lock out of sync synchronized (m_locks) { // if already locked if (m_locks.containsKey(entry.getReference())) return null; // make the edit from the Resource edit = m_user.newContainerEdit(entry); // store the edit in the locks by reference m_locks.put(entry.getReference(), edit); } } return edit; } /** * Commit the changes and release the lock. * * @param user * The Edit to commit. */ public void commitContainer(Edit edit) { // form the SQL statement and the var w/ the XML Document doc = StorageUtils.createDocument(); edit.toXml(doc, new Stack()); String xml = StorageUtils.writeDocumentToString(doc); String statement = doubleStorageSql.getUpdateSql(m_containerTableName, m_containerTableIdField); Object[] fields = new Object[2]; fields[0] = xml; fields[1] = edit.getReference(); if (m_locksAreInDb) { // use this connection that is stored with the lock Connection lock = (Connection) m_locks.get(edit.getReference()); if (lock == null) { M_log.warn("commitContainer(): edit not in locks"); return; } // update, commit, release the lock's connection m_sql.dbUpdateCommit(statement, fields, null, lock); // remove the lock m_locks.remove(edit.getReference()); } else if (m_locksAreInTable) { // process the update m_sql.dbWrite(statement, fields); // remove the lock statement = doubleStorageSql.getDeleteLocksSql(); // collect the fields Object lockFields[] = new Object[2]; lockFields[0] = m_containerTableName; lockFields[1] = doubleStorageSql.getRecordId(edit.getReference()); boolean ok = m_sql.dbWrite(statement, lockFields); if (!ok) { M_log.warn("commitContainer: missing lock for table: " + lockFields[0] + " key: " + lockFields[1]); } } else { // just process the update m_sql.dbWrite(statement, fields); // remove the lock m_locks.remove(edit.getReference()); } } /** * Cancel the changes and release the lock. * * @param user * The Edit to cancel. */ public void cancelContainer(Edit edit) { if (m_locksAreInDb) { // use this connection that is stored with the lock Connection lock = (Connection) m_locks.get(edit.getReference()); if (lock == null) { M_log.warn("cancelContainer(): edit not in locks"); return; } // rollback and release the lock's connection m_sql.dbCancel(lock); // release the lock m_locks.remove(edit.getReference()); } else if (m_locksAreInTable) { // remove the lock String statement = doubleStorageSql.getDeleteLocksSql(); // collect the fields Object lockFields[] = new Object[2]; lockFields[0] = m_containerTableName; lockFields[1] = doubleStorageSql.getRecordId(edit.getReference()); boolean ok = m_sql.dbWrite(statement, lockFields); if (!ok) { M_log.warn("cancelContainer: missing lock for table: " + lockFields[0] + " key: " + lockFields[1]); } } else { // release the lock m_locks.remove(edit.getReference()); } } /** * Remove this (locked) Container. * * @param user * The Edit to remove. */ public void removeContainer(Edit edit) { // form the SQL delete statement String statement = doubleStorageSql.getDeleteSql(m_containerTableName, m_containerTableIdField); Object[] fields = new Object[1]; fields[0] = edit.getReference(); if (m_locksAreInDb) { // use this connection that is stored with the lock Connection lock = (Connection) m_locks.get(edit.getReference()); if (lock == null) { M_log.warn("removeContainer(): edit not in locks"); return; } // process the delete statement, commit, and release the lock's connection m_sql.dbUpdateCommit(statement, fields, null, lock); // release the lock m_locks.remove(edit.getReference()); } else if (m_locksAreInTable) { // process the delete statement m_sql.dbWrite(statement, fields); // remove the lock statement = doubleStorageSql.getDeleteLocksSql(); // collect the fields Object lockFields[] = new Object[2]; lockFields[0] = m_containerTableName; lockFields[1] = doubleStorageSql.getRecordId(edit.getReference()); boolean ok = m_sql.dbWrite(statement, lockFields); if (!ok) { M_log.warn("remove: missing lock for table: " + lockFields[0] + " key: " + lockFields[1]); } } else { // process the delete statement m_sql.dbWrite(statement, fields); // release the lock m_locks.remove(edit.getReference()); } } /** * Read one Resource from xml * * @param container * The container for this resource. * @param xml * An string containing the xml which describes the resource. * @return The Resource object created from the xml. */ protected Entity readResource(Entity container, String xml) { try { if ( m_user instanceof SAXEntityReader ) { SAXEntityReader sm_user = (SAXEntityReader) m_user; DefaultEntityHandler deh = sm_user.getDefaultHandler(sm_user.getServices()); deh.setContainer(container); StorageUtils.processString(xml, deh); return deh.getEntity(); } else { // read the xml Document doc = StorageUtils.readDocumentFromString(xml); //The resulting doc could be null if (doc == null) { M_log.warn("null xml document passed to readResource for container" + container.getId()); return null; } // verify the root element Element root = doc.getDocumentElement(); if (!root.getTagName().equals(m_resourceEntryTagName)) { M_log.warn("readResource(): not = " + m_resourceEntryTagName + " : " + root.getTagName()); return null; } // re-create a resource Entity entry = m_user.newResource(container, root); return entry; } } catch (Exception e) { M_log.warn("readResource(): "+e.getMessage()); M_log.info("readResource(): ", e); return null; } } /** * Check if a Resource by this id exists. * * @param container * The container for this resource. * @param id * The id. * @return true if a Resource by this id exists, false if not. */ public boolean checkResource(Entity container, String id) { // just see if the record exists String sql = doubleStorageSql.getSelectIdSql(m_resourceTableName, m_resourceTableIdField, m_resourceTableContainerIdField); Object[] fields = new Object[2]; fields[0] = container.getReference(); fields[1] = id; List ids = m_sql.dbRead(sql, fields, null); return (!ids.isEmpty()); } /** * Get the Resource with this id, or null if not found. * * @param container * The container for this resource. * @param id * The id. * @return The Resource with this id, or null if not found. */ public Entity getResource(Entity container, String id) { Entity entry = null; // get the user from the db String sql = doubleStorageSql.getSelectXml4Sql(m_resourceTableName, m_resourceTableIdField, m_resourceTableContainerIdField); Object[] fields = new Object[2]; fields[0] = container.getReference(); fields[1] = id; List xml = m_sql.dbRead(sql, fields, null); if (!xml.isEmpty()) { // create the Resource from the db xml entry = readResource(container, (String) xml.get(0)); } return entry; } /** * Count all Resources * @param container * The container for this resource. */ public int getCount(Entity container) { // read With or without a filter String sql = doubleStorageSql.getCountSql(m_resourceTableName, m_resourceTableContainerIdField); Object[] fields = new Object[1]; fields[0] = container.getReference(); List countList = m_sql.dbRead(sql, fields, null); if ( countList.isEmpty() ) return 0; Object obj = countList.get(0); String str = (String) obj; return Integer.parseInt(str); } /** * Count all Resources - This takes two approaches depending * on whether this table has search fields. If searchfields * are available we can do a SELECT COUNT WHERE. Otherwise * we retrieve the records and run the filter on each record. * @param container * The container for this resource. * @param filter * A filter object to accept / reject the searches * @param search * Search string */ public int getCount(Entity container, Filter filter) { if ( filter == null ) return getCount(container); // System.out.println("getCount e="+container+" sf="+filter); // If we have search fields - do a quick select count with a where clause if ( m_resourceTableSearchFields != null && filter instanceof SearchFilter ) { int searchFieldCount = 0; String searchString = ((SearchFilter) filter).getSearchString(); if ( searchString != null && searchString.length() > 0 ) { String searchWhere = doubleStorageSql.getSearchWhereClause(m_resourceTableSearchFields); if ( searchWhere != null && searchWhere.length() > 0 ) { searchFieldCount = m_resourceTableSearchFields.length; String sql = doubleStorageSql.getCountSqlWhere(m_resourceTableName, m_resourceTableContainerIdField, searchWhere); Object[] fields = new Object[1+searchFieldCount]; fields[0] = container.getReference(); for ( int i=0; i < searchFieldCount; i++) fields[i+1] = "%" + searchString + "%"; List countList = m_sql.dbRead(sql, fields, null); if ( countList.isEmpty() ) return 0; Object obj = countList.get(0); String str = (String) obj; return Integer.parseInt(str); } } } // No search fields - retrieve, filter and count String sql = doubleStorageSql.getSelectXml5Sql(m_resourceTableName, m_resourceTableContainerIdField, null, false); Object[] fields = new Object[1]; fields[0] = container.getReference(); List all = m_sql.dbRead(sql, fields, new SearchFilterReader(container, filter, null, true)); int count = all.size(); return count; } /** * Get all Resources. * * @param container * The container for this resource. * @return The list (Resource) of all Resources. */ public List getAllResources(Entity container) { return getAllResources(container, null, null, true, null); } /** * Get all Resources. * * @param container * The container for this resource. * @param filter * conditional for select statement * @return The list (Resource) of all Resources. */ public List getAllResources(Entity container, Filter filter) { return getAllResources(container, filter, null, true, null); } /** * Get all Resources. * * @param container * The container for this resource. * @param sqlFilter * conditional for select statement * @return The list (Resource) of all Resources. */ public List getAllResources(Entity container, String sqlFilter) { return getAllResources(container, null, sqlFilter, true, null); } /** * Deal with the fact that we can get a PagingPosition from a query filter * or directly as a parameter. In time remove the option to use * PagingPosition. */ // TODO: Remove all methods with PagingPostition and switch to Filter private PagingPosition fixPagingPosition(Filter filter, PagingPosition pos) { if ( filter == null ) return pos; if ( pos != null ) { M_log.warn("The use of methods with PagingPosition should switch to using Search (SAK-13584) - Chuck"); return pos; } if ( filter instanceof Search ) { Search q = (Search) filter; if ( q.getLimit() > 0 && q.getLimit() >= q.getStart() ) { return new PagingPosition((int) q.getStart(), (int) q.getLimit()); } } return null; } /** * Get all Resources. * * @param container * The container for this resource. * @param softFilter * an optional software filter * @param sqlFilter * an optional conditional for select statement * @param asc * true means ascending * @param pager * an optional range of elements to return inclusive * @return The list (Resource) of all Resources. */ public List getAllResources(Entity container, Filter softFilter, String sqlFilter, boolean asc, PagingPosition pager) { return getAllResources(container, softFilter, sqlFilter, asc, pager, null); } /** * Get all Resources. * * @param container * The container for this resource. * @param softFilter * an optional software filter * @param sqlFilter * an optional conditional for select statement * @param asc * true means ascending * @param pager * an optional range of elements to return inclusive * @param bindVariables * an optional list of bind variables * @return The list (Resource) of all Resources. */ public List getAllResources(Entity container, Filter softFilter, String sqlFilter, boolean asc, PagingPosition pager, List <Object> bindVariables) { pager = fixPagingPosition(softFilter, pager); // System.out.println("getAllResources e="+container+" sf="+softFilter+" sqlf="+sqlFilter+" asc="+asc+" pag="+pager); // Get the orders and get the ORDER BY clause Order[] orders = null; if ( softFilter instanceof Search ) { orders = ((Search) softFilter).getOrders(); } String orderString = doubleStorageSql.getOrderClause(orders, m_resourceTableOrderField, asc); // Turn the search string into a WHERE clause if we can int searchFieldCount = 0; String searchString = null; if ( m_resourceTableSearchFields != null && softFilter instanceof SearchFilter ) { searchString = ((SearchFilter) softFilter).getSearchString(); if ( searchString != null && searchString.length() > 0 ) { String searchWhere = doubleStorageSql.getSearchWhereClause(m_resourceTableSearchFields); if ( searchWhere != null && searchWhere.length() > 0 ) { if (sqlFilter == null ) { sqlFilter = searchWhere; } else { sqlFilter = sqlFilter + " and " + searchWhere ; } searchFieldCount = m_resourceTableSearchFields.length; } } } String sql = doubleStorageSql.getSelectXml5filterSql(m_resourceTableName, m_resourceTableContainerIdField, orderString, sqlFilter); // Add Paging to the Search if requested // TODO: Someday make this think Filter and emulate PagingPosition boolean pagedInSql = false; if ( pager != null ) { String limitedSql = doubleStorageSql.addLimitToQuery(sql, pager.getFirst()-1, pager.getLast()-1); if ( limitedSql != null ) { pagedInSql = true; sql = limitedSql; } else { // We don't subtract 1 because TOP is a count, not zero based like LIMIT String topSql = doubleStorageSql.addTopToQuery(sql, pager.getLast()); if ( topSql != null ) { sql = topSql; } } } Object[] fields = new Object[1+searchFieldCount]; fields[0] = container.getReference(); for ( int i=0; i < searchFieldCount; i++) fields[i+1] = "%" + searchString + "%"; if (bindVariables != null && bindVariables.size() > 0) { // Add the bind variables to the fields to substitute in the prepared statement fields = ArrayUtils.addAll(fields, bindVariables.toArray(new Object[fields.length])); } // System.out.println("getAllResources="+sql); // If we are paged in SQL - then do not pass in the pager List all = m_sql.dbRead(sql, fields, new SearchFilterReader(container, softFilter, pagedInSql ? null : pager, false)); return all; } /** matchXml - Perform an optional pre-de-serialize match if desired * * This is just a dummy implementation - this wil be overridden in the * class that extends this class if a particular storage wants to take * advantage of this feature. * * Return value: * -1 indicates - definite "no" * 0 indicates - maybe - continue and parse the Xml * 1 indicates - "yes" - we know in this rouinte this is a match */ public int matchXml(String xml, String search) { return 0; } public class SearchFilterReader implements SqlReader { private Filter m_filter; private String m_search = null; private PagingPosition m_pager; private Entity m_container; private boolean m_doCount = false; private int count = 0; // If we are only counting - return a tiny thing - not a big thing private final Integer intValue = 1; public SearchFilterReader(Entity container, Filter filter, PagingPosition pager, boolean doCount) { m_container = container; m_filter = filter; if ( filter instanceof SearchFilter ) m_search = ( (SearchFilter) filter).getSearchString(); m_pager = pager; m_doCount = doCount; } public Object readSqlResultRecord(ResultSet result) throws SqlReaderFinishedException { try { String theXml = result.getString(1); if ( m_pager != null && count > m_pager.getLast() ) { throw new SqlReaderFinishedException(); } int iTest = 0; // Don't know if we have a match if ( m_search != null ) { iTest = matchXml(theXml, m_search); } // If it is clearly rejected from pre-parse match if ( iTest == -1 ) return null; // If it is a match and we are just counting - no parsing // needed if ( iTest == 1 && m_doCount ) return intValue; // If it is known to be accepted (1) or unsure (0), // parse the Xml and continue Entity entry = readResource(m_container, theXml); if ( entry == null ) return null; // If there is no indication from matchXml if ( iTest == 0 && m_search != null) { if ( ! m_filter.accept(entry) ) return null; } count++; if ( m_pager != null && count < m_pager.getFirst() ) return null; if ( m_pager != null && count > m_pager.getLast() ) { throw new SqlReaderFinishedException(); } if ( m_doCount ) return intValue; return entry; } catch (SQLException ignore) { return null; } } } /** * Add a new Resource with this id. * * @param container * The container for this resource. * @param id * The id. * @param others * Other fields for the newResource call * @return The locked Resource object with this id, or null if the id is in use. */ public Edit putResource(Entity container, String id, Object[] others) { // create one with just the id, and perhaps some other fields, too Entity entry = m_user.newResource(container, id, others); // form the XML and SQL for the insert Document doc = StorageUtils.createDocument(); entry.toXml(doc, new Stack()); String xml = StorageUtils.writeDocumentToString(doc); String statement = doubleStorageSql.getInsertSql3(m_resourceTableName, insertFields(m_containerTableIdField, m_resourceTableIdField, m_resourceTableOtherFields, "XML"), valuesParams(m_resourceTableOtherFields)); Object[] flds = m_user.storageFields(entry); if (flds == null) flds = new Object[0]; Object[] fields = new Object[flds.length + 3]; System.arraycopy(flds, 0, fields, 2, flds.length); fields[0] = container.getReference(); fields[1] = entry.getId(); fields[fields.length - 1] = xml; // process the insert boolean ok = m_sql.dbWrite(statement, fields); // if this failed, assume a key conflict (i.e. id in use) if (!ok) return null; // now get a lock on the record for edit Edit edit = editResource(container, id); if (edit == null) { M_log.warn("putResource(): didn't get a lock!"); return null; } return edit; } /** * Get a lock on the Resource with this id, or null if a lock cannot be gotten. * * @param container * The container for this resource. * @param id * The user id. * @return The locked Resource with this id, or null if this records cannot be locked. */ public Edit editResource(Entity container, String id) { Edit edit = null; if (m_locksAreInDb) { if ("oracle".equals(m_sql.getVendor())) { // read the record and get a lock on it (non blocking) String statement = doubleStorageSql.getSelectXml6Sql(m_resourceTableName, m_resourceTableIdField, m_resourceTableContainerIdField, StorageUtils.escapeSql(id), StorageUtils.escapeSql(container.getReference())); StringBuilder result = new StringBuilder(); Connection lock = m_sql.dbReadLock(statement, result); // for missing or already locked... if ((lock == null) || (result.length() == 0)) return null; // make first a Resource, then an Edit Entity entry = readResource(container, result.toString()); edit = m_user.newResourceEdit(container, entry); // store the lock for this object m_locks.put(entry.getReference(), lock); } else { throw new UnsupportedOperationException("Record locking only available when configured with Oracle database"); } } // if the locks are in a separate table in the db else if (m_locksAreInTable) { // get the entry, and check for existence Entity entry = getResource(container, id); if (entry == null) return null; // write a lock to the lock table - if we can do it, we get the lock String statement = doubleStorageSql.getInsertSql2(); // we need session id and user id String sessionId = UsageSessionService.getSessionId(); if (sessionId == null) { sessionId = ""; } // collect the fields Object fields[] = new Object[4]; fields[0] = m_resourceTableName; fields[1] = doubleStorageSql.getRecordId(container.getReference() + "/" + id); fields[2] = TimeService.newTime(); fields[3] = sessionId; // add the lock - if fails, someone else has the lock boolean ok = m_sql.dbWriteFailQuiet(null, statement, fields); if (!ok) { return null; } // make the edit from the Resource edit = m_user.newResourceEdit(container, entry); } // otherwise, get the lock locally else { // get the entry, and check for existence Entity entry = getResource(container, id); if (entry == null) return null; // we only sync this getting - someone may release a lock out of sync synchronized (m_locks) { // if already locked if (m_locks.containsKey(entry.getReference())) return null; // make the edit from the Resource edit = m_user.newResourceEdit(container, entry); // store the edit in the locks by reference m_locks.put(entry.getReference(), edit); } } return edit; } /** * Commit the changes and release the lock. * * @param container * The container for this resource. * @param user * The Edit to commit. */ public void commitResource(Entity container, Edit edit) { // form the SQL statement and the var w/ the XML Document doc = StorageUtils.createDocument(); edit.toXml(doc, new Stack()); String xml = StorageUtils.writeDocumentToString(doc); String statement = doubleStorageSql.getUpdate2Sql(m_resourceTableName, m_resourceTableIdField, m_resourceTableContainerIdField, updateSet(m_resourceTableOtherFields)); Object[] flds = m_user.storageFields(edit); if (flds == null) flds = new Object[0]; Object[] fields = new Object[flds.length + 3]; System.arraycopy(flds, 0, fields, 0, flds.length); fields[fields.length - 3] = xml; fields[fields.length - 2] = container.getReference(); fields[fields.length - 1] = edit.getId(); if (m_locksAreInDb) { // use this connection that is stored with the lock Connection lock = (Connection) m_locks.get(edit.getReference()); if (lock == null) { M_log.warn("commitResource(): edit not in locks"); return; } // update, commit, release the lock's connection m_sql.dbUpdateCommit(statement, fields, null, lock); // remove the lock m_locks.remove(edit.getReference()); } else if (m_locksAreInTable) { // process the update m_sql.dbWrite(statement, fields); // remove the lock statement = doubleStorageSql.getDeleteLocksSql(); // collect the fields Object lockFields[] = new Object[2]; lockFields[0] = m_resourceTableName; lockFields[1] = doubleStorageSql.getRecordId(container.getReference() + "/" + edit.getId()); boolean ok = m_sql.dbWrite(statement, lockFields); if (!ok) { M_log.warn("commitResource: missing lock for table: " + lockFields[0] + " key: " + lockFields[1]); } } else { // just process the update m_sql.dbWrite(statement, fields); // remove the lock m_locks.remove(edit.getReference()); } } /** * Cancel the changes and release the lock. * * @param container * The container for this resource. * @param user * The Edit to cancel. */ public void cancelResource(Entity container, Edit edit) { if (m_locksAreInDb) { // use this connection that is stored with the lock Connection lock = (Connection) m_locks.get(edit.getReference()); if (lock == null) { M_log.warn("cancelResource(): edit not in locks"); return; } // rollback and release the lock's connection m_sql.dbCancel(lock); // release the lock m_locks.remove(edit.getReference()); } else if (m_locksAreInTable) { // remove the lock String statement = doubleStorageSql.getDeleteLocksSql(); // collect the fields Object lockFields[] = new Object[2]; lockFields[0] = m_resourceTableName; lockFields[1] = doubleStorageSql.getRecordId(container.getReference() + "/" + edit.getId()); boolean ok = m_sql.dbWrite(statement, lockFields); if (!ok) { M_log.warn("cancelResource: missing lock for table: " + lockFields[0] + " key: " + lockFields[1]); } } else { // release the lock m_locks.remove(edit.getReference()); } } /** * Remove this (locked) Resource. * * @param container * The container for this resource. * @param user * The Edit to remove. */ public void removeResource(Entity container, Edit edit) { // form the SQL delete statement String statement = doubleStorageSql.getDelete2Sql(m_resourceTableName, m_resourceTableIdField, m_resourceTableContainerIdField); Object[] fields = new Object[2]; fields[0] = container.getReference(); fields[1] = edit.getId(); if (m_locksAreInDb) { // use this connection that is stored with the lock Connection lock = (Connection) m_locks.get(edit.getReference()); if (lock == null) { M_log.warn("removeResource(): edit not in locks"); return; } // process the delete statement, commit, and release the lock's connection m_sql.dbUpdateCommit(statement, fields, null, lock); // release the lock m_locks.remove(edit.getReference()); } else if (m_locksAreInTable) { // process the delete statement m_sql.dbWrite(statement, fields); // remove the lock statement = doubleStorageSql.getDeleteLocksSql(); // collect the fields Object lockFields[] = new Object[2]; lockFields[0] = m_resourceTableName; lockFields[1] = doubleStorageSql.getRecordId(container.getReference() + "/" + edit.getId()); boolean ok = m_sql.dbWrite(statement, lockFields); if (!ok) { M_log.warn("removeResource: missing lock for table: " + lockFields[0] + " key: " + lockFields[1]); } } else { // process the delete statement m_sql.dbWrite(statement, fields); // release the lock m_locks.remove(edit.getReference()); } } /** * Form a string of n question marks with commas, for sql value statements, one for each item in the values array, or an empty string if null. * * @param values * The values to be inserted into the sql statement. * @return A sql statement fragment for the values part of an insert, one for each value in the array. */ protected String valuesParams(String[] fields) { if ((fields == null) || (fields.length == 0)) return ""; StringBuilder buf = new StringBuilder(); for (int i = 0; i < fields.length; i++) { buf.append(" ?,"); } return buf.toString(); } /** * Form a string of n name=?, for sql update set statements, one for each item in the values array, or an empty string if null. * * @param values * The values to be inserted into the sql statement. * @return A sql statement fragment for the values part of an insert, one for each value in the array. */ protected String updateSet(String[] fields) { if ((fields == null) || (fields.length == 0)) return ""; StringBuilder buf = new StringBuilder(); for (int i = 0; i < fields.length; i++) { buf.append(fields[i] + " = ?,"); } return buf.toString(); } /** * Form a string of (field, field, field), for sql insert statements, one for each item in the fields array, plus one before, and one after. * * @param before1 * The first field name. * @param before2 * (options) second field name. * @param values * The extra field names, in the middle. * @param after * The last field name. * @return A sql statement fragment for the insert fields. */ protected String insertFields(String before1, String before2, String[] fields, String after) { StringBuilder buf = new StringBuilder(); buf.append(" ("); buf.append(before1); buf.append(","); if (before2 != null) { buf.append(before2); buf.append(","); } if (fields != null) { for (int i = 0; i < fields.length; i++) { buf.append(fields[i] + ","); } } buf.append(after); buf.append(")"); return buf.toString(); } /** * Get resources filtered by date and count and drafts, in descending (latest first) order * * @param afterDate * if null, no date limit, else limited to only messages after this date. * @param limitedToLatest * if 0, no count limit, else limited to only the latest this number of messages. * @param draftsForId * how to handle drafts: null means no drafts, "*" means all, otherwise drafts only if created by this userId. * @param pubViewOnly * if true, include only messages marked pubview, else include any. * @return A list of Message objects that meet the criteria; may be empty */ public List getResources(final Entity container, Time afterDate, int limitedToLatest, String draftsForId, boolean pubViewOnly) { // if we are limiting, and are filtering out drafts or doing pubview, and don't have draft/owner/pubview support, filter here after boolean canLimit = true; boolean filterAfter = false; if ((limitedToLatest > 0) && ((((m_resourceTableDraftField == null) || (m_resourceTableOwnerField == null)) && (!"*".equals(draftsForId))) || ((m_resourceTablePubViewField == null) && pubViewOnly))) { canLimit = false; filterAfter = true; } StringBuilder buf = new StringBuilder(); int numFields = 1; // start the outer statement, later finished with a limiting clause if ((limitedToLatest > 0) && canLimit) { if ("oracle".equals(m_sql.getVendor())) { buf.append("select XML from ("); buf.append("select XML from " + m_resourceTableName); } else if ("mysql".equals(m_sql.getVendor())) { buf.append("select messages.XML from ("); buf.append("select XML from " + m_resourceTableName); } else // if ("hsqldb".equals(m_sql.getVendor())) { // according to SQL2000 specification (used by HSQLDB) the limit clause appears first buf.append("select limit 0 " + limitedToLatest + " XML from " + m_resourceTableName); } } else { buf.append("select XML from " + m_resourceTableName); } buf.append(" where (" + m_resourceTableContainerIdField + " = ?"); if ((m_resourceTableOrderField != null) && (afterDate != null)) { buf.append(" and " + m_resourceTableOrderField + " > ?"); numFields++; } // deal with drafts if we can if ((m_resourceTableDraftField != null) && (m_resourceTableOwnerField != null)) { // if draftsForId is null, we don't want any drafts if (draftsForId == null) { buf.append(" and " + m_resourceTableDraftField + " = '0'"); } // else a "*" means we take all drafts else if (!"*".equals(draftsForId)) { // we want only drafts if the owner field matches buf.append(" and ( " + m_resourceTableDraftField + " = '0' or " + m_resourceTableOwnerField + " = ? )"); numFields++; } } // pubview if ((m_resourceTablePubViewField != null) && pubViewOnly) { buf.append(" and " + m_resourceTablePubViewField + " = '1'"); } // close the where buf.append(")"); if (m_resourceTableOrderField != null) { buf.append(" order by " + m_resourceTableOrderField + " desc"); } boolean useLimitField = false; if ((limitedToLatest > 0) && canLimit) { if ("oracle".equals(m_sql.getVendor())) { buf.append(" ) where rownum <= ?"); numFields++; useLimitField = true; } else if ("mysql".equals(m_sql.getVendor())) { buf.append(" ) AS messages LIMIT " + limitedToLatest); useLimitField = false; } else // if ("hsqldb".equals(m_sql.getVendor())) { // the limit clause appears elsewhere in HSQLDB SQL statements, not here. } } // build up the fields Object fields[] = new Object[numFields]; fields[0] = container.getReference(); int pos = 1; if ((m_resourceTableOrderField != null) && (afterDate != null)) { fields[pos++] = afterDate; } if ((m_resourceTableDraftField != null) && (m_resourceTableOwnerField != null) && (draftsForId != null) && (!"*".equals(draftsForId))) { fields[pos++] = draftsForId; } if (useLimitField) { fields[pos++] = Integer.valueOf(limitedToLatest); } List all = m_sql.dbRead(buf.toString(), fields, new SqlReader() { public Object readSqlResultRecord(ResultSet result) { try { // get the xml and parse into a Resource String xml = result.getString(1); Entity entry = readResource(container, xml); return entry; } catch (SQLException ignore) { return null; } } }); // after filter for draft / pubview and limit if (filterAfter) { Vector v = new Vector(); // deal with drafts / pubview for (Iterator i = all.iterator(); i.hasNext();) { Entity r = (Entity) i.next(); Entity candidate = null; if (m_user.isDraft(r)) { // if some drafts if ((draftsForId != null) && (m_user.getOwnerId(r).equals(draftsForId))) { candidate = r; } } else { candidate = r; } // if we have a candidate to add, and we need pub view only if ((candidate != null) && pubViewOnly) { // if this is not pub view, skip it if ((candidate.getProperties().getProperty(ResourceProperties.PROP_PUBVIEW) == null)) { candidate = null; } } if (candidate != null) { v.add(candidate); } } // pick what we need if (limitedToLatest < v.size()) { all = v.subList(0, limitedToLatest); } else { all = v; } } return all; } /** * Access a list of container ids match (start with) the root. * * @param context * The id root to match. * @return A List (String) of container id which match the root. */ public List getContainerIdsMatching(String root) { // the id of each container will be the part that follows the root reference final int pos = root.length(); // read all users from the db String sql = doubleStorageSql.getSelect9Sql(m_containerTableName, m_containerTableIdField); Object fields[] = new Object[1]; fields[0] = root + "%"; List all = m_sql.dbRead(sql, fields, new SqlReader() { public Object readSqlResultRecord(ResultSet result) { try { // get the reference form and pull off the id (what follows after the root) String ref = result.getString(1); String id = ref.substring(pos); return id; } catch (SQLException ignore) { return null; } } }); return all; } }