All Downloads are FREE. Search and download functionalities are using the official Maven repository.

ca.nrc.cadc.vos.server.db.NodeDAO Maven / Gradle / Ivy

The newest version!
/*
************************************************************************
*******************  CANADIAN ASTRONOMY DATA CENTRE  *******************
**************  CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES  **************
*
*  (c) 2022.                            (c) 2022.
*  Government of Canada                 Gouvernement du Canada
*  National Research Council            Conseil national de recherches
*  Ottawa, Canada, K1A 0R6              Ottawa, Canada, K1A 0R6
*  All rights reserved                  Tous droits réservés
*
*  NRC disclaims any warranties,        Le CNRC dénie toute garantie
*  expressed, implied, or               énoncée, implicite ou légale,
*  statutory, of any kind with          de quelque nature que ce
*  respect to the software,             soit, concernant le logiciel,
*  including without limitation         y compris sans restriction
*  any warranty of merchantability      toute garantie de valeur
*  or fitness for a particular          marchande ou de pertinence
*  purpose. NRC shall not be            pour un usage particulier.
*  liable in any event for any          Le CNRC ne pourra en aucun cas
*  damages, whether direct or           être tenu responsable de tout
*  indirect, special or general,        dommage, direct ou indirect,
*  consequential or incidental,         particulier ou général,
*  arising from the use of the          accessoire ou fortuit, résultant
*  software.  Neither the name          de l'utilisation du logiciel. Ni
*  of the National Research             le nom du Conseil National de
*  Council of Canada nor the            Recherches du Canada ni les noms
*  names of its contributors may        de ses  participants ne peuvent
*  be used to endorse or promote        être utilisés pour approuver ou
*  products derived from this           promouvoir les produits dérivés
*  software without specific prior      de ce logiciel sans autorisation
*  written permission.                  préalable et particulière
*                                       par écrit.
*
*  This file is part of the             Ce fichier fait partie du projet
*  OpenCADC project.                    OpenCADC.
*
*  OpenCADC is free software:           OpenCADC est un logiciel libre ;
*  you can redistribute it and/or       vous pouvez le redistribuer ou le
*  modify it under the terms of         modifier suivant les termes de
*  the GNU Affero General Public        la “GNU Affero General Public
*  License as published by the          License” telle que publiée
*  Free Software Foundation,            par la Free Software Foundation
*  either version 3 of the              : soit la version 3 de cette
*  License, or (at your option)         licence, soit (à votre gré)
*  any later version.                   toute version ultérieure.
*
*  OpenCADC is distributed in the       OpenCADC est distribué
*  hope that it will be useful,         dans l’espoir qu’il vous
*  but WITHOUT ANY WARRANTY;            sera utile, mais SANS AUCUNE
*  without even the implied             GARANTIE : sans même la garantie
*  warranty of MERCHANTABILITY          implicite de COMMERCIALISABILITÉ
*  or FITNESS FOR A PARTICULAR          ni d’ADÉQUATION À UN OBJECTIF
*  PURPOSE.  See the GNU Affero         PARTICULIER. Consultez la Licence
*  General Public License for           Générale Publique GNU Affero
*  more details.                        pour plus de détails.
*
*  You should have received             Vous devriez avoir reçu une
*  a copy of the GNU Affero             copie de la Licence Générale
*  General Public License along         Publique GNU Affero avec
*  with OpenCADC.  If not, sesrc/jsp/index.jspe          OpenCADC ; si ce n’est
*  .      pas le cas, consultez :
*                                       .
*
*  $Revision: 4 $
*
************************************************************************
*/

package ca.nrc.cadc.vos.server.db;

import java.net.URI;
import java.net.URISyntaxException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Timestamp;
import java.sql.Types;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.UUID;

import javax.security.auth.Subject;
import javax.sql.DataSource;

import ca.nrc.cadc.auth.NumericPrincipal;
import org.apache.log4j.Logger;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.PreparedStatementCreator;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.transaction.CannotCreateTransactionException;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;

import ca.nrc.cadc.auth.IdentityManager;
import ca.nrc.cadc.date.DateUtil;
import ca.nrc.cadc.db.DBUtil;
import ca.nrc.cadc.db.LongRowMapper;
import ca.nrc.cadc.net.TransientException;
import ca.nrc.cadc.profiler.Profiler;
import ca.nrc.cadc.util.CaseInsensitiveStringComparator;
import ca.nrc.cadc.util.FileMetadata;
import ca.nrc.cadc.util.HexUtil;
import ca.nrc.cadc.vos.ContainerNode;
import ca.nrc.cadc.vos.DataNode;
import ca.nrc.cadc.vos.LinkNode;
import ca.nrc.cadc.vos.Node;
import ca.nrc.cadc.vos.NodeProperty;
import ca.nrc.cadc.vos.VOS;
import ca.nrc.cadc.vos.VOS.NodeBusyState;
import ca.nrc.cadc.vos.VOSURI;
import ca.nrc.cadc.vos.server.NodeID;

/**
 * Helper class for implementing NodePersistence with a
 * relational database back end for metadata. This class is
 * NOT thread-safe and the caller must instantiate a new one
 * in each thread (e.g. app-server request thread).
 *
 * @author majorb
 */
public class NodeDAO
{
    private static Logger log = Logger.getLogger(NodeDAO.class);
    private static final int CHILD_BATCH_SIZE = 1000;

    // temporarily needed by NodeMapper
    static final String NODE_TYPE_DATA = "D";
    static final String NODE_TYPE_CONTAINER = "C";
    static final String NODE_TYPE_LINK = "L";

    private static final int NODE_NAME_COLUMN_SIZE = 256;
    private static final int NODE_PROPERTY_COLUMN_SIZE = 700;

    // Database connection.
    protected DataSource dataSource;
    protected NodeSchema nodeSchema;
    protected String authority;
    protected IdentityManager identManager;
    protected String deletedNodePath;

    protected JdbcTemplate jdbc;
    private DataSourceTransactionManager transactionManager;
    private DefaultTransactionDefinition defaultTransactionDef;
    private DefaultTransactionDefinition dirtyReadTransactionDef;
    private TransactionStatus transactionStatus;

    // reusable object for recursive admin methods
    private NodePutStatementCreator adminStatementCreator;

    // instrument for unit tests of admin methods
    int numTxnStarted = 0;
    int numTxnCommitted = 0;

    private DateFormat dateFormat;
    private Calendar cal;

    private Map identityCache = new HashMap();

    private Profiler prof = new Profiler(NodeDAO.class);

    public static class NodeSchema
    {
        public String nodeTable;
        public String propertyTable;
        boolean limitWithTop;

        public String deltaIndexName;

        /**
         * Constructor for specifying the table where Node(s) and NodeProperty(s) are
         * stored.
         * @param nodeTable fully qualified name of node table
         * @param propertyTable fully qualified name of property table
         * @param limitWithTop - true if the RDBMS uses TOP, false for LIMIT
         * are writable, false if they are read-only in the DB
         */
        public NodeSchema(String nodeTable, String propertyTable,
                boolean limitWithTop)
        {
            this.nodeTable = nodeTable;
            this.propertyTable = propertyTable;
            this.limitWithTop = limitWithTop;
        }

    }
    private static String[] NODE_COLUMNS = new String[]
    {
        "parentID", // FK, for join to parent
        "name",
        "type",
        "busyState",
        "isPublic",
        "isLocked",
        "ownerID",
        "creatorID",
        "groupRead",
        "groupWrite",
        "lastModified",
        // semantic file metadata
        "contentType",
        "contentEncoding",
        // LinkNode uri
        "link",
        "storageID",
        // physical file metadata
        "contentLength",
        "contentMD5"
    };

    /**
     * NodeDAO Constructor. This class was developed and tested using a
     * Sybase ASE RDBMS. Some SQL (update commands in particular) may be non-standard.
     *
     * @param dataSource
     * @param nodeSchema
     * @param authority
     * @param identManager
     */
    public NodeDAO(DataSource dataSource, NodeSchema nodeSchema,
            String authority, IdentityManager identManager, String deletedNodePath)
    {
        this.dataSource = dataSource;
        this.nodeSchema = nodeSchema;
        this.authority = authority;
        this.identManager = identManager;
        this.deletedNodePath = deletedNodePath;

        this.defaultTransactionDef = new DefaultTransactionDefinition();
        defaultTransactionDef.setIsolationLevel(DefaultTransactionDefinition.ISOLATION_REPEATABLE_READ);
        this.dirtyReadTransactionDef = new DefaultTransactionDefinition();
        dirtyReadTransactionDef.setIsolationLevel(DefaultTransactionDefinition.ISOLATION_READ_UNCOMMITTED);
        this.jdbc = new JdbcTemplate(dataSource);
        this.transactionManager = new DataSourceTransactionManager(dataSource);

        this.dateFormat = DateUtil.getDateFormat(DateUtil.IVOA_DATE_FORMAT, DateUtil.UTC);
        this.cal = Calendar.getInstance(DateUtil.UTC);
    }

    // convenience during refactor
    protected String getNodeTableName()
    {
        return nodeSchema.nodeTable;
    }

    // convenience during refactor
    protected String getNodePropertyTableName()
    {
        return nodeSchema.propertyTable;
    }

    /**
     * Start a transaction to the data source.
     */
    protected void startTransaction()
    {
        if (transactionStatus != null)
            throw new IllegalStateException("transaction already in progress");
        log.debug("startTransaction");
        this.transactionStatus = transactionManager.getTransaction(defaultTransactionDef);
        log.debug("startTransaction: OK");
        numTxnStarted++;
    }

    /**
     * Commit the transaction to the data source.
     */
    protected void commitTransaction()
    {
        if (transactionStatus == null)
            throw new IllegalStateException("no transaction in progress");
        log.debug("commitTransaction");
        transactionManager.commit(transactionStatus);
        this.transactionStatus = null;
        log.debug("commit: OK");
        numTxnCommitted++;
    }

    /**
     * Rollback the transaction to the data source.
     */
    protected void rollbackTransaction()
    {
        if (transactionStatus == null)
            throw new IllegalStateException("no transaction in progress");
        log.debug("rollbackTransaction");
        transactionManager.rollback(transactionStatus);
        this.transactionStatus = null;
        log.debug("rollback: OK");
    }

    /**
     * Checks that the specified node has already been persisted. This will pass if this
     * node was obtained from the getPath method.
     * @param node
     */
    protected void expectPersistentNode(Node node)
    {
        if (node == null)
            throw new IllegalArgumentException("node cannot be null");
        if (node.appData == null)
            throw new IllegalArgumentException("node is not a persistent node: " + node.getUri().getPath());
    }

    /**
     * Get a complete path from the root container. For the container nodes in
     * the returned node, only child nodes on the path will be included in the
     * list of children; other children are not included. Nodes returned from
     * this method will have some but not all properties set. Specifically, any
     * properties that are inherently single-valued and stored in the Node table
     * are included, as are the access-control properties (isPublic, group-read,
     * and group-write). The remaining properties for a node can be obtained by
     * calling getProperties(Node).
     *
     * @see getProperties(Node)
     * @param path
     * @return the last node in the path, with all parents or null if not found
     */
    public Node getPath(String path) throws TransientException
    {
    	return this.getPath(path, false);
    }

