diff --git a/src/Jackalope/Session.php b/src/Jackalope/Session.php
index 2754a5f9..cc2aeb0f 100644
--- a/src/Jackalope/Session.php
+++ b/src/Jackalope/Session.php
@@ -38,6 +38,7 @@ class Session implements \PHPCR\SessionInterface
protected $repository;
protected $workspace;
protected $objectManager;
+ protected $utx = null;
protected $credentials;
protected $logout = false;
/**
@@ -63,12 +64,18 @@ public function __construct($factory, Repository $repository, $workspaceName, \P
$this->factory = $factory;
$this->repository = $repository;
$this->objectManager = $this->factory->get('ObjectManager', array($transport, $this));
+ $this->utx = $this->factory->get('Transaction\\UserTransaction', array($transport, $this));
$this->workspace = $this->factory->get('Workspace', array($this, $this->objectManager, $workspaceName));
$this->credentials = $credentials;
$this->namespaceRegistry = $this->workspace->getNamespaceRegistry();
self::registerSession($this);
}
+ public function getTransactionManager()
+ {
+ return $this->utx;
+ }
+
/**
* Returns the Repository object through which this session was acquired.
*
@@ -458,7 +465,14 @@ public function removeItem($absPath)
*/
public function save()
{
- $this->objectManager->save();
+ if (! $this->utx->inTransaction())
+ {
+ $this->utx->begin();
+ $this->objectManager->save();
+ $this->utx->commit();
+ } else {
+ $this->objectManager->save();
+ }
}
/**
diff --git a/src/Jackalope/Transaction/UserTransaction.php b/src/Jackalope/Transaction/UserTransaction.php
new file mode 100644
index 00000000..b4cd5f9c
--- /dev/null
+++ b/src/Jackalope/Transaction/UserTransaction.php
@@ -0,0 +1,167 @@
+factory = $factory;
+ $this->transport = $transport;
+ $this->session = $session;
+ }
+
+
+ /**
+ * Begin new transaction associated with current session.
+ *
+ * @return void
+ *
+ * @throws \PHPCR\UnsupportedRepositoryOperationException Thrown if a transaction
+ * is already started and the transaction implementation or backend does not
+ * support nested transactions.
+ *
+ * @throws \PHPCR\RepositoryException Thrown if the transaction implementation
+ * encounters an unexpected error condition.
+ */
+ public function begin()
+ {
+ if ($this->inTransaction) {
+ throw new \PHPCR\UnsupportedRepositoryOperationException("Nested transactions are not supported.");
+ }
+
+ $this->transport->beginTransaction();
+ $this->inTransaction = true;
+ }
+
+ /**
+ *
+ * Complete the transaction associated with the current session.
+ * TODO: Make shure RollbackException and AccessDeniedException are thrown by the transport
+ * if corresponding problems occure
+ *
+ * @return void
+ *
+ * @throws \PHPCR\Transaction\RollbackException Thrown to indicate that the
+ * transaction has been rolled back rather than committed.
+ * @throws \PHPCR\AccessDeniedException Thrown to indicate that the
+ * session is not allowed to commit the transaction.
+ * @throws LogicException Thrown if the current
+ * session is not associated with a transaction.
+ * @throws \PHPCR\RepositoryException Thrown if the transaction implementation
+ * encounters an unexpected error condition.
+ */
+ public function commit()
+ {
+ if (! $this->inTransaction) {
+ throw new LogicException("No transaction to commit.");
+ }
+
+ $this->transport->commitTransaction();
+ $this->inTransaction = false;
+ }
+
+ /**
+ *
+ * Obtain the status if the current session is inside of a transaction or not.
+ *
+ * @return boolean
+ *
+ * @throws \PHPCR\RepositoryException Thrown if the transaction implementation
+ * encounters an unexpected error condition.
+ */
+ public function inTransaction()
+ {
+ //TODO Is there a way to ask for the transaction status via webdav?
+ return $this->inTransaction;
+ }
+
+ /**
+ *
+ * Roll back the transaction associated with the current session.
+ * TODO: Make shure AccessDeniedException is thrown by the transport
+ * if corresponding problems occure
+ *
+ * @return void
+ *
+ * @throws \PHPCR\AccessDeniedException Thrown to indicate that the
+ * application is not allowed to roll back the transaction.
+ * @throws LogicException Thrown if the current
+ * session is not associated with a transaction.
+ * @throws \PHPCR\RepositoryException Thrown if the transaction implementation
+ * encounters an unexpected error condition.
+ */
+ public function rollback()
+ {
+ if (! $this->inTransaction) {
+ throw new LogicException("No transaction to rollback.");
+ }
+
+ $this->transport->rollbackTransaction();
+ $this->inTransaction = false;
+ $this->session->clear();
+ }
+
+ /**
+ *
+ * Modify the timeout value that is associated with transactions started by
+ * the current application with the begin method. If an application has not
+ * called this method, the transaction service uses some default value for the
+ * transaction timeout.
+ *
+ * @param int $seconds The value of the timeout in seconds. If the value is zero,
+ * the transaction service restores the default value. If the value is
+ * negative a RepositoryException is thrown.
+ *
+ * @return void
+ *
+ * @throws \PHPCR\RepositoryException Thrown if the transaction implementation
+ * encounters an unexpected error condition.
+ */
+ public function setTransactionTimeout($seconds = 0)
+ {
+ if ($seconds < 0) {
+ throw new \PHPCR\RepositoryException("Value must be possitiv or 0. ". $seconds ." given.");
+ }
+ $this->transport->setTransactionTimeout($seconds);
+ }
+}
diff --git a/src/Jackalope/Transport/Davex/Client.php b/src/Jackalope/Transport/Davex/Client.php
index 28fc16e2..20f3df9e 100755
--- a/src/Jackalope/Transport/Davex/Client.php
+++ b/src/Jackalope/Transport/Davex/Client.php
@@ -147,6 +147,14 @@ class Client implements TransportInterface
*/
protected $checkLoginOnServer = true;
+ /**
+ * The transaction token received by a LOCKing request
+ *
+ * Is FALSE while no transaction running.
+ * @var string|FALSE
+ */
+ protected $transactionToken = false;
+
/**
* Create a transport pointing to a server url.
*
@@ -196,7 +204,7 @@ public function sendExpect($send = true)
/**
* Opens a cURL session if not yet one open.
*
- * @return null|false False in case there is already an open connection, else null;
+ * @return Jackalope\Transport\Davex\Request The Request
*/
protected function getRequest($method, $uri)
{
@@ -406,6 +414,7 @@ public function getNode($path)
$path .= '.0.json';
$request = $this->getRequest(Request::GET, $path);
+ $request->setTransactionId($this->transactionToken);
try {
return $request->executeJson();
} catch (\PHPCR\PathNotFoundException $e) {
@@ -436,7 +445,7 @@ public function getNodes($paths)
}
}
$body = array();
-
+
$url = $this->encodePathForDavex($url).".0.json";
foreach ($paths as $path) {
$body[] = http_build_query(array(":get"=>$path));
@@ -445,6 +454,7 @@ public function getNodes($paths)
$request = $this->getRequest(Request::POST, $url);
$request->setBody($body);
$request->setContentType('application/x-www-form-urlencoded');
+ $request->setTransactionId($this->transactionToken);
try {
$data = $request->executeJson();
return $data->nodes;
@@ -452,8 +462,8 @@ public function getNodes($paths)
throw new \PHPCR\ItemNotFoundException($e->getMessage(), $e->getCode(), $e);
} catch (\PHPCR\RepositoryException $e) {
if ($e->getMessage() == 'HTTP 403: Prefix must not be empty (org.apache.jackrabbit.spi.commons.conversion.IllegalNameException)') {
- throw new \PHPCR\UnsupportedRepositoryOperationException("Jackalope currently needs a patched jackrabbit for Session->getNodes() to work. Until our patches make it into the official distribution, see https://github.com/jackalope/jackrabbit/blob/2.2-jackalope/README.jackalope.patches.md for details and downloads.");
- }
+ throw new \PHPCR\UnsupportedRepositoryOperationException("Jackalope currently needs a patched jackrabbit for Session->getNodes() to work. Until our patches make it into the official distribution, see https://github.com/jackalope/jackrabbit/blob/2.2-jackalope/README.jackalope.patches.md for details and downloads.");
+ }
throw $e;
}
}
@@ -487,6 +497,7 @@ public function getBinaryStream($path)
{
$path = $this->encodePathForDavex($path);
$request = $this->getRequest(Request::GET, $path);
+ $request->setTransactionId($this->transactionToken);
$curl = $request->execute(true);
switch($curl->getHeader('Content-Type')) {
case 'text/xml; charset=utf-8':
@@ -575,6 +586,7 @@ protected function getNodeReferences($path, $name = null, $weak_reference = fals
$path = $this->encodePathForDavex($path);
$identifier = $weak_reference ? 'weakreferences' : 'references';
$request = $this->getRequest(Request::PROPFIND, $path);
+ $request->setTransactionId($this->transactionToken);
$request->setBody($this->buildPropfindRequest(array('dcr:'.$identifier)));
$request->setDepth(0);
$dom = $request->executeDom();
@@ -607,6 +619,7 @@ public function checkinItem($path)
$path = $this->encodePathForDavex($path);
try {
$request = $this->getRequest(Request::CHECKIN, $path);
+ $request->setTransactionId($this->transactionToken);
$curl = $request->execute(true);
if ($curl->getHeader("Location")) {
return $this->stripServerRootFromUri(urldecode($curl->getHeader("Location")));
@@ -636,6 +649,7 @@ public function checkoutItem($path)
$path = $this->encodePathForDavex($path);
try {
$request = $this->getRequest(Request::CHECKOUT, $path);
+ $request->setTransactionId($this->transactionToken);
$request->execute();
} catch (\Jackalope\Transport\Davex\HTTPErrorException $e) {
if ($e->getCode() == 405) {
@@ -661,6 +675,7 @@ public function restoreItem($removeExisting, $versionPath, $path)
$request = $this->getRequest(Request::UPDATE, $path);
$request->setBody($body);
+ $request->setTransactionId($this->transactionToken);
$request->execute(); // errors are checked in request
}
@@ -668,6 +683,7 @@ public function getVersionHistory($path)
{
$path = $this->encodePathForDavex($path);
$request = $this->getRequest(Request::GET, $path."/jcr:versionHistory");
+ $request->setTransactionId($this->transactionToken);
$resp = $request->execute();
return $resp;
}
@@ -695,6 +711,7 @@ public function query(\PHPCR\Query\QueryInterface $query)
$path = $this->addWorkspacePathToUri('/');
$request = $this->getRequest(Request::SEARCH, $path);
+ $request->setTransactionId($this->transactionToken);
$request->setBody($body);
$rawData = $request->execute();
@@ -737,6 +754,7 @@ public function deleteNode($path)
$path = $this->encodePathForDavex($path);
$request = $this->getRequest(Request::DELETE, $path);
+ $request->setTransactionId($this->transactionToken);
$request->execute();
return true;
}
@@ -777,6 +795,7 @@ public function copyNode($srcAbsPath, $dstAbsPath, $srcWorkspace = null)
$request = $this->getRequest(Request::COPY, $srcAbsPath);
$request->setDepth(Request::INFINITY);
$request->addHeader('Destination: '.$this->addWorkspacePathToUri($dstAbsPath));
+ $request->setTransactionId($this->transactionToken);
$request->execute();
}
@@ -797,6 +816,7 @@ public function moveNode($srcAbsPath, $dstAbsPath)
$request = $this->getRequest(Request::MOVE, $srcAbsPath);
$request->setDepth(Request::INFINITY);
$request->addHeader('Destination: '.$this->addWorkspacePathToUri($dstAbsPath));
+ $request->setTransactionId($this->transactionToken);
$request->execute();
}
@@ -827,12 +847,14 @@ public function storeNode(\PHPCR\NodeInterface $node)
$request = $this->getRequest(Request::MKCOL, $path);
$request->setBody($body);
+ $request->setTransactionId($this->transactionToken);
$request->execute();
// store single-valued multivalue properties separately
foreach ($buffer as $path => $body) {
$request = $this->getRequest(Request::PUT, $path);
$request->setBody($body);
+ $request->setTransactionId($this->transactionToken);
$request->execute();
}
@@ -932,6 +954,7 @@ public function storeProperty(\PHPCR\PropertyInterface $property)
$request->setContentType('jcr-value/'.strtolower($type));
}
$request->setBody($body);
+ $request->setTransactionId($this->transactionToken);
$request->execute();
return true;
@@ -997,6 +1020,7 @@ public function getNodePathForIdentifier($uuid)
{
$request = $this->getRequest(Request::REPORT, $this->workspaceUri);
$request->setBody($this->buildLocateRequest($uuid));
+ $request->setTransactionId($this->transactionToken);
$dom = $request->executeDom();
/* answer looks like
@@ -1031,6 +1055,7 @@ public function getNamespaces()
{
$request = $this->getRequest(Request::REPORT, $this->workspaceUri);
$request->setBody($this->buildReportRequest('dcr:registerednamespaces'));
+ $request->setTransactionId($this->transactionToken);
$dom = $request->executeDom();
if ($dom->firstChild->localName != 'registerednamespaces-report'
@@ -1081,6 +1106,7 @@ public function registerNamespace($prefix, $uri)
$request = $this->getRequest(Request::PROPPATCH, $this->workspaceUri);
$namespaces[$prefix] = $uri;
$request->setBody($this->buildRegisterNamespaceRequest($namespaces));
+ $request->setTransactionId($this->transactionToken);
$request->execute();
return true;
}
@@ -1104,6 +1130,7 @@ public function unregisterNamespace($prefix)
$namespaces = $this->getNamespaces();
unset($namespaces[$prefix]);
$request->setBody($this->buildRegisterNamespaceRequest($namespaces));
+ $request->setTransactionId($this->transactionToken);
$request->execute();
return true;
*/
@@ -1121,6 +1148,7 @@ public function getNodeTypes($nodeTypes = array())
{
$request = $this->getRequest(Request::REPORT, $this->workspaceUriRoot);
$request->setBody($this->buildNodeTypesRequest($nodeTypes));
+ $request->setTransactionId($this->transactionToken);
$dom = $request->executeDom();
if ($dom->firstChild->localName != 'nodeTypes') {
@@ -1134,6 +1162,81 @@ public function getNodeTypes($nodeTypes = array())
return $this->typeXmlConverter->getNodeTypesFromXml($dom);
}
+
+
+ /**
+ * Initiates a «local transaction» on the root node
+ *
+ * @return string The received transaction token
+ * @throws \PHPCR\RepositoryException If no transaction token received
+ */
+ public function beginTransaction()
+ {
+ $request = $this->getRequest(Request::LOCK, $this->workspaceUriRoot);
+ $request->setDepth('infinity');
+ $request->setTransactionId($this->transactionToken);
+ $request->setBody(''.
+ ''.
+ ' '.
+ ' '.
+ '');
+
+ $dom = $request->executeDom();
+ $hrefs = $dom->getElementsByTagNameNS(self::NS_DAV, 'href');
+
+ if (!$hrefs->length) {
+ throw new \PHPCR\RepositoryException('No transaction token received');
+ }
+ $this->transactionToken = $hrefs->item(0)->textContent;
+ return $this->transactionToken;
+ }
+
+ /**
+ * Ends the transaction started with {@link beginTransaction()}
+ *
+ * @param string $tag Either 'commit' or 'rollback'
+ */
+ protected function endTransaction($tag) {
+
+ if ($tag != 'commit' && $tag != 'rollback') {
+ throw new \InvalidArgumentException('Expected \'commit\' or \'rollback\' as argument');
+ }
+
+ $request = $this->getRequest(Request::UNLOCK, $this->workspaceUriRoot);
+ $request->setLockToken($this->transactionToken);
+ $request->setBody(''.
+ ''.
+ ' '.
+ '');
+
+ $request->execute();
+ $this->transactionToken = false;
+ }
+
+ /**
+ * Commits a transaction started with {@link beginTransaction()}
+ */
+ public function commitTransaction() {
+ $this->endTransaction('commit');
+ }
+
+ /**
+ * Rollbacks a transaction started with {@link beginTransaction()}
+ */
+ public function rollbackTransaction() {
+ $this->endTransaction('rollback');
+ }
+
+ /**
+ * Sets the default transaction timeout
+ *
+ * @param int $seconds The value of the timeout in seconds
+ */
+ public function setTransactionTimeout($seconds) {
+ throw new NotImplementedException();
+ }
+
+
/**
* Register namespaces and new node types or update node types based on a
* jackrabbit cnd string
@@ -1149,6 +1252,7 @@ public function getNodeTypes($nodeTypes = array())
public function registerNodeTypesCnd($cnd, $allowUpdate)
{
$request = $this->getRequest(Request::PROPPATCH, $this->workspaceUri);
+ $request->setTransactionId($this->transactionToken);
$request->setBody($this->buildRegisterNodeTypeRequest($cnd, $allowUpdate));
$request->execute();
return true;
@@ -1190,6 +1294,7 @@ public function getPermissions($path)
$request = $this->getRequest(Request::REPORT, $this->workspaceUri);
$request->setBody($body);
+ $request->setTransactionId($this->transactionToken);
$dom = $request->executeDom();
foreach($dom->getElementsByTagNameNS(self::NS_DAV, 'current-user-privilege-set') as $node) {
diff --git a/src/Jackalope/Transport/Davex/Request.php b/src/Jackalope/Transport/Davex/Request.php
index 18168d23..20bf8025 100644
--- a/src/Jackalope/Transport/Davex/Request.php
+++ b/src/Jackalope/Transport/Davex/Request.php
@@ -89,6 +89,19 @@ class Request
*/
const PROPPATCH = 'PROPPATCH';
+ /**
+ * Identifier of the 'LOCK' http request method
+ * @var string
+ */
+ const LOCK = 'LOCK';
+
+
+ /**
+ * Identifier of the 'UNLOCK' http request method
+ * @var string
+ */
+ const UNLOCK = 'UNLOCK';
+
/**
* Identifier of the 'COPY' http request method.
* @var string
@@ -166,6 +179,19 @@ class Request
/** @var array[]string A list of additional HTTP headers to be sent */
protected $additionalHeaders = array();
+ /**
+ * The lock token active for this request otherwise FALSE for no locking
+ * @var string|FALSE
+ */
+ protected $lockToken = false;
+
+ /**
+ * The transaction id active for this request otherwise FALSE for not
+ * performing a transaction
+ * @var string|FALSE
+ */
+ protected $transactionId = false;
+
/**
* Initiaties the NodeTypes request object.
*
@@ -221,6 +247,16 @@ public function addHeader($header)
$this->additionalHeaders[] = $header;
}
+ public function setLockToken($lockToken)
+ {
+ $this->lockToken = (string) $lockToken;
+ }
+
+ public function setTransactionId($transactionId)
+ {
+ $this->transactionId = (string) $transactionId;
+ }
+
protected function prepareCurl($curl, $getCurlObject)
{
if ($this->credentials instanceof \PHPCR\SimpleCredentials) {
@@ -236,6 +272,14 @@ protected function prepareCurl($curl, $getCurlObject)
);
$headers = array_merge($headers, $this->additionalHeaders);
+ if ($this->lockToken) {
+ $headers[] = 'Lock-Token: <'.$this->lockToken.'>';
+ }
+
+ if ($this->transactionId) {
+ $headers[] = 'TransactionId: <'.$this->transactionId.'>';
+ }
+
$curl->setopt(CURLOPT_RETURNTRANSFER, true);
$curl->setopt(CURLOPT_CUSTOMREQUEST, $this->method);
@@ -338,6 +382,14 @@ protected function singleRequest($getCurlObject)
);
$headers = array_merge($headers, $this->additionalHeaders);
+ if ($this->lockToken) {
+ $headers[] = 'Lock-Token: <'.$this->lockToken.'>';
+ }
+
+ if ($this->transactionId) {
+ $headers[] = 'TransactionId: <'.$this->transactionId.'>';
+ }
+
$this->curl->setopt(CURLOPT_RETURNTRANSFER, true);
$this->curl->setopt(CURLOPT_CUSTOMREQUEST, $this->method);
$this->curl->setopt(CURLOPT_URL, reset($this->uri));
@@ -400,6 +452,9 @@ protected function handleError($curl, $response, $httpCode)
throw new \PHPCR\ItemNotFoundException($exceptionMsg);
case 'javax.jcr.nodetype.ConstraintViolationException':
throw new \PHPCR\NodeType\ConstraintViolationException($exceptionMsg);
+ //TODO: Two more errors needed for Transactions. How does the corresponding Jackrabbit response look like?
+ // javax.transaction.RollbackException => \PHPCR\Transaction\RollbackException
+ // java.lang.SecurityException => \PHPCR\AccessDeniedException
//TODO: map more errors here?
default: