<?php
/*
 * Copyright (c) 2013, webvariants GbR, http://www.webvariants.de
 *
 * This file is released under the terms of the MIT license. You can find the
 * complete text in the attached LICENSE file or online at:
 *
 * http://www.opensource.org/licenses/mit-license.php
 */

/**
 * Meta-Provider
 *
 * Diese Klasse stellt die API zum Zugriff auf die erweiterten Metainfos dar.
 * Sämtlicher Zugriff auf die Metadaten/-infos sollte von außerhalb dieses
 * AddOns *NUR* über diese Methoden erfolgen. Hier kann in keinem Fall Schaden
 * angerichtet werden.
 *
 * Von einigen Methoden gibt es 4fache Ausführungen: Eine für Artikel-Metadaten,
 * eine für Kategorien, eine für Medien und eine zusammengefasste für Objekte
 * aller Art. Der Methode für Objekte muss zusätzlich der Typ der Metadaten/-infos
 * mit übergeben werden. Es wird empfohlen, die spezifischen Methoden direkt
 * zu nutzen, da diese einfacher anzuwenden sind (bieten nur die wirklich
 * benötigten Parameter).Einzige Ausnahme ist getObjectMetaData, die Mutter aller
 * Hilfsmethoden: Da diese eine SQL-Abfrage erwartet ist sie nicht public. Dort
 * ist der Anwender demzufolge zum Nutzen der spezifischen Methoden gezwungen.
 *
 * Alle Methoden, die eine Angabe zum Artikeltyp erlauben, akzeptieren diese
 * als String (interner Name) oder als Zahl (ID). Die Umwandlung in das
 * benötigte Format erfolgt automatisch, um den Nutzer von der lästigen Arbeit,
 * selber getIdForName() aufrufen zu müssen, zu befreien.
 *
 * @ingroup external
 */
abstract class WV2_MetaProvider {
	/**
	 * Artikeltyp ermitteln
	 *
	 * Diese Methode gibt die ID des Typs eines Artikels zurück.
	 *
	 * @param  mixed $article  der Artikel
	 * @return mixed           der Name des Artikeltyps oder false, falls noch keiner zugewiesen wurde
	 */
	public static function getArticleTypeName($article = null) {
		if (!$article) $article = WV_Sally::getArticle($article);
		return $article->hasType() ? $article->getType() : false;
	}

	/**
	 * Prüft, ob ein Artikel von einem bestimmten Typ ist.
	 *
	 * @param  string $articleType  der Name des Artikeltyps oder null, falls alle
	 * @param  mixed  $article      der Artikel
	 * @return bool                 true oder false
	 */
	public static function isArticeOfType($articleType, $article = null) {
		$type = self::getArticleTypeName($article);
		return $type !== false && $articleType === $type;
	}

	/**
	 * (Benötigte) Metainfos ermitteln
	 *
	 * Diese Methode ermittelt für einen Artikeltyp die dazugehörigen Metainfos.
	 * Wird für den Artikeltyp -1 übergeben, so wird ein leeres Array
	 * zurückgegeben, da der Typ "kein Typ" keine Metainfos besitzt.
	 *
	 * @param  string $articleType  Der Name des Artikeltyps
	 * @return array                eine Liste von _WV2_Info-Objekten
	 */
	public static function getMetaInfosForArticleType($articleType) {
		$namespace = 'metainfo.infos';
		$cache     = sly_Core::cache();
		$key       = sly_Cache::generateKey('for', $articleType);
		$infos     = $cache->get($namespace, $key, null);

		if (!is_array($infos)) {
			$all   = self::getAllArticleMetaInfos(null);
			$infos = array();

			foreach ($all as $name => $metainfo) {
				if (in_array($articleType, $metainfo->getArticleTypes())) {
					$infos[$name] = $metainfo;
				}
			}

			$cache->set($namespace, $key, $infos);
		}

		return $infos;
	}

	/**
	 * Artikelanzahl ermitteln
	 *
	 * Diese Methode ermittelt, wie viele Artikel einem Artikeltyp angehören.
	 *
	 * @param  mixed   $articleType  der Artikeltyp als ID oder Name
	 * @param  boolean $onlineOnly
	 * @param  int     $clang
	 * @return int                   die Anzahl der zugehörigen Artikel
	 */
	public static function getArticleCountByType($articleType, $onlineOnly = true, $clang = null) {
		$namespace = 'metainfo.acount';
		$clang     = WV_Sally::clang($clang);
		$cache     = sly_Core::cache();
		$key       = sly_Cache::generateKey('by_type', $articleType, $onlineOnly, $clang);

		if ($cache->exists($namespace, $key)) {
			$count = $cache->get($namespace, $key);
		}
		else {
			$sql   = sly_DB_Persistence::getInstance();
			$where = array('clang' => $clang, 'type' => $articleType);

			if ($onlineOnly) {
				$where['status'] = 1;
			}

			$count = $sql->magicFetch('article', 'COUNT(id)', $where);
			$cache->set($namespace, $key, $count);
		}

		return $count;
	}

	/**
	 * Artikelliste ermitteln
	 *
	 * Diese Methode erzeugt eine Liste von sly_Model_Article-Objekten basierend
	 * auf den gegebenen Filterkriterien zurück.
	 *
	 * Für das Sortierkriterium ($sortby) stehen die Spalten als sly_article zur
	 * Verfügung.
	 *
	 * @param  string $articleType  der Artikeltyp
	 * @param  bool   $onlineOnly   wenn true, werden nur online Artikel berücksichtigt
	 * @param  string $sortby       das Sortierkriterium (aus der Relation article)
	 * @param  string $direction    die Sortierrichtung
	 * @param  int    $limit        maximale Anzahl von Zeilen
	 * @param  int    $offset       Offset
	 * @param  int    $clang        die gewünschte Sprache
	 * @return array                Liste von passenden sly_Model_Article-Objekten
	 */
	public static function getArticlesByType($articleType, $onlineOnly = true, $sortby = 'updatedate', $direction = 'ASC', $limit = null, $offset = null, $clang = null) {
		$clang   = WV_Sally::clang($clang);
		$service = sly_Service_Factory::getArticleService();
		$where   = array('type' => $articleType, 'clang' => $clang);

		if ($onlineOnly) {
			$where['status'] = 1;
		}

		// cut off table name (might be given when the metalist helper tries
		// to create a list and is given 'article.pos' as a sorting order.
		// In getObjectsWithMetaData() we allow this, so we should allow this here, too.
		if (strpos($sortby, '.') !== false) {
			$sortby = substr($sortby, strpos($sortby, '.') + 1);
		}

		return $service->find($where, null, $sortby.' '.$direction, $offset, $limit);
	}

	/**
	 * Alle verfügbaren Metainfos holen
	 *
	 * Diese Methode gibt alle vorhandenen Metainfos (nicht die Metadaten!)
	 * zurück.
	 *
	 * @param  int $type  der Typ (WV2_Metainfo::TYPE-Konstanten)
	 * @return array      eine Liste von _WV2_Info-Objekten, die passen
	 */
	public static function getAllObjectMetaInfos($type, $metapage = null, $articleType = null) {
		$infos       = array();
		$type        = (int) $type;
		$metapage    = $type === WV2_Metainfo::TYPE_ARTICLE ? $metapage : null;
		$articleType = $type === WV2_Metainfo::TYPE_ARTICLE ? $articleType : null;
		$cache       = sly_Core::cache();
		$namespace   = 'metainfo.infos';
		$key         = sly_Cache::generateKey($type, $metapage, $articleType);
		$infos       = $cache->get($namespace, $key, null);

		if (!is_array($infos)) {
			$all   = WV2_Metainfo::loadAll();
			$infos = array();

			if (isset($all[$type])) {
				$infos = $all[$type];

				// filter metainfos if required

				if ($metapage !== null || $articleType !== null) {
					foreach ($infos as $name => $metainfo) {
						if ($metapage !== null && $metainfo->getMetapage() !== $metapage) {
							unset($infos[$name]);
							continue;
						}

						if ($articleType !== null && !in_array($articleType, $metainfo->getArticleTypes())) {
							unset($infos[$name]);
						}
					}
				}
			}

			$cache->set($namespace, $key, $infos);
		}

		return $infos;
	}

	public static function getAllArticleMetaInfos($metapage = null, $articleType = null) {
		return self::getAllObjectMetaInfos(WV2_Metainfo::TYPE_ARTICLE, $metapage, $articleType);
	}

	public static function getAllCategoryMetaInfos() {
		return self::getAllObjectMetaInfos(WV2_Metainfo::TYPE_CATEGORY);
	}

	public static function getAllMediumMetaInfos() {
		return self::getAllObjectMetaInfos(WV2_Metainfo::TYPE_MEDIUM);
	}

	public static function getAllUserMetaInfos() {
		return self::getAllObjectMetaInfos(WV2_Metainfo::TYPE_USER);
	}

	/**
	 * Metadaten für ein einzelnes Objekt ermitteln
	 *
	 * Diese Methode ermittelt für ein bestimmtes Objekt die angelegten
	 * Metadaten. Zurückgegeben wird ein Array aus _WV2_MetaData-Objekten. Gibt
	 * es nur ein Metadatum, wird dieses direkt (ohne Array) zurückgegeben. Gibt
	 * es kein Metadatum, so wird ein MetaData-Objekt mit dem Standardwert
	 * zurückgegeben. Ist der Standardwert null, wird in diesem Fall direkt null
	 * zurückgegeben.
	 *
	 * Wenn ein Array zurückgegeben wird, so ist es assoziativ mit den
	 * Metainfo-Namen als Schlüsseln.
	 *
	 * Wenn als als Standardwert etwas anderes als null angegeben wird und der
	 * Anwender sicher ist, dass es nur ein Metadatum geben kann (weil er z.B.
	 * $metainfoToken angegeben hat), kann man immer direkt getValue() bzw.
	 * getKey() auf den Rückgabewert dieser Methode anwenden.
	 *
	 * @param  int     $object     die ID des Objekts oder ein Objekt eines bestimmten Typs (sly_Model_Article, sly_Model_Category, ...)
	 * @param  mixed   $article    der Artikel
	 * @param  mixed   $category   die Kategorie
	 * @param  mixed   $medium     das Medium
	 * @param  mixed   $metainfo   der Name der Metainformation oder null für alle
	 * @param  mixed   $default    der Standardwert, falls kein Metadatum gefunden wurde
	 * @param  int     $clang      die gewünschte Sprache
	 * @param  int     $type       der Typ (WV2_Metainfo::TYPE-Konstanten)
	 * @param  boolean $raw        wenn true, werden die noch serialisierten Daten zurückgegeben
	 * @return array               Liste der Metdaten wie oben beschrieben
	 */
	protected static function getMetaDataForObject($object, $metainfo = null, $default = null, $clang = null, $type = WV2_Metainfo::TYPE_ARTICLE, $raw = false) {
		// prepare data
		$type         = (int) $type;
		$cache        = sly_Core::cache();
		$clang        = WV_Sally::clang($clang);
		$objectID     = WV2_Metainfo::getIDForObject($object, false);
		$namespace    = 'metainfo.data';
		$rawKey       = sly_Cache::generateKey('raw', $objectID, $clang, $type);
		$deserialKey  = sly_Cache::generateKey('deserialized', $objectID, $clang, $type);
		$rawData      = null;
		$deserialized = null;

		// check cache
		if (!$raw) {
			$deserialized = $cache->get($namespace, $deserialKey, null);
		}

		if ($deserialized === null) {
			$rawData = $cache->get($namespace, $rawKey, null);
		}

		// query database
		if ($deserialized === null && $rawData === null) {
			$service = new _WV2_Service_Metadata();
			$rawData = $service->findObjectData($objectID, $type, $clang);

			// store raw data
			$cache->set($namespace, $rawKey, $rawData);
		}

		// deserialize raw values
		if (!$raw && $deserialized === null) {
			$deserialized = array();

			foreach ($rawData as $name => $date) {
				$info   = WV2_Metainfo::getMetainfo($name, $type);
				$value  = $info->deserialize($date);
				$params = $info->getParams();

				if ($info->datatypeCall('isValueEmpty', array($value, $params))) {
					$value = null;
				}

				$deserialized[$name] = $value;
			}

			// store deserialized data
			$cache->set($namespace, $deserialKey, $deserialized);
		}

		$return = $raw ? $rawData : $deserialized;

		if ($metainfo !== null) {
			if (!array_key_exists($metainfo, $return)) {
				throw new WV2_Exception('Could not find meta data for "'.$metainfo.'" on object '.$objectID.'/'.$clang.'!');
			}

			return $return[$metainfo] === null ? $default : $return[$metainfo];
		}

		// de-arrayfy (only if not in raw mode)
		if (!$raw && count($return) === 1) {
			$first = reset($return);
			return $first === null ? $default : $first;
		}

		return $return;
	}

	public static function articleData($metainfo = null, $default = null, $clang = null, $article = null, $raw = false) {
		$article = $article ? WV_Sally::getIDForArticle($article) : sly_Core::getCurrentArticleId();
		return self::getMetaDataForObject($article, $metainfo, $default, $clang, WV2_Metainfo::TYPE_ARTICLE, $raw);
	}

	public static function categoryData($metainfo = null, $default = null, $clang = null, $category = null, $raw = false) {
		$category = $category ? WV_Sally::getIDForCategory($category) : WV_Sally::getCurrentCategoryID();
		return self::getMetaDataForObject($category, $metainfo, $default, $clang, WV2_Metainfo::TYPE_CATEGORY, $raw);
	}

	public static function mediumData($medium, $metainfo = null, $default = null, $clang = null, $raw = false) {
		return self::getMetaDataForObject(WV_Sally::getIDForMedium($medium), $metainfo, $default, $clang, WV2_Metainfo::TYPE_MEDIUM, $raw);
	}

	public static function userData($user, $metainfo = null, $default = null, $clang = null, $raw = false) {
		return self::getMetaDataForObject($user, $metainfo, $default, $clang, WV2_Metainfo::TYPE_USER, $raw);
	}

	public static function objectData($object, $type, $metainfo = null, $default = null, $clang = null, $raw = false) {
		return self::getMetaDataForObject(WV2_Metainfo::getIDForObject($object), $metainfo, $default, $clang, $type, $raw);
	}

	/**
	 * Passende Objekte anhand ihrer Metadaten ermitteln
	 *
	 * Gibt ein Array aus sly_Model_Article/sly_Model_Category/OOMedia-Objekten
	 * zurück, die einen bestimmten Typ und eine bestimmte MetaInfo haben.
	 * Zusätzlich kann angegeben werden, ob die Metainfo einen bestimmten Wert
	 * hat ($value) und wie dieser Wert zu verstehen ist (ob er nur enthalten
	 * sein muss oder ob der Wert exakt dem Suchbegriff entsprechen muss).
	 *
	 * Die nötigen Konstanten für den $operator-Parameter sind in der Klasse WV2
	 * definiert. Nicht jeder Datentyp unterstützt jeden Filterparameter.
	 *
	 * Es wird empfohlen, direkt die Methoden getXXXWithMetaData zu verwenden, da
	 * diese auch nur die benötigten / erlaubten Parameter enthalten.
	 *
	 * Als Sortierkriterium muss eine Angabe der Form "tabelle.spalte" angegeben
	 * werden. Mögliche Tabellen sind
	 *
	 *  - article (sly_article)
	 *  - metadata (wv2_meta)
	 *  - metainfo (wv2_metainfo)
	 *  - file (nur für Medien relevant)
	 *  - medium (Alias für file)
	 *
	 * Wird eine nicht bekannte Tabelle angegeben, wird die Angabe einfach
	 * ignoriert.
	 *
	 * @param  mixed   $metainfo     der Name der Metainformation
	 * @param  boolean $onlineOnly   wenn true, werden nur online Artikel berücksichtigt (default: true)
	 * @param  string  $value        der gesuchte Metdaten-Wert oder null, falls egal
	 * @param  int     $operator     Anweisungen an den Datentyp, wie die Suche nach dem Wert ($value) zu erfolgen hat
	 * @param  string  $sort         eine optionale "ORDER BY"-Klausel (ohne "ORDER BY")
	 * @param  mixed   $articleType  die ID / der Name des Artikeltyps oder null, falls alle
	 * @param  int     $clang        die Sprache der Artikel / Kategorien / Medien (WV_Sally::CLANG-Konstanten)
	 * @param  int     $type         der Typ (WV2_Metainfo::TYPE-Konstanten)
	 * @return array                 eine Liste von passenden Artikeln / Kategorien / Medien
	 */
	public static function getObjectsWithMetaData($metainfo, $onlineOnly = true, $value = null, $operator = null, $sort = null, $articleType = null, $clang = null, $type = WV2_Metainfo::TYPE_ARTICLE) {
		$cache     = sly_Core::cache();
		$namespace = 'metainfo.objects';
		$cacheKey  = sly_Cache::generateKey('metainfo', $metainfo, $onlineOnly, $value, $operator, $sort, $articleType, $clang, $type);
		$data      = $cache->get($namespace, $cacheKey, false);
		$return    = array();

		if ($data === false) {
			list($sortTable, $sortColumn) = strpos($sort, '.') === false ? array('', $sort) : explode('.', $sort, 2);

			switch ($sortTable) {
				case 'metadata':
				case 'article':
				case 'category':
				case 'medium':
				case 'file':
				case 'user':     $sortTable = 'o'; break;
				case '':         break;
				default:         throw new UnexpectedValueException('Unexpected sort table "'.$sortTable.'" given.');
			}

			$sort  = $sortTable === '' ? $sortColumn : $sortTable.'.'.$sortColumn;
			$where = array('m.metainfo' => $metainfo);
			$joins = self::getObjectTableJoins($type);

			switch ($type) {
				case WV2_Metainfo::TYPE_ARTICLE:
					$where = array_filter(array('m.metainfo' => $metainfo, 'o.type' => $articleType, 'o.status' => $onlineOnly));
					break;

				case WV2_Metainfo::TYPE_CATEGORY:
					$where = array_filter(array('m.metainfo' => $metainfo, 'o.status' => $onlineOnly));
					break;

				case WV2_Metainfo::TYPE_USER:
					$where = array_filter(array('m.metainfo' => $metainfo, 'o.status' => $onlineOnly));
					break;
			}

			// add generic parameters
			if ($clang !== null && !in_array($type, array(WV2_Metainfo::TYPE_USER, WV2_Metainfo::TYPE_MEDIUM))) {
				$where['o.clang'] = WV_Sally::clang($clang);
			}

			// query the database
			$db     = sly_DB_Persistence::getInstance();
			$select = 'm.metainfo, m.object_id, m.clang, m.value';

			$db->select('wv2_meta m', $select, $where, null, $sort, null, null, null, $joins);
			$rows = $db->all();

			// empty result set?
			if (empty($rows)) {
				$cache->set($namespace, $cacheKey, array());
				return array();
			}

			// get datatype to decode the value
			if ($value !== null) {
				$datatype = _WV2_Info::getDatatypeWithParams($metainfo, $type);
				$params   = $datatype['params'];
				$datatype = WV_Datatype::factory($datatype['datatype']);
			}

			$data = array();

			// check values
			foreach ($rows as $row) {
				if ($value === null || $datatype->isValueContained($value, $row['value'], $params, $operator)) {
					$data[] = array('object_id' => (int) $row['object_id'], 'clang' => (int) $row['clang']);
				}
			}

			$cache->set($namespace, $cacheKey, $data);
		}

		foreach ($data as $date) {
			$obj = WV2_Metainfo::getObject($date['object_id'], $date['clang'], $type);
			if ($obj) $return[] = $obj;
		}

		return $return;
	}

	public static function getArticlesWithMetaData($metainfo, $onlineOnly = true, $value = null, $operator = null, $sort = null, $articleType = null, $clang = null) {
		return self::getObjectsWithMetaData($metainfo, $onlineOnly, $value, $operator, $sort, $articleType, $clang, WV2_Metainfo::TYPE_ARTICLE);
	}

	public static function getCategoriesWithMetaData($metainfo, $onlineOnly = true, $value = null, $operator = null, $sort = null, $clang = null) {
		return self::getObjectsWithMetaData($metainfo, $onlineOnly, $value, $operator, $sort, null, $clang, WV2_Metainfo::TYPE_CATEGORY);
	}

	public static function getMediaWithMetaData($metainfo, $value = null, $operator = null, $sort = null) {
		return self::getObjectsWithMetaData($metainfo, true, $value, $operator, $sort, null, null, WV2_Metainfo::TYPE_MEDIUM);
	}

	public static function getUsersWithMetaData($metainfo, $onlineOnly = true, $value = null, $operator = null, $sort = 'user.id') {
		return self::getObjectsWithMetaData($metainfo, $onlineOnly, $value, $operator, $sort, null, null, WV2_Metainfo::TYPE_USER);
	}

	/**
	 * Werte einer Metainfo erfahren
	 *
	 * Diese Methode ermittelt alle möglichen Werte, die eine Metainfo annehmen
	 * kann bzw. angenommen hat. Bei Strings macht nur die Suche nach
	 * angenommenen Werten Sinn, bei SELECTs auch die Suche nach den möglichen
	 * Werten.
	 *
	 * @param  mixed  $metainfo         die Metainfo
	 * @param  bool   $getOnlyExisting  wenn true, werden nur die Werte zurückgegeben, die eine Metainfo auch wirklich angenommen hat
	 * @param  bool   $onlineOnly       wenn true werden nur online Artikel berücksichtigt (nur für Artikel/Kategorien und nur wenn onlyExisting = true)
	 * @param  mixed  $objects          ein Array von Objekten, um die Auswahl weiter einzuschränken, oder null
	 * @param  int    $type             der Typ (WV2_Metainfo::TYPE-Konstanten)
	 * @return array                    eine Liste von Alternativen
	 */
	protected static function getObjectMetaInfoValueSet($metainfo, $getOnlyExisting = false, $clang = null, $onlineOnly = true, $objects = null, $type = WV2_Metainfo::TYPE_ARTICLE) {
		if (is_array($objects)) {
			foreach ($objects as $idx => $object) {
				$objects[$idx] = WV2_Metainfo::getIDForObject($object, false);
			}

			// impossible combination
			if ($getOnlyExisting && empty($objects)) {
				return array();
			}
		}

		// Da PHP keine Arrays zulässt, bei denen die Keys zwar
		// Strings, aber Zahlen sind ("8" wird immer zu 8 konvertiert),
		// muss der Datentyp explizit angeben, ob seine Liste assoziativ
		// oder normal zu behandeln ist.

		$data       = _WV2_Info::getDatatypeWithParams($metainfo, $type);
		$datatype   = WV_Datatype::factory($data['datatype']);
		$isAssoc    = $datatype->usesAssociativeResults();
		$cache      = sly_Core::cache();
		$namespace  = 'metainfo.valuesets';
		$cacheKey   = sly_Cache::generateKey('metainfo', $metainfo, $getOnlyExisting, $onlineOnly, $objects, $clang, $type);
		$cachedData = $cache->get($namespace, $cacheKey);

		if ($cachedData !== null) {
			return $cachedData;
		}

		$datalist = array();

		if ($getOnlyExisting) {
			$db     = sly_DB_Persistence::getInstance();
			$select = 'DISTINCT value';
			$table  = 'wv2_meta m';
			$joins  = null;
			$where  = array(
				'm.meta_type' => $type,
				'm.metainfo'  => $metainfo,
				'm.clang'     => WV_Sally::clang($clang)
			);

			if (is_array($objects)) {
				$where['m.object_id'] = $objects;
			}

			if ($onlineOnly) {
				$joins = self::getObjectTableJoins($type);
				$where['o.status'] = 1;
			}

			$db->select($table, $select, $where, null, null, null, null, null, $joins);

			foreach ($db->all() as $row) {
				$value    = $datatype->deserializeValue($row['value'], $data['params']);
				$datalist = $isAssoc ? sly_merge($datalist, $value) : array_merge($datalist, sly_makeArray($value));
			}

			$datalist = array_unique($datalist);
		}
		else {
			$datalist = $datatype->extractValuesFromParams($data['params']);
		}

		$cache->set($namespace, $cacheKey, $datalist);
		return $datalist;
	}

	public static function getArticleMetaInfoValueSet($metainfo, $getOnlyExisting = false, $onlineOnly = true, $objects = null, $clang = null) {
		return self::getObjectMetaInfoValueSet($metainfo, $getOnlyExisting, $clang, $onlineOnly, $objects, WV2_Metainfo::TYPE_ARTICLE);
	}

	public static function getCategoryMetaInfoValueSet($metainfo, $getOnlyExisting = false, $onlineOnly = true, $objects = null, $clang = null) {
		return self::getObjectMetaInfoValueSet($metainfo, $getOnlyExisting, $clang, $onlineOnly, $objects, WV2_Metainfo::TYPE_CATEGORY);
	}

	public static function getMediumMetaInfoValueSet($metainfo, $getOnlyExisting = false, $objects = null, $clang = null) {
		return self::getObjectMetaInfoValueSet($metainfo, $getOnlyExisting, $clang, false, $objects, WV2_Metainfo::TYPE_MEDIUM);
	}

	public static function getUserMetaInfoValueSet($metainfo, $getOnlyExisting = false, $objects = null, $clang = null) {
		return self::getObjectMetaInfoValueSet($metainfo, $getOnlyExisting, $clang, false, $objects, WV2_Metainfo::TYPE_USER);
	}

	/**
	 * Prüfen, ob Wert vorhanden ist
	 *
	 * Diese Methode prüft, ob ein bstimmtes Objekt ein bestimmtes Metadatum
	 * besitzt.
	 *
	 * @param  mixed  $object    das Objekt
	 * @param  mixed  $metainfo  die Metainfo
	 * @param  mixed  $value     der gesuchte Wert
	 * @param  int    $clang     die Sprache des Objekts
	 * @param  int    $type      der Typ des Objekts
	 * @return boolean           true, wenn der Artikel die gesuchte Information bestitzt, sonst false
	 */
	protected static function hasObjectValue($object, $metainfo, $value, $clang = null, $type = WV2_Metainfo::TYPE_ARTICLE) {
		$metadata = self::getMetaDataForObject($object, $metainfo, null, $clang, $type);
		if (!$metadata) return false;

		$v = $metadata->getValue();

		if (!is_array($v)) return $value == $v;
		return in_array($value, array_keys($v)) || in_array($value, array_values($v));
	}

	public static function hasArticleValue($metainfo, $value, $clang = null, $article) {
		return self::hasObjectValue(WV_Sally::getIDForArticle($article), $metainfo, $value, $clang, WV2_Metainfo::TYPE_ARTICLE);
	}

	public static function hasCategoryValue($metainfo, $value, $clang = null, $category) {
		return self::hasObjectValue(WV_Sally::getIDForCategory($category), $metainfo, $value, $clang, WV2_Metainfo::TYPE_CATEGORY);
	}

	public static function hasMediumValue($medium, $metainfo, $value, $clang = null) {
		return self::hasObjectValue(WV_Sally::getIDForMedium($medium), $metainfo, $value, $clang, WV2_Metainfo::TYPE_MEDIUM);
	}

	public static function hasUserValue($user, $metainfo, $value, $clang = null) {
		return self::hasObjectValue($user, $metainfo, $value, $clang, WV2_Metainfo::TYPE_USER);
	}

	/**
	 * Metadaten für alle Objekte ermitteln
	 *
	 * Diese Methode liefert NICHT die Metadaten eines einzelnes Objekt zurück,
	 * sondern die Metadaten aller Objekte, die die gewählte Metainformation
	 * besitzen. Daher kann man ihr auch nicht die ID eines Objekts übergeben.
	 * Sie dient primär als Hilfsmethode für getXXXValueSet().
	 *
	 * @param  mixed      $metainfo       die Metainfo
	 * @param  int|string $articleType    die ID / der Name des gesuchten Artikeltyps oder null für keine Angabe
	 * @param  int        $clang          die gewünschte Sprache
	 * @param  int        $type           der Typ des Objekts
	 * @return array                      Liste von _WV2_MetaData-Objekten
	 */
	public static function getObjectMetaDataForToken($metainfo = null, $clang = null, $type = WV2_Metainfo::TYPE_ARTICLE) {
		switch ($type) {
			case WV2_Metainfo::TYPE_ARTICLE:  return self::getArticleMetaDataForToken($metainfo, null, $clang);
			case WV2_Metainfo::TYPE_CATEGORY: return self::getCategoryMetaDataForToken($metainfo, $clang);
			case WV2_Metainfo::TYPE_MEDIUM:   return self::getMediumMetaDataForToken($metainfo, $clang);
		}
	}

	public static function getArticleMetaDataForToken($metainfo = null, $articleType = null, $clang = null) {
		$articleType     = WV2_Metainfo::getIDForArticleType($articleType, true);
		$params['clang'] = WV_Sally::clang($clang);

		if ($metainfo)    $params['metainfo'] = $metainfo;
		if ($articleType) $params['type']     = $articleType;

		return self::getArticleMetaData($params);
	}

	public static function getCategoryMetaDataForToken($metainfo = null, $clang = null) {
		$params['clang'] = WV_Sally::clang($clang);

		if ($metainfo) {
			$params['metainfo'] = $metainfo;
		}

		return self::getCategoryMetaData($params);
	}

	public static function getMediumMetaDataForToken($metainfo = null, $clang = null) {
		$params['clang'] = WV_Sally::clang($clang);

		if ($metainfo) {
			$params['metainfo'] = $metainfo;
		}

		return self::getMediumMetaData($params);
	}

	public static function getUserMetaDataForToken($metainfo = null, $clang = null) {
		$params['clang'] = WV_Sally::clang($clang);

		if ($metainfo) {
			$params['metainfo'] = $metainfo;
		}

		return self::getUserMetaData($params);
	}

	/**
	 * Metadaten ermitteln
	 *
	 * Gibt eine Liste von Metadaten zurück. Sie dient als Basismethode zur
	 * Abfrage von Metdaten von Artikeln und bietet verschiedene Parameter zur
	 * Selektion, Gruppierung oder Sortierung. Die anderen MetaData-Methoden
	 * sind quasi Shortcuts für häufig benutzte "Queries".
	 *
	 * Im $params-Array werden folgende Elemente in der folgenden Reihenfolge
	 * abgearbeitet:
	 *
	 * - 'articleID' => int | array
	 *   (wenn gesetzt, werden nur die Metadaten für einen Artikel / eine Menge
	 *   von Artikeln ermittelt)
	 * - 'name' => string | array
	 *   (wenn gesetzt, werden nur die Metadaten für eine bestimmte Metainfo
	 *   geholt (kann als Alternative zu 'id' dienen))
	 * - 'id'  => int | array
	 *   (wenn gesetzt, werden nur die Metadaten für eine bestimmte Metainfo
	 *   geholt (kann als Alternative zu 'name' dienen))
	 * - 'categoryID' => int | array
	 *   (wenn gesetzt, werden nur Artikel einer bestimmten Kategorie / von
	 *   bestimmten Kategorien ermittelt)
	 * - 'type' => string | array
	 *   (wenn gesetzt, werden die ermittelten Artikel nochmals nach ihrem Typ /
	 *   ihren Typen gefiltert)
	 * - 'orderby' => string
	 *   (kann jedes Attribut aus den Relationen article, wv2_article_meta und
	 *   wv2_metainfo sein; Standard ist "id")
	 * - 'direction' => ASC | DESC
	 *   (die Sortierreihenfolge als String; Standard ist ASC)
	 *
	 * Die Parameter in $params können auch als ein JSON-kodierter String
	 * angegeben werden ("{articleID:5}").
	 *
	 * @param  array   $params       die Suchparameter
	 * @param  int     $objectType
	 * @param  boolean $assocResult  if true, the result will be by object and clang
	 * @return array                 die Liste der passenden Metadaten
	 */
	public static function getObjectMetaData(array $params, $objectType, $assocResult = false) {
		$query     = '';
		$cache     = sly_Core::cache();
		$namespace = 'metainfo.data';
		$cacheKey  = sly_Cache::generateKey('params', $params, $objectType);
		$data      = $cache->get($namespace, $cacheKey);

		// cache hit
		if ($data !== null) {
			$result = array();

			foreach ($data as $record) {
				$metainfo = WV2_Metainfo::getMetainfo($record['metainfo'], $objectType);
				$value    = $metainfo->deserialize($record['value']);

				if ($assocResult) {
					$result[$record['object_id']][$record['clang']][$record['metainfo']] = $value;
				}
				else {
					$result[] = $value;
				}
			}

			return $result;
		}

		if (!is_array($params)) {
			$params = json_decode($params, true);
		}

		// Parameter auspacken. extract() wäre uns zu unsicher, daher lieber
		// Stück für Stück von Hand.

		$articleID  = sly_makeArray(isset($params['articleID'])  ? $params['articleID']  : null);
		$categoryID = sly_makeArray(isset($params['categoryID']) ? $params['categoryID'] : null);
		$mediumID   = sly_makeArray(isset($params['mediumID'])   ? $params['mediumID']   : null);
		$userID     = sly_makeArray(isset($params['userID'])     ? $params['userID']     : null);
		$objectID   = sly_makeArray(isset($params['objectID'])   ? $params['objectID']   : null);
		$clang      = sly_makeArray(isset($params['clang'])      ? $params['clang']      : null);
		$type       = sly_makeArray(isset($params['type'])       ? $params['type']       : null);
		$metainfo   = sly_makeArray(isset($params['metainfo'])   ? $params['metainfo']   : null);

		// Parameter vorbereiten und entschärfen

		$articleID  = array_map('intval', $articleID);
		$categoryID = array_map('intval', $categoryID);
		$mediumID   = array_map('intval', $mediumID);
		$userID     = array_map('intval', $userID);
		$objectID   = array_map('intval', $objectID);
		$clang      = array_map('intval', $clang);

		// Minimieren

		$articleID  = array_unique($articleID);
		$categoryID = array_unique($categoryID);
		$mediumID   = array_unique($mediumID);
		$userID     = array_unique($userID);
		$objectID   = array_unique($objectID);
		$type       = array_unique($type);
		$clang      = array_unique($clang);
		$metainfo   = array_unique($metainfo);

		if (in_array(WV_Sally::CLANG_ALL, $clang)) {
			$clang = array();
		}

		// build query

		$where = array('m.meta_type' => $objectType);

		if (!empty($articleID)) $where['m.object_id'] = $articleID;
		if (!empty($mediumID))  $where['m.object_id'] = $mediumID;
		if (!empty($userID))    $where['m.object_id'] = $userID;
		if (!empty($objectID))  $where['m.object_id'] = $objectID;
		if (!empty($type))      $where['o.type']      = $type;
		if (!empty($clang))     $where['m.clang']     = $clang;
		if (!empty($metainfo))  $where['m.metainfo']  = $metainfo;

		// Die Angabe der Kategorie ist mehrdeutig. Ist sie die einzige Angabe,
		// ist die ID der Metadaten (object_id) für Kategorie-Metadaten gemeint.
		// Ist jedoch auch $articleID gesetzt, so ist sie eine zusätzliche
		// Einschränkung für Artikel-Metadaten. Die beiden public-Methoden kümmern
		// sich bereits darum, nur die gewünschten Parameter durchzulassen.

		if (!empty($categoryID)) {
			$column         = isset($params['articleID']) ? 'a.re_id' : 'm.object_id';
			$where[$column] = $categoryID;
		}

		// nearly done

		$orderby   = isset($params['orderby'])   ? $params['orderby']   : 'o.id';
		$direction = isset($params['direction']) ? $params['direction'] : 'ASC';
		$select    = 'm.*';
		$table     = 'wv2_meta m';
		$joins     = self::getObjectTableJoins($objectType);
		$db        = sly_DB_Persistence::getInstance();
		$result    = array();
		$cacheData = array();

		$db->select($table, $select, $where, null, $orderby.' '.$direction, null, null, null, $joins);

		foreach ($db->all() as $row) {
			$metainfo = WV2_Metainfo::getMetainfo($row['metainfo'], $objectType);
			$value    = $metainfo->deserialize($row['value']);

			if ($assocResult) {
				$result[$row['object_id']][$row['clang']][$row['metainfo']] = $value;
			}
			else {
				$result[] = $value;
			}

			$cacheData[] = array(
				'value'     => $row['value'],
				'metainfo'  => $row['metainfo'],
				'object_id' => (int) $row['object_id'],
				'clang'     => (int) $row['clang']
			);
		}

		$cache->set($namespace, $cacheKey, $cacheData);
		return $result;
	}

	public static function getArticleMetaData($params = null, $assocResult = false) {
		return self::getObjectMetaData($params, WV2_Metainfo::TYPE_ARTICLE, $assocResult);
	}

	public static function getCategoryMetaData($params = null, $assocResult = false) {
		if (is_array($params) && isset($params['articleID'])) unset($params['articleID']);
		return self::getObjectMetaData($params, WV2_Metainfo::TYPE_CATEGORY, $assocResult);
	}

	public static function getMediumMetaData($params = null, $assocResult = false) {
		return self::getObjectMetaData($params, WV2_Metainfo::TYPE_MEDIUM, $assocResult);
	}

	public static function getUserMetaData($params = null, $assocResult = false) {
		return self::getObjectMetaData($params, WV2_Metainfo::TYPE_USER, $assocResult);
	}

	/* Filter-System */

	public static function filterArticles(WV_IFilter $filter, $sortby = 'article.pos', $direction = 'ASC', $limitClause = '', $distinct = false) {
		$cache     = sly_Core::cache();
		$namespace = 'metainfo.filter.articles';
		$cacheKey  = sly_Cache::generateKey('filter', $filter, $sortby, $direction, $limitClause, $distinct);
		$data      = $cache->get($namespace, $cacheKey);

		if (!empty($data)) {
			$articles = array();

			foreach ($data as $record) {
				$articles[] = sly_Util_Article::findById($record['id'], $record['clang']);
			}

			return $articles;
		}

		$filter = new WV_Filter_And($filter, new WV_Filter_IsArticle());
		$tables = array('article');
		$tables = array_unique(array_merge($tables, $filter->getRequiredTables()));

		// JOIN-Bedingungen

		$connectors = array(
			'wv2_meta' => '~wv2_meta.object_id = ~article.id AND ~wv2_meta.clang = ~article.clang AND ~wv2_meta.meta_type = '.WV2_Metainfo::TYPE_ARTICLE
		);

		// Query zusammensetzen

		$prefix          = sly_Core::getTablePrefix();
		$columnsToSelect = array($prefix.'article.id', $prefix.'article.clang');

		if (in_array('getExtraColumns', get_class_methods($filter))) {
			$columnsToSelect = array_merge($columnsToSelect, $filter->getExtraColumns($tables));
		}

		$cacheData = array();
		$sql       = WV_SQL::getInstance();
		$articles  = array();

		if (strpos($sortby, '.') !== false) {
			$sortby = $prefix.$sortby;
		}

		$sql->query(
			'SELECT '.($distinct ? 'DISTINCT ' : '').implode(', ', $columnsToSelect).' '.
			'FROM '.self::constructFilterQuery($tables, $connectors, $filter, 'article').' '.
			'ORDER BY '.$sortby.' '.$direction.' '.$limitClause
		);

		foreach ($sql as $row) {
			$articles[]  = sly_Util_Article::findById($row['id'], $row['clang']);
			$cacheData[] = $row;
		}

		$cache->set($namespace, $cacheKey, $cacheData);
		return $articles;
	}

	public static function filterArticlesCount(WV_IFilter $filter) {
		$cache     = sly_Core::cache();
		$namespace = 'metainfo.filter.articles';
		$cacheKey  = sly_Cache::generateKey('filter_count', $filter);
		$count     = $cache->get($namespace, $cacheKey);

		if (!empty($count)) {
			// old versions of this method used to store the count in an array('count' => $count)
			return is_array($count) ? (int) reset($count) : $count;
		}

		$filter = new WV_Filter_And($filter, new WV_Filter_IsArticle());
		$tables = array('article');
		$tables = array_unique(array_merge($tables, $filter->getRequiredTables()));

		// JOIN-Bedingungen

		$connectors = array(
			'wv2_meta' => '~wv2_meta.object_id = ~article.id AND ~wv2_meta.clang = ~article.clang AND ~wv2_meta.meta_type = '.WV2_Metainfo::TYPE_ARTICLE
		);

		// Query zusammensetzen

		$prefix = sly_Core::getTablePrefix();
		$sql    = WV_SQL::getInstance();
		$count  = null;

		$sql->query('SELECT COUNT(DISTINCT '.$prefix.'article.id) as article_count FROM '.self::constructFilterQuery($tables, $connectors, $filter, 'article'));

		foreach ($sql as $row) {
			$count = (int) $row['article_count'];
		}

		if ($count !== null) {
			$cache->set($namespace, $cacheKey, $count);
		}

		return $count;
	}

	public static function filterCategories(WV_IFilter $filter, $sortby = 'article.pos', $direction = 'ASC', $limitClause = '') {
		$cache     = sly_Core::cache();
		$namespace = 'metainfo.filter.categories';
		$cacheKey  = sly_Cache::generateKey('filter', $filter, $sortby, $direction, $limitClause);
		$data      = $cache->get($namespace, $cacheKey);

		if (!empty($data)) {
			$categories = array();

			foreach ($data as $record) {
				$categories[] = sly_Util_Category::findById($record['id'], $record['clang']);
			}

			return $categories;
		}

		$filter = new WV_Filter_And($filter, new WV_Filter_IsCategory());
		$tables = array('article');
		$tables = array_unique(array_merge($tables, $filter->getRequiredTables()));

		// JOIN-Bedingungen

		$connectors = array(
			'wv2_meta' => '~wv2_meta.object_id = ~article.id AND ~wv2_meta.meta_type = '.WV2_Metainfo::TYPE_CATEGORY
		);

		// Query zusammensetzen

		$prefix          = sly_Core::getTablePrefix();
		$columnsToSelect = array($prefix.'article.id', $prefix.'article.clang');

		if (in_array('getExtraColumns', get_class_methods($filter))) {
			$columnsToSelect = array_merge($columnsToSelect, $filter->getExtraColumns($tables));
		}

		$sql        = WV_SQL::getInstance();
		$categories = array();
		$cacheData  = array();

		if (strpos($sortby, '.') !== false) {
			$sortby = $prefix.$sortby;
		}

		$sql->query(
			'SELECT '.implode(', ', $columnsToSelect).' '.
			'FROM '.self::constructFilterQuery($tables, $connectors, $filter, 'article').' '.
			'ORDER BY '.$sortby.' '.$direction.' '.$limitClause
		);

		foreach ($sql as $row) {
			$categories[] = sly_Util_Category::findById($row['id'], $row['clang']);
			$cacheData[]  = $row;
		}

		$cache->set($namespace, $cacheKey, $cacheData);
		return $categories;
	}

	public static function filterMedia(WV_IFilter $filter, $sortby = 'file.id', $direction = 'ASC', $limitClause = '') {
		$cache     = sly_Core::cache();
		$namespace = 'metainfo.filter.media';
		$cacheKey  = sly_Cache::generateKey('filter', $filter, $sortby, $direction, $limitClause);
		$data      = $cache->get($namespace, $cacheKey);

		if (!empty($data)) {
			$media = array();

			foreach ($data as $record) {
				$media[] = sly_Util_Medium::findById($record);
			}

			return $media;
		}

		$tables = array('file');
		$tables = array_unique(array_merge($tables, $filter->getRequiredTables()));

		// JOIN-Bedingungen

		$connectors = array(
			'wv2_meta' => '~wv2_meta.object_id = ~file.id AND ~wv2_meta.meta_type = '.WV2_Metainfo::TYPE_MEDIUM
		);

		// Query zusammensetzen

		$prefix          = sly_Core::getTablePrefix();
		$columnsToSelect = array($prefix.'file.id');

		if (in_array('getExtraColumns', get_class_methods($filter))) {
			$columnsToSelect = array_merge($columnsToSelect, $filter->getExtraColumns($tables));
		}

		$sql       = WV_SQL::getInstance();
		$media     = array();
		$cacheData = array();

		if (strpos($sortby, '.') !== false) {
			$sortby = $prefix.$sortby;
		}

		$sql->query(
			'SELECT '.implode(', ', $columnsToSelect).' '.
			'FROM '.self::constructFilterQuery($tables, $connectors, $filter, 'file').' '.
			'ORDER BY '.$sortby.' '.$direction.' '.$limitClause
		);

		foreach ($sql as $row) {
			$media[]     = sly_Util_Medium::findById($row['id']);
			$cacheData[] = $row['id'];
		}

		$cache->set($namespace, $cacheKey, $cacheData);
		return $media;
	}

	public static function filterUser(WV_IFilter $filter, $sortby = 'user.id', $direction = 'ASC', $limitClause = '') {
		$cache     = sly_Core::cache();
		$namespace = 'metainfo.filter.user';
		$cacheKey  = sly_Cache::generateKey('filter', $filter, $sortby, $direction, $limitClause);
		$data      = $cache->get($namespace, $cacheKey);

		if (!empty($data)) {
			$user = array();

			foreach ($data as $record) {
				$user[] = sly_Util_User::findById($record);
			}

			return $user;
		}

		$tables = array('user');
		$tables = array_unique(array_merge($tables, $filter->getRequiredTables()));

		// JOIN-Bedingungen

		$connectors = array(
			'wv2_meta' => '~wv2_meta.object_id = ~user.id AND ~wv2_meta.meta_type = '.WV2_Metainfo::TYPE_USER
		);

		// Query zusammensetzen

		$prefix          = sly_Core::getTablePrefix();
		$columnsToSelect = array($prefix.'user.id');

		if (in_array('getExtraColumns', get_class_methods($filter))) {
			$columnsToSelect = array_merge($columnsToSelect, $filter->getExtraColumns($tables));
		}

		$sql       = WV_SQL::getInstance();
		$user      = array();
		$cacheData = array();

		if (strpos($sortby, '.') !== false) {
			$sortby = $prefix.$sortby;
		}

		$sql->query(
			'SELECT '.implode(', ', $columnsToSelect).' '.
			'FROM '.self::constructFilterQuery($tables, $connectors, $filter, 'user').' '.
			'ORDER BY '.$sortby.' '.$direction.' '.$limitClause
		);

		foreach ($sql as $row) {
			$user[]      = sly_Util_User::findById($row['id']);
			$cacheData[] = $row['id'];
		}

		$cache->set($namespace, $cacheKey, $cacheData);
		return $user;
	}

	public static function filterMetaData(WV_IFilter $filter, $sortby = 'wv2_meta.metainfo', $direction = 'ASC') {
		$cache     = sly_Core::cache();
		$namespace = 'metainfo.filter.data';
		$cacheKey  = sly_Cache::generateKey('filter', $filter, $sortby, $direction);
		$data      = $cache->get($namespace, $cacheKey);

		if (!empty($data)) {
			return $data;
		}

		$tables = array('wv2_meta');
		$tables = array_unique(array_merge($tables, $filter->getRequiredTables()));

		// JOIN-Bedingungen

		$connectors = array(
			'article' => '~wv2_meta.object_id = ~article.id',
			'file'    => '~wv2_meta.object_id = ~file.id AND ~wv2_meta.meta_type = '.WV2_Metainfo::TYPE_MEDIUM,
			'user'    => '~wv2_meta.object_id = ~user.id AND ~wv2_meta.meta_type = '.WV2_Metainfo::TYPE_USER
		);

		// Query zusammensetzen

		$sql    = WV_SQL::getInstance();
		$prefix = sly_Core::getTablePrefix();
		$data   = array();

		$sql->query(
			'SELECT '.
			$prefix.'wv2_meta.metainfo, '.
			$prefix.'wv2_meta.value, '.
			$prefix.'wv2_meta.object_id, '.
			$prefix.'wv2_meta.meta_type, '.
			$prefix.'wv2_meta.clang '.
			'FROM '.self::constructFilterQuery($tables, $connectors, $filter, 'wv2_meta').' '.
			'ORDER BY '.$prefix.$sortby.' '.$direction
		);

		foreach ($sql as $row) {
			$data[] = $row;
		}

		$cache->set($namespace, $cacheKey, $data);
		return $data;
	}

	protected static function constructFilterQuery(&$tables, &$connectors, $filter, $masterTable) {
		$prefix         = sly_Core::getTablePrefix();
		$joinConditions = array('1');

		foreach ($tables as &$table) {
			if ($table != $masterTable) { // Die Haupttabelle muss sich nicht an sich selbst joinen.
				$connector = $connectors[$table];
				$connector = str_replace('~', $prefix, $connector);
				$joinConditions[] = $connector;
			}

			$table = $prefix.$table;
		}

		// Den Filtern die Möglichkeit geben, die Liste der Tabellen nochmals
		// zu erweitern (notwendig, wenn eine Tabelle mehrfach gejoined werden
		// soll).

		if (in_array('getExtraTables', get_class_methods($filter))) {
			$tables = array_merge($tables, $filter->getExtraTables($tables));
		}

		$tmp    = new WV_Filter_And(array());
		$join   = implode(' AND ', $tmp->optimize($joinConditions));
		$filter = implode(' AND ', $tmp->optimize(array($filter->toSQL())));
		$where  = $tmp->optimize(array($join, $filter));
		$where  = array_values(array_filter($where));
		$where  = count($where) === 1 ? $where[0] : ('('.implode(') AND (', $where).')');

		return implode(', ', $tables).' WHERE '.$where;
	}

	private static function getObjectTableJoins($objectType) {
		switch ($objectType) {
			case WV2_Metainfo::TYPE_ARTICLE:
				$joins = 'LEFT JOIN ~article o ON o.id = m.object_id AND o.clang = m.clang';
				break;

			case WV2_Metainfo::TYPE_CATEGORY:
				$joins = 'LEFT JOIN ~article o ON o.id = m.object_id AND o.clang = m.clang';
				break;

			case WV2_Metainfo::TYPE_MEDIUM:
				$joins = 'LEFT JOIN ~file o ON o.id = m.object_id';
				break;

			case WV2_Metainfo::TYPE_USER:
				$joins = 'LEFT JOIN ~user o ON o.id = m.object_id';
				break;

			default:
				throw new UnexpectedValueException(t('metainfo_unkown_object_type', $objectType));
		}

		return str_replace('~', sly_Core::getTablePrefix(), $joins);
	}
}