    /**
     * Get a complete path from the root container. For the container nodes in
     * the returned node, only child nodes on the path will be included in the
     * list of children; other children are not included. Nodes returned from
     * this method will have some but not all properties set. Specifically, any
     * properties that are inherently single-valued and stored in the Node table
     * are included, as are the access-control properties (isPublic, group-read,
     * and group-write). The remaining properties for a node can be obtained by
     * calling getProperties(Node).
     *
     * @see getProperties(Node)
     * @param path
     * @param allowPartialPath
     * @return the last node in the path, with all parents or null if not found
     */
    public Node getPath(String path, boolean allowPartialPath) throws TransientException
    {
        return this.getPath(path, allowPartialPath, true);
    }

    /**
     * Get a complete path from the root container. For the container nodes in
     * the returned node, only child nodes on the path will be included in the
     * list of children; other children are not included. Nodes returned from
     * this method will have some but not all properties set. Specifically, any
     * properties that are inherently single-valued and stored in the Node table
     * are included, as are the access-control properties (isPublic, group-read,
     * and group-write). The remaining properties for a node can be obtained by
     * calling getProperties(Node).
     *
     * @see getProperties(Node)
     * @param path
     * @param allowPartialPath
     * @return the last node in the path, with all parents or null if not found
     */
    public Node getPath(String path, boolean allowPartialPath, boolean resolveMetadata) throws TransientException
    {
        log.debug("getPath: " + path);
        if (path.length() > 0 && path.charAt(0) == '/')
            path = path.substring(1);
        // generate single join query to extract path
        NodePathStatementCreator npsc = new NodePathStatementCreator(
                path.split("/"), getNodeTableName(), getNodePropertyTableName(), allowPartialPath);

        TransactionStatus dirtyRead = null;
        try
        {
            dirtyRead = transactionManager.getTransaction(dirtyReadTransactionDef);
            prof.checkpoint("TransactionManager.getTransaction");

            // execute query with NodePathExtractor
            Node ret = (Node) jdbc.query(npsc, new NodePathExtractor());
            prof.checkpoint("NodePathStatementCreator");

            transactionManager.commit(dirtyRead);
            dirtyRead = null;
            prof.checkpoint("commit.NodePathStatementCreator");

            // for non-LinkNode,
            //if ((ret != null) && !(ret.getUri().getPath().equals("/" + path)))
            //{
            //	if (!(ret instanceof LinkNode))
            //		ret = null;
            //}

            getOwners(ret, resolveMetadata);
            return ret;
        }
        catch(CannotCreateTransactionException ex)
        {
            log.error("failed to create transaction: " + path, ex);
            dirtyRead = null;
            if (DBUtil.isTransientDBException(ex))
                throw new TransientException("failed to get node: " + path, ex);
            else
                throw new RuntimeException("failed to get node: " + path, ex);
        }
        catch(Throwable t)
        {

            if (dirtyRead != null)
                try
                {
                    log.error("rollback dirtyRead for node: " + path, t);
                    transactionManager.rollback(dirtyRead);
                    dirtyRead = null;
                    prof.checkpoint("rollback.NodePathStatementCreator");
                }
                catch(Throwable oops) { log.error("failed to dirtyRead rollback transaction", oops); }

            if (DBUtil.isTransientDBException(t))
                throw new TransientException("failed to get node " + path, t);
            else
                throw new RuntimeException("failed to get node: " + path, t);
        }
        finally
        {
            if (dirtyRead != null)
                try
                {
                    log.warn("put: BUG - dirtyRead transaction still open in finally... calling rollback");
                    transactionManager.rollback(dirtyRead);
                }
                catch(Throwable oops) { log.error("failed to rollback dirtyRead transaction in finally", oops); }
        }
    }

    /**
     * Load all the properties for the specified Node.
     *
     * @param node
     */
    public void getProperties(Node node) throws TransientException
    {
        log.debug("getProperties: " + node.getUri().getPath() + ", " + node.getClass().getSimpleName());
        expectPersistentNode(node);

        log.debug("getProperties: " + node.getUri().getPath() + ", " + node.getClass().getSimpleName());
        String sql = getSelectNodePropertiesByID(node);
        log.debug("getProperties: " + sql);

        TransactionStatus dirtyRead = null;
        try
        {
            dirtyRead = transactionManager.getTransaction(dirtyReadTransactionDef);
            prof.checkpoint("TransactionManager.getTransaction");

            List props = jdbc.query(sql, new NodePropertyMapper());
            node.getProperties().addAll(props);
            prof.checkpoint("getProperties");
            transactionManager.commit(dirtyRead);
            dirtyRead = null;
            prof.checkpoint("commit.getProperties");
        }
        catch(CannotCreateTransactionException ex)
        {
            log.error("failed to create transaction: " + node.getUri().getPath(), ex);
            dirtyRead = null;
            if (DBUtil.isTransientDBException(ex))
                throw new TransientException("failed to get node: " + node.getUri().getPath(), ex);
            else
                throw new RuntimeException("failed to get node: " + node.getUri().getPath(), ex);
        }
        catch(Throwable t)
        {
            log.error("rollback dirtyRead for node: " + node.getUri().getPath(), t);
            try
            {
                transactionManager.rollback(dirtyRead);
                dirtyRead = null;
                prof.checkpoint("rollback.getProperties");
            }
            catch(Throwable oops) { log.error("failed to dirtyRead rollback transaction", oops); }

            if (DBUtil.isTransientDBException(t))
                throw new TransientException("failed to get node: " + node.getUri().getPath(), t);
            else
                throw new RuntimeException("failed to get node: " + node.getUri().getPath(), t);
        }
        finally
        {
            if (dirtyRead != null)
                try
                {
                    log.warn("put: BUG - dirtyRead transaction still open in finally... calling rollback");
                    transactionManager.rollback(dirtyRead);
                }
                catch(Throwable oops) { log.error("failed to rollback dirtyRead transaction in finally", oops); }
        }
    }

    /**
     * Load a single child node of the specified container.
     *
     * @param parent
     * @param name
     */
    public void getChild(ContainerNode parent, String name) throws TransientException
    {
        getChild(parent, name, true);
    }

    /**
     * Load a single child node of the specified container.
     *
     * @param parent
     * @param name
     */
    public void getChild(ContainerNode parent, String name, boolean resolveMetadata) throws TransientException
    {
        log.debug("getChild: " + parent.getUri().getPath() + ", " + name);
        expectPersistentNode(parent);

        String sql = getSelectChildNodeSQL(parent);
        log.debug("getChild: " + sql);

        TransactionStatus dirtyRead = null;
        try
        {
            dirtyRead = transactionManager.getTransaction(dirtyReadTransactionDef);
            prof.checkpoint("TransactionManager.getTransaction");

            List nodes = jdbc.query(sql, new Object[] { name },
                new NodeMapper(authority, parent.getUri().getPath()));
            prof.checkpoint("getSelectChildNodeSQL");

            transactionManager.commit(dirtyRead);
            dirtyRead = null;
            prof.checkpoint("commit.getSelectChildNodeSQL");

            if (nodes.size() > 1)
                throw new IllegalStateException("BUG - found " + nodes.size() + " child nodes named " + name
                    + " for container " + parent.getUri().getPath());

            getOwners(nodes, resolveMetadata);
            addChildNodes(parent, nodes);
        }
        catch(CannotCreateTransactionException ex)
        {
            log.error("failed to create transaction: " + parent.getUri().getPath(), ex);
            if (DBUtil.isTransientDBException(ex))
                throw new TransientException("failed to get node: " + parent.getUri().getPath(), ex);
            else
                throw new RuntimeException("failed to get node: " + parent.getUri().getPath(), ex);
        }
        catch(Throwable t)
        {
            log.error("rollback dirtyRead for node: " + parent.getUri().getPath(), t);
            try
            {
                transactionManager.rollback(dirtyRead);
                dirtyRead = null;
                prof.checkpoint("rollback.getSelectChildNodeSQL");
            }
            catch(Throwable oops) { log.error("failed to dirtyRead rollback transaction", oops); }

            if (DBUtil.isTransientDBException(t))
                throw new TransientException("failed to get node: " + parent.getUri().getPath(), t);
            else
                throw new RuntimeException("failed to get node: " + parent.getUri().getPath(), t);
        }
        finally
        {
            if (dirtyRead != null)
                try
                {
                    log.warn("put: BUG - dirtyRead transaction still open in finally... calling rollback");
                    transactionManager.rollback(dirtyRead);
                }
                catch(Throwable oops) { log.error("failed to rollback dirtyRead transaction in finally", oops); }
        }
    }
    
    public void getChildren(ContainerNode parent) throws TransientException {
        this.getChildren(parent, null, null, null, true, true);
    }
    
    public void getChildren(ContainerNode parent,  VOSURI start, Integer limit) throws TransientException {
        this.getChildren(parent, start, limit, null, true, true);
    }

    /**
     * 
     * @param parent
     * @param start
     * @param limit
     * @param sortProperty
     * @param sortAsc
     * @param resolveMetadata
     * @throws TransientException
     */
    public void getChildren(ContainerNode parent,  VOSURI start, Integer limit, URI sortProperty, Boolean sortAsc, boolean resolveMetadata) throws TransientException
    {
        log.debug("getChildren: " + parent.getUri().getPath() + ", " + parent.getClass().getSimpleName());
        expectPersistentNode(parent);

        Object[] args = null;
        if (start != null) {
            args = new Object[] { start.getName() };
            if (sortProperty != null) {
                // get the persistent node
                Node startNode = this.getPath(start.getPath(), true, false);
                if (startNode == null) {
                    throw new IllegalArgumentException("offset child doesn't exist");
                }
                args = new Object[] { startNode.getPropertyValue(sortProperty.toString()) };
            }
        }
        else {
            args = new Object[0];
        }

        // we must re-run the query in case server-side content changed since the argument node
        // was called, e.g. from delete(node) or markForDeletion(node)
        String sql = getSelectNodesByParentSQL(parent, limit, (start!=null), sortProperty, sortAsc);
        log.debug("getChildren: " + sql);

        TransactionStatus dirtyRead = null;
        try
        {
            dirtyRead = transactionManager.getTransaction(dirtyReadTransactionDef);
            prof.checkpoint("TransactionManager.getTransaction");

            List nodes = jdbc.query(sql,  args,
                new NodeMapper(authority, parent.getUri().getPath()));
            prof.checkpoint("getSelectNodesByParentSQL");

            transactionManager.commit(dirtyRead);
            dirtyRead = null;
            prof.checkpoint("commit.getSelectNodesByParentSQL");

            getOwners(nodes, resolveMetadata);

            addChildNodes(parent, nodes);
        }
        catch(CannotCreateTransactionException ex)
        {
            log.error("failed to create transaction: " + parent.getUri().getPath(), ex);
            if (DBUtil.isTransientDBException(ex))
                throw new TransientException("failed to get node: " + parent.getUri().getPath(), ex);
            else
                throw new RuntimeException("failed to get node: " + parent.getUri().getPath(), ex);
        }
        catch(Throwable t)
        {
            log.error("rollback dirtyRead for node: " + parent.getUri().getPath(), t);
            try
            {
                transactionManager.rollback(dirtyRead);
                dirtyRead = null;
                prof.checkpoint("rollback.getSelectNodesByParentSQL");
            }
            catch(Throwable oops) { log.error("failed to dirtyRead rollback transaction", oops); }

            if (DBUtil.isTransientDBException(t))
                throw new TransientException("failed to get node: " + parent.getUri().getPath(), t);
            else
                throw new RuntimeException("failed to get node: " + parent.getUri().getPath(), t);
        }
        finally
        {
            if (dirtyRead != null)
                try
                {
                    log.warn("put: BUG - dirtyRead transaction still open in finally... calling rollback");
                    transactionManager.rollback(dirtyRead);
                }
                catch(Throwable oops) { log.error("failed to rollback dirtyRead transaction in finally", oops); }
        }
    }

    /**
     * Add the provided children to the parent.
     */
    private void addChildNodes(ContainerNode parent, List nodes)
    {
        if (parent.getNodes().isEmpty())
        {
            for (Node n : nodes)
            {
                log.debug("adding child to list: " + n.getUri().getPath());
                parent.getNodes().add(n);
                n.setParent(parent);
            }
        }
        else
        {
            // 'nodes' will not have duplicates, but 'parent.getNodes()' may
            // already contain some of 'nodes'.
            List existingChildren = new ArrayList(parent.getNodes().size());
            existingChildren.addAll(parent.getNodes());
            for (Node n : nodes)
            {
                if (!existingChildren.contains(n))
                {
                    log.debug("adding child to list: " + n.getUri().getPath());
                    n.setParent(parent);
                    parent.getNodes().add(n);
                }
                else
                    log.debug("child already in list, not adding: " + n.getUri().getPath());
            }
        }
    }
    
    private void getOwners(List nodes, boolean resolve)
    {
        for (Node n : nodes)
        {
            getOwners(n, resolve);
        }
    }

    private void getOwners(Node node, boolean resolve)
    {
        if (node == null || node.appData == null)
            return;

        NodeID nid = (NodeID) node.appData;
        if (nid.owner != null)
            return; // already loaded (parent loop below)

        String ownerPropertyString = null;
        Subject s;
        if (resolve)
        {
            s = identityCache.get(nid.ownerObject);

            if (s == null)
            {
                log.debug("lookup subject for owner=" + nid.ownerObject);
                s = identManager.toSubject(nid.ownerObject);
                prof.checkpoint("IdentityManager.toSubject");
                identityCache.put(nid.ownerObject, s);
            }
            else
            {
                log.debug("found cached subject for owner=" + nid.ownerObject);
            }

            ownerPropertyString = identManager.toDisplayString(s);
        }
        else
        {
            log.debug("creating numeric principal only subject.");
            s = new Subject();
            if (nid.ownerObject != null)
            {
                Integer ownerInt = (Integer) nid.ownerObject;
                UUID numericID = new UUID(0L, (long) ownerInt);
                s.getPrincipals().add(new NumericPrincipal(numericID));
                ownerPropertyString = ownerInt.toString();
            }
        }

        nid.owner = s;
        if (ownerPropertyString != null)
            node.getProperties().add(new NodeProperty(VOS.PROPERTY_URI_CREATOR, ownerPropertyString));

        Node parent = node.getParent();
        while (parent != null)
        {
            getOwners(parent, resolve);
            parent = parent.getParent();
        }
    }

    /**
     * Store the specified node. The node must be attached to a parent container and
     * the parent container must have already been persisted.
     *
     * @param node
     * @param creator
     * @return the same node but with generated internal ID set in the appData field
     */
    public Node put(Node node, Subject creator) throws TransientException
    {
        log.debug("put: " + node.getUri().getPath() + ", " + node.getClass().getSimpleName());

        // if parent is null, this is just a new root-level node,
        if (node.getParent() != null && node.getParent().appData == null)
            throw new IllegalArgumentException("parent of node is not a persistent node: " + node.getUri().getPath());

        if (node.appData != null) // persistent node == update == not supported
            throw new UnsupportedOperationException("update of existing node not supported; try updateProperties");

        if (node.getName().length() > NODE_NAME_COLUMN_SIZE)
            throw new IllegalArgumentException("length of node name exceeds limit ("+NODE_NAME_COLUMN_SIZE+"): " + node.getName());

        try
        {
            // call IdentityManager outside resource lock to avoid deadlock
            NodeID nodeID = new NodeID();
            nodeID.owner = creator;
            nodeID.ownerObject = identManager.toOwner(creator);
            if (NODE_TYPE_DATA.equals(getNodeType(node))) {
                UUID uuid = UUID.randomUUID();
                nodeID.storageID = uuid.toString();
            }
            node.appData = nodeID;

            startTransaction();
            prof.checkpoint("start.NodePutStatementCreator");
            NodePutStatementCreator npsc = new NodePutStatementCreator(nodeSchema, false);
            npsc.setValues(node, null);
            KeyHolder keyHolder = new GeneratedKeyHolder();
            jdbc.update(npsc, keyHolder);
            nodeID.id = new Long(keyHolder.getKey().longValue());
            prof.checkpoint("NodePutStatementCreator");

            Iterator propertyIterator = node.getProperties().iterator();
            while (propertyIterator.hasNext())
            {
                NodeProperty prop = propertyIterator.next();

                if (prop.getPropertyValue() != null && prop.getPropertyValue().length() > NODE_PROPERTY_COLUMN_SIZE)
                    throw new IllegalArgumentException("length of node property value exceeds limit ("+NODE_PROPERTY_COLUMN_SIZE+"): " + prop.getPropertyURI());

                if ( usePropertyTable(prop.getPropertyURI()) )
                {
                    PropertyStatementCreator ppsc = new PropertyStatementCreator(nodeSchema, nodeID, prop, false);
                    jdbc.update(ppsc);
                    prof.checkpoint("PropertyStatementCreator");
                }
                // else: already persisted by the NodePutStatementCreator above
                // note: very important that the node owner (creator property) is excluded
                // by the above usePropertyTable returning false
            }

            commitTransaction();
            prof.checkpoint("commit.NodePutStatementCreator");

            node.getProperties().add(new NodeProperty(VOS.PROPERTY_URI_CREATOR, identManager.toDisplayString(creator)));
            if (node instanceof ContainerNode)
                node.getProperties().add(new NodeProperty(VOS.PROPERTY_URI_CONTENTLENGTH, Long.toString(0)));
            return node;
        }
        catch(CannotCreateTransactionException ex)
        {
            log.error("failed to create transaction: " + node.getUri().getPath(), ex);
            if (DBUtil.isTransientDBException(ex))
                throw new TransientException("failed to get node: " + node.getUri().getPath(), ex);
            else
                throw new RuntimeException("failed to get node: " + node.getUri().getPath(), ex);
        }
        catch(Throwable t)
        {
            log.error("rollback for node: " + node.getUri().getPath(), t);
            try
            {
                rollbackTransaction();
                prof.checkpoint("rollback.NodePutStatementCreator");
            }
            catch(Throwable oops) { log.error("failed to rollback transaction", oops); }

            if (DBUtil.isTransientDBException(t))
                throw new TransientException("failed to persist node: " + node.getUri(), t);
            else
                throw new RuntimeException("failed to persist node: " + node.getUri(), t);
        }
        finally
        {
            if (transactionStatus != null)
                try
                {
                    log.warn("put: BUG - transaction still open in finally... calling rollback");
                    rollbackTransaction();
                }
                catch(Throwable oops) { log.error("failed to rollback transaction in finally", oops); }
        }
    }

    /**
     * Recursive delete of a node. This method irrevocably deletes a node and all
     * the child nodes below it.
     *
     * @param node
     */
    public void delete(Node node) throws TransientException
    {
        log.debug("delete: " + node.getUri().getPath() + ", " + node.getClass().getSimpleName());
        expectPersistentNode(node);

        try
        {
            if (node instanceof ContainerNode)
            {
                ContainerNode dest = (ContainerNode) getPath(deletedNodePath);
                // need a unique name under /deletedPath
                String idName = getNodeID(node) + "-" + node.getName();
                node.setName(idName);
                // move handles the transaction, container size changes, rename, and reparent
                move(node, dest);
            }
            else
            {
                startTransaction();
                prof.checkpoint("start.delete");

                // lock the child
                String sql = getUpdateLockSQL(node);
                jdbc.update(sql);
                prof.checkpoint("getUpdateLockSQL");

                if (node instanceof DataNode)
                {
                    // get the contentLength value
                    sql = getSelectContentLengthForDeleteSQL(node);

                    // Note: if node size is null, the jdbc template
                    // will return zero.
                    Long sizeDifference = jdbc.queryForObject(sql, new LongRowMapper());
                    prof.checkpoint("getSelectContentLengthSQL");

                    // delete the node only if it is not busy
                    deleteNode(node, true);

                    // apply the negative size difference to the parent
                    sql = this.getApplySizeDiffSQL(node.getParent(), sizeDifference, false);
                    log.debug(sql);
                    jdbc.update(sql);
                    prof.checkpoint("getApplySizeDiffSQL");
                }
                else if (node instanceof LinkNode)
                {
                    // delete the node
                    deleteNode(node, false);
                }
                else
                	throw new RuntimeException("BUG - unsupported node type: " + node.getClass());

                commitTransaction();
                prof.checkpoint("commit.delete");
            }
            log.debug("Node deleted: " + node.getUri().getPath());
        }
        catch(CannotCreateTransactionException ex)
        {
            log.error("failed to create transaction: " + node.getUri().getPath(), ex);
            if (DBUtil.isTransientDBException(ex))
                throw new TransientException("failed to get node: " + node.getUri().getPath(), ex);
            else
                throw new RuntimeException("failed to get node: " + node.getUri().getPath(), ex);
        }
        catch(IllegalStateException ex)
        {
            log.debug(ex.toString());
            if (transactionStatus != null)
                try
                {
                    rollbackTransaction();
                    prof.checkpoint("rollback.delete");
                }
                catch(Throwable oops) { log.error("failed to rollback transaction", oops); }
            throw ex;
        }
        catch (Throwable t)
        {
            log.error("delete rollback for node: " + node.getUri().getPath(), t);
            if (transactionStatus != null)
                try
                {
                    rollbackTransaction();
                    prof.checkpoint("rollback.delete");
                }
                catch(Throwable oops) { log.error("failed to rollback transaction", oops); }

            if (DBUtil.isTransientDBException(t))
                throw new TransientException("failed to delete " + node.getUri().getPath(), t);
            else
                throw new RuntimeException("failed to delete " + node.getUri().getPath(), t);
        }
        finally
        {
            if (transactionStatus != null)
                try
                {
                    log.warn("delete - BUG - transaction still open in finally... calling rollback");
                    rollbackTransaction();
                }
                catch(Throwable oops) { log.error("failed to rollback transaction in finally", oops); }
        }
    }

    private void deleteNode(Node node, boolean notBusyOnly)
    {
        // delete properties: FK constraint -> node
        String sql = getDeleteNodePropertiesSQL(node);
        log.debug(sql);
        jdbc.update(sql);
        prof.checkpoint("getDeleteNodePropertiesSQL");

        // delete the node
        sql = getDeleteNodeSQL(node, notBusyOnly);
        log.debug(sql);
        int count = jdbc.update(sql);
        prof.checkpoint("getDeleteNodeSQL");
        if (count == 0)
            throw new IllegalStateException("node busy or path changed during delete: "+node.getUri());
    }

    /**
     * Change the busy stateof a node from a known state to another.
     *
     * @param node
     * @param curState
     * @param newState
     */
    public void setBusyState(DataNode node, NodeBusyState curState, NodeBusyState newState) throws TransientException
    {
        log.debug("setBusyState: " + node.getUri().getPath() + ", " + curState + " -> " + newState);
        expectPersistentNode(node);

        try
        {
            startTransaction();
            prof.checkpoint("start.getSetBusyStateSQL");
            String sql = getSetBusyStateSQL(node, curState, newState);
            log.debug(sql);
            int num = jdbc.update(sql);
            prof.checkpoint("getSetBusyStateSQL");
            if (num != 1)
                throw new IllegalStateException("setBusyState " + curState + " -> " + newState + " failed: " + node.getUri());
            commitTransaction();
            prof.checkpoint("commit.getSetBusyStateSQL");
        }
        catch(CannotCreateTransactionException ex)
        {
            log.error("failed to create transaction: " + node.getUri().getPath(), ex);
            if (DBUtil.isTransientDBException(ex))
                throw new TransientException("failed to get node: " + node.getUri().getPath(), ex);
            else
                throw new RuntimeException("failed to get node: " + node.getUri().getPath(), ex);
        }
        catch(IllegalStateException ex)
        {
            log.debug(ex.toString());
            if (transactionStatus != null)
                try
                {
                    rollbackTransaction();
                    prof.checkpoint("rollback.getSetBusyStateSQL");
                }
                catch(Throwable oops) { log.error("failed to rollback transaction", oops); }
            throw ex;
        }
        catch (Throwable t)
        {
            log.error("Delete rollback for node: " + node.getUri().getPath(), t);
            if (transactionStatus != null)
                try
                {
                    rollbackTransaction();
                    prof.checkpoint("rollback.getSetBusyStateSQL");
                }
                catch(Throwable oops) { log.error("failed to rollback transaction", oops); }

            if (DBUtil.isTransientDBException(t))
                throw new TransientException("failed to updateNodeMetadata " + node.getUri().getPath(), t);
            else
                throw new RuntimeException("failed to updateNodeMetadata " + node.getUri().getPath(), t);
        }
        finally
        {
            if (transactionStatus != null)
                try
                {
                    log.warn("delete - BUG - transaction still open in finally... calling rollback");
                    rollbackTransaction();
                }
                catch(Throwable oops) { log.error("failed to rollback transaction in finally", oops); }
        }
    }

    /**
     * Update the size of specified node, plus set BusyState of the DataNode to not-busy.
     *
     * @param node
     * @param meta the new metadata
     * @param strict If the update should only occur if the lastModified date is the same.
     */
    public void updateNodeMetadata(DataNode node, FileMetadata meta, boolean strict) throws TransientException
    {
        log.debug("updateNodeMetadata: " + node.getUri().getPath());

        expectPersistentNode(node);

        try
        {
            startTransaction();
            prof.checkpoint("start.DataNodeUpdateStatementCreator");

            // get the last modified date object if matching on the update
            Date lastModified = null;
            if (strict)
            {
                String lastModStr = node.getPropertyValue(VOS.PROPERTY_URI_DATE);
                lastModified = dateFormat.parse(lastModStr);
            }

            // update contentLength and md5
            DataNodeUpdateStatementCreator dnup = new DataNodeUpdateStatementCreator(
                    getNodeID(node), meta.getContentLength(), meta.getMd5Sum(), lastModified);
            int num = jdbc.update(dnup);
            prof.checkpoint("DataNodeUpdateStatementCreator");
            log.debug("updateMetadata, rows updated: " + num);
            if (strict && num != 1)
                throw new IllegalStateException("Node has different lastModified value.");

            // last, update the busy state of the target node
            String trans = getSetBusyStateSQL(node, NodeBusyState.busyWithWrite, NodeBusyState.notBusy);
            log.debug(trans);
            num = jdbc.update(trans);
            prof.checkpoint("getSetBusyStateSQL");
            if (num != 1)
                throw new IllegalStateException("updateFileMetadata requires a node with busyState=W: "+node.getUri());

            // now safe to update properties of the target node
            List props = new ArrayList();
            NodeProperty np;

            np = findOrCreate(node, VOS.PROPERTY_URI_CONTENTENCODING, meta.getContentEncoding());
            if (np != null)
                props.add(np);
            np = findOrCreate(node, VOS.PROPERTY_URI_TYPE, meta.getContentType());
            if (np != null)
                props.add(np);

            doUpdateProperties(node, props);

            commitTransaction();
            prof.checkpoint("commit.DataNodeUpdateStatementCreator");
        }
        catch(IllegalStateException ex)
        {
            log.debug(ex.toString());
            if (transactionStatus != null)
                try
                {
                    rollbackTransaction();
                    prof.checkpoint("rollback.DataNodeUpdateStatementCreator");
                }
                catch(Throwable oops) { log.error("failed to rollback transaction", oops); }
            throw ex;
        }
        catch (Throwable t)
        {
            log.error("updateNodeMetadata rollback for node: " + node.getUri().getPath(), t);
            if (transactionStatus != null)
                try
                {
                    rollbackTransaction();
                    prof.checkpoint("rollback.DataNodeUpdateStatementCreator");
                }
                catch(Throwable oops) { log.error("failed to rollback transaction", oops); }

            if (DBUtil.isTransientDBException(t))
                throw new TransientException("failed to updateNodeMetadata " + node.getUri().getPath(), t);
            else
                throw new RuntimeException("failed to updateNodeMetadata " + node.getUri().getPath(), t);

        }
        finally
        {
            if (transactionStatus != null)
                try
                {
                    log.warn("delete - BUG - transaction still open in finally... calling rollback");
                    rollbackTransaction();
                }
                catch(Throwable oops) { log.error("failed to rollback transaction in finally", oops); }
        }
    }

    // find existing prop, mark for delete if value is null or set value
    // create new prop with value
    private NodeProperty findOrCreate(Node node, String uri, String value)
    {
        NodeProperty np = node.findProperty(uri);
        if (np == null && value == null)
            return null;

        if (value == null)
            np.setMarkedForDeletion(true);
        else if (np == null)
            np = new NodeProperty(uri, value);
        else
            np.setValue(value);

        return np;
    }

    /**
     * Update the properties associated with this node.  New properties are added,
     * changed property values are updated, and properties marked for deletion are
     * removed. NOTE: support for multiple values not currently implemented.
     *
     * @param node the current persisted node
     * @param properties the new properties
     * @return the modified node
     */
    public Node updateProperties(Node node, List properties) throws TransientException
    {
        log.debug("updateProperties: " + node.getUri().getPath() + ", " + node.getClass().getSimpleName());
        expectPersistentNode(node);

        try
        {
            startTransaction();
            prof.checkpoint("start.updateProperties");

            Node ret = doUpdateProperties(node, properties);

            commitTransaction();
            prof.checkpoint("commit.updateProperties");

            return ret;
        }
        catch (Throwable t)
        {
            log.error("Update rollback for node: " + node.getUri().getPath(), t);
            if (transactionStatus != null)
                try
                {
                    rollbackTransaction();
                    prof.checkpoint("rollback.updateProperties");
                }
                catch(Throwable oops) { log.error("failed to rollback transaction", oops); }

            if (DBUtil.isTransientDBException(t))
                throw new TransientException("failed to update properties:  " + node.getUri().getPath(), t);
            else
                throw new RuntimeException("failed to update properties:  " + node.getUri().getPath(), t);
        }
        finally
        {
            if (transactionStatus != null)
                try
                {
                    log.warn("updateProperties - BUG - transaction still open in finally... calling rollback");
                    rollbackTransaction();
                }
                catch(Throwable oops) { log.error("failed to rollback transaction in finally", oops); }
        }
    }

    private Node doUpdateProperties(Node node, List properties)
    {
        NodeID nodeID = (NodeID) node.appData;

        // Iterate through the user properties and the db properties,
        // potentially updating, deleting or adding new ones
        List updates = new ArrayList();
        for (NodeProperty prop : properties)
        {
            boolean propTable = usePropertyTable(prop.getPropertyURI());
            NodeProperty cur = node.findProperty(prop.getPropertyURI());
            // Does this property exist already?
            log.debug("updateProperties: " + prop + " vs. " + cur);

            if (cur != null)
            {
                if (prop.isMarkedForDeletion())
                {
                    if (propTable)
                    {
                        log.debug("doUpdateNode " + prop.getPropertyURI() + " to be deleted from NodeProperty");
                        PropertyStatementCreator ppsc = new PropertyStatementCreator(nodeSchema, nodeID, prop);
                        updates.add(ppsc);
                    }
                    else
                    {
                        log.debug("doUpdateNode " + prop.getPropertyURI() + " to be set to null in Node");
                    }
                    boolean rm = node.getProperties().remove(prop);
                    log.debug("removed " + prop.getPropertyURI() + " from node: " + rm);
                }
                else // update
                {
                    String currentValue = cur.getPropertyValue();
                    if (!currentValue.equals(prop.getPropertyValue()))
                    {

                        if (prop.getPropertyValue() != null && prop.getPropertyValue().length() > NODE_PROPERTY_COLUMN_SIZE)
                            throw new IllegalArgumentException("length of node property value exceeds limit ("+NODE_PROPERTY_COLUMN_SIZE+"): " + prop.getPropertyURI());

                        log.debug("doUpdateNode " + prop.getPropertyURI() + ": "
                                + currentValue + " != " + prop.getPropertyValue());
                        if (propTable)
                        {
                            log.debug("doUpdateNode " + prop.getPropertyURI() + " to be updated in NodeProperty");
                            PropertyStatementCreator ppsc = new PropertyStatementCreator(nodeSchema, nodeID, prop, true);
                            updates.add(ppsc);
                        }
                        else
                        {
                            log.debug("doUpdateNode " + prop.getPropertyURI() + " to be updated in Node");
                        }
                        cur.setValue(prop.getPropertyValue());
                    }
                    else
                    {
                        log.debug("Value unchanged, not updating node property: " + prop.getPropertyURI());
                    }
                }
            }
            else if ( !prop.isMarkedForDeletion() )
            {
                if (prop.getPropertyValue() != null && prop.getPropertyValue().length() > NODE_PROPERTY_COLUMN_SIZE)
                    throw new IllegalArgumentException("length of node property value exceeds limit ("+NODE_PROPERTY_COLUMN_SIZE+"): " + prop.getPropertyURI());

                if (propTable)
                {
                    log.debug("doUpdateNode " + prop.getPropertyURI() + " to be inserted into NodeProperty");
                    PropertyStatementCreator ppsc = new PropertyStatementCreator(nodeSchema, nodeID, prop);
                    updates.add(ppsc);
                }
                else
                {
                    log.debug("doUpdateNode " + prop.getPropertyURI() + " to be inserted into Node");
                }
                node.getProperties().add(prop);
            }
        }
        // OK: update Node, then NodeProperty(s)
        NodePutStatementCreator npsc = new NodePutStatementCreator(nodeSchema, true);
        npsc.setValues(node, null);
        jdbc.update(npsc);
        prof.checkpoint("NodePutStatementCreator");

        for (PropertyStatementCreator psc : updates)
        {
            jdbc.update(psc);
            prof.checkpoint("PropertyStatementCreator");
        }

        return node;
    }

    /**
     * Move the node to inside the destination container.
     *
     * @param src The node to move
     * @param dest The container in which to move the node.
     */
    public void move(Node src, ContainerNode dest) throws TransientException
    {
        log.debug("move: " + src.getUri() + " to " + dest.getUri() + " as " + src.getName());
        expectPersistentNode(src);
        expectPersistentNode(dest);

        // move rule checking
        if (src instanceof ContainerNode)
        {
            // check that we are not moving root or a root container
            if (src.getParent() == null || src.getParent().getUri().isRoot())
                throw new IllegalArgumentException("Cannot move a root container.");

            // check that 'src' is not in the path of 'dest' so that
            // circular paths are not created
            Node target = dest;
            Long srcNodeID = getNodeID(src);
            Long targetNodeID = null;
            while (target != null && !target.getUri().isRoot())
            {
                targetNodeID = getNodeID(target);
                if (targetNodeID.equals(srcNodeID))
                    throw new IllegalArgumentException("Cannot move to a contained sub-node.");
                target = target.getParent();
            }
        }

        try
        {
            startTransaction();
            prof.checkpoint("start.move");

            // get the lock
            String sql = this.getUpdateLockSQL(src);
            jdbc.update(sql);
            prof.checkpoint("getUpdateLockSQL");

            Long contentLength = new Long(0);
            if (!(src instanceof LinkNode))
            {
                // get the contentLength
                sql = this.getSelectContentLengthForDeleteSQL(src);

                // Note: if contentLength is null, jdbc template will return zero.
                contentLength = jdbc.queryForObject(sql, new LongRowMapper());
                prof.checkpoint("getSelectContentLengthSQL");
            }

            // re-parent the node
            ContainerNode srcParent = src.getParent();
            src.setParent(dest);

            // update the node with the new parent and potentially new name
            NodePutStatementCreator putStatementCreator = new NodePutStatementCreator(nodeSchema, true);
            putStatementCreator.setValues(src, null);
            int count = jdbc.update(putStatementCreator);
            prof.checkpoint("NodePutStatementCreator");
            if (count == 0)
            {
                // tried to move a busy data node
                throw new IllegalStateException("src node busy: "+src.getUri());
            }

            if (!(src instanceof LinkNode))
            {
                // apply the size difference
                String sql1 = getApplySizeDiffSQL(srcParent, contentLength, false);
                String sql2 = getApplySizeDiffSQL(dest, contentLength, true);

                // these operations should happen in either child-parent or nodeID order
                // for consistency to avoid deadlocks
                if ( srcParent.getParent() != null && src.getParent().equals(dest) )
                {
                    // OK: sql1 is child and sql2 is parent
                }
                else if ( dest.getParent() != null && dest.getParent().equals(srcParent) )
                {
                    // sql1 is parent and sql2 is child: swap
                    String swap = sql1;
                    sql1 = sql2;
                    sql2 = swap;
                }
                else if (getNodeID(srcParent) > getNodeID(dest))
                {
                    String swap = sql1;
                    sql1 = sql2;
                    sql2 = swap;
                }

                log.debug(sql1);
                jdbc.update(sql1);
                prof.checkpoint("getApplySizeDiffSQL");
                log.debug(sql2);
                jdbc.update(sql2);
                prof.checkpoint("getApplySizeDiffSQL");
            }

            // recursive chown removed since it is costly and nominally incorrect

            commitTransaction();
            prof.checkpoint("commit.move");
        }
        catch(IllegalStateException ex)
        {
            log.debug(ex.toString());
            if (transactionStatus != null)
                try
                {
                    rollbackTransaction();
                    prof.checkpoint("rollback.move");
                }
                catch(Throwable oops) { log.error("failed to rollback transaction", oops); }
            throw ex;
        }
        catch (Throwable t)
        {
            log.error("move rollback for node: " + src.getUri().getPath(), t);
            if (transactionStatus != null)
                try
                {
                    rollbackTransaction();
                    prof.checkpoint("rollback.move");
                }
                catch(Throwable oops) { log.error("failed to rollback transaction", oops); }

            if (t instanceof IllegalStateException)
                throw (IllegalStateException) t;
            else if (DBUtil.isTransientDBException(t))
                throw new TransientException("failed to move:  " + src.getUri().getPath(), t);
            else
                throw new RuntimeException("failed to move:  " + src.getUri().getPath(), t);
        }
        finally
        {
            if (transactionStatus != null)
            {
                try
                {
                    log.warn("move - BUG - transaction still open in finally... calling rollback");
                    rollbackTransaction();
                }
                catch(Throwable oops) { log.error("failed to rollback transaction in finally", oops); }
            }
        }
    }


    /**
     * Copy the node to the new path.
     *
     * @param src 
     * @param dest
     * @throws UnsupportedOperationException Until implementation is complete.
     */
    public void copy(Node src, ContainerNode dest) throws TransientException
    {
        log.debug("copy: " + src.getUri() + " to " + dest.getUri() + " as " + src.getName());
        throw new UnsupportedOperationException("Copy not implemented.");
    }

    // admin functions

    private int commitBatch(String name, int batchSize, int count, boolean dryrun)
    {
        return commitBatch(name, batchSize, count, dryrun, false);
    }
    private int commitBatch(String name, int batchSize, int count, boolean dryrun, boolean force)
    {
        if (!dryrun && (count >= batchSize || force))
        {
            commitTransaction();
            log.info(name + " batch committed: " + count);
            count = 0;
            startTransaction();
        }
        return count;
    }

    /**
     * Recursively delete a container in one or more batchSize-d transactions. The
     * actual number of rows deleted per transaction is not exact since deletion of
     * node properties is done via a single delete statement.
     * 

* Note: this method does not update parent container node sizes while * deleting nodes and should not be used on actual user content. It can also * fail at some point and have committed some deletions due to the batching; * the caller should resolve the issue and then call it again to continue * (delete is bottom up so there are never any orphans). * * @param node * @param batchSize */ void delete(Node node, int batchSize, boolean dryrun) throws TransientException { log.debug("delete: " + node.getUri().getPath() + "," + batchSize); expectPersistentNode(node); if (batchSize < 1) throw new IllegalArgumentException("batchSize must be positive"); try { this.adminStatementCreator = new NodePutStatementCreator(nodeSchema, true); if (!dryrun) startTransaction(); int count = deleteNode(node, batchSize, 0, dryrun); if (!dryrun) { commitTransaction(); log.info("delete batch committed: " + count); } } catch (Throwable t) { log.error("chown rollback for node: " + node.getUri().getPath(), t); if (transactionStatus != null) try { rollbackTransaction(); } catch(Throwable oops) { log.error("failed to rollback transaction", oops); } if (DBUtil.isTransientDBException(t)) throw new TransientException("failed to delete: " + node.getUri().getPath(), t); else throw new RuntimeException("failed to delete: " + node.getUri().getPath(), t); } finally { if (transactionStatus != null) { try { log.warn("chown - BUG - transaction still open in finally... calling rollback"); rollbackTransaction(); } catch(Throwable oops) { log.error("failed to rollback transaction in finally", oops); } } } } private int deleteNode(Node node, int batchSize, int count, boolean dryrun) { log.debug("deleteNode: " + node.getClass().getSimpleName() + " " + node.getUri().getPath()); // delete children: depth first so we don't leave orphans if (node instanceof ContainerNode) { ContainerNode cn = (ContainerNode) node; count = deleteChildren(cn, batchSize, count, dryrun); } String sql = null; Long sizeDifference = null; // get the contentLength value sql = getSelectContentLengthForDeleteSQL(node); log.debug(sql); if (!dryrun) { // Note: if node size is null, the jdbc template // will return zero. sizeDifference = jdbc.queryForObject(sql, new LongRowMapper()); prof.checkpoint("getSelectContentLengthSQL"); } // delete properties: so we don't violate FK constraint -> node sql = getDeleteNodePropertiesSQL(node); log.debug(sql); if (!dryrun) count += jdbc.update(sql); // delete the node itself sql = getDeleteNodeSQL(node, false); // admin mode: ignore busy state log.debug(sql); if (!dryrun) { int num = jdbc.update(sql); if (num == 0) throw new IllegalStateException("node busy or path changed during delete: "+node.getUri()); count += num; } count = commitBatch("delete", batchSize, count, dryrun); // apply the negative size difference to the parent if (node.getParent() != null && sizeDifference != null) { sql = getApplySizeDiffSQL(node.getParent(), sizeDifference, false); log.debug(sql); if (!dryrun) { jdbc.update(sql); prof.checkpoint("getApplySizeDiffSQL"); } } return count; } private int deleteChildren(ContainerNode container, int batchSize, int count, boolean dryrun) { String sql = getSelectNodesByParentSQL(container, CHILD_BATCH_SIZE, false); log.debug(sql); NodeMapper mapper = new NodeMapper(authority, container.getUri().getPath()); List children = jdbc.query(sql, new Object[0], mapper); Object[] args = new Object[1]; boolean foundChildren = false; while (children.size() > 0) { foundChildren = true; Node cur = null; for (Node child : children) { cur = child; child.setParent(container); count = deleteNode(child, batchSize, count, dryrun); count = commitBatch("delete", batchSize, count, dryrun); } sql = getSelectNodesByParentSQL(container,CHILD_BATCH_SIZE, true); log.debug(sql); args[0] = cur.getName(); children = jdbc.query(sql, args, mapper); children.remove(cur); // the query is name >= cur and we already processed cur } // always force commit before going back up to parent to avoid deadlocks with nodePropagation if (foundChildren) count = commitBatch("delete", batchSize, count, dryrun, true); return count; } /** * Change ownership of a Node (optionally recursive) in one or more * batchSize-d transactions. * * @param node * @param newOwner * @param recursive * @param batchSize */ void chown(Node node, Subject newOwner, boolean recursive, int batchSize, boolean dryrun) throws TransientException { log.debug("chown: " + node.getUri().getPath() + ", " + newOwner + ", " + recursive + "," + batchSize); expectPersistentNode(node); if (batchSize < 1) throw new IllegalArgumentException("batchSize must be positive"); try { Object newOwnerObj = identManager.toOwner(newOwner); this.adminStatementCreator = new NodePutStatementCreator(nodeSchema, true); if (!dryrun) startTransaction(); int count = chownNode(node, newOwnerObj, recursive, batchSize, 0, dryrun); if (!dryrun) commitTransaction(); log.debug("chown batch committed: " + count); } catch (Throwable t) { log.error("chown rollback for node: " + node.getUri().getPath(), t); if (transactionStatus != null) try { rollbackTransaction(); } catch(Throwable oops) { log.error("failed to rollback transaction", oops); } if (DBUtil.isTransientDBException(t)) throw new TransientException("failed to chown: " + node.getUri().getPath(), t); else throw new RuntimeException("failed to chown: " + node.getUri().getPath(), t); } finally { if (transactionStatus != null) { try { log.warn("chown - BUG - transaction still open in finally... calling rollback"); rollbackTransaction(); } catch(Throwable oops) { log.error("failed to rollback transaction in finally", oops); } } } } private int chownNode(Node node, Object newOwnerObject, boolean recursive, int batchSize, int count, boolean dryrun) { // update the node with the specfied owner. adminStatementCreator.setValues(node, newOwnerObject); if (!dryrun) count += jdbc.update(adminStatementCreator); count = commitBatch("chown", batchSize, count, dryrun); if (recursive && (node instanceof ContainerNode)) count = chownChildren((ContainerNode) node, newOwnerObject, batchSize, count, dryrun); return count; } private int chownChildren(ContainerNode container, Object newOwnerObj, int batchSize, int count, boolean dryrun) { String sql = getSelectNodesByParentSQL(container, CHILD_BATCH_SIZE, false); NodeMapper mapper = new NodeMapper(authority, container.getUri().getPath()); List children = jdbc.query(sql, new Object[0], mapper); Object[] args = new Object[1]; while (children.size() > 0) { Node cur = null; for (Node child : children) { cur = child; child.setParent(container); count = chownNode(child, newOwnerObj, true, batchSize, count, dryrun); } sql = getSelectNodesByParentSQL(container,CHILD_BATCH_SIZE, true); args[0] = cur.getName(); children = jdbc.query(sql, args, mapper); children.remove(cur); // the query is name >= cur and we already processed cur } return count; } /** * Admin function. * @param limit The maximum to return * @return A list of outstanding node size propagations */ List getOutstandingPropagations(int limit, boolean dataNodesOnly) { try { String sql = this.getFindOutstandingPropagationsSQL(limit, dataNodesOnly); log.debug("getOutstandingPropagations (limit " + limit + "): " + sql); NodeSizePropagationExtractor propagationExtractor = new NodeSizePropagationExtractor(); List propagations = (List) jdbc.query(sql, propagationExtractor); return propagations; } catch (Throwable t) { String message = "getOutstandingPropagations failed: " + t.getMessage(); log.error(message, t); throw new RuntimeException(message, t); } } /** * Admin function. * @param propagation */ void applyPropagation(NodeSizePropagation propagation) throws TransientException { log.debug("applyPropagation: " + propagation); try { startTransaction(); // apply progagation updates String[] propagationSQL = getApplyDeltaSQL(propagation); for (String sql : propagationSQL) { log.debug(sql); int rowsUpdated = jdbc.update(sql); // each statement should update exactly one row if (rowsUpdated != 1) throw new RuntimeException("node structure changed, aborting on transation: " + sql); } commitTransaction(); log.debug("applyPropagation committed."); } catch (Throwable t) { log.error("applyPropagation rollback", t); if (transactionStatus != null) try { rollbackTransaction(); } catch(Throwable oops) { log.error("failed to rollback transaction", oops); } if (DBUtil.isTransientDBException(t)) throw new TransientException("failed to apply propagation.", t); else throw new RuntimeException("failed to apply propagation.", t); } finally { if (transactionStatus != null) { try { log.warn("applyPropagation - BUG - transaction still open in finally... calling rollback"); rollbackTransaction(); } catch(Throwable oops) { log.error("failed to rollback transaction in finally", oops); } } } } /** * Extract the internal nodeID of the provided node from the appData field. * @param node * @return a nodeID or null for a non-persisted node */ protected static Long getNodeID(Node node) { if (node == null || node.appData == null) { return null; } if (node.appData instanceof NodeID) { return ((NodeID) node.appData).getID(); } return null; } /** * The resulting SQL must use a PreparedStatement with one argument * (child node name). The ResultSet can be processsed with a NodeMapper. * * @param parent * @return SQL prepared statement string */ protected String getSelectChildNodeSQL(ContainerNode parent) { StringBuilder sb = new StringBuilder(); sb.append("SELECT nodeID"); for (String col : NODE_COLUMNS) { sb.append(","); sb.append(col); } sb.append(" FROM "); sb.append(getNodeTableName()); Long nid = getNodeID(parent); if (nid != null) { sb.append(" WHERE parentID = "); sb.append(getNodeID(parent)); } else sb.append(" WHERE parentID IS NULL"); sb.append(" AND name = ?"); return sb.toString(); } /** * The resulting SQL is a simple select statement. The ResultSet can be * processed with a NodeMapper. * * @param parent The node to query for. * @param limit * @param hasStart * @return simple SQL statement select for use with NodeMapper */ protected String getSelectNodesByParentSQL(ContainerNode parent, Integer limit, boolean hasStart) { return getSelectNodesByParentSQL(parent, limit, hasStart, null, true); } /** * The resulting SQL is a simple select statement. The ResultSet can be * processed with a NodeMapper. * * @param parent The parent * @param limit Max results * @param hasStart If an offset was specified * @param sortProperty Alternate sort column (can be null) * @param sortAsc Sort direction (can be null) * @return sql string */ protected String getSelectNodesByParentSQL(ContainerNode parent, Integer limit, boolean hasStart, URI sortProperty, Boolean sortAsc) { StringBuilder sb = new StringBuilder(); sb.append("SELECT nodeID"); for (String col : NODE_COLUMNS) { sb.append(","); sb.append(col); } sb.append(" FROM "); sb.append(getNodeTableName()); Long nid = getNodeID(parent); if (nid != null) { sb.append(" WHERE parentID = "); sb.append(getNodeID(parent)); } else { sb.append(" WHERE parentID IS NULL"); } String sortColumn = "name"; if (sortProperty != null) { switch (sortProperty.toString()) { case VOS.PROPERTY_URI_DATE: sortColumn = "lastModified"; break; case VOS.PROPERTY_URI_CONTENTLENGTH: sortColumn = "contentLength"; break; default: throw new UnsupportedOperationException("can't sort on column " + sortProperty); } } if (hasStart) { if (sortAsc == null || sortAsc) { sb.append(" AND " + sortColumn + " >= ?"); } else { sb.append(" AND " + sortColumn + " <= ?"); } } if (hasStart || limit != null) { sb.append(" ORDER BY " + sortColumn); if (sortAsc != null && !sortAsc) { sb.append(" DESC"); } } if (limit != null) { if (nodeSchema.limitWithTop) // TOP, eg sybase sb.replace(0, 6, "SELECT TOP " + limit); else // LIMIT, eg postgresql { sb.append(" LIMIT "); sb.append(limit); } } log.debug("getSelectedNodesByParentSQL: " + sb.toString()); return sb.toString(); } /** * The resulting SQL is a simple select statement. The ResultSet can be * processed with a NodePropertyMapper. * * @param node the node for which properties are queried * @return simple SQL string */ protected String getSelectNodePropertiesByID(Node node) { StringBuilder sb = new StringBuilder(); sb.append("SELECT propertyURI, propertyValue FROM "); sb.append(getNodePropertyTableName()); sb.append(" WHERE nodeID = "); sb.append(getNodeID(node)); return sb.toString(); } /** * @param node The node to delete * @return The SQL string for deleting the node from the database. */ protected String getDeleteNodeSQL(Node node, boolean notBusyOnly) { StringBuilder sb = new StringBuilder(); sb.append("DELETE FROM "); sb.append(getNodeTableName()); sb.append(" WHERE nodeID = "); sb.append(getNodeID(node)); sb.append(" AND parentID = "); sb.append(getNodeID(node.getParent())); if (notBusyOnly) { sb.append(" AND busyState = '"); sb.append(VOS.NodeBusyState.notBusy.getValue()); sb.append("'"); } return sb.toString(); } /** * @param node Delete the properties of this node. * @return The SQL string for performing property deletion. */ protected String getDeleteNodePropertiesSQL(Node node) { StringBuilder sb = new StringBuilder(); sb.append("DELETE FROM "); sb.append(getNodePropertyTableName()); sb.append(" WHERE nodeID = "); sb.append(getNodeID(node)); return sb.toString(); } protected String getSetBusyStateSQL(DataNode node, NodeBusyState curState, NodeBusyState newState) { StringBuilder sb = new StringBuilder(); sb.append("UPDATE "); sb.append(getNodeTableName()); sb.append(" SET busyState='"); sb.append(newState.getValue()); sb.append("', lastModified='"); // always tweak the date Date now = new Date(); setPropertyValue(node, VOS.PROPERTY_URI_DATE, dateFormat.format(now), true); sb.append(dateFormat.format(now)); sb.append("'"); sb.append(" WHERE nodeID = "); sb.append(getNodeID(node)); sb.append(" AND busyState='"); sb.append(curState.getValue()); sb.append("'"); return sb.toString(); } /** * @param dest * @param size * @param increment * @return The SQL string for applying a negative or positive * delta to the parent of the target node. */ protected String getApplySizeDiffSQL(ContainerNode dest, long size, boolean increment) { StringBuilder sb = new StringBuilder(); sb.append("UPDATE "); sb.append(getNodeTableName()); sb.append(" SET delta = coalesce(delta, 0) "); if (increment) sb.append("+ "); else sb.append("- "); sb.append(size); sb.append(" WHERE nodeID = "); sb.append(getNodeID(dest)); return sb.toString(); } protected String[] getApplyDeltaSQL(NodeSizePropagation propagation) { List sql = new ArrayList(); Date now = new Date(); // update 1 adjusts the conentLength and date. for data nodes, the // child is only locked. StringBuilder sb = new StringBuilder(); sb.append("UPDATE "); sb.append(getNodeTableName()); if (NODE_TYPE_DATA.equals(propagation.getChildType())) { // set the type equal to the existing type (no-op lock) sb.append(" SET type = '"); sb.append(NODE_TYPE_DATA); sb.append("'"); } else if (NODE_TYPE_CONTAINER.equals(propagation.getChildType())) { // apply the delta to the contentLength sb.append(" SET contentLength = coalesce(contentLength, 0) + coalesce(delta, 0),"); // tweak the date sb.append(" lastModified = '"); sb.append(dateFormat.format(now)); sb.append("'"); } else { throw new IllegalStateException("Wrong node type for delta application."); } sb.append(" WHERE nodeID = "); sb.append(propagation.getChildID()); if (propagation.getParentID() == null) { sb.append(" AND parentID IS NULL"); } else { sb.append(" AND parentID = "); sb.append(propagation.getParentID()); } sql.add(sb.toString()); // update 2 adjusts the parent delta if (propagation.getParentID() != null) { sb = new StringBuilder(); sb.append("UPDATE "); sb.append(getNodeTableName()); sb.append(" SET delta = coalesce(delta, 0) + "); sb.append("(SELECT coalesce(delta, 0) FROM "); sb.append(getNodeTableName()); sb.append(" WHERE nodeID = "); sb.append(propagation.getChildID()); sb.append("), lastModified = '"); sb.append(dateFormat.format(now)); sb.append("'"); sb.append(" WHERE nodeID = "); sb.append(propagation.getParentID()); sql.add(sb.toString()); } // update 3 resets the child delta sb = new StringBuilder(); sb.append("UPDATE "); sb.append(getNodeTableName()); sb.append(" SET delta = 0"); sb.append(" WHERE nodeID = "); sb.append(propagation.getChildID()); if (propagation.getParentID() == null) { sb.append(" AND parentID IS NULL"); } else { sb.append(" AND parentID = "); sb.append(propagation.getParentID()); } sql.add(sb.toString()); return sql.toArray(new String[0]); } protected String[] getRootUpdateLockSQL(Node n1, Node n2) { Node root1 = n1; Node root2 = null; while (root1.getParent() != null) { root1 = root1.getParent(); } if (n2 != null) { root2 = n2; while (root2.getParent() != null) { root2 = root2.getParent(); } } return getUpdateLockSQL(root1, root2); } protected String[] getUpdateLockSQL(Node n1, Node n2) { Node[] nodes = null; Long id1 = getNodeID(n1); Long id2 = null; if (n2 != null) id2 = getNodeID(n2); if ( n2 == null || id1.compareTo(id2) == 0 ) // same node nodes = new Node[] { n1 }; else if (id1.compareTo(id2) < 0) nodes = new Node[] { n1, n2 }; else nodes = new Node[] { n2, n1 }; String[] ret = new String[nodes.length]; for (int i=0; i coreProps; private boolean usePropertyTable(String uri) { if (coreProps == null) { // lazy init of the static set: thread-safe enough by // doing the assignment to the static last Set core = new TreeSet(new CaseInsensitiveStringComparator()); core.add(VOS.PROPERTY_URI_ISPUBLIC); core.add(VOS.PROPERTY_URI_ISLOCKED); // note: very important that the node owner (creator property) is here core.add(VOS.PROPERTY_URI_CREATOR); core.add(VOS.PROPERTY_URI_CONTENTLENGTH); core.add(VOS.PROPERTY_URI_TYPE); core.add(VOS.PROPERTY_URI_CONTENTENCODING); core.add(VOS.PROPERTY_URI_CONTENTMD5); core.add(VOS.PROPERTY_URI_DATE); core.add(VOS.PROPERTY_URI_GROUPREAD); core.add(VOS.PROPERTY_URI_GROUPWRITE); coreProps = core; } return !coreProps.contains(uri); } private class DataNodeUpdateStatementCreator implements PreparedStatementCreator { private Long len; private String md5; private Long nodeID; private Date lastModified; public DataNodeUpdateStatementCreator(Long nodeID, Long len, String md5, Date lastModified) { this.nodeID = nodeID; this.len = len; this.md5 = md5; this.lastModified = lastModified; } @Override public PreparedStatement createPreparedStatement(Connection conn) throws SQLException { StringBuilder sb = new StringBuilder(); sb.append("UPDATE "); sb.append(getNodeTableName()); sb.append(" SET "); sb.append("lastModified = ?, "); sb.append("delta = ? - coalesce(contentLength, 0) + coalesce(delta, 0), contentLength = ?, contentMD5 = ?"); sb.append(" WHERE nodeID = ?"); if (lastModified != null) { sb.append(" AND lastModified = ?"); } String sql = sb.toString(); log.debug(sql); sb = new StringBuilder("values: "); PreparedStatement prep = conn.prepareStatement(sql); int col = 1; Date now = new Date(); Timestamp ts = new Timestamp(now.getTime()); prep.setTimestamp(col++, ts, cal); sb.append(now); sb.append(","); if (len == null) { prep.setLong(col++, 0); prep.setNull(col++, Types.BIGINT); } else { prep.setLong(col++, len); prep.setLong(col++, len); } sb.append(len); sb.append(","); sb.append(len); sb.append(","); if (md5 == null) prep.setNull(col++, Types.VARBINARY); else prep.setBytes(col++, HexUtil.toBytes(md5)); sb.append(md5); sb.append(","); prep.setLong(col++, nodeID); sb.append(nodeID); if (lastModified != null) { Timestamp lastModTs = new Timestamp(lastModified.getTime()); prep.setTimestamp(col++, lastModTs, cal); sb.append(","); sb.append(lastModTs); } log.debug(sb.toString()); return prep; } } private class PropertyStatementCreator implements PreparedStatementCreator { private NodeSchema ns; private boolean update; private NodeID nodeID; private NodeProperty prop; public PropertyStatementCreator(NodeSchema ns, NodeID nodeID, NodeProperty prop) { this(ns, nodeID, prop, false); } public PropertyStatementCreator(NodeSchema ns, NodeID nodeID, NodeProperty prop, boolean update) { this.ns = ns; this.nodeID = nodeID; this.prop = prop; this.update = update; } // if we care about caching the statement, we should look into prepared // statement caching by the driver @Override public PreparedStatement createPreparedStatement(Connection conn) throws SQLException { String sql; PreparedStatement prep; if (prop.isMarkedForDeletion()) sql = getDeleteSQL(); else if (update) sql = getUpdateSQL(); else sql = getInsertSQL(); log.debug(sql); prep = conn.prepareStatement(sql); setValues(prep); return prep; } void setValues(PreparedStatement ps) throws SQLException { int col = 1; if (prop.isMarkedForDeletion()) { ps.setLong(col++, nodeID.getID()); ps.setString(col++, prop.getPropertyURI()); log.debug("setValues: " + nodeID.getID() + "," + prop.getPropertyURI()); } else if (update) { ps.setString(col++, prop.getPropertyValue()); ps.setLong(col++, nodeID.getID()); ps.setString(col++, prop.getPropertyURI()); log.debug("setValues: " + prop.getPropertyValue() + "," + nodeID.getID() + "," + prop.getPropertyURI()); } else { ps.setLong(col++, nodeID.getID()); ps.setString(col++, prop.getPropertyURI()); ps.setString(col++, prop.getPropertyValue()); ps.setLong(col++, nodeID.getID()); ps.setString(col++, prop.getPropertyURI()); log.debug("setValues: " + nodeID.getID() + "," + prop.getPropertyURI() + "," + prop.getPropertyValue() + "," + nodeID.getID() + "," + prop.getPropertyURI()); } } public String getSQL() { if (update) return getUpdateSQL(); return getInsertSQL(); } private String getInsertSQL() { StringBuilder sb = new StringBuilder(); sb.append("INSERT INTO "); sb.append(ns.propertyTable); sb.append(" (nodeID,propertyURI,propertyValue) SELECT ?, ?, ?"); sb.append(" WHERE NOT EXISTS (SELECT * FROM "); sb.append(ns.propertyTable); sb.append(" WHERE nodeID=? and propertyURI=?)"); return sb.toString(); } private String getUpdateSQL() { StringBuilder sb = new StringBuilder(); sb.append("UPDATE "); sb.append(ns.propertyTable); sb.append(" SET propertyValue = ?"); sb.append(" WHERE nodeID = ?"); sb.append(" AND propertyURI = ?"); return sb.toString(); } private String getDeleteSQL() { StringBuilder sb = new StringBuilder(); sb.append("DELETE FROM "); sb.append(ns.propertyTable); sb.append(" WHERE nodeID = ?"); sb.append(" AND propertyURI = ?"); return sb.toString(); } } private class NodePutStatementCreator implements PreparedStatementCreator { private NodeSchema ns; private boolean update; private Node node = null; private Object differentOwner = null; public NodePutStatementCreator(NodeSchema ns, boolean update) { this.ns = ns; this.update = update; } // if we care about caching the statement, we should look into prepared // statement caching by the driver @Override public PreparedStatement createPreparedStatement(Connection conn) throws SQLException { PreparedStatement prep; if (update) { String sql = getUpdateSQL(); log.debug(sql); prep = conn.prepareStatement(sql); } else { String sql = getInsertSQL(); log.debug(sql); prep = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); } setValues(prep); return prep; } public void setValues(Node node, Object differentOwner) { this.node = node; this.differentOwner = differentOwner; } void setValues(PreparedStatement ps) throws SQLException { StringBuilder sb = new StringBuilder(); int col = 1; if (node.getParent() != null) { long v = getNodeID(node.getParent()); ps.setLong(col, v); sb.append(v); } else { ps.setNull(col, Types.BIGINT); sb.append("null"); } col++; sb.append(","); String name = node.getName(); ps.setString(col++, name); sb.append(name); sb.append(","); ps.setString(col++, getNodeType(node)); sb.append(getNodeType(node)); sb.append(","); ps.setString(col++, NodeBusyState.notBusy.getValue()); sb.append(getBusyState(node)); sb.append(","); ps.setBoolean(col++, node.isPublic()); setPropertyValue(node, VOS.PROPERTY_URI_ISPUBLIC, Boolean.toString(node.isPublic()), false); sb.append(node.isPublic()); sb.append(","); ps.setBoolean(col++, node.isLocked()); if (node.isLocked()) setPropertyValue(node, VOS.PROPERTY_URI_ISLOCKED, Boolean.toString(node.isLocked()), false); sb.append(node.isLocked()); sb.append(","); String pval; Object ownerObject = null; NodeID nodeID = (NodeID) node.appData; ownerObject = nodeID.ownerObject; if (differentOwner != null) ownerObject = differentOwner; if (ownerObject == null) throw new IllegalStateException("cannot update a node without an owner."); // ownerID and creatorID data type //int ownerDataType = identManager.getOwnerType(); int ownerDataType = Types.OTHER; if (ownerObject instanceof String) { ownerDataType = Types.VARCHAR; } else if (ownerObject instanceof Integer) { ownerDataType = Types.INTEGER; } // ownerID ps.setObject(col++, ownerObject, ownerDataType); sb.append(ownerObject); sb.append(","); // always use the value in nodeID.ownerObject ps.setObject(col++, nodeID.ownerObject, ownerDataType); sb.append(nodeID.ownerObject); sb.append(","); //log.debug("setValues: " + sb); pval = node.getPropertyValue(VOS.PROPERTY_URI_GROUPREAD); if (pval != null) ps.setString(col, pval); else ps.setNull(col, Types.VARCHAR); col++; sb.append(pval); sb.append(","); //log.debug("setValues: " + sb); pval = node.getPropertyValue(VOS.PROPERTY_URI_GROUPWRITE); if (pval != null) ps.setString(col, pval); else ps.setNull(col, Types.VARCHAR); col++; sb.append(pval); sb.append(","); //log.debug("setValues: " + sb); // always tweak the date Date now = new Date(); setPropertyValue(node, VOS.PROPERTY_URI_DATE, dateFormat.format(now), true); //java.sql.Date dval = new java.sql.Date(now.getTime()); Timestamp ts = new Timestamp(now.getTime()); ps.setTimestamp(col, ts, cal); col++; sb.append(dateFormat.format(now)); sb.append(","); //log.debug("setValues: " + sb); pval = node.getPropertyValue(VOS.PROPERTY_URI_TYPE); if (pval != null) ps.setString(col, pval); else ps.setNull(col, Types.VARCHAR); col++; sb.append(pval); sb.append(","); pval = node.getPropertyValue(VOS.PROPERTY_URI_CONTENTENCODING); if (pval != null) ps.setString(col, pval); else ps.setNull(col, Types.VARCHAR); col++; sb.append(pval); sb.append(","); //log.debug("setValues:" + sb.toString()); pval = null; if (node instanceof LinkNode) { URI targetURI = ((LinkNode)node).getTarget(); pval = NodeMapper.createDBLinkString(targetURI, authority); ps.setString(col, pval); } else ps.setNull(col, Types.LONGVARCHAR); col++; sb.append(pval); sb.append(","); String storageID = nodeID.storageID; if (storageID != null) { ps.setString(col, storageID); sb.append(storageID); } else { ps.setNull(col, Types.VARCHAR); sb.append("null"); } col++; if (update) { ps.setLong(col, getNodeID(node)); sb.append(","); sb.append(getNodeID(node)); } log.debug("setValues: " + sb); } private String getInsertSQL() { StringBuilder sb = new StringBuilder(); sb.append("INSERT INTO "); sb.append(ns.nodeTable); sb.append(" ("); // we never insert or update physical file (DataNode) metadata here int numCols = NODE_COLUMNS.length - 2; for (int c=0; c 0) sb.append(","); sb.append(NODE_COLUMNS[c]); } sb.append(") VALUES ("); for (int c=0; c 0) sb.append(","); sb.append("?"); } sb.append(")"); return sb.toString(); } private String getUpdateSQL() { StringBuilder sb = new StringBuilder(); sb.append("UPDATE "); sb.append(ns.nodeTable); // we never insert or update physical file (DataNode) metadata here int numCols = NODE_COLUMNS.length - 2; sb.append(" SET "); for (int c=0; c 0) sb.append(","); sb.append(NODE_COLUMNS[c]); sb.append(" = ?"); } sb.append(" WHERE nodeID = ? AND busyState = '"); sb.append(NodeBusyState.notBusy.getValue()); sb.append("'"); return sb.toString(); } } private class NodePathStatementCreator implements PreparedStatementCreator { private String[] path; private String nodeTablename; private String propTableName; private boolean allowPartialPath; public NodePathStatementCreator(String[] path, String nodeTablename, String propTableName, boolean allowPartialPath) { this.path = path; this.nodeTablename = nodeTablename; this.propTableName = propTableName; this.allowPartialPath = allowPartialPath; } @Override public PreparedStatement createPreparedStatement(Connection conn) throws SQLException { String sql = getSQL(); log.debug("SQL: " + sql); PreparedStatement ret = conn.prepareStatement(sql); for (int i=0; i 0) sb.append(","); for (int c=0; c 0) sb.append(","); sb.append(acur); sb.append("."); sb.append(NODE_COLUMNS[c]); } sb.append(","); sb.append(acur); sb.append(".nodeID"); } sb.append(" FROM "); String aprev; for (int i=0; i 0) { if (this.allowPartialPath) sb.append(" LEFT"); sb.append(" JOIN "); } sb.append(nodeTablename); sb.append(" AS "); sb.append(acur); if (i == 0) { sb.append(" JOIN "); sb.append(nodeTablename); sb.append(" AS "); sb.append(acur+0); sb.append(" ON ("); sb.append(acur); sb.append(".parentID IS NULL"); sb.append(" AND "); sb.append(acur); sb.append(".nodeID="); sb.append(acur+0); sb.append(".nodeID"); sb.append(" AND "); sb.append(acur); sb.append(".name = ? )"); } else { sb.append(" ON ("); sb.append(aprev); sb.append(".nodeID="); sb.append(acur); sb.append(".parentID"); sb.append(" AND "); sb.append(acur); sb.append(".name = ? )"); } } return sb.toString(); } } private class NodePathExtractor implements ResultSetExtractor { private int columnsPerNode; public NodePathExtractor() { this.columnsPerNode = NODE_COLUMNS.length + 1; } @Override public Object extractData(ResultSet rs) throws SQLException, DataAccessException { boolean done = false; Node ret = null; Node root = null; String curPath = ""; int numColumns = rs.getMetaData().getColumnCount(); while ( !done && rs.next() ) { if (root == null) // reading first row completely { log.debug("reading path from row 1"); int col = 1; Node cur = null; while (!done && (col < numColumns)) { log.debug("readNode at " + col + ", path="+curPath); Node n = readNode(rs, col, curPath); if (n == null) { done = true; } else { ret = n; // always return the last node log.debug("readNode: " + n.getUri()); curPath = n.getUri().getPath(); col += columnsPerNode; if (root == null) // root container { cur = n; root = cur; } else { ((ContainerNode) cur).getNodes().add(n); n.setParent((ContainerNode) cur); cur = n; } } } } else log.warn("found extra rows, expected only 0 or 1"); } return ret; } private Node readNode(ResultSet rs, int col, String basePath) throws SQLException { Long parentID = null; Object o = rs.getObject(col++); if (o != null) { Number n = (Number) o; parentID = new Long(n.longValue()); } String name = rs.getString(col++); String type = rs.getString(col++); String busyString = getString(rs, col++); boolean isPublic = rs.getBoolean(col++); boolean isLocked = rs.getBoolean(col++); Object ownerObject = rs.getObject(col++); String owner = null; Object creatorObject = rs.getObject(col++); // unused String groupRead = getString(rs, col++); String groupWrite = getString(rs, col++); Date lastModified = rs.getTimestamp(col++, cal); String contentType = getString(rs, col++); String contentEncoding = getString(rs, col++); String linkStr = getString(rs, col++); String storageID = getString(rs, col++); Long contentLength = null; o = rs.getObject(col++); if (o != null) { Number n = (Number) o; contentLength = new Long(n.longValue()); } log.debug("readNode: contentLength = " + contentLength); Object contentMD5 = rs.getObject(col++); Long nodeID = null; o = rs.getObject(col++); if (o != null) { Number n = (Number) o; nodeID = new Long(n.longValue()); } String path = basePath + "/" + name; VOSURI vos; try { vos = new VOSURI(new URI("vos", authority, path, null, null)); } catch(URISyntaxException bug) { throw new RuntimeException("BUG - failed to create vos URI", bug); } Node node = null; // Since we support partial paths, a node not in the Node table is // returned with all columns having null values. Instead of // checking all columns, if we do not have the following condition, // return a null node. //if (!((parentID == null) && (nodeID == null) && (name == null) && (type == null))) if (nodeID != null) // found another node { if (NODE_TYPE_CONTAINER.equals(type)) { node = new ContainerNode(vos); } else if (NODE_TYPE_DATA.equals(type)) { node = new DataNode(vos); ((DataNode) node).setBusy(NodeBusyState.getStateFromValue(busyString)); } else if (NODE_TYPE_LINK.equals(type)) { URI link = NodeMapper.extractLinkURI(linkStr, authority); node = new LinkNode(vos, link); } else { throw new IllegalStateException("Unknown node database type: " + type); } NodeID nid = new NodeID(); nid.id = nodeID; nid.ownerObject = ownerObject; nid.storageID = storageID; node.appData = nid; if (contentType != null) { node.getProperties().add(new NodeProperty(VOS.PROPERTY_URI_TYPE, contentType)); } if (contentEncoding != null) { node.getProperties().add(new NodeProperty(VOS.PROPERTY_URI_CONTENTENCODING, contentEncoding)); } if (contentLength != null) node.getProperties().add(new NodeProperty(VOS.PROPERTY_URI_CONTENTLENGTH, contentLength.toString())); else node.getProperties().add(new NodeProperty(VOS.PROPERTY_URI_CONTENTLENGTH, "0")); if (contentMD5 != null && contentMD5 instanceof byte[]) { byte[] md5 = (byte[]) contentMD5; if (md5.length < 16) { byte[] tmp = md5; md5 = new byte[16]; System.arraycopy(tmp, 0, md5, 0, tmp.length); // extra space is init with 0 } String contentMD5String = HexUtil.toHex(md5); node.getProperties().add(new NodeProperty(VOS.PROPERTY_URI_CONTENTMD5, contentMD5String)); } if (lastModified != null) { node.getProperties().add(new NodeProperty(VOS.PROPERTY_URI_DATE, dateFormat.format(lastModified))); } if (groupRead != null) { node.getProperties().add(new NodeProperty(VOS.PROPERTY_URI_GROUPREAD, groupRead)); } if (groupWrite != null) { node.getProperties().add(new NodeProperty(VOS.PROPERTY_URI_GROUPWRITE, groupWrite)); } if (owner != null) { node.getProperties().add(new NodeProperty(VOS.PROPERTY_URI_CREATOR, owner)); } node.getProperties().add(new NodeProperty(VOS.PROPERTY_URI_ISPUBLIC, Boolean.toString(isPublic))); if (isLocked) node.getProperties().add(new NodeProperty(VOS.PROPERTY_URI_ISLOCKED, Boolean.toString(isLocked))); // set the read-only flag on the properties for (String propertyURI : VOS.READ_ONLY_PROPERTIES) { int propertyIndex = node.getProperties().indexOf(new NodeProperty(propertyURI, "")); if (propertyIndex != -1) { node.getProperties().get(propertyIndex).setReadOnly(true); } } } return node; } private String getString(ResultSet rs, int col) throws SQLException { String ret = rs.getString(col); if (ret != null) { ret = ret.trim(); if (ret.length() == 0) ret = null; } return ret; } } private class NodeSizePropagationExtractor implements ResultSetExtractor { @Override public Object extractData(ResultSet rs) throws SQLException, DataAccessException { List propagations = new ArrayList(rs.getFetchSize()); long childID; String childType; Long parentID; Object parentObject; Number parentNumber; NodeSizePropagation propagation = null; int col; while (rs.next()) { col = 1; childID = rs.getLong(col++); childType = rs.getString(col++); parentID = null; parentObject = rs.getObject(col++); if (parentObject != null) { parentNumber = (Number) parentObject; parentID = new Long(parentNumber.longValue()); } propagation = new NodeSizePropagation(childID, childType, parentID); propagations.add(propagation); } return propagations; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy