CopyController.php
1 <?php
2 /**
3  * wCMF - wemove Content Management Framework
4  * Copyright (C) 2005-2015 wemove digital solutions GmbH
5  *
6  * Licensed under the terms of the MIT License.
7  *
8  * See the LICENSE file distributed with this work for
9  * additional information.
10  */
12 
22 
23 /**
24  * CopyController is used to copy or move Node instances.
25  *
26  * The controller supports the following actions:
27  *
28  * <div class="controller-action">
29  * <div> __Action__ move </div>
30  * <div>
31  * Move the given Node and its children to the given target Node (delete original Node).
32  * | Parameter | Description
33  * |-----------------------|-------------------------
34  * | _in_ `oid` | The object id of the Node to move. The Node and all of its children will be moved
35  * | _in_ `targetOid` | The object id of the parent to attach the moved Node to (if it does not accept the Node type an error occurs) (optional, if empty the new Node has no parent)
36  * | _out_ `oid` | The object id of the newly created Node
37  * </div>
38  * </div>
39  *
40  * <div class="controller-action">
41  * <div> __Action__ copy </div>
42  * <div>
43  * Copy the given Node and its children to the given target Node (keep original Node).
44  * | Parameter | Description
45  * |-----------------------|-------------------------
46  * | _in_ `oid` | The object id of the Node to move. The Node and all of its children will be moved
47  * | _in_ `targetOid` | The object id of the parent to attach the moved Node to (if it does not accept the Node type an error occurs) (optional, if empty the new Node has no parent)
48  * | _in_ `nodesPerCall` | The number of Node instances to copy in one call (default: 50)
49  * | _in_ `recursive` | Boolean whether to copy children too (default: _true_)
50  * </div>
51  * </div>
52  *
53  * For additional actions and parameters see [BatchController actions](@ref BatchController).
54  *
55  * @author ingo herwig <ingo@wemove.com>
56  */
58 
59  // session name constants
60  private $REQUEST = 'CopyController.request';
61  private $OBJECT_MAP = 'CopyController.objectmap';
62  private $ITERATOR_ID = 'CopyController.iteratorid';
63 
64  private $_targetNode = null;
65 
66  // default values, maybe overriden by corresponding request values (see above)
67  private $_NODES_PER_CALL = 50;
68 
69  /**
70  * @see Controller::initialize()
71  */
72  public function initialize(Request $request, Response $response) {
73  parent::initialize($request, $response);
74 
75  // initialize controller
76  if ($request->getAction() != 'continue') {
77  $session = $this->getSession();
78 
79  // set defaults
80  if (!$request->hasValue('nodesPerCall')) {
81  $request->setValue('nodesPerCall', $this->_NODES_PER_CALL);
82  }
83  if (!$request->hasValue('recursive')) {
84  $request->setValue('recursive', true);
85  }
86 
87  // store request in session
88  $session->set($this->REQUEST, $request);
89  $map = array();
90  $session->set($this->OBJECT_MAP, $map);
91  }
92  }
93 
94  /**
95  * @see Controller::validate()
96  */
97  protected function validate() {
98  $request = $this->getRequest();
99  $response = $this->getResponse();
100  if ($request->getAction() != 'continue') {
101  // check request values
102  $oid = ObjectId::parse($request->getValue('oid'));
103  if(!$oid) {
104  $response->addError(ApplicationError::get('OID_INVALID',
105  array('invalidOids' => array($request->getValue('oid')))));
106  return false;
107  }
108  if($request->hasValue('targetoid')) {
109  $targetoid = ObjectId::parse($request->getValue('targetoid'));
110  if(!$targetoid) {
111  $response->addError(ApplicationError::get('OID_INVALID',
112  array('invalidOids' => array($request->getValue('targetoid')))));
113  return false;
114  }
115  }
116 
117  // check if the parent accepts this node type (only when not adding to root)
118  $addOk = true;
119  if ($request->hasValue('targetoid')) {
120  $persistenceFacade = $this->getPersistenceFacade();
121  $targetOID = ObjectId::parse($request->getValue('targetoid'));
122  $nodeOID = ObjectId::parse($request->getValue('oid'));
123 
124  $targetNode = $this->getTargetNode($targetOID);
125  $nodeType = $nodeOID->getType();
126  $targetType = $targetOID->getType();
127 
128  $tplNode = $persistenceFacade->create($targetType, 1);
129  $possibleChildren = NodeUtil::getPossibleChildren($targetNode, $tplNode);
130  if (!in_array($nodeType, array_keys($possibleChildren))) {
131  $response->addError(ApplicationError::get('ASSOCIATION_INVALID'));
132  $addOk = false;
133  }
134  else {
135  $template = $possibleChildren[$nodeType];
136  if (!$template->getProperty('canCreate')) {
137  $response->addError(ApplicationError::get('ASSOCIATION_INVALID'));
138  $addOk = false;
139  }
140  }
141  }
142  if (!$addOk) {
143  return false;
144  }
145  }
146  // do default validation
147  return parent::validate();
148  }
149 
150  /**
151  * @see BatchController::getWorkPackage()
152  */
153  protected function getWorkPackage($number) {
154  $request = $this->getRequest();
155  $message = $this->getMessage();
156  $name = '';
157  if ($request->getAction() == 'move') {
158  $name = $message->getText('Moving');
159  }
160  else if ($request->getAction() == 'copy') {
161  $name = $message->getText('Copying');
162  }
163  $name .= ': '.$request->getValue('oid');
164 
165  if ($number == 0) {
166  return array('name' => $name, 'size' => 1, 'oids' => array(1), 'callback' => 'startProcess');
167  }
168  else {
169  return null;
170  }
171  }
172 
173  /**
174  * Copy/Move the first node (oids parameter will be ignored)
175  * @param $oids The oids to process
176  */
177  protected function startProcess($oids) {
178  $session = $this->getSession();
179  $persistenceFacade = $this->getPersistenceFacade();
180 
181  // restore the request from session
182  $request = $session->get($this->REQUEST);
183  $action = $request->getAction();
184  $targetOID = ObjectId::parse($request->getValue('targetoid'));
185  $nodeOID = ObjectId::parse($request->getValue('oid'));
186 
187  // do the action
188  if ($action == 'move') {
189  $persistenceFacade = $this->getPersistenceFacade();
190  $transaction = $persistenceFacade->getTransaction();
191  $transaction->begin();
192  // with move action, we only need to attach the Node to the new target
193  // the children will not be loaded, they will be moved automatically
194  $nodeCopy = $persistenceFacade->load($nodeOID);
195  if ($nodeCopy) {
196  if ($targetOID != null) {
197  // attach the node to the target node
198  $parentNode = $this->getTargetNode($targetOID);
199  $parentNode->addNode($nodeCopy);
200  }
201 
202  $this->modify($nodeCopy);
203 
204  // set the result and finish
205  $this->endProcess($nodeCopy->getOID());
206 
207  $logger = $this->getLogger();
208  if ($logger->isInfoEnabled()) {
209  $logger->info("Moved: ".$nodeOID." to ".$parentNode->getOID());
210  }
211  }
212  $transaction->commit();
213  }
214  else if ($action == 'copy') {
215  // with copy action, we need to attach a copy of the Node to the new target,
216  // the children need to be loaded and treated in the same way too
217  $iterator = new PersistentIterator($persistenceFacade, $session, $nodeOID,
218  array('composite'));
219  $iteratorID = $iterator->save();
220  $session->set($this->ITERATOR_ID, $iteratorID);
221 
222  // copy the first node in order to reduce the number of calls for a single copy
223  $nodeCopy = $this->copyNode($iterator->current());
224  if ($nodeCopy) {
225  if ($targetOID != null) {
226  // attach the copy to the target node
227  $parentNode = $this->getTargetNode($targetOID);
228  $parentNode->addNode($nodeCopy);
229  }
230 
231  $this->modify($nodeCopy);
232 
233  $iterator->next();
234 
235  // proceed if nodes are left
236  if ($request->getBooleanValue('recursive') && $iterator->valid()) {
237  $iteratorID = $iterator->save();
238  $session->set($this->ITERATOR_ID, $iteratorID);
239 
240  $name = $this->getMessage()->getText('Copying tree: continue with %0%',
241  array($iterator->current()));
242  $this->addWorkPackage($name, 1, array(null), 'copyNodes');
243  }
244  else {
245  // set the result and finish
246  $this->endProcess($nodeCopy->getOID());
247  }
248  }
249  }
250  }
251 
252  /**
253  * Copy nodes provided by the persisted iterator (oids parameter will be ignored)
254  * @param $oids The oids to process
255  */
256  protected function copyNodes($oids) {
257  $session = $this->getSession();
258  $persistenceFacade = $this->getPersistenceFacade();
259 
260  // restore the request from session
261  $request = $session->get($this->REQUEST);
262  $action = $request->getAction();
263  $targetOID = ObjectId::parse($request->getValue('targetoid'));
264  $nodeOID = ObjectId::parse($request->getValue('oid'));
265 
266  // check for iterator in session
267  $iterator = null;
268  $iteratorID = $session->get($this->ITERATOR_ID);
269  if ($iteratorID != null) {
270  $iterator = PersistentIterator::load($persistenceFacade, $session, $iteratorID);
271  }
272 
273  // no iterator, finish
274  if ($iterator == null) {
275  // set the result and finish
276  $this->endProcess($this->getCopyOID($nodeOID));
277  }
278 
279  // process _NODES_PER_CALL nodes
280  $counter = 0;
281  while ($iterator->valid() && $counter < $request->getValue('nodesPerCall')) {
282  $currentOID = $iterator->current();
283  $this->copyNode($currentOID);
284 
285  $iterator->next();
286  $counter++;
287  }
288 
289  // decide what to do next
290  if ($iterator->valid()) {
291  // proceed with current iterator
292  $iteratorID = $iterator->save();
293  $session->set($this->ITERATOR_ID, $iteratorID);
294 
295  $name = $this->getMessage()->getText('Copying tree: continue with %0%',
296  array($iterator->current()));
297  $this->addWorkPackage($name, 1, array(null), 'copyNodes');
298  }
299  else {
300  // set the result and finish
301  $this->endProcess($this->getCopyOID($nodeOID));
302  }
303  }
304 
305  /**
306  * Finish the process and set the result
307  * @param $oid The object id of the newly created Node
308  */
309  protected function endProcess(ObjectId $oid) {
310  $response = $this->getResponse();
311  $response->setValue('oid', $oid);
312 
313  $session = $this->getSession();
314 
315  // clear session variables
316  $tmp = null;
317  $session->set($this->REQUEST, $tmp);
318  $session->set($this->OBJECT_MAP, $tmp);
319  $session->set($this->ITERATOR_ID, $tmp);
320  }
321 
322  /**
323  * Create a copy of the node with the given object id. The returned
324  * node is already persisted.
325  * @param $oid The oid of the node to copy
326  * @return The copied Node or null
327  */
328  protected function copyNode(ObjectId $oid) {
329  $logger = $this->getLogger();
330  if ($logger->isDebugEnabled()) {
331  $logger->debug("Copying node ".$oid);
332  }
333  $persistenceFacade = $this->getPersistenceFacade();
334  $transaction = $persistenceFacade->getTransaction();
335  $transaction->begin();
336 
337  // load the original node
338  $node = $persistenceFacade->load($oid);
339  if ($node == null) {
340  throw new PersistenceException("Can't load node '".$oid."'");
341  }
342 
343  // check if we already have a copy of the node
344  $nodeCopy = $this->getCopy($node->getOID());
345  if ($nodeCopy == null) {
346  // if not, create it
347  $nodeCopy = $persistenceFacade->create($node->getType());
348  $node->copyValues($nodeCopy, false);
349  }
350 
351  // save copy
352  $this->registerCopy($node, $nodeCopy);
353 
354  if ($logger->isInfoEnabled()) {
355  $logger->info("Copied: ".$node->getOID()." to ".$nodeCopy->getOID());
356  }
357  if ($logger->isDebugEnabled()) {
358  $logger->debug($nodeCopy->__toString());
359  }
360 
361  // create the connections to already copied relatives
362  // this must be done after saving the node in order to have a correct oid
363  $mapper = $node->getMapper();
364  $relations = $mapper->getRelations();
365  foreach ($relations as $relation) {
366  if ($relation->getOtherNavigability()) {
367  $otherRole = $relation->getOtherRole();
368  $relativeValue = $node->getValue($otherRole);
369  $relatives = $relation->isMultiValued() ? $relativeValue :
370  ($relativeValue != null ? array($relativeValue) : array());
371  foreach ($relatives as $relative) {
372  $copiedRelative = $this->getCopy($relative->getOID());
373  if ($copiedRelative != null) {
374  $nodeCopy->addNode($copiedRelative, $otherRole);
375  if ($logger->isDebugEnabled()) {
376  $logger->debug("Added ".$copiedRelative->getOID()." to ".$nodeCopy->getOID());
377  $logger->debug($copiedRelative->__toString());
378  }
379  }
380  }
381  }
382  }
383  $changedOids = $transaction->commit();
384  $this->updateCopyOIDs($changedOids);
385 
386  return $nodeCopy;
387  }
388 
389  /**
390  * Get the target node from the request parameter targetoid
391  * @param $targetOID The oid of the target node
392  * @return Node instance
393  */
394  protected function getTargetNode(ObjectId $targetOID) {
395  if ($this->_targetNode == null) {
396  // load parent node
397  $persistenceFacade = $this->getPersistenceFacade();
398  $targetNode = $persistenceFacade->load($targetOID);
399  $this->_targetNode = $targetNode;
400  }
401  return $this->_targetNode;
402  }
403 
404  /**
405  * Register a copied node in the session for later reference
406  * @param $origNode A reference to the original node
407  * @param $copyNode A reference to the copied node
408  */
409  protected function registerCopy(PersistentObject $origNode, PersistentObject $copyNode) {
410  $session = $this->getSession();
411  $registry = $session->get($this->OBJECT_MAP);
412  // store oid in the registry
413  $registry[$origNode->getOID()->__toString()] = $copyNode->getOID()->__toString();
414  $session->set($this->OBJECT_MAP, $registry);
415  }
416 
417  /**
418  * Update the copied oids in the registry
419  * @param $oidMap Map of changed oids (key: old value, value: new value)
420  */
421  protected function updateCopyOIDs(array $oidMap) {
422  $session = $this->getSession();
423  $registry = $session->get($this->OBJECT_MAP);
424  // registry maybe deleted already if it's the last step
425  if ($registry) {
426  $flippedRegistry = array_flip($registry);
427  foreach ($oidMap as $oldOid => $newOid) {
428  if (isset($flippedRegistry[$oldOid])) {
429  $key = $flippedRegistry[$oldOid];
430  unset($flippedRegistry[$oldOid]);
431  $flippedRegistry[$newOid] = $key;
432  }
433  }
434  $registry = array_flip($flippedRegistry);
435  $session->set($this->OBJECT_MAP, $registry);
436  }
437  }
438 
439  /**
440  * Get the object id of the copied node for a node id
441  * @param $origOID The object id of the original node
442  * @return ObjectId or null, if it does not exist already
443  */
444  protected function getCopyOID(ObjectId $origOID) {
445  $session = $this->getSession();
446  $registry = $session->get($this->OBJECT_MAP);
447 
448  // check if the oid exists in the registry
449  $oidStr = $origOID->__toString();
450  if (!isset($registry[$oidStr])) {
451  $logger = $this->getLogger();
452  if ($logger->isDebugEnabled()) {
453  $logger->debug("Copy of ".$oidStr." not found.");
454  }
455  return null;
456  }
457 
458  $copyOID = ObjectId::parse($registry[$oidStr]);
459  return $copyOID;
460  }
461 
462  /**
463  * Get the copied node for a node id
464  * @param $origOID The object id of the original node
465  * @return Copied Node or null, if it does not exist already
466  */
467  protected function getCopy(ObjectId $origOID) {
468  $copyOID = $this->getCopyOID($origOID);
469  if ($copyOID != null) {
470  $persistenceFacade = $this->getPersistenceFacade();
471  $nodeCopy = $persistenceFacade->load($copyOID);
472  return $nodeCopy;
473  }
474  else {
475  return null;
476  }
477  }
478 
479  /**
480  * Modify the given Node before save action (Called only for the copied root Node, not for its children)
481  * @note Subclasses will override this to implement special application requirements.
482  * @param $node A reference to the Node to modify.
483  * @return Boolean whether the Node was modified (default: false)
484  */
485  protected function modify(PersistentObject $node) {
486  return false;
487  }
488 }
489 ?>
Response holds the response values that are used as output from Controller instances.
Definition: Response.php:20
getRequest()
Get the Request instance.
Definition: Controller.php:190
copyNode(ObjectId $oid)
Create a copy of the node with the given object id.
getOID()
Get the object id of the PersistentObject.
getCopyOID(ObjectId $origOID)
Get the object id of the copied node for a node id.
getMessage()
Get the Message instance.
Definition: Controller.php:254
static load($persistenceFacade, $session, $uid)
Load an iterator state from the session.
addWorkPackage($name, $size, $oids, $callback, $args=null)
Add a work package to session.
endProcess(ObjectId $oid)
Finish the process and set the result.
ObjectId is the unique identifier of an object.
Definition: ObjectId.php:27
registerCopy(PersistentObject $origNode, PersistentObject $copyNode)
Register a copied node in the session for later reference.
initialize(Request $request, Response $response)
getAction()
Get the name of the action.
updateCopyOIDs(array $oidMap)
Update the copied oids in the registry.
modify(PersistentObject $node)
Modify the given Node before save action (Called only for the copied root Node, not for its children)...
Request holds the request values that are used as input to Controller instances.
Definition: Request.php:20
BatchController is used to process complex, longer running actions, that need to be divided into seve...
copyNodes($oids)
Copy nodes provided by the persisted iterator (oids parameter will be ignored)
static parse($oid)
Parse a serialized object id string into an ObjectId instance.
Definition: ObjectId.php:144
hasValue($name)
Check for existance of a value.
PersistenceException signals an exception in the persistence service.
__toString()
Get a string representation of the object id.
Definition: ObjectId.php:204
CopyController is used to copy or move Node instances.
Application controllers.
Definition: namespaces.php:3
setValue($name, $value)
Set a value.
startProcess($oids)
Copy/Move the first node (oids parameter will be ignored)
static get($code, $data=null)
Factory method for retrieving a predefind error instance.
PersistentIterator is used to iterate over a tree/list built of oids using a Depth-First-Algorithm.
getLogger()
Get the Logger instance.
Definition: Controller.php:206
getResponse()
Get the Response instance.
Definition: Controller.php:198
getSession()
Get the Session instance.
Definition: Controller.php:214
getCopy(ObjectId $origOID)
Get the copied node for a node id.
getTargetNode(ObjectId $targetOID)
Get the target node from the request parameter targetoid.
getPersistenceFacade()
Get the PersistenceFacade instance.
Definition: Controller.php:222
PersistentObject defines the interface of all persistent objects.