add versioning support
authorKiv4h <jeffrey.reichardt@googlemail.com>
Thu, 13 Dec 2012 14:21:11 +0000 (15:21 +0100)
committerKiv4h <jeffrey.reichardt@googlemail.com>
Thu, 13 Dec 2012 14:21:11 +0000 (15:21 +0100)
com.woltlab.wcf/objectTypeDefinition.xml
wcfsetup/install/files/lib/data/VersionableDatabaseObject.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/VersionableDatabaseObjectAction.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/VersionableDatabaseObjectEditor.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/cache/builder/VersionCacheBuilder.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/database/editor/MySQLDatabaseEditor.class.php
wcfsetup/install/files/lib/system/package/PackageInstallationDispatcher.class.php
wcfsetup/install/files/lib/system/package/PackageUninstallationDispatcher.class.php
wcfsetup/install/files/lib/system/version/VersionHandler.class.php [new file with mode: 0644]

index 18a21176e1786b6a8f8b14690d254b511707682e..dac95324854d507176e35565d45c69d40d360c6d 100644 (file)
@@ -17,5 +17,9 @@
                <definition>
                        <name>com.woltlab.wcf.modifiableContent</name>
                </definition>
+
+               <definition>
+                       <name>com.woltlab.wcf.versionableObject</name>
+               </definition>           
        </import>
 </data>
\ No newline at end of file
diff --git a/wcfsetup/install/files/lib/data/VersionableDatabaseObject.class.php b/wcfsetup/install/files/lib/data/VersionableDatabaseObject.class.php
new file mode 100644 (file)
index 0000000..8094c98
--- /dev/null
@@ -0,0 +1,37 @@
+<?php
+namespace wcf\data;
+use wcf\util\StringUtil;
+
+/**
+ * Abstract class for all versionable data classes.
+ *
+ * @author             Jeffrey Reichardt
+ * @copyright  2001-2012 WoltLab GmbH
+ * @license            GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf
+ * @subpackage data
+ * @category   Community Framework
+ */
+abstract class VersionableDatabaseObject extends DatabaseObject {      
+       /**
+        * name of the object type
+        * @var string
+        */
+       public $objectTypeName = '';
+       
+       /**
+        * Returns suffix of database tables.
+        *
+        * @return string
+        */
+       protected static function getDatabaseVersionTableName() {
+               return static::getDatabaseTableName().'_version';
+       }
+       
+       /**
+        * Returns name of index in version table.
+        */
+       protected static function getDatabaseVersionTableIndexName() {
+               return 'version'.ucfirst(static::getDatabaseIndexTableName());
+       }
+}
diff --git a/wcfsetup/install/files/lib/data/VersionableDatabaseObjectAction.class.php b/wcfsetup/install/files/lib/data/VersionableDatabaseObjectAction.class.php
new file mode 100644 (file)
index 0000000..83d5243
--- /dev/null
@@ -0,0 +1,116 @@
+<?php
+namespace wcf\data;
+
+/**
+ * Abstract class for all versionable data actions.
+ *
+ * @author             Jeffrey Reichardt
+ * @copyright  2001-2012 WoltLab GmbH
+ * @license            GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf
+ * @subpackage data
+ * @category   Community Framework
+ */
+abstract class VersionableDatabaseObjectAction extends AbstractDatabaseObjectAction {
+       
+       /**
+        * Validates restoring an version
+        */
+       public function validateRestore() {
+               parent::validateUpdate();
+       }
+       
+       /**
+        * Deletes database object and returns the number of deleted objects.
+        *
+        * @return      integer
+        */
+       public function delete() {
+               if (!count($this->objects)) {
+                       $this->readObjects();
+               }
+               
+               // get index name
+               $indexName = call_user_func(array($this->className, 'getDatabaseTableIndexName'));
+               
+               // get ids
+               $objectIDs = array();
+               foreach ($this->objects as $object) {
+                       $objectIDs[] = $object->__get($indexName);
+               }
+               
+               // execute action
+               return call_user_func(array($this->className, 'deleteAll'), $objectIDs);
+       }
+       
+       /**
+        * Updates data.
+        */
+       public function update() {
+               if (!count($this->objects)) {
+                       $this->readObjects();
+               }
+               
+               //get index name
+               $indexName = call_user_func(array($this->className, 'getDatabaseTableIndexName'));
+               
+               if (isset($this->parameters['data'])) {
+                       foreach ($this->objects as $object) {
+                               $this->update($this->parameters['data']);
+                               //create revision retroactively
+                               $this->createRevision();
+                       }
+               }
+       }
+       
+       /**
+        * Creates a new revision.
+        */
+       protected function createRevision() {
+               $indexName = call_user_func(array($this->className, 'getDatabaseTableIndexName'));
+       
+               foreach($this->objects as $object) {
+                       call_user_func(array($this->className, 'createRevision'), array_merge($object->getData(), array($indexName =>$object->__get($indexName))));
+               }
+       }
+       
+       /**
+        * Deletes revision.
+        */
+       protected function deleteRevision() {
+               if (!count($this->objects)) {
+                       $this->readObjects();
+               }
+               
+               // get index name
+               $indexName = call_user_func(array($this->className, 'getDatabaseTableIndexName'));
+               
+               // get ids
+               $objectIDs = array();
+               foreach ($this->objects as $object) {
+                       $objectIDs[] = $object->__get($indexName);
+               }
+               
+               // execute action
+               return call_user_func(array($this->className, 'deleteRevision'), $objectIDs);
+       }
+       
+       /**
+        * Restores an revision.
+        */
+       public function restore() {
+               if (!count($this->objects)) {
+                       $this->readObjects();
+               }
+
+               //currently we only support restoring one version
+               foreach($this->objects as $object) {
+                       $objectType = VersionHandler::getInstance()->getObjectTypeByName($object->objectTypeName);
+                       $restoreObject = VersionHandler::getInstance()->getVersionByID($objectType->objectTypeID, $this->parameters['restoreObjectID']);
+
+                       $this->parameters['data'] = $restoreObject->getData();
+               }
+               
+               $this->update();
+       }
+}
diff --git a/wcfsetup/install/files/lib/data/VersionableDatabaseObjectEditor.class.php b/wcfsetup/install/files/lib/data/VersionableDatabaseObjectEditor.class.php
new file mode 100644 (file)
index 0000000..2cbd931
--- /dev/null
@@ -0,0 +1,72 @@
+<?php
+namespace wcf\data;
+use wcf\system\WCF;
+
+/**
+ * Abstract class for all versionable editor classes.
+ *
+ * @author             Jeffrey Reichardt
+ * @copyright  2001-2012 WoltLab GmbH
+ * @license            GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf
+ * @subpackage data
+ * @category   Community Framework
+ */
+abstract class VersionableDatabaseObjectEditor extends DatabaseObjectEditor {
+       /**
+        * @see wcf\data\IEditableObject::create()
+        */
+       public static function createRevision(array $parameters = array()) {
+               $keys = $values = '';
+               $statementParameters = array();
+               foreach ($parameters as $key => $value) {
+                       if (!empty($keys)) {
+                               $keys .= ',';
+                               $values .= ',';
+                       }
+                       
+                       $keys .= $key;
+                       $values .= '?';
+                       $statementParameters[] = $value;
+               }
+               
+               // save object
+               $sql = "INSERT INTO     ".static::getDatabaseVersionTableName()."
+                                       (".$keys.")
+                       VALUES          (".$values.")";
+               $statement = WCF::getDB()->prepareStatement($sql);
+               $statement->execute($statementParameters);
+               
+               // return new object
+               $id = WCF::getDB()->getInsertID(static::getDatabaseVersionTableName(), static::getDatabaseVersionTableIndexName());
+
+               return new static::$baseClass($id);
+       }
+               
+       /**
+        * @see wcf\data\IEditableObject::delete()
+        */
+       public function deleteRevision(array $objectIDs = array()) {
+               static::deleteAll(array($this->__get(static::getDatabaseVersionTableIndexName())));
+       }
+       
+       /**
+        * @see wcf\data\IEditableObject::deleteAll()
+        */
+       public static function deleteAll(array $objectIDs = array()) {
+               $affectedCount = static::deleteAll($objectIDs);
+               
+               //delete versions
+               $sql = "DELETE FROM     ".static::getDatabaseVersionTableName()."
+                               WHERE ".static::getDatabaseTableIndexName()." = ?";
+               $statement = WCF::getDB()->prepareStatement($sql);
+
+               WCF::getDB()->beginTransaction();
+               foreach ($objectIDs as $objectID) {
+                       $statement->executeUnbuffered(array($objectID));
+               }
+               WCF::getDB()->commitTransaction();
+               
+               return $affectedCount;
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/cache/builder/VersionCacheBuilder.class.php b/wcfsetup/install/files/lib/system/cache/builder/VersionCacheBuilder.class.php
new file mode 100644 (file)
index 0000000..e51b8dc
--- /dev/null
@@ -0,0 +1,48 @@
+<?php
+namespace wcf\system\cache\builder;
+use wcf\data\object\type\ObjectTypeCache;
+use wcf\system\package\PackageDependencyHandler;
+
+/**
+ * Caches the versions for a certain package and object type.
+ *
+ * @author             Jeffrey Reichardt
+ * @copyright  2001-2012 WoltLab GmbH
+ * @license            GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf
+ * @subpackage system.cache.builder
+ * @category   Community Framework
+ */
+class VersionCacheBuilder implements ICacheBuilder {
+       /**
+        * @see wcf\system\cache\ICacheBuilder::getData()
+        */
+       public function getData(array $cacheResource) { 
+               //get object types
+               $objectTypes = ObjectTypeCache::getInstance()->getObjectTypes('com.woltlab.wcf.versionableObject');
+               
+               $data = array(
+                       'versions' => array(),
+                       'versionIDs' => array()
+               );      
+               
+               foreach ($objectTypes as $objectTypeID => $objectType) {
+                       $processorObject = $objectType->getProcessor();
+                       
+                       $sql = "SELECT 
+                                               * 
+                                       FROM ".$processorObject::getDatabaseVersionTableName();
+                       $statement = WCF::getDB()->prepareStatement($sql);
+                       $statement->execute(array());
+                       
+                       while ($row = $statement->fetchArray()) {
+                               $object = new $objectType->className(null, $row);
+                               $data['versions'][$objectTypeID][$object->{$processorObject::getDatabaseIndexName()}] = $object;
+                               $data['versionIDs'][$objectTypeID][$object->{$processorObject::getDatabaseIndexName()}][] = $object->{$processorObject::getDatabaseVersionTableIndexName()};
+                       }
+                       
+               }
+               
+               return $data;
+       }
+}
index 6b34b1ffc04c26363fee27c3c4ca12701e936293..e5fe929582d4a259d2cd7aeb073880986931f083 100644 (file)
@@ -1,6 +1,7 @@
 <?php
 namespace wcf\system\database\editor;
 use wcf\system\database\util\PreparedStatementConditionBuilder;
+use wcf\system\Regex;
 
 /**
  * Database editor implementation for MySQL4.1 or higher.
@@ -36,8 +37,19 @@ class MySQLDatabaseEditor extends DatabaseEditor {
                $statement = $this->dbObj->prepareStatement($sql);
                $statement->execute();
                while ($row = $statement->fetchArray()) {
-                       $columns[] = $row['Field'];
-               }
+                               $typeMatches = Regex::compile('([a-z]+)\(([0-9]+)\)', Regex::CASE_INSENSITIVE)->match($row['Type']);
+                               
+                       $columns[] = array('name' => $row['Field'], 
+                                                                       'data' => array(
+                                                                                               'type' => $typeMatches[1], 
+                                                                                               'length' => $typeMatches[2],
+                                                                                               'notNull' => (($row['Null'] == 'YES') ? true : false), 
+                                                                                               'key' => (($row['Key'] == 'PRI') ? 'PRIMARY' : (($row['Key'] == 'UNI') ? 'UNIQUE' : '')),
+                                                                                               'default' => $row['Default'], 
+                                                                                               'autoIncrement' => ($row['Extra'] == 'auto_increment' ? true : false)
+                                                                                       )
+                                                               );
+               }
                return $columns;
        }
        
index b221662b13dfd7d34c6e83156e4b7f1cbe52aaa8..7f6485dab9e5948b484b8b522d0c4ea93a29760d 100644 (file)
@@ -76,6 +76,12 @@ class PackageInstallationDispatcher {
         */
        const CONFIG_FILE = 'config.inc.php';
        
+       /**
+        * holds state of structuring version tables
+        * @var boolean
+        */
+       protected $requireRestructureVersionTables = false;     
+       
        /**
         * Creates a new instance of PackageInstallationDispatcher.
         *
@@ -160,7 +166,11 @@ class PackageInstallationDispatcher {
                        
                        // reset stylesheets
                        StyleHandler::resetStylesheets();
-               }
+               }       
+               
+               if ($this->requireRestructureVersionTables) {
+                       $this->restructureVersionTables();
+               }                       
                
                return $step;
        }
@@ -444,6 +454,10 @@ class PackageInstallationDispatcher {
                        throw new SystemException("'".$className."' does not implement 'wcf\system\package\plugin\IPackageInstallationPlugin'");
                }
                
+               if ($plugin instanceof \wcf\system\package\plugin\SQLPackageInstallationPlugin || $plugin instanceof \wcf\system\package\plugin\ObjectTypePackageInstallationPlugin) {
+                       $this->requireRestructureVersionTables = true;
+               }
+               
                // execute PIP
                try {
                        $document = $plugin->{$this->action}();
@@ -931,4 +945,52 @@ class PackageInstallationDispatcher {
                        break;
                }
        }
+
+       /*
+        * Restructure version tables.
+        */
+       protected function restructureVersionTables() {
+               $objectTypes = \wcf\system\version\VersionHandler::getInstance()->getObjectTypes();
+               
+               if (empty($objectTypes)) {
+                       return;
+               }
+               
+               //base structure of version tables
+               $versionTableBaseColumns = array();
+               $versionTableBaseColumns[] = array('name' => 'versionID', 'data' => array('type' => 'INT', 'key' => 'PRIMARY', 'autoIncrement' => 'AUTO_INCREMENT'));
+               $versionTableBaseColumns[] = array('name' => 'versionUserID', 'data' => array('type' => 'INT'));
+               $versionTableBaseColumns[] = array('name' => 'versionUsername', 'data' => array('type' => 'VARCHAR', 'length' => 255));
+               $versionTableBaseColumns[] = array('name' => 'versionTime', 'data' => array('type' => 'INT'));
+       
+               foreach ($objectTypes as $objectTypeID => $objectType) {                                                
+                       //get structure of base table
+                       $baseTableColumns = WCF::getDB()->getEditor()->getColumns($objectType::getDatabaseTableName());
+                       //get structure of version table
+                       $versionTableColumns = WCF::getDB()->getEditor()->getColumns($objectType::getDatabaseVersionTableName());
+                       
+                       if (empty($versionTableColumns)){
+                               $columns = array_merge($versionTableBaseColumns, $baseTableColumns);
+
+                               WCF::getDB()->getEditor()->createTable($objectType::getDatabaseVersionTableName(), $columns);
+                       }
+                       else {
+                               //check garbage columns in versioned table
+                               foreach ($versionTableColumns as $columnData) {
+                                       if (!array_search($columnData['name'], $baseTableColumns, true)) {
+                                               //delete column
+                                               WCF::getDB()->getEditor()->dropColumn($objectType::getDatabaseVersionTableName(), $columnData['name']);
+                                       }
+                               }
+                               
+                               //check new columns for versioned table
+                               foreach ($baseTableColumns as $columnData) {
+                                       if (!array_search($columnData['name'], $versionTableColumns, true)) {
+                                               //add colum
+                                               WCF::getDB()->getEditor()->addColumn($objectType::getDatabaseVersionTableName(), $columnData['name'], $columnData['data']);
+                                       }
+                               }
+                       }
+               }
+       }       
 }
index ae6f96ed1e19abd1acd5737377ea2b7d8f1d4959..e26427ea87bb62724985eae6cb49d6d83be82337 100644 (file)
@@ -96,6 +96,10 @@ class PackageUninstallationDispatcher extends PackageInstallationDispatcher {
                        ApplicationHandler::rebuild();
                }
                
+               if ($this->requireRestructureVersionTables) {
+                       $this->restructureVersionTables();
+               }               
+               
                // return next node
                return $node;
        }
@@ -103,8 +107,13 @@ class PackageUninstallationDispatcher extends PackageInstallationDispatcher {
        /**
         * @see wcf\system\package\PackageInstallationDispatcher::executePIP()
         */
-       protected function executePIP(array $nodeData) {
+       protected function executePIP(array $nodeData) {        
                $pip = new $nodeData['className']($this);
+               
+               if ($pip instanceof \wcf\system\package\plugin\SQLPackageInstallationPlugin || $pip instanceof \wcf\system\package\plugin\ObjectTypePackageInstallationPlugin) {
+                       $this->requireRestructureVersionTables = true;
+               }               
+               
                $pip->uninstall();
        }
        
diff --git a/wcfsetup/install/files/lib/system/version/VersionHandler.class.php b/wcfsetup/install/files/lib/system/version/VersionHandler.class.php
new file mode 100644 (file)
index 0000000..f1cf531
--- /dev/null
@@ -0,0 +1,138 @@
+<?php
+namespace wcf\system\version;
+use wcf\data\VersionableDatabaseObject;
+use wcf\system\SingletonFactory;
+use wcf\data\object\type\ObjectTypeCache;
+use wcf\system\cache\CacheHandler;
+
+/**
+* Handles versions of DatabaseObjects.
+*
+* @author              Jeffrey Reichardt
+* @copyright   2001-2012 WoltLab GmbH
+* @license             GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+* @package     com.woltlab.wcf
+* @subpackage  system.version
+* @category    Community Framework
+*/
+class VersionHandler extends SingletonFactory {
+       /**
+       * cached categories
+       * @var array<wcf\data\VersionableDatabaseObject>
+       */
+       protected $versions = array();
+
+       /**
+       * maps each version id to its object type id and object type version id
+       * @var array<array>
+       */
+       protected $versionIDs = array();
+
+       /**
+       * mapes the names of the version object types to the object type ids
+       * @var array<integer>
+       */
+       protected $objectTypeIDs = array();
+
+       /**
+       * list of version object types
+       * @var array<wcf\data\object\type>
+       */
+       protected $objectTypes = array();
+
+       /**
+       * Returns all version of object with the given object type id and object id.
+       *
+       * @param integer $objectTypeID
+       * @param integer $objectID
+       *
+       * @return array<wcf\data\VersionableDatabaseObject>
+       */
+       public function getVersions($objectTypeID, $objectID) {
+               if (isset($this->versions[$objectTypeID][$objectID])) {
+                       return $this->versions[$objectTypeID][$objectID];
+               }
+
+               return array();
+       }
+
+       /**
+       * Returns the category object with the given category id.
+       *
+       * @param integer $objectTypeID
+       * @param integer $versionID
+       *
+       * @return wcf\data\VersionableDatabaseObject
+       */
+       public function getVersionByID($objectTypeID, $versionID) {
+               if (isset($this->versionIDs[$objectTypeID][$versionID])) {
+                       return $this->versionIDs[$objectTypeID][$versionID];
+               }
+
+               return null;
+       }
+
+       /**
+       * Gets the object type with the given id.
+       *
+       * @param integer $objectTypeID
+       *
+       * @return wcf\data\object\type\ObjectType
+       */
+       public function getObjectType($objectTypeID) {
+               if (isset($this->objectTypeIDs[$objectTypeID])) {
+                       return $this->getObjectTypeByName($this->objectTypeIDs[$objectTypeID]);
+               }
+
+               return null;
+       }
+
+       /**
+       * Gets the object type with the given name.
+       *
+       * @param string $objectTypeName
+       *
+       * @return wcf\data\object\type\ObjectType
+       */
+       public function getObjectTypeByName($objectTypeName) {
+               if (isset($this->objectTypes[$objectTypeName])) {
+                       return $this->objectTypes[$objectTypeName];
+               }
+
+               return null;
+       }
+
+       /**
+       * @see wcf\system\SingletonFactory::init()
+       */
+       protected function init() {
+               $this->objectTypes = ObjectTypeCache::getInstance()->getObjectTypes('com.woltlab.wcf.versionableObject');
+               
+               foreach ($this->objectTypes as $objectType) {
+                       $this->objectTypeIDs[$objectType->objectTypeID] = $objectType->objectType;
+               }
+
+               $cacheName = 'version';
+               CacheHandler::getInstance()->addResource($cacheName, WCF_DIR.'cache/cache.'.$cacheName.'.php', 'wcf\system\cache\builder\VersionCacheBuilder');
+               $this->versions = CacheHandler::getInstance()->get($cacheName, 'versions');
+               $this->versionIDs = CacheHandler::getInstance()->get($cacheName, 'versionIDs');
+       }
+
+       /**
+       * Reloads the version cache.
+       */
+       public function reloadCache() {
+               CacheHandler::getInstance()->clearResource('version');
+
+               $this->init();
+       }
+       
+       /**
+        * Returns a list of object types
+        *
+        * @return array<wcf\data\object\type\ObjectType>
+        */
+       public function getObjectTypes() {
+               return $this->objectTypes;
+       }
+}
\ No newline at end of file