Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
ca.nrc.cadc.vos.server.db.NodeDAO Maven / Gradle / Ivy
/*
************************************************************************
******************* 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;
}
}
}