From 5471189fe6b8d2b4ef2608a57b7ea24518a1dcb8 Mon Sep 17 00:00:00 2001 From: Bernhard Reiter Date: Mon, 8 Sep 2014 19:58:43 +0200 Subject: [PATCH] Implement Tag and TagMapper classes. Subclassed from \OCP\AppFramework\Db\Entity and Mapper, respectively. This will allow us to also deal with shared tags. --- lib/private/server.php | 7 +- lib/private/tagging/tag.php | 85 +++++++++++++++ lib/private/tagging/tagmapper.php | 77 +++++++++++++ lib/private/tagmanager.php | 17 ++- lib/private/tags.php | 172 ++++++++++++++---------------- tests/lib/tags.php | 3 +- 6 files changed, 262 insertions(+), 99 deletions(-) create mode 100644 lib/private/tagging/tag.php create mode 100644 lib/private/tagging/tagmapper.php diff --git a/lib/private/server.php b/lib/private/server.php index 7fa06298b2..ff34cfdccb 100644 --- a/lib/private/server.php +++ b/lib/private/server.php @@ -14,6 +14,7 @@ use OC\Security\Crypto; use OC\Security\SecureRandom; use OCP\IServerContainer; use OCP\ISession; +use OC\Tagging\TagMapper; /** * Class Server @@ -68,9 +69,13 @@ class Server extends SimpleContainer implements IServerContainer { $this->registerService('PreviewManager', function ($c) { return new PreviewManager(); }); + $this->registerService('TagMapper', function($c) { + return new TagMapper($c->getDb()); + }); $this->registerService('TagManager', function ($c) { + $tagMapper = $c->query('TagMapper'); $user = \OC_User::getUser(); - return new TagManager($user); + return new TagManager($tagMapper, $user); }); $this->registerService('RootFolder', function ($c) { // TODO: get user and user manager from container as well diff --git a/lib/private/tagging/tag.php b/lib/private/tagging/tag.php new file mode 100644 index 0000000000..d0cd6bbb96 --- /dev/null +++ b/lib/private/tagging/tag.php @@ -0,0 +1,85 @@ + +* +* This library is free software; you can redistribute it and/or +* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE +* License as published by the Free Software Foundation; either +* version 3 of the License, or any later version. +* +* This library is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU AFFERO GENERAL PUBLIC LICENSE for more details. +* +* You should have received a copy of the GNU Affero General Public +* License along with this library. If not, see . +* +*/ + +namespace OC\Tagging; + +use \OCP\AppFramework\Db\Entity; + +/** + * Class to represent a tag. + * + * @method string getOwner() + * @method void setOwner(string $owner) + * @method string getType() + * @method void setType(string $type) + * @method string getName() + * @method void setName(string $name) + */ +class Tag extends Entity { + + protected $owner; + protected $type; + protected $name; + + /** + * Constructor. + * + * @param string $owner The tag's owner + * @param string $type The type of item this tag is used for + * @param string $name The tag's name + */ + public function __construct($owner = null, $type = null, $name = null) { + $this->setOwner($owner); + $this->setType($type); + $this->setName($name); + } + + /** + * Transform a database columnname to a property + * @param string $columnName the name of the column + * @return string the property name + */ + public function columnToProperty($columnName){ + if ($columnName === 'category') { + return 'name'; + } elseif ($columnName === 'uid') { + return 'owner'; + } else { + return parent::columnToProperty($columnName); + } + } + + /** + * Transform a property to a database column name + * @param string $property the name of the property + * @return string the column name + */ + public function propertyToColumn($property){ + if ($property === 'name') { + return 'category'; + } elseif ($property === 'owner') { + return 'uid'; + } else { + return parent::propertyToColumn($property); + } + } +} diff --git a/lib/private/tagging/tagmapper.php b/lib/private/tagging/tagmapper.php new file mode 100644 index 0000000000..b5929e2618 --- /dev/null +++ b/lib/private/tagging/tagmapper.php @@ -0,0 +1,77 @@ + +* +* This library is free software; you can redistribute it and/or +* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE +* License as published by the Free Software Foundation; either +* version 3 of the License, or any later version. +* +* This library is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU AFFERO GENERAL PUBLIC LICENSE for more details. +* +* You should have received a copy of the GNU Affero General Public +* License along with this library. If not, see . +* +*/ + +namespace OC\Tagging; + +use \OCP\AppFramework\Db\Mapper, + \OCP\AppFramework\Db\DoesNotExistException, + \OCP\IDb; + +/** + * Thin wrapper around \OCP\AppFramework\Db\Mapper. + */ +class TagMapper extends Mapper { + + /** + * Constructor. + * + * @param IDb $db Instance of the Db abstraction layer. + */ + public function __construct(IDb $db) { + parent::__construct($db, 'vcategory', 'OC\Tagging\Tag'); + } + + /** + * Load tags from the database. + * + * @param array|string $owners The user(s) whose tags we are going to load. + * @param string $type The type of item for which we are loading tags. + * @return array An array of Tag objects. + */ + public function loadTags($owners, $type) { + if(!is_array($owners)) { + $owners = array($owners); + } + + $sql = 'SELECT `id`, `uid`, `type`, `category` FROM `' . $this->getTableName() . '` ' + . 'WHERE `uid` IN (' . str_repeat('?,', count($owners)-1) . '?) AND `type` = ? ORDER BY `category`'; + return $this->findEntities($sql, array_merge($owners, array($type))); + } + + /** + * Check if a given Tag object already exists in the database. + * + * @param Tag $tag The tag to look for in the database. + * @return bool + */ + public function tagExists($tag) { + $sql = 'SELECT `id`, `uid`, `type`, `category` FROM `' . $this->getTableName() . '` ' + . 'WHERE `uid` = ? AND `type` = ? AND `category` = ?'; + try { + $this->findEntity($sql, array($tag->getOwner(), $tag->getType(), $tag->getName())); + } catch (DoesNotExistException $e) { + return false; + } + return true; + } +} + diff --git a/lib/private/tagmanager.php b/lib/private/tagmanager.php index 72648e9b93..7a3216de03 100644 --- a/lib/private/tagmanager.php +++ b/lib/private/tagmanager.php @@ -33,6 +33,8 @@ namespace OC; +use OC\Tagging\TagMapper; + class TagManager implements \OCP\ITagManager { /** @@ -42,13 +44,22 @@ class TagManager implements \OCP\ITagManager { */ private $user; + /** + * TagMapper + * + * @var TagMapper + */ + private $mapper; + /** * Constructor. * - * @param string $user The user whos data the object will operate on. + * @param TagMapper $mapper Instance of the TagMapper abstraction layer. + * @param string $user The user whose data the object will operate on. */ - public function __construct($user) { + public function __construct(TagMapper $mapper, $user) { + $this->mapper = $mapper; $this->user = $user; } @@ -62,7 +73,7 @@ class TagManager implements \OCP\ITagManager { * @return \OCP\ITags */ public function load($type, $defaultTags=array()) { - return new Tags($this->user, $type, $defaultTags); + return new Tags($this->mapper, $this->user, $type, $defaultTags); } } diff --git a/lib/private/tags.php b/lib/private/tags.php index b1bd3b13d4..5a962c4891 100644 --- a/lib/private/tags.php +++ b/lib/private/tags.php @@ -34,6 +34,9 @@ namespace OC; +use \OC\Tagging\Tag, + \OC\Tagging\TagMapper; + class Tags implements \OCP\ITags { /** @@ -64,6 +67,13 @@ class Tags implements \OCP\ITags { */ private $user; + /** + * The Mapper we're using to communicate our Tag objects to the database. + * + * @var TagMapper + */ + private $mapper; + const TAG_TABLE = '*PREFIX*vcategory'; const RELATION_TABLE = '*PREFIX*vcategory_to_object'; @@ -72,10 +82,13 @@ class Tags implements \OCP\ITags { /** * Constructor. * - * @param string $user The user whos data the object will operate on. - * @param string $type + * @param TagMapper $mapper Instance of the TagMapper abstraction layer. + * @param string $user The user whose data the object will operate on. + * @param string $type The type of items for which tags will be loaded. + * @param array $defaultTags Tags that should be created at construction. */ - public function __construct($user, $type, $defaultTags = array()) { + public function __construct(TagMapper $mapper, $user, $type, $defaultTags = array()) { + $this->mapper = $mapper; $this->user = $user; $this->type = $type; $this->loadTags($defaultTags); @@ -86,27 +99,13 @@ class Tags implements \OCP\ITags { * */ protected function loadTags($defaultTags=array()) { - $this->tags = array(); - $result = null; - $sql = 'SELECT `id`, `category` FROM `' . self::TAG_TABLE . '` ' - . 'WHERE `uid` = ? AND `type` = ? ORDER BY `category`'; try { - $stmt = \OCP\DB::prepare($sql); - $result = $stmt->execute(array($this->user, $this->type)); - if (\OCP\DB::isError($result)) { - \OCP\Util::writeLog('core', __METHOD__. ', DB error: ' . \OCP\DB::getErrorMessage($result), \OCP\Util::ERROR); - } + $this->tags = $this->mapper->loadTags(array($this->user), $this->type); } catch(\Exception $e) { \OCP\Util::writeLog('core', __METHOD__.', exception: '.$e->getMessage(), \OCP\Util::ERROR); } - if(!is_null($result)) { - while( $row = $result->fetchRow()) { - $this->tags[$row['id']] = $row['category']; - } - } - if(count($defaultTags) > 0 && count($this->tags) === 0) { $this->addMultiple($defaultTags, true); } @@ -127,10 +126,10 @@ class Tags implements \OCP\ITags { /** * Get the tags for a specific user. * - * This returns an array with id/name maps: + * This returns an array with maps containing each tag's properties: * [ - * ['id' => 0, 'name' = 'First tag'], - * ['id' => 1, 'name' = 'Second tag'], + * ['id' => 0, 'name' = 'First tag', 'owner' = 'User', 'type' => 'tagtype'], + * ['id' => 1, 'name' = 'Shared tag', 'owner' = 'Other user', 'type' => 'tagtype'], * ] * * @return array @@ -140,16 +139,19 @@ class Tags implements \OCP\ITags { return array(); } - $tags = array_values($this->tags); - uasort($tags, 'strnatcasecmp'); + usort($this->tags, function($a, $b) { + return strnatcasecmp($a->getName(), $b->getName()); + }); $tagMap = array(); - foreach($tags as $tag) { - if($tag !== self::TAG_FAVORITE) { + foreach($this->tags as $tag) { + if($tag->getName() !== self::TAG_FAVORITE) { $tagMap[] = array( - 'id' => $this->array_searchi($tag, $this->tags), - 'name' => $tag - ); + 'id' => $tag->getId(), + 'name' => $tag->getName(), + 'owner' => $tag->getOwner(), + 'type' => $tag->getType() + ); } } return $tagMap; @@ -174,7 +176,7 @@ class Tags implements \OCP\ITags { \OCP\Util::writeLog('core', __METHOD__.', Cannot use empty tag names', \OCP\Util::DEBUG); return false; } - $tagId = $this->array_searchi($tag, $this->tags); + $tagId = $this->getTagId($tag); } if($tagId === false) { @@ -217,7 +219,7 @@ class Tags implements \OCP\ITags { * @return bool */ public function hasTag($name) { - return $this->in_arrayi($name, $this->tags); + return $this->getTagId($name) !== false; } /** @@ -233,35 +235,21 @@ class Tags implements \OCP\ITags { \OCP\Util::writeLog('core', __METHOD__.', Cannot add an empty tag', \OCP\Util::DEBUG); return false; } - if($this->hasTag($name)) { + if($this->hasTag($name)) { // FIXME \OCP\Util::writeLog('core', __METHOD__.', name: ' . $name. ' exists already', \OCP\Util::DEBUG); return false; } try { - $result = \OCP\DB::insertIfNotExist( - self::TAG_TABLE, - array( - 'uid' => $this->user, - 'type' => $this->type, - 'category' => $name, - ) - ); - if (\OCP\DB::isError($result)) { - \OCP\Util::writeLog('core', __METHOD__. 'DB error: ' . \OCP\DB::getErrorMessage($result), \OCP\Util::ERROR); - return false; - } elseif((int)$result === 0) { - \OCP\Util::writeLog('core', __METHOD__.', Tag already exists: ' . $name, \OCP\Util::DEBUG); - return false; - } + $tag = new Tag($this->user, $this->type, $name); + $tag = $this->mapper->insert($tag); + $this->tags[] = $tag; } catch(\Exception $e) { \OCP\Util::writeLog('core', __METHOD__.', exception: '.$e->getMessage(), \OCP\Util::ERROR); return false; } - $id = \OCP\DB::insertid(self::TAG_TABLE); - \OCP\Util::writeLog('core', __METHOD__.', id: ' . $id, \OCP\Util::DEBUG); - $this->tags[$id] = $name; - return $id; + \OCP\Util::writeLog('core', __METHOD__.', id: ' . $tag->getId(), \OCP\Util::DEBUG); + return $tag->getId(); } /** @@ -280,27 +268,21 @@ class Tags implements \OCP\ITags { return false; } - $id = $this->array_searchi($from, $this->tags); - if($id === false) { + $key = $this->array_searchi($from, $this->tags); // FIXME: owner. or renameById() ? + if($key === false) { \OCP\Util::writeLog('core', __METHOD__.', tag: ' . $from. ' does not exist', \OCP\Util::DEBUG); return false; } - $sql = 'UPDATE `' . self::TAG_TABLE . '` SET `category` = ? ' - . 'WHERE `uid` = ? AND `type` = ? AND `id` = ?'; try { - $stmt = \OCP\DB::prepare($sql); - $result = $stmt->execute(array($to, $this->user, $this->type, $id)); - if (\OCP\DB::isError($result)) { - \OCP\Util::writeLog('core', __METHOD__. 'DB error: ' . \OCP\DB::getErrorMessage($result), \OCP\Util::ERROR); - return false; - } + $tag = $this->tags[$key]; + $tag->setName($to); + $this->tags[$key] = $this->mapper->update($tag); } catch(\Exception $e) { \OCP\Util::writeLog('core', __METHOD__.', exception: '.$e->getMessage(), \OCP\Util::ERROR); return false; } - $this->tags[$id] = $to; return true; } @@ -322,9 +304,8 @@ class Tags implements \OCP\ITags { $newones = array(); foreach($names as $name) { - if(($this->in_arrayi( - $name, $this->tags) == false) && $name !== '') { - $newones[] = $name; + if(!$this->hasTag($name) && $name !== '') { + $newones[] = new Tag($this->user, $this->type, $name); } if(!is_null($id) ) { // Insert $objectid, $categoryid pairs if not exist. @@ -346,12 +327,9 @@ class Tags implements \OCP\ITags { if(is_array($this->tags)) { foreach($this->tags as $tag) { try { - \OCP\DB::insertIfNotExist(self::TAG_TABLE, - array( - 'uid' => $this->user, - 'type' => $this->type, - 'category' => $tag, - )); + if (!$this->mapper->tagExists($tag)) { + $this->mapper->insert($tag); + } } catch(\Exception $e) { \OCP\Util::writeLog('core', __METHOD__.', exception: '.$e->getMessage(), \OCP\Util::ERROR); @@ -366,7 +344,7 @@ class Tags implements \OCP\ITags { // For some reason this is needed or array_search(i) will return 0..? ksort($tags); foreach(self::$relations as $relation) { - $tagId = $this->array_searchi($relation['tag'], $tags); + $tagId = $this->getTagId($relation['tag']); \OCP\Util::writeLog('core', __METHOD__ . 'catid, ' . $relation['tag'] . ' ' . $tagId, \OCP\Util::DEBUG); if($tagId) { try { @@ -527,7 +505,7 @@ class Tags implements \OCP\ITags { if(!$this->hasTag($tag)) { $this->add($tag); } - $tagId = $this->array_searchi($tag, $this->tags); + $tagId = $this->getTagId($tag); } else { $tagId = $tag; } @@ -560,7 +538,7 @@ class Tags implements \OCP\ITags { \OCP\Util::writeLog('core', __METHOD__.', Tag name is empty', \OCP\Util::DEBUG); return false; } - $tagId = $this->array_searchi($tag, $this->tags); + $tagId = $this->getTagId($tag); } else { $tagId = $tag; } @@ -579,7 +557,7 @@ class Tags implements \OCP\ITags { } /** - * Delete tags from the + * Delete tags from the database. * * @param string[] $names An array of tags to delete * @return bool Returns false on error @@ -598,20 +576,17 @@ class Tags implements \OCP\ITags { $id = null; if($this->hasTag($name)) { - $id = $this->array_searchi($name, $this->tags); - unset($this->tags[$id]); - } - try { - $stmt = \OCP\DB::prepare('DELETE FROM `' . self::TAG_TABLE . '` WHERE ' - . '`uid` = ? AND `type` = ? AND `category` = ?'); - $result = $stmt->execute(array($this->user, $this->type, $name)); - if (\OCP\DB::isError($result)) { - \OCP\Util::writeLog('core', __METHOD__. 'DB error: ' . \OCP\DB::getErrorMessage($result), \OCP\Util::ERROR); + $key = $this->array_searchi($name, $this->tags); + $tag = $this->tags[$key]; + $id = $tag->getId(); + unset($this->tags[$key]); + try { + $this->mapper->delete($tag); + } catch(\Exception $e) { + \OCP\Util::writeLog('core', __METHOD__ . ', exception: ' + . $e->getMessage(), \OCP\Util::ERROR); + return false; } - } catch(\Exception $e) { - \OCP\Util::writeLog('core', __METHOD__ . ', exception: ' - . $e->getMessage(), \OCP\Util::ERROR); - return false; } if(!is_null($id) && $id !== false) { try { @@ -635,19 +610,28 @@ class Tags implements \OCP\ITags { return true; } - // case-insensitive in_array - private function in_arrayi($needle, $haystack) { + // case-insensitive array_search + protected function array_searchi($needle, $haystack, $mem='getName') { if(!is_array($haystack)) { return false; } - return in_array(strtolower($needle), array_map('strtolower', $haystack)); + return array_search(strtolower($needle), array_map( + function($tag) use($mem) { + return strtolower(call_user_func(array($tag, $mem))); + }, $haystack) + ); } - // case-insensitive array_search - private function array_searchi($needle, $haystack) { - if(!is_array($haystack)) { + /** + * Get a tag's ID. + * + * @param string $name The tag name to look for. + * @return string The tag's id or false if it hasn't been saved yet. + */ + private function getTagId($name) { + if (($key = $this->array_searchi($name, $this->tags)) === false) { return false; } - return array_search(strtolower($needle), array_map('strtolower', $haystack)); + return $this->tags[$key]->getId(); } } diff --git a/tests/lib/tags.php b/tests/lib/tags.php index 9195587f1d..4d9b8558fd 100644 --- a/tests/lib/tags.php +++ b/tests/lib/tags.php @@ -34,7 +34,8 @@ class Test_Tags extends PHPUnit_Framework_TestCase { $this->objectType = uniqid('type_'); OC_User::createUser($this->user, 'pass'); OC_User::setUserId($this->user); - $this->tagMgr = new OC\TagManager($this->user); + $this->tagMapper = new OC\Tagging\TagMapper(new OC\AppFramework\Db\Db()); + $this->tagMgr = new OC\TagManager($this->tagMapper, $this->user); }