wCMF  3.6
 All Classes Namespaces Files Functions Variables Groups Pages
class.NodeXMLDBMapper.php
Go to the documentation of this file.
1 <?php
2 /**
3  * wCMF - wemove Content Management Framework
4  * Copyright (C) 2005-2014 wemove digital solutions GmbH
5  *
6  * Licensed under the terms of any of the following licenses
7  * at your choice:
8  *
9  * - GNU Lesser General Public License (LGPL)
10  * http://www.gnu.org/licenses/lgpl.html
11  * - Eclipse Public License (EPL)
12  * http://www.eclipse.org/org/documents/epl-v10.php
13  *
14  * See the license.txt file distributed with this work for
15  * additional information.
16  *
17  * $Id: class.NodeXMLDBMapper.php 1462 2014-02-04 23:52:27Z iherwig $
18  */
19 require_once(BASE."wcmf/lib/util/class.Message.php");
20 require_once(BASE."wcmf/lib/persistence/class.PersistenceMapper.php");
21 require_once(BASE."wcmf/lib/persistence/class.PersistenceFacade.php");
22 require_once(BASE."wcmf/lib/persistence/converter/class.DataConverter.php");
23 require_once(BASE."wcmf/lib/model/class.Node.php");
24 require_once(BASE."wcmf/lib/util/class.XMLUtil.php");
25 require_once(BASE."wcmf/lib/util/class.Log.php");
26 
27 /**
28  * Some constants describing the data types
29  */
30 define("DATATYPE_DONTCARE", 0);
31 define("DATATYPE_ATTRIBUTE", 1);
32 define("DATATYPE_ELEMENT", 2);
33 define("DATATYPE_IGNORE", 3); // all data items >= DATATYPE_IGNORE wont be shown in human readable node discriptions
34 /**
35  * Some constants describing the build process
36  */
37 define("BUILDDEPTH_INFINITE", -1); // build complete tree from given root on
38 define("BUILDDEPTH_SINGLE", -2); // build only given node
39 define("BUILDDEPTH_GROUPED", -3); // build tree from given root on respecting the root property defined in element relations
40  // NODE: BUILDDEPTH_GROUPED is not supported yet!
41 
42 /**
43  * @class NodeXMLDBMapper
44  * @ingroup Mapper
45  * @brief NodeXMLDBMapper maps Node objects to a xml file using the CXmlDb class.
46  * http://sourceforge.net/projects/phpxmldb
47  *
48  * @todo insert doctype, dtd into XML file
49  * @todo when inserting children to a Node with text content the content is duplicated
50  *
51  * @author ingo herwig <ingo@wemove.com>
52  */
54 {
55  // xml file variables
56  var $_filename = '';
57  var $_doctype = '';
58  var $_dtd = '';
59  // the XMLUtil instance
60  var $_db = null;
61  // transaction status
62  var $_inTransaction = false;
63 
64  /**
65  * Constructor.
66  * @param params Initialization data given in an assoziative array with the following keys:
67  * filename, doctype, dtd
68  */
69  function NodeXMLDBMapper($params)
70  {
71  if (isset($params['filename']) && isset($params['doctype']) && isset($params['dtd']))
72  {
73  $this->_filename = $params['filename'];
74  $this->_doctype = $params['doctype'];
75  $this->_dtd = $params['dtd'];
76 
77  // setup xml database
78  $this->_db = new XMLUtil();
79  $this->_db->SetOptions(array('TimeStampFlag' => 0, 'XmlOptions' => array(XML_OPTION_SKIP_WHITE => 1)));
80 
81  // Security settings
82  $this->aDbPermissions = array(
83  'GetNodeData' => XMLDB_PERMISSION_ENABLE,
84  'GetChildData' => XMLDB_PERMISSION_ENABLE,
85  'InsertNode' => XMLDB_PERMISSION_ENABLE,
86  'UpdateNode' => XMLDB_PERMISSION_ENABLE,
87  'RemoveNode' => XMLDB_PERMISSION_ENABLE
88  );
89  // Pass down the security mode settings.
90  $this->_db->bSecureMode = TRUE;
91  foreach ($this->aDbPermissions as $MethodName => $Permission)
92  $this->_db->aPermissions[$MethodName] = $Permission;
93  }
94  else
95  WCMFException::throwEx("Wrong parameters for constructor.", __FILE__, __LINE__);
96 
97  $this->_dataConverter = new DataConverter();
98 
99  // call commitTransaction() on shutdown for automatic transaction end
100  register_shutdown_function(array(&$this, 'commitTransaction'));
101  }
102  /**
103  * Set the data file. Ends the transaction on the existing file.
104  * @param params Initialization data given in an assoziative array with the following keys:
105  * filename, doctype, dtd
106  */
107  function setFilename($params)
108  {
109  if (array_key_exists('filename', $params) && array_key_exists('doctype', $params) && array_key_exists('dtd', $params))
110  {
111  if ($params['filename'] == $this->_filename)
112  return;
113 
114  // commit transaction on exisiting file
115  $this->commitTransaction();
116 
117  $this->_filename = $params['filename'];
118  $this->_doctype = $params['doctype'];
119  $this->_dtd = $params['dtd'];
120  }
121  else
122  WCMFException::throwEx("Wrong parameters.", __FILE__, __LINE__);
123  }
124  /**
125  * @see PersistenceMapper::getPkNames()
126  */
127  function getPkNames()
128  {
129  return array('id' => DATATYPE_IGNORE);
130  }
131  /**
132  * @see PersistenceMapper::loadImpl()
133  */
134  function &loadImpl($oid, $buildDepth, $buildAttribs=null, $buildTypes=null)
135  {
136  if ($buildDepth < 0 && !in_array($buildDepth, array(BUILDDEPTH_INFINITE, BUILDDEPTH_SINGLE)))
137  WCMFException::throwEx("Build depth not supported: $buildDepth", __FILE__, __LINE__);
138 
139  // auto start transaction
140  $this->startReadTransaction();
141  //WCMFException::throwEx("No Connection. Start transaction first", __FILE__, __LINE__);
142 
143  $persistenceFacade = &PersistenceFacade::getInstance();
144  $nodeDef = PersistenceFacade::decomposeOID($oid);
145 
146  // check buildTypes
147  if (is_array($buildTypes) && !in_array($nodeDef['type'], $buildTypes))
148  return null;
149 
150  // get node definition (default values, properties)
151  $nodeData = $this->getNodeDefinition($nodeDef['type']);
152 
153  // find value with type DATATYPE_ELEMENT (only one is allowed)
154  foreach ($nodeData['_datadef'] as $dataDef)
155  if ($dataDef['app_data_type'] == DATATYPE_ELEMENT)
156  {
157  $elementName = $dataDef['name'];
158  break;
159  }
160 
161  // select node data
162  if (strlen($elementName) > 0)
163  {
164  $nodeData['_data'] = $this->_db->GetNodeData($oid, $elementName);
165  if ($nodeData['_data'] == null)
166  WCMFException::throwEx("Could not load Node with OID: ".$oid, __FILE__, __LINE__);
167  }
168 
169  // construct node
170  $node = &$this->createObject($nodeDef['type'], $oid);
171 
172  // set node properties
173  foreach($nodeData['_properties'] as $property)
174  $node->setProperty($property['name'], $property['value']);
175  $i = 0;
176  $parentoids = array();
177  while (array_key_exists('pid'+$i, $nodeData['_data']))
178  {
179  $parentoid = PersistenceFacade::composeOID(array('type' => $nodeData['_data']['ptype'+$i], 'id' => $nodeData['_data']['pid'+$i]));
180  array_push($parentoids, $parentoid);
181  $i++;
182  }
183  $node->setProperty('parentoids', $parentoids);
184 
185  // get attributes to load
186  $attribs = null;
187  if ($buildAttribs != null && isset($buildAttribs[$nodeDef['type']]))
188  $attribs = $buildAttribs[$nodeDef['type']];
189 
190  // set node data
191  foreach($nodeData['_datadef'] as $dataItem)
192  {
193  if ($attribs == null || in_array($dataItem['name'], $attribs))
194  {
195  $value = $this->_dataConverter->convertStorageToApplication($nodeData['_data'][$dataItem['name']], $dataItem['db_data_type'], $dataItem['name']);
196  $node->setValue($dataItem['name'], $value, $dataItem['app_data_type']);
197  $valueProperties = array();
198  foreach($dataItem as $key => $value)
199  if ($key != 'name' && $key != 'app_data_type')
200  $valueProperties[$key] = $value;
201  $node->setValueProperties($dataItem['name'], $valueProperties, $dataItem['app_data_type']);
202  }
203  }
204 
205  // recalculate build depth for the next generation
206  if ($buildDepth != BUILDDEPTH_SINGLE && $buildDepth != BUILDDEPTH_INFINITE && $buildDepth > 0)
207  $newBuildDepth = $buildDepth-1;
208  else
209  $newBuildDepth = $buildDepth;
210 
211  // get children of this node
212  $childoids = array();
213  $childrenData = $this->_db->GetChildData($oid);
214  foreach($childrenData as $childData)
215  {
216  $childoid = PersistenceFacade::composeOID(array('type' => $childData['type'], 'id' => $childData['id']));
217  array_push($childoids, $childoid);
218  if ( ($buildDepth != BUILDDEPTH_SINGLE) && ($buildDepth > 0 || $buildDepth == BUILDDEPTH_INFINITE) )
219  {
220  $childNode = &$persistenceFacade->load($childoid, $newBuildDepth, $buildAttribs, $buildTypes);
221  if ($childNode != null)
222  $node->addChild($childNode);
223  }
224  }
225  $node->setProperty('childoids', $childoids);
226  $node->setState(STATE_CLEAN, false);
227  return $node;
228  }
229  /**
230  * @see PersistenceMapper::createImpl()
231  */
232  function &createImpl($type, $buildDepth, $buildAttribs)
233  {
234  if ($buildDepth < 0 && !in_array($buildDepth, array(BUILDDEPTH_INFINITE, BUILDDEPTH_SINGLE, BUILDDEPTH_REQUIRED)))
235  WCMFException::throwEx("Build depth not supported: $buildDepth", __FILE__, __LINE__);
236 
237  $persistenceFacade = &PersistenceFacade::getInstance();
238 
239  // get node definition (default values, properties)
240  $nodeData = $this->getNodeDefinition($type);
241 
242  // construct node
243  $node = &$this->createObject($type);
244 
245  // set node properties
246  foreach($nodeData['_properties'] as $property)
247  $node->setProperty($property['name'], $property['value']);
248  // NOTE: we do not set the 'parentoids' property because it might be ambiguous (e.g. 'name' type might be used in different contexts)
249 
250  // get attributes to load
251  $attribs = null;
252  if ($buildAttribs != null && isset($buildAttribs[$type]))
253  $attribs = $buildAttribs[$type];
254 
255  // set node data
256  foreach($nodeData['_datadef'] as $dataItem)
257  {
258  if ($attribs == null || in_array($dataItem['name'], $attribs))
259  {
260  $value = $this->_dataConverter->convertStorageToApplication($dataItem['default'], $dataItem['db_data_type'], $dataItem['name']);
261  $node->setValue($dataItem['name'], $value, $dataItem['app_data_type']);
262  $valueProperties = array();
263  foreach($dataItem as $key => $value)
264  if ($key != 'name' && $key != 'app_data_type')
265  $valueProperties[$key] = $value;
266  $node->setValueProperties($dataItem['name'], $valueProperties, $dataItem['app_data_type']);
267  }
268  }
269 
270  // recalculate build depth for the next generation
271  if ($buildDepth != BUILDDEPTH_REQUIRED && $buildDepth != BUILDDEPTH_SINGLE && $buildDepth != BUILDDEPTH_INFINITE && $buildDepth > 0)
272  $newBuildDepth = $buildDepth-1;
273  else
274  $newBuildDepth = $buildDepth;
275 
276  // prevent infinite recursion
277  if ($buildDepth < BUILDDEPTH_MAX)
278  {
279  // set children of this node
280  foreach ($nodeData['_children'] as $childData)
281  {
282  // set 'minOccurs', 'maxOccurs'
283  if (!isset($childData['minOccurs']))
284  $childData['minOccurs'] = 0; // default value
285  if (!isset($childData['maxOccurs']))
286  $childData['maxOccurs'] = 1; // default value
287 
288  if ( ($buildDepth != BUILDDEPTH_SINGLE) && (($buildDepth > 0) || ($buildDepth == BUILDDEPTH_INFINITE) ||
289  (($buildDepth == BUILDDEPTH_REQUIRED) && $childData['minOccurs'] > 0 && $childData['aggregation'] == true)) )
290  {
291  $childNode = &$persistenceFacade->create($childData['type'], $newBuildDepth, $buildAttribs);
292  $childNode->setProperty('minOccurs', $childData['minOccurs']);
293  $childNode->setProperty('maxOccurs', $childData['maxOccurs']);
294  $childNode->setProperty('aggregation', $childData['aggregation']);
295  $childNode->setProperty('composition', $childData['composition']);
296  $node->addChild($childNode);
297  }
298  }
299  }
300  return $node;
301  }
302  /**
303  * @see PersistenceMapper::saveImpl()
304  */
305  function saveImpl(&$node)
306  {
307  if ($node->getType() == $this->_db->getRootNodeName())
308  return false;
309 
310  // auto start transaction
311  $this->startTransaction();
312  //WCMFException::throwEx("No Connection. Start transaction first", __FILE__, __LINE__);
313 
314  // prepare node data
315  // escape all values
316  $appValues = array();
317  foreach ($node->getDataTypes() as $type)
318  foreach ($node->getValueNames($type) as $valueName)
319  {
320  $properties = $node->getValueProperties($valueName, $type);
321  $appValues[$type][$valueName] = $node->getValue($valueName, $type);
322  // NOTE: strip slashes from "'" and """ first
323  $value = str_replace(array("\'","\\\""), array("'", "\""), $appValues[$type][$valueName]);
324  $node->setValue($valueName, $this->_dataConverter->convertApplicationToStorage($value, $properties['db_data_type'], $valueName), $type);
325  }
326 
327  $persistenceFacade = &PersistenceFacade::getInstance();
328  if ($node->getState() == STATE_NEW)
329  {
330  // insert new node as child of given parent
331  $parentOID = null;
332  $parent = $node->getParent();
333  if ($parent != null)
334  $parentOID = $parent->getOID();
335  else if (sizeof($node->getProperty('parentoids')) > 0)
336  {
337  $poids = $node->getProperty('parentoids');
338  $parentOID = $poids[0];
339  }
340 
341  // add to root, if parentOID is invalid
342  if (!PersistenceFacade::isValidOID($parentOID))
343  $parentOID = null;
344 
345  $newID = $this->_db->InsertNode($node, $parentOID);
346  if($newID == 0)
347  WCMFException::throwEx("Error inserting node ".$node->getType().": ".$this->_db->getErrorMsg(), __FILE__, __LINE__);
348 
349  // log action
350  $this->logAction(&$node);
351  }
352  else if ($node->getState() == STATE_DIRTY)
353  {
354  // save existing node
355  // precondition: the node exists in the database
356 
357  // log action
358  $this->logAction(&$node);
359 
360  // save node
361  if(!$this->_db->UpdateNode($node))
362  WCMFException::throwEx("Error updating node ".$node->getType().": ".$this->_db->getErrorMsg(), __FILE__, __LINE__);
363  }
364 
365  // set escaped values back to application values
366  foreach ($node->getDataTypes() as $type)
367  foreach ($node->getValueNames($type) as $valueName)
368  $node->setValue($valueName, $appValues[$type][$valueName], $type);
369 
370  $node->setState(STATE_CLEAN, false);
371  // postcondition: the node is saved to the db
372  // the node state is STATE_CLEAN
373  // attributes are only inserted if their values differ from ''
374 
375  return true;
376  }
377  /**
378  * @see PersistenceMapper::deleteImpl()
379  */
380  function deleteImpl($oid, $recursive=true)
381  {
382  // auto start transaction
383  $this->startTransaction();
384  //WCMFException::throwEx("No Connection. Start transaction first", __FILE__, __LINE__);
385 
386  $persistenceFacade = &PersistenceFacade::getInstance();
387  $nodeDef = PersistenceFacade::decomposeOID($oid);
388 
389  // log action
390  if ($this->isLogging())
391  {
392  $obj = &$persistenceFacade->load($oid, BUILDDEPTH_SINGLE);
393  if ($obj)
394  $this->logAction(&$obj);
395  }
396 
397  // delete node
398  if(!$this->_db->RemoveNode($oid))
399  WCMFException::throwEx("Error deleting node ".$oid.": ".$this->_db->getErrorMsg(), __FILE__, __LINE__);
400 
401  // delete children (in xml there is no difference between aggregation and composition, all children are deleted)
402  if ($recursive)
403  {
404  $childrenData = $this->_db->GetChildData($oid);
405  foreach($childrenData as $childData)
406  {
407  $childoid = PersistenceFacade::composeOID(array('type' => $childData['type'], 'id' => $childData['id']));
408  $persistenceFacade->delete($childoid, $recursive);
409  }
410  }
411  // postcondition: the node and all of its children are deleted from db
412  return true;
413  }
414  /**
415  * @see PersistenceMapper::getOIDs()
416  * @note The orderby, pagingInfo parameters are not supported by this mapper.
417  */
418  function getOIDs($type, $criteria=null, $orderby=null, &$pagingInfo)
419  {
420  // use criteria condition if criteria is given
421  $attribCondStr = "";
422  if ($criteria != null)
423  {
424  $attribCondStr = "[@";
425  foreach($criteria as $name => $value)
426  $attribCondStr .= $name."='".$value."'][@";
427  $attribCondStr = substr($attribCondStr, 0, strlen($attribCondStr)-strlen("[@"));
428  }
429 
430  // auto start transaction
431  $this->startReadTransaction();
432  //WCMFException::throwEx("No Connection. Start transaction first", __FILE__, __LINE__);
433 
434  // construct query
435  $nodeQuery = "descendant::".$type.$attribCondStr;
436  // execute query
437  $oids = $this->_db->GetOIDs($nodeQuery);
438 
439  return $oids;
440  }
441  /**
442  * @see PersistenceFacade::loadObjects()
443  */
444  function loadObjects($type, $buildDepth, $criteria=null, $orderby=null, &$pagingInfo, $buildAttribs=null, $buildTypes=null)
445  {
446  WCMFException::throwEx("Method NodeXMLDBMapper::loadObjects() is not implemented.", __FILE__, __LINE__);
447  }
448  /**
449  * @see PersistenceMapper::getOrderBy()
450  */
451  function getOrderBy()
452  {
453  WCMFException::throwEx("Order is not supported by mapper class: ".get_class($this), __FILE__, __LINE__);
454  }
455  /**
456  * Start a non blocking read transaction
457  */
459  {
460  Log::debug("start read transaction on ".$this->_filename, __CLASS__);
461  $this->openDatabase(false);
462  }
463  /**
464  * @see PersistenceMapper::startTransaction()
465  * From now on all calls to save() and delete() will be executed to a temporary tree
466  * that will be saved by the call to commitTransaction().
467  */
468  function startTransaction()
469  {
470  if (!$this->_inTransaction)
471  {
472  Log::debug("start transaction on ".$this->_filename, __CLASS__);
473  $this->openDatabase();
474  $this->_inTransaction = true;
475  }
476  }
477  /**
478  * @see PersistenceMapper::commitTransaction()
479  * Save the temporary tree.
480  */
481  function commitTransaction()
482  {
483  if ($this->_inTransaction)
484  {
485  Log::debug("end transaction on ".$this->_filename, __CLASS__);
486  $this->closeDatabase();
487  $this->_inTransaction = false;
488  }
489  }
490  /**
491  * @see PersistenceMapper::rollbackTransaction()
492  * Nothing to do since the changes have to be explicitely committed.
493  */
495  {
496  }
497  /**
498  * Open the XML database
499  * @param lock True/False wether a lock is required or not
500  */
501  function openDatabase($lock=true)
502  {
503  // open database (create if not existing, read-write)
504  if ($this->_db->DbFileName != $this->_filename)
505  {
506  if (!$this->_db->Open($this->_filename, TRUE, $lock))
507  {
508  $this->_db = null;
509  WCMFException::throwEx("Could not open XML input: ".$this->_filename, __FILE__, __LINE__);
510  }
511  chmod($this->_filename, 0775);
512  $this->_db->XmlDb->setVerbose(0);
513  Log::debug("opened ".$this->_filename, __CLASS__);
514  }
515  }
516  /**
517  * Close the XML database
518  */
519  function closeDatabase()
520  {
521  $result = $this->_db->Close();
522  Log::debug("closed ".$this->_filename." with result ".$result, __CLASS__);
523  }
524 
525  /**
526  * TEMPLATE METHODS
527  * Subclasses will override these to define their Node type.
528  */
529 
530  /**
531  * Factory method for the supported object type.
532  * @note Subclasses must implement this method to define their object type.
533  * @param type The type of object to create
534  * @param oid The object id (maybe null)
535  * @return A reference to the created object.
536  */
537  function &createObject($type, $oid=null)
538  {
539  WCMFException::throwEx("createObject() must be implemented by derived class: ".get_class($this), __FILE__, __LINE__);
540  }
541  /**
542  * Get the Node type definition.
543  * @note Subclasses will override this to define their Node type.
544  * @param type The type of the Node
545  * @return An assoziative array with unchangeable keys '_properties', '_datadef', '_children', '_data'. The keys itself hold the following structures:
546  *
547  * - @em _properties: An array of assoziative arrays with the keys 'name', 'value' for every property
548  * (e.g. array('visible' => 1))
549  * - @em _datadef: An array of assoziative arrays with the keys 'name', 'app_data_type', 'db_data_type', 'default' plus application specific keys
550  * for every data item. All keys except 'name' and 'app_data_type' will become keys in the Nodes valueProperties array
551  * hold for each data item.
552  * (e.g. array('name' => 'title', 'db_data_type' => 'data_txt', 'app_data_type' => DATATYPE_ATTRIBUTE, 'default' => 'Hello World!')) @n
553  * Known attributes are:
554  * - @em default: The default value (will be set when creating a blank Node, see PersistenceMapper::create())
555  * - @em restrictions_match: A regular expression that the value must match (e.g. '[0-3][0-9]\.[0-1][0-9]\.[0-9][0-9][0-9][0-9]' for date values)
556  * - @em restrictions_not_match: A regular expression that the value must NOT match
557  * - @em is_editable: true, false whether the value should be editable, see FormUtil::getInputControl()
558  * - @em input_type: The HTML input type for the value, see FormUtil::getInputControl()
559  * - @em _children: An array of assoziative arrays with the keys 'type', 'minOccurs', 'maxOccurs', 'aggregation', 'composition' for every child
560  * (e.g. array('type' => 'textblock', 'minOccurs' => 0, 'maxOccurs' => 'unbounded', 'aggregation' => true, 'composition' => false))
561  * - @em _data: An assoziative array where the keys are the data item names defined in the @em _datadef array
562  * (e.g. array('title' => 'Hello User!'))
563  * @note The @em _data array will be overridden with data provided by the db select. No need for definition at this point!
564  */
565  function getNodeDefinition($type) { return array(); }
566 }
567 ?>
568 
const STATE_DIRTY
& createObject($type, $oid=null)
debug($message, $category)
Definition: class.Log.php:39
const DATATYPE_ELEMENT
loadObjects($type, $buildDepth, $criteria=null, $orderby=null, &$pagingInfo, $buildAttribs=null, $buildTypes=null)
& loadImpl($oid, $buildDepth, $buildAttribs=null, $buildTypes=null)
deleteImpl($oid, $recursive=true)
throwEx($message, $file='', $line='')
XMLUtil helps in using XML files as storage. XMLUtil is a subclass of CXmlDb that is customized for u...
const STATE_CLEAN
const BUILDDEPTH_INFINITE
const STATE_NEW
const DATATYPE_IGNORE
const BUILDDEPTH_MAX
decomposeOID($oid, $validate=true)
const BUILDDEPTH_REQUIRED
getOIDs($type, $criteria=null, $orderby=null, &$pagingInfo)
DataConverter is the base class for all converter classes. It defines the interface for converting da...
& createImpl($type, $buildDepth, $buildAttribs)
PersistenceMapper is the base class for all mapper classes.
const BUILDDEPTH_SINGLE
NodeXMLDBMapper maps Node objects to a xml file using the CXmlDb class. http://sourceforge.net/projects/phpxmldb.