org.sakaiproject.util.BaseDbDualSingleStorage.java Source code

Java tutorial

Introduction

Here is the source code for org.sakaiproject.util.BaseDbDualSingleStorage.java

Source

/**********************************************************************************
 * $URL: https://source.sakaiproject.org/svn/kernel/trunk/kernel-util/src/main/java/org/sakaiproject/util/BaseDbDualSingleStorage.java $
 * $Id: BaseDbDualSingleStorage.java 82133 2010-09-07 21:45:01Z aaronz@vt.edu $
 ***********************************************************************************
 *
 * Copyright (c) 2003, 2004, 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.ArrayList;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Vector;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.sakaiproject.db.api.SqlReader;
import org.sakaiproject.db.api.SqlService;
import org.sakaiproject.entity.api.Edit;
import org.sakaiproject.entity.api.Entity;
import org.sakaiproject.entity.api.serialize.EntityParseException;
import org.sakaiproject.entity.api.serialize.EntityReader;
import org.sakaiproject.entity.api.serialize.EntityReaderHandler;
import org.sakaiproject.event.cover.UsageSessionService;
import org.sakaiproject.javax.Filter;
import org.sakaiproject.time.cover.TimeService;

/**
 * <p>
 * BaseDbDualSingleStorage is a class that stores Resources (of some type) in a
 * database, <br />
 * provides locked access, and generally implements a services "storage" class.
 * The resources are encoded into two fields.
 * Optionally a second storage can be provided which is where the items are loaded
 * from when a putDeleteResource is called.
 * 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:
 * <br /> - just the Resource Id field is enough to distinguish one Resource
 * from another <br /> - a resource's reference is based on no more than the
 * resource id <br /> - a resource's id cannot change.
 * </p>
 * <p>
 * In order to handle Unicode characters properly, the SQL statements executed
 * by this class <br />
 * should not embed Unicode characters into the SQL statement text; rather,
 * Unicode values <br />
 * should be inserted as fields in a PreparedStatement. Databases handle Unicode
 * better in fields.
 * </p>
 */
public class BaseDbDualSingleStorage implements DbSingleStorage {
    public static final String STORAGE_FIELDS = "XML, BINARY_ENTITY";

    /** Our logger. */
    private static Log M_log = LogFactory.getLog(BaseDbDualSingleStorage.class);

    /** 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 additional field names in the resource table that go between the two
     * ids and the xml
     */
    protected String[] m_resourceTableOtherFields = null;

    /** The xml tag name for the element holding each actual resource entry. */
    protected String m_resourceEntryTagName = null;

    /** If true, we do our locks in the remote database. */
    protected boolean m_locksAreInDb = false;

    /**
     * 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 SingleStorageUser m_user = null;

    /**
     * Locks, keyed by reference, holding Connections (or, if locks are done
     * locally, holding an Edit).
     */
    protected Hashtable m_locks = null;

    /** If set, we treat reasource ids as case insensitive. */
    protected boolean m_caseInsensitive = false;

    /** Injected (by constructor) SqlService. */
    protected SqlService m_sql = null;

    /** contains a map of the database dependent handlers. */
    protected static Map<String, MultiSingleStorageSql> databaseBeans;

    /** The db handler we are using. */
    protected MultiSingleStorageSql singleStorageSql;

    private long ttotal = 0;

    private int ntime = 0;

    private int rntime = 0;

    private long rttotal = 0;

    private long rmtotal = 0;

    private long mtotal = 0;

    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 setSingleStorageSql(String vendor) {
        this.singleStorageSql = (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, MultiSingleStorageSql>();
        databaseBeans.put("default", new MultiSingleStorageSqlDefault(STORAGE_FIELDS));
        databaseBeans.put("hsql", new MultiSingleStorageSqlHSql(STORAGE_FIELDS));
        databaseBeans.put("mysql", new MultiSingleStorageSqlMySql(STORAGE_FIELDS));
        databaseBeans.put("oracle", new MultiSingleStorageSqlOracle(STORAGE_FIELDS));
    }

    /**
     * Construct.
     * 
     * @param resourceTableName
     *        Table name for resources.
     * @param resourceTableIdField
     *        The field in the resource table that holds the id.
     * @param resourceTableOtherFields
     *        The other fields in the resource table (between the two id and the
     *        xml fields).
     * @param locksInDb
     *        If true, we do our locks in the remote database, otherwise we do
     *        them here.
     * @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. This must implement EntityReader interface as well.
     * @param sqlService
     *        The SqlService.
     */
    public BaseDbDualSingleStorage(String resourceTableName, String resourceTableIdField,
            String[] resourceTableOtherFields, boolean locksInDb, String resourceEntryName, SingleStorageUser user,
            SqlService sqlService) {
        this(resourceTableName, resourceTableIdField, resourceTableOtherFields, locksInDb, resourceEntryName, user,
                sqlService, null);
    }

    // support for SAK-12874
    protected DbSingleStorage m_storage = null;

    /**
     * Construct.
     * 
     * @param resourceTableName
     *        Table name for resources.
     * @param resourceTableIdField
     *        The field in the resource table that holds the id.
     * @param resourceTableOtherFields
     *        The other fields in the resource table (between the two id and the
     *        xml fields).
     * @param locksInDb
     *        If true, we do our locks in the remote database, otherwise we do
     *        them here.
     * @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.
     * @param storage
     *        The storage for the normal resource (only used by delete storage), this is how we load the original resource.
     */
    public BaseDbDualSingleStorage(String resourceTableName, String resourceTableIdField,
            String[] resourceTableOtherFields, boolean locksInDb, String resourceEntryName, SingleStorageUser user,
            SqlService sqlService, DbSingleStorage storage) {
        m_resourceTableName = resourceTableName;
        m_resourceTableIdField = resourceTableIdField;
        m_resourceTableOtherFields = resourceTableOtherFields;
        m_locksAreInDb = locksInDb;
        m_resourceEntryTagName = resourceEntryName;
        m_user = user;
        m_sql = sqlService;

        // support for SAK-12874
        m_storage = storage;
        if (m_storage == null && m_resourceTableName != null
                && m_resourceTableName.toUpperCase().contains("DELETE")) {
            // warn if the delete storage does not have the main storage set
            M_log.warn("resource storage is not set, delete table resource file paths will be invalid");
        }

        setSingleStorageSql(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 Resource from xml
     * 
     * @param xml
     *        An string containing the xml which describes the resource.
     * @return The Resource object created from the xml.
     */
    protected Entity readResource(String xml, byte[] blob) {
        Runtime r = Runtime.getRuntime();
        long ms = r.freeMemory();
        long start = System.currentTimeMillis();
        String type = "";
        try {
            type = "direct";
            EntityReader de_user = (EntityReader) m_user;
            EntityReaderHandler de_handler = de_user.getHandler();
            return de_handler.parse(null, xml, blob);
        } catch (Exception e) {
            M_log.warn("readResource(): " + e.getMessage());
            M_log.warn("readResource(): ", e);
            return null;
        } finally {
            long t = System.currentTimeMillis() - start;
            long me = r.freeMemory();
            long md = ms - me;
            if (md >= 0) {
                rmtotal += md;
            } else {
                if (rntime != 0) {
                    rmtotal += (rmtotal / rntime);
                }
            }
            rttotal += t;
            rntime++;
            if (rntime % 100 == 0) {
                double a = (1.0 * rttotal) / (1.0 * rntime);
                double m = (1.0 * rmtotal) / (1.0 * rntime);
                M_log.debug("Average " + type + " Parse now " + (a) + "ms " + m + " bytes");
            }

        }
    }

    /**
     * Check if a Resource by this id exists.
     * 
     * @param id
     *        The id.
     * @return true if a Resource by this id exists, false if not.
     */
    public boolean checkResource(String id) {
        // just see if the record exists
        String sql = singleStorageSql.getResourceIdSql(m_resourceTableIdField, m_resourceTableName);

        Object fields[] = new Object[1];
        fields[0] = caseId(id);
        List ids = m_sql.dbRead(sql, fields, null);

        return (!ids.isEmpty());
    }

    /**
     * Get the Resource with this id, or null if not found.
     * 
     * @param id
     *        The id.
     * @return The Resource with this id, or null if not found.
     */
    public Entity getResource(String id) {
        Entity entry = null;

        // get the user from the db
        List xml = null;
        String sql = singleStorageSql.getXmlSql(m_resourceTableIdField, m_resourceTableName);
        Object fields[] = new Object[1];
        fields[0] = caseId(id);
        xml = loadResources(sql, fields);

        if (!xml.isEmpty()) {
            // create the Resource from the db xml
            entry = (Entity) xml.get(0);
        }

        return entry;
    }

    public boolean isEmpty() {
        // count
        int count = countAllResources();
        return (count == 0);
    }

    public List getAllResources() {
        List all = new Vector();

        // read all users from the db
        List xml = null;
        String sql = singleStorageSql.getXmlSql(m_resourceTableName);
        xml = loadResources(sql, null);
        // %%% + "order by " + m_resourceTableOrderField + " asc";

        // process all result xml into user objects
        if (!xml.isEmpty()) {
            for (int i = 0; i < xml.size(); i++) {
                Entity entry = (Entity) xml.get(i);
                if (entry != null)
                    all.add(entry);
            }
        }

        return all;
    }

    public List getAllResources(int first, int last) {
        Object[] fields = singleStorageSql.getXmlFields(first, last);
        List xml = null;
        String sql = singleStorageSql.getXmlSql(m_resourceTableIdField, m_resourceTableName, first, last);
        xml = loadResources(sql, fields);

        List rv = new Vector();

        // process all result xml into user objects
        if (!xml.isEmpty()) {
            for (int i = 0; i < xml.size(); i++) {
                Entity entry = (Entity) xml.get(i);
                if (entry != null)
                    rv.add(entry);
            }
        }

        return rv;
    }

    public int countAllResources() {
        // read all count
        String sql = singleStorageSql.getNumRowsSql(m_resourceTableName);

        List results = m_sql.dbRead(sql, null, new SqlReader() {
            public Object readSqlResultRecord(ResultSet result) {
                try {
                    int count = result.getInt(1);
                    return Integer.valueOf(count);
                } catch (SQLException ignore) {
                    return null;
                }
            }
        });

        if (results.isEmpty())
            return 0;

        return ((Integer) results.get(0)).intValue();
    }

    public int countSelectedResourcesWhere(String sqlWhere) {
        // read all where count
        String sql = singleStorageSql.getNumRowsSql(m_resourceTableName, sqlWhere);
        List results = m_sql.dbRead(sql, null, new SqlReader() {
            public Object readSqlResultRecord(ResultSet result) {
                try {
                    int count = result.getInt(1);
                    return Integer.valueOf(count);
                } catch (SQLException ignore) {
                    return null;
                }
            }
        });

        if (results.isEmpty())
            return 0;

        return ((Integer) results.get(0)).intValue();
    }

    /**
     * Get all Resources where the given field matches the given value.
     * 
     * @param field
     *        The db field name for the selection.
     * @param value
     *        The value to select.
     * @return The list of all Resources that meet the criteria.
     */
    public List getAllResourcesWhere(String field, String value) {
        // read all users from the db
        String sql = singleStorageSql.getXmlSql(field, m_resourceTableName);
        Object[] fields = new Object[1];
        fields[0] = value;
        // %%% + "order by " + m_resourceTableOrderField + " asc";
        return loadResources(sql, fields);
    }

    /**
     * Get all Resources where the given field matches the given value.
     * 
     * @param field
     *        The db field name for the selection.
     * @param value
     *        The value to select.
     * @return The list of all Resources that meet the criteria.
     */
    public List getAllResourcesWhere(String selectBy, String selectByValue, String orderBy, int first,
            int pageSize) {
        // read all users from the db
        String sql = singleStorageSql.getXmlWhereLimitSql(selectBy, orderBy, m_resourceTableName, first, pageSize);
        Object[] fields = new Object[1];
        fields[0] = selectByValue;
        // %%% + "order by " + m_resourceTableOrderField + " asc";
        return loadResources(sql, fields);
    }

    protected List loadResources(String sql, Object[] fields) {
        List all = m_sql.dbRead(sql, fields, new SqlReader() {
            public Object readSqlResultRecord(ResultSet result) {
                try {
                    // create the Resource from the db xml
                    return readResource(result.getString(1), result.getBytes(2));
                } catch (SQLException ignore) {
                    return null;
                }
            }
        });
        return all;
    }

    public List getAllResourcesWhereLike(String field, String value) {
        String sql = singleStorageSql.getXmlLikeSql(field, m_resourceTableName);
        Object[] fields = new Object[1];
        fields[0] = value;
        // %%% + "order by " + m_resourceTableOrderField + " asc";

        return loadResources(sql, fields);
    }

    /**
     * Get selected Resources, filtered by a test on the id field
     * 
     * @param filter
     *        A filter to select what gets returned.
     * @return The list of selected Resources.
     */
    public List getSelectedResources(final Filter filter) {
        List all = new Vector();
        // read all users from the db
        String sql = singleStorageSql.getXmlAndFieldSql(m_resourceTableIdField, m_resourceTableName);
        // %%% + "order by " + m_resourceTableOrderField + " asc";

        List xml = m_sql.dbRead(sql, null, new SqlReader() {
            public Object readSqlResultRecord(ResultSet result) {
                try {
                    // read the id m_resourceTableIdField
                    String id = result.getString(1);

                    // read the xml
                    String xml = result.getString(2);
                    byte[] blob = result.getBytes(3);

                    if (!filter.accept(caseId(id)))
                        return null;

                    return readResource(xml, blob);
                } catch (SQLException ignore) {
                    return null;
                }
            }
        });
        // process all result xml into user objects
        if (!xml.isEmpty()) {
            for (int i = 0; i < xml.size(); i++) {
                Entity entry = (Entity) xml.get(i);
                if (entry != null)
                    all.add(entry);
            }
        }

        return all;
    }

    /**
     * Get selected Resources, using a supplied where clause
     * 
     * @param sqlWhere
     *        The SQL where clause.
     * @return The list of selected Resources.
     */
    public List getSelectedResourcesWhere(String sqlWhere) {
        List all = new Vector();

        // read all users from the db
        String sql = singleStorageSql.getXmlWhereSql(m_resourceTableName, sqlWhere);
        List xml = loadResources(sql, null);
        // process all result xml into user objects
        if (!xml.isEmpty()) {
            for (int i = 0; i < xml.size(); i++) {
                Entity entry = (Entity) xml.get(i);
                if (entry != null)
                    all.add(entry);
            }
        }

        return all;
    }

    /**
     * Add a new Resource with this id.
     * 
     * @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(String id, Object[] others) {
        // create one with just the id, and perhaps some other fields as well
        Entity entry = m_user.newResource(null, id, others);

        // form the XML and SQL for the insert
        Object blob = getBlob(entry);
        String statement = null;
        if (blob instanceof byte[]) {
            statement = // singleStorageSql.
                    "insert into " + m_resourceTableName
                            + insertFields(m_resourceTableIdField, m_resourceTableOtherFields, "BINARY_ENTITY, XML")
                            + " values ( ?, " + valuesParams(m_resourceTableOtherFields) + " ? , NULL )";
        } else {
            statement = // singleStorageSql.
                    "insert into " + m_resourceTableName
                            + insertFields(m_resourceTableIdField, m_resourceTableOtherFields,
                                    "XML, BINARY_ENTITY ")
                            + " values ( ?, " + valuesParams(m_resourceTableOtherFields) + " ?, NULL )";

        }

        Object[] flds = m_user.storageFields(entry);
        if (flds == null)
            flds = new Object[0];
        Object[] fields = new Object[flds.length + 2];
        System.arraycopy(flds, 0, fields, 1, flds.length);
        fields[0] = caseId(entry.getId());
        fields[fields.length - 1] = blob;

        // 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(id);
        if (edit == null) {
            M_log.warn("putResource(): didn't get a lock!");
            return null;
        }

        return edit;
    }

    /**
     * store the record in content_resource_delete table along with
     * resource_uuid and date
     */
    public Edit putDeleteResource(String id, String uuid, String userId, Object[] others) {
        // support for SAK-12874
        Entity entry = null;
        if (m_storage != null) {
            // use the object being deleted
            entry = m_storage.getResource(id);
        }
        if (entry == null) {
            // failsafe to the old method
            entry = m_user.newResource(null, id, others);
        }

        // form the XML and SQL for the insert
        Object blob = getBlob(entry);
        String statement = null;
        if (blob instanceof byte[]) {
            statement = "insert into " + m_resourceTableName
                    + insertDeleteFields(m_resourceTableIdField, m_resourceTableOtherFields, "RESOURCE_UUID",
                            "DELETE_DATE", "DELETE_USERID", "BINARY_ENTITY, XML")
                    + " values ( ?, " + valuesParams(m_resourceTableOtherFields) + " ? ,? ,? ,?, NULL)";

        } else {
            statement = "insert into " + m_resourceTableName
                    + insertDeleteFields(m_resourceTableIdField, m_resourceTableOtherFields, "RESOURCE_UUID",
                            "DELETE_DATE", "DELETE_USERID", "XML, BINARY_ENTITY")
                    + " values ( ?, " + valuesParams(m_resourceTableOtherFields) + " ? ,? ,? ,?, NULL)";
        }

        Object[] flds = m_user.storageFields(entry);
        if (flds == null)
            flds = new Object[0];
        Object[] fields = new Object[flds.length + 5];
        System.arraycopy(flds, 0, fields, 1, flds.length);
        fields[0] = caseId(entry.getId());
        // uuid added here
        fields[fields.length - 4] = uuid;
        // date added here
        fields[fields.length - 3] = TimeService.newTime();// .toStringLocalDate();

        // userId added here
        fields[fields.length - 2] = userId;
        fields[fields.length - 1] = blob;

        // 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(id);
        if (edit == null) {
            M_log.warn("putResourceDelete(): didn't get a lock!");
            return null;
        }

        return edit;
    }

    /** Construct the SQL statement */
    protected String insertDeleteFields(String before, String[] fields, String uuid, String date, String userId,
            String after) {
        StringBuilder buf = new StringBuilder();
        buf.append(" (");
        buf.append(before);
        buf.append(",");
        if (fields != null) {
            for (int i = 0; i < fields.length; i++) {
                buf.append(fields[i] + ",");
            }
        }
        buf.append(uuid);
        buf.append(",");
        buf.append(date);
        buf.append(",");
        buf.append(userId);
        buf.append(",");
        buf.append(after);
        buf.append(")");

        return buf.toString();
    }

    /** update XML attribute on properties and remove locks */
    public void commitDeleteResource(Edit edit, String uuid) {
        // form the SQL statement and the var w/ the XML
        Object blob = getBlob(edit);
        String statement = null;
        if (blob instanceof byte[]) {
            statement = "update " + m_resourceTableName + " set " + updateSet(m_resourceTableOtherFields)
                    + " BINARY_ENTITY = ?, XML = NULL where ( RESOURCE_UUID = ? )";

        } else {
            statement = "update " + m_resourceTableName + " set " + updateSet(m_resourceTableOtherFields)
                    + " XML = ?, BINARY_ENTITY = NULL where ( RESOURCE_UUID = ? )";
        }
        Object[] flds = m_user.storageFields(edit);
        if (flds == null)
            flds = new Object[0];
        Object[] fields = new Object[flds.length + 2];
        System.arraycopy(flds, 0, fields, 0, flds.length);
        fields[fields.length - 2] = blob;
        fields[fields.length - 1] = uuid;// caseId(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 = singleStorageSql.getDeleteLocksSql();

            // collect the fields
            Object lockFields[] = new Object[2];
            lockFields[0] = m_resourceTableName;
            lockFields[1] = internalRecordId(caseId(edit.getId()));
            boolean ok = m_sql.dbWrite(statement, lockFields);
            if (!ok) {
                M_log.warn("commit: 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());
        }
    }

    /**
     * Get a lock on the Resource with this id, or null if a lock cannot be
     * gotten.
     * 
     * @param id
     *        The user id.
     * @return The locked Resource with this id, or null if this records cannot
     *         be locked.
     */
    public Edit editResource(String id) {
        Edit edit = null;

        if (m_locksAreInDb) {
            if ("oracle".equals(m_sql.getVendor())) {
                final List<Entity> l = new ArrayList<Entity>();
                Connection lock = null;
                if (m_user instanceof EntityReaderHandler) {
                    // read the record and get a lock on it (non blocking)
                    String statement = "select XML from " + m_resourceTableName + " where ( "
                            + m_resourceTableIdField + " = '" + StorageUtils.escapeSql(caseId(id)) + "' )"
                            + " for update nowait";
                    lock = m_sql.dbReadLock(statement, new SqlReader() {

                        public Object readSqlResultRecord(ResultSet result) {
                            try {
                                l.add(readResource(result.getString(1), result.getBytes(2)));
                            } catch (SQLException e) {
                                M_log.warn("Failed to retrieve record ", e);
                            }
                            return null;
                        }

                    });
                } else {
                    // read the record and get a lock on it (non blocking)
                    String statement = "select BENTRY, XML from " + m_resourceTableName + " where ( "
                            + m_resourceTableIdField + " = '" + StorageUtils.escapeSql(caseId(id)) + "' )"
                            + " for update nowait";
                    lock = m_sql.dbReadLock(statement, new SqlReader() {

                        public Object readSqlResultRecord(ResultSet result) {

                            try {
                                l.add(readResource(result.getString(1), result.getBytes(2)));
                            } catch (SQLException e) {
                                M_log.warn("Failed to retrieve record ", e);
                            }
                            return null;
                        }

                    });

                }

                // for missing or already locked...
                if ((lock == null) || (l.size() == 0))
                    return null;

                // make first a Resource, then an Edit
                Entity entry = l.get(0);
                edit = m_user.newResourceEdit(null, 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) {
            // read the record - fail if not there
            Entity entry = getResource(id);
            if (entry == null)
                return null;

            // write a lock to the lock table - if we can do it, we get the lock
            String statement = singleStorageSql.getInsertLocks();

            // 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] = internalRecordId(caseId(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;
            }

            // we got the lock! - make the edit from the Resource
            edit = m_user.newResourceEdit(null, entry);
        }

        // otherwise, get the lock locally
        else {
            // get the entry, and check for existence
            Entity entry = getResource(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(null, 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 commitResource(Edit edit) {
        // form the SQL statement and the var w/ the XML
        Object blob = getBlob(edit);
        String statement = null;
        if (blob instanceof byte[]) {
            statement = "update " + m_resourceTableName + " set " + updateSet(m_resourceTableOtherFields)
                    + " BINARY_ENTITY = ?, XML = NULL where ( " + m_resourceTableIdField + " = ? )";

        } else {
            statement = "update " + m_resourceTableName + " set " + updateSet(m_resourceTableOtherFields)
                    + " XML = ?, BINARY_ENTITY = NULL where ( " + m_resourceTableIdField + " = ? )";
        }
        Object[] flds = m_user.storageFields(edit);
        if (flds == null)
            flds = new Object[0];
        Object[] fields = new Object[flds.length + 2];
        System.arraycopy(flds, 0, fields, 0, flds.length);
        fields[fields.length - 2] = blob;
        fields[fields.length - 1] = caseId(edit.getId());

        // singleStorageSql.getUpdateXml(m_resourceTableIdField,
        // m_resourceTableOtherFields, m_resourceTableName);

        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 = singleStorageSql.getDeleteLocksSql();

            // collect the fields
            Object lockFields[] = new Object[2];
            lockFields[0] = m_resourceTableName;
            lockFields[1] = internalRecordId(caseId(edit.getId()));
            boolean ok = m_sql.dbWrite(statement, lockFields);
            if (!ok) {
                M_log.warn("commit: 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 cancelResource(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 = singleStorageSql.getDeleteLocksSql();

            // collect the fields
            Object lockFields[] = new Object[2];
            lockFields[0] = m_resourceTableName;
            lockFields[1] = internalRecordId(caseId(edit.getId()));
            boolean ok = m_sql.dbWrite(statement, lockFields);
            if (!ok) {
                M_log.warn("cancel: missing lock for table: " + lockFields[0] + " key: " + lockFields[1]);
            }
        }

        else {
            // release the lock
            m_locks.remove(edit.getReference());
        }
    }

    /**
     * Remove this (locked) Resource.
     * 
     * @param user
     *        The Edit to remove.
     */
    public void removeResource(Edit edit) {
        // form the SQL delete statement
        String statement = singleStorageSql.getDeleteSql(m_resourceTableIdField, m_resourceTableName);

        Object fields[] = new Object[1];
        fields[0] = caseId(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 = singleStorageSql.getDeleteLocksSql();

            // collect the fields
            Object lockFields[] = new Object[2];
            lockFields[0] = m_resourceTableName;
            lockFields[1] = internalRecordId(caseId(edit.getId()));
            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());
        }
    }

    /**
     * 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 before
     *        The first 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 before, String[] fields, String after) {
        StringBuilder buf = new StringBuilder();
        buf.append(" (");

        buf.append(before);
        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();
    }

    /**
     * Fix the case of resource ids to support case insensitive ids if enabled
     * 
     * @param The
     *        id to fix.
     * @return The id, case modified as needed.
     */
    protected String caseId(String id) {
        if (m_caseInsensitive) {
            return id.toLowerCase();
        }

        return id;
    }

    /**
     * Enable / disable case insensitive ids.
     * 
     * @param setting
     *        true to set case insensitivity, false to set case sensitivity.
     */
    protected void setCaseInsensitivity(boolean setting) {
        m_caseInsensitive = setting;
    }

    /**
     * Return a record ID to use internally in the database. This is needed for
     * databases (MySQL) that have limits on key lengths. The hash code ensures
     * that the record ID will be unique, even if the DB only considers a prefix
     * of a very long record ID.
     * 
     * @param recordId
     * @return The record ID to use internally in the database
     */
    private String internalRecordId(String recordId) {
        if ("mysql".equals(m_sql.getVendor())) {
            if (recordId == null)
                recordId = "null";
            return recordId.hashCode() + " - " + recordId;
        } else
        // oracle, hsqldb
        {
            return recordId;
        }
    }

    /**
     * @param entry
     * @return
     */
    private Object getBlob(Entity entry) {
        Runtime r = Runtime.getRuntime();
        long ms = r.freeMemory();
        long start = System.currentTimeMillis();
        try {
            EntityReader er_user = (EntityReader) m_user;
            try {

                EntityReaderHandler erHandler = er_user.getHandler();
                return erHandler.serialize(entry);

            } catch (EntityParseException ep) {
                M_log.warn("Unable to Serialize Entity, falling back to XML " + entry.getId(), ep);
            }
            return null;
        } finally {
            long t = System.currentTimeMillis() - start;
            long me = r.freeMemory();
            long md = ms - me;
            if (md >= 0) {
                mtotal += md;
            } else {
                if (ntime != 0) {
                    mtotal += (mtotal / ntime);
                }
            }
            ttotal += t;
            ntime++;
            if (ntime % 100 == 0) {
                double a = (1.0 * ttotal) / (1.0 * ntime);
                double m = (1.0 * mtotal) / (1.0 * ntime);
                M_log.debug("Average Serialization now " + (a) + "ms " + m + " bytes");
            }

        }
    }

}