XMLExportController.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 
29 
30 /**
31  * XMLExportController exports the content tree into an XML file.
32  *
33  * The controller supports the following actions:
34  *
35  * <div class="controller-action">
36  * <div> __Action__ _default_ </div>
37  * <div>
38  * Initiate the export.
39  * | Parameter | Description
40  * |------------------------|-------------------------
41  * | _in_ `docFile` | The name of the file to write to (path relative to script main location) (default: 'export.xml')
42  * | _in_ `docType` | The document type (will be written into XML header) (default: '')
43  * | _in_ `dtd` | The dtd (will be written into XML header) (default: '')
44  * | _in_ `docRootElement` | The root element of the document (use this to enclose different root types if necessary) (default: 'Root')
45  * | _in_ `docLinebreak` | The linebreak character(s) to use (default: '\n')
46  * | _in_ `docIndent` | The indent character(s) to use (default: ' ')
47  * | _in_ `nodesPerCall` | The number of nodes to process in one call (default: 10)
48  * </div>
49  * </div>
50  *
51  * For additional actions and parameters see [BatchController actions](@ref BatchController).
52  *
53  * @author ingo herwig <ingo@wemove.com>
54  */
56  const CACHE_SECTION = 'xmlexport';
57  const CACHE_KEY_ROOT_OIDS = 'rootOids';
58  const CACHE_KEY_EXPORTED_OIDS = 'exportedOids';
59 
60  // session name constants
61  private $ITERATOR_ID = 'XMLExportController.iteratorid';
62 
63  // documentInfo passes the current document info/status from one call to the next:
64  // An assoziative array with keys 'docFile', 'docType', 'dtd', 'docLinebreak', 'docIndent', 'nodesPerCall',
65  // 'lastIndent' and 'tagsToClose' where the latter is an array of assoziative arrays with keys 'indent', 'name'
66  private $DOCUMENT_INFO = 'XMLExportController.documentinfo';
67 
68  // default values, maybe overriden by corresponding request values (see above)
69  private $_DOCFILE = "export.xml";
70  private $_DOCTYPE = "";
71  private $_DTD = "";
72  private $_DOCROOTELEMENT = "Root";
73  private $_DOCLINEBREAK = "\n";
74  private $_DOCINDENT = " ";
75  private $_NODES_PER_CALL = 10;
76 
77  private $_cache = null;
78  private $_fileUtil = null;
79 
80  /**
81  * Constructor
82  * @param $session
83  * @param $persistenceFacade
84  * @param $permissionManager
85  * @param $actionMapper
86  * @param $localization
87  * @param $message
88  * @param $configuration
89  * @param $cache
90  */
91  public function __construct(Session $session,
92  PersistenceFacade $persistenceFacade,
93  PermissionManager $permissionManager,
94  ActionMapper $actionMapper,
95  Localization $localization,
96  Message $message,
97  Configuration $configuration,
98  Cache $cache) {
99  parent::__construct($session, $persistenceFacade, $permissionManager,
100  $actionMapper, $localization, $message, $configuration);
101  $this->_cache = $cache;
102  }
103 
104  /**
105  * Get the FileUtil instance
106  * @return FileUtil
107  */
108  protected function getFileUtil() {
109  if ($this->_fileUtil == null) {
110  $this->_fileUtil = new FileUtil();
111  }
112  return $this->_fileUtil;
113  }
114 
115  /**
116  * @see Controller::initialize()
117  */
118  public function initialize(Request $request, Response $response) {
119  parent::initialize($request, $response);
120 
121  // construct initial document info
122  if ($request->getAction() != 'continue') {
123  $session = $this->getSession();
124 
125  $docFile = $request->hasValue('docFile') ? $request->getValue('docFile') : $this->getDownloadFile();
126  $docType = $request->hasValue('docType') ? $request->getValue('docType') : $this->_DOCTYPE;
127  $dtd = $request->hasValue('dtd') ? $request->getValue('dtd') : $this->_DTD;
128  $docRootElement = $request->hasValue('docRootElement') ? $request->getValue('docRootElement') : $this->_DOCROOTELEMENT;
129  $docLinebreak = $request->hasValue('docLinebreak') ? $request->getValue('docLinebreak') : $this->_DOCLINEBREAK;
130  $docIndent = $request->hasValue('docIndent') ? $request->getValue('docIndent') : $this->_DOCINDENT;
131  $nodesPerCall = $request->hasValue('nodesPerCall') ? $request->getValue('nodesPerCall') : $this->_NODES_PER_CALL;
132 
133  $documentInfo = array('docFile' => $docFile, 'docType' => $docType, 'dtd' => $dtd, 'docRootElement' => $docRootElement,
134  'docLinebreak' => $docLinebreak, 'docIndent' => $docIndent, 'nodesPerCall' => $nodesPerCall,
135  'lastIndent' => 0, 'tagsToClose' => array());
136 
137  // store document info in session
138  $session->set($this->DOCUMENT_INFO, $documentInfo);
139  }
140  }
141 
142  /**
143  * @see BatchController::getWorkPackage()
144  */
145  protected function getWorkPackage($number) {
146  if ($number == 0) {
147  return array('name' => $this->getMessage()->getText('Initialization'),
148  'size' => 1, 'oids' => array(1), 'callback' => 'initExport');
149  }
150  else {
151  return null;
152  }
153  }
154 
155  /**
156  * @see BatchController::getDownloadFile()
157  */
158  protected function getDownloadFile() {
159  $cacheDir = session_save_path().DIRECTORY_SEPARATOR;
160  return $cacheDir.$this->_DOCFILE;
161  }
162 
163  /**
164  * Initialize the XML export (oids parameter will be ignored)
165  * @param $oids The oids to process
166  * @note This is a callback method called on a matching work package, see BatchController::addWorkPackage()
167  */
168  protected function initExport($oids) {
169  $session = $this->getSession();
170  $fileUtil = $this->getFileUtil();
171  // restore document state from session
172  $documentInfo = $session->get($this->DOCUMENT_INFO);
173  $filename = $documentInfo['docFile'];
174 
175  // delete export file
176  if (file_exists($filename)) {
177  unlink($filename);
178  }
179 
180  // start document
181  $fileHandle = fopen($filename, "a");
182  $fileUtil->fputsUnicode($fileHandle, '<?xml version="1.0" encoding="UTF-8"?>'.$documentInfo['docLinebreak']);
183  if ($documentInfo['docType'] != "") {
184  $fileUtil->fputsUnicode($fileHandle, '<!DOCTYPE '.$documentInfo['docType'].' SYSTEM "'.$documentInfo['dtd'].'">'.$documentInfo['docLinebreak']);
185  }
186  $fileUtil->fputsUnicode($fileHandle, '<'.$documentInfo['docRootElement'].'>'.$documentInfo['docLinebreak']);
187  fclose($fileHandle);
188 
189  // get root types from ini file
190  $rootOIDs = array();
191  $config = $this->getConfiguration();
192  $rootTypes = $config->getValue('rootTypes', 'application');
193  if (is_array($rootTypes)) {
194  $persistenceFacade = $this->getPersistenceFacade();
195  foreach($rootTypes as $rootType) {
196  $rootOIDs = array_merge($rootOIDs, $persistenceFacade->getOIDs($rootType));
197  }
198  }
199 
200  // store root object ids in session
201  $nextOID = array_shift($rootOIDs);
202  $this->_cache->put(self::CACHE_SECTION, self::CACHE_KEY_ROOT_OIDS, $rootOIDs);
203 
204  // empty exported oids
205  $tmp = array();
206  $this->_cache->put(self::CACHE_SECTION, self::CACHE_KEY_EXPORTED_OIDS, $tmp);
207 
208  // create work package for first root node
209  $this->addWorkPackage(
210  $this->getMessage()->getText('Exporting tree: start with %0%', array($nextOID)),
211  1, array($nextOID), 'exportNodes');
212  }
213 
214  /**
215  * Serialize all Nodes with given oids to XML
216  * @param $oids The oids to process
217  * @note This is a callback method called on a matching work package, see BatchController::addWorkPackage()
218  */
219  protected function exportNodes($oids) {
220  // Export starts from root oids and iterates over all children.
221  // On every call we have to decide what to do:
222  // - If there is an iterator stored in the session we are inside a tree and continue iterating (_NODES_PER_CALL nodes)
223  // until the iterator finishes
224  // - If the oids array holds one value!=null this is assumed to be an root oid and a new iterator is constructed
225  // - If there is no iterator and no oid given, we return
226 
227  $session = $this->getSession();
228  $persistenceFacade = $this->getPersistenceFacade();
229  $message = $this->getMessage();
230 
231  // restore document state from session
232  $documentInfo = $session->get($this->DOCUMENT_INFO);
233 
234  // check for iterator in session
235  $iterator = null;
236  $iteratorID = $session->get($this->ITERATOR_ID);
237  if ($iteratorID != null) {
238  $iterator = PersistentIterator::load($persistenceFacade, $session, $iteratorID);
239  }
240  // no iterator but oid given, start with new root oid
241  if ($iterator == null && sizeof($oids) > 0 && $oids[0] != null) {
242  $iterator = new PersistentIterator($persistenceFacade, $session, $oids[0]);
243  }
244  // no iterator, no oid, finish
245  if ($iterator == null) {
246  $this->addWorkPackage($message->getText('Finish'), 1, array(null), 'finishExport');
247  return;
248  }
249 
250  // process _NODES_PER_CALL nodes
251  $fileHandle = fopen($documentInfo['docFile'], "a");
252  $counter = 0;
253  while ($iterator->valid() && $counter < $documentInfo['nodesPerCall']) {
254  // write node
255  $documentInfo = $this->writeNode($fileHandle, $iterator->current(), $iterator->key()+1, $documentInfo);
256  $iterator->next();
257  $counter++;
258  }
259  $this->endTags($fileHandle, 0, $documentInfo);
260  fclose($fileHandle);
261 
262  // save document state to session
263  $session->set($this->DOCUMENT_INFO, $documentInfo);
264 
265  // decide what to do next
266  $rootOIDs = $this->_cache->get(self::CACHE_SECTION, self::CACHE_KEY_ROOT_OIDS);
267  if (!$iterator->valid() && sizeof($rootOIDs) > 0) {
268  // if the current iterator is finished, set iterator null and proceed with the next root oid
269  $nextOID = array_shift($rootOIDs);
270  $iterator = null;
271  // store remaining root oids in session
272  $this->_cache->put(self::CACHE_SECTION, self::CACHE_KEY_ROOT_OIDS, $rootOIDs);
273  // unset iterator id to start with new root oid
274  $tmp = null;
275  $session->set($this->ITERATOR_ID, $tmp);
276 
277  $name = $message->getText('Exporting tree: start with %0%', array($nextOID));
278  $this->addWorkPackage($name, 1, array($nextOID), 'exportNodes');
279  }
280  elseif ($iterator->valid()) {
281  // proceed with current iterator
282  $iteratorID = $iterator->save();
283  $session->set($this->ITERATOR_ID, $iteratorID);
284 
285  $name = $message->getText('Exporting tree: continue with %0%', array($iterator->current()));
286  $this->addWorkPackage($name, 1, array(null), 'exportNodes');
287  }
288  else {
289  // finish
290  $this->addWorkPackage($message->getText('Finish'), 1, array(null), 'finishExport');
291  }
292  }
293 
294  /**
295  * Finish the XML export (oids parameter will be ignored)
296  * @param $oids The oids to process
297  * @note This is a callback method called on a matching work package, see BatchController::addWorkPackage()
298  */
299  protected function finishExport($oids) {
300  $session = $this->getSession();
301  $fileUtil = $this->getFileUtil();
302  // restore document state from session
303  $documentInfo = $session->get($this->DOCUMENT_INFO);
304 
305  // end document
306  $fileHandle = fopen($documentInfo['docFile'], "a");
307  $this->endTags($fileHandle, 0, $documentInfo);
308  $fileUtil->fputsUnicode($fileHandle, '</'.$documentInfo['docRootElement'].'>'.$documentInfo['docLinebreak']);
309  fclose($fileHandle);
310 
311  // clear session variables
312  $tmp = null;
313  $this->_cache->put(self::CACHE_SECTION, self::CACHE_KEY_ROOT_OIDS, $tmp);
314  $session->set($this->ITERATOR_ID, $tmp);
315  $session->set($this->DOCUMENT_INFO, $tmp);
316  }
317 
318  /**
319  * Ends all tags up to $curIndent level
320  * @param $fileHandle The file handle to write to
321  * @param $curIndent The depth of the node in the tree
322  * @param $documentInfo A reference to an assoziative array (see DOCUMENT_INFO)
323  */
324  protected function endTags($fileHandle, $curIndent, &$documentInfo) {
325  $fileUtil = $this->getFileUtil();
326  $lastIndent = $documentInfo['lastIndent'];
327 
328  // write last opened and not closed tags
329  if ($curIndent < $lastIndent) {
330  for ($i=$lastIndent-$curIndent; $i>0; $i--) {
331  $closeTag = array_shift($documentInfo['tagsToClose']);
332  if ($closeTag) {
333  $fileUtil->fputsUnicode($fileHandle, str_repeat($documentInfo['docIndent'], $closeTag["indent"]).'</'.$closeTag["name"].'>'.$documentInfo['docLinebreak']);
334  }
335  }
336  }
337  }
338 
339  /**
340  * Serialize a Node to XML
341  * @param $fileHandle The file handle to write to
342  * @param $oid The oid of the node
343  * @param $depth The depth of the node in the tree
344  * @param $documentInfo An assoziative array (see DOCUMENT_INFO)
345  * @return The updated document state
346  */
347  protected function writeNode($fileHandle, ObjectId $oid, $depth, $documentInfo) {
348  $persistenceFacade = $this->getPersistenceFacade();
349  $fileUtil = $this->getFileUtil();
350 
351  // load node and get element name
352  $node = $persistenceFacade->load($oid);
353  $elementName = $persistenceFacade->getSimpleType($node->getType());
354 
355  // check if the node is written already
356  $exportedOids = $this->_cache->get(self::CACHE_SECTION, self::CACHE_KEY_EXPORTED_OIDS);
357  if (!in_array($oid->__toString(), $exportedOids)) {
358  // write node
359  $mapper = $node->getMapper();
360 
361  $hasUnvisitedChildren = $this->getNumUnvisitedChildren($node) > 0;
362 
363  $curIndent = $depth;
364  $this->endTags($fileHandle, $curIndent, $documentInfo);
365 
366  // write object's content
367  // open start tag
368  $fileUtil->fputsUnicode($fileHandle, str_repeat($documentInfo['docIndent'], $curIndent).'<'.$elementName);
369  // write object attributes
370  $attributes = $mapper->getAttributes();
371  foreach ($attributes as $curAttribute) {
372  $attributeName = $curAttribute->getName();
373  $value = $node->getValue($attributeName);
374  if ($value) {
375  $fileUtil->fputsUnicode($fileHandle, ' '.$attributeName.'="'.$this->formatValue($value).'"');
376  }
377  }
378  // close start tag
379  $fileUtil->fputsUnicode($fileHandle, '>');
380  if ($hasUnvisitedChildren) {
381  $fileUtil->fputsUnicode($fileHandle, $documentInfo['docLinebreak']);
382  }
383 
384  // remember end tag if not closed
385  if ($hasUnvisitedChildren) {
386  $closeTag = array("name" => $elementName, "indent" => $curIndent);
387  array_unshift($documentInfo['tagsToClose'], $closeTag);
388  }
389  else {
390  $fileUtil->fputsUnicode($fileHandle, '</'.$elementName.'>'.$documentInfo['docLinebreak']);
391  }
392  // remember current indent
393  $documentInfo['lastIndent'] = $curIndent;
394 
395  // register exported node
396  $exportedOids[] = $oid->__toString();
397  $this->_cache->put(self::CACHE_SECTION, self::CACHE_KEY_EXPORTED_OIDS, $exportedOids);
398  }
399  // return the updated document info
400  return $documentInfo;
401  }
402 
403  /**
404  * Get number of children of the given node, that were not visited yet
405  * @param $node
406  * @return Integer
407  */
408  protected function getNumUnvisitedChildren(Node $node) {
409  $exportedOids = $this->_cache->get(self::CACHE_SECTION, self::CACHE_KEY_EXPORTED_OIDS);
410 
411  $childOIDs = array();
412  $mapper = $node->getMapper();
413  $relations = $mapper->getRelations('child');
414  foreach ($relations as $relation) {
415  if ($relation->getOtherNavigability()) {
416  $childValue = $node->getValue($relation->getOtherRole());
417  if ($childValue != null) {
418  $children = $relation->isMultiValued() ? $childValue : array($childValue);
419  foreach ($children as $child) {
420  $childOIDs[] = $child->getOID();
421  }
422  }
423  }
424  }
425  $numUnvisitedChildren = 0;
426  foreach ($childOIDs as $childOid) {
427  if (!in_array($childOid->__toString(), $exportedOids)) {
428  $numUnvisitedChildren++;
429  }
430  }
431  return $numUnvisitedChildren;
432  }
433 
434  /**
435  * Format a value for XML output
436  * @param $value The value to format
437  * @return The formatted value
438  * @note Subclasses may overrite this for special application requirements
439  */
440  protected function formatValue($value) {
441  return htmlentities(str_replace(array("\r", "\n"), array("", ""), nl2br($value)), ENT_QUOTES);
442  }
443 }
444 ?>
Response holds the response values that are used as output from Controller instances.
Definition: Response.php:20
getValue($name)
Definition: Node.php:91
exportNodes($oids)
Serialize all Nodes with given oids to XML.
Localization defines the interface for storing localized entity instances and retrieving them back...
formatValue($value)
Format a value for XML output.
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.
writeNode($fileHandle, ObjectId $oid, $depth, $documentInfo)
Serialize a Node to XML.
ObjectId is the unique identifier of an object.
Definition: ObjectId.php:27
Message is used to get localized messages to be used in the user interface.
Definition: Message.php:23
getAction()
Get the name of the action.
Session is the interface for session implementations and defines access to session variables...
Definition: Session.php:21
initialize(Request $request, Response $response)
getConfiguration()
Get the Configuration instance.
Definition: Controller.php:262
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...
hasValue($name)
Check for existance of a value.
Cache defines the interface for cache implementations.
Definition: Cache.php:21
__construct(Session $session, PersistenceFacade $persistenceFacade, PermissionManager $permissionManager, ActionMapper $actionMapper, Localization $localization, Message $message, Configuration $configuration, Cache $cache)
Constructor.
PermissionManager implementations are used to handle all authorization requests.
Implementations of Configuration give access to the application configuration.
__toString()
Get a string representation of the object id.
Definition: ObjectId.php:204
initExport($oids)
Initialize the XML export (oids parameter will be ignored)
endTags($fileHandle, $curIndent, &$documentInfo)
Ends all tags up to $curIndent level.
Application controllers.
Definition: namespaces.php:3
getNumUnvisitedChildren(Node $node)
Get number of children of the given node, that were not visited yet.
ActionMapper implementations are responsible for instantiating and executing Controllers based on the...
XMLExportController exports the content tree into an XML file.
FileUtil provides basic support for file functionality like HTTP file upload.
Definition: FileUtil.php:22
PersistenceFacade defines the interface for PersistenceFacade implementations.
PersistentIterator is used to iterate over a tree/list built of oids using a Depth-First-Algorithm.
getSession()
Get the Session instance.
Definition: Controller.php:214
finishExport($oids)
Finish the XML export (oids parameter will be ignored)
getValue($name, $default=null, $filter=null, $options=null)
Get a value.
Node adds the concept of relations to PersistentObject.
Definition: Node.php:34
getPersistenceFacade()
Get the PersistenceFacade instance.
Definition: Controller.php:222