| Current File : /home/jvzmxxx/wiki/extensions/Wikibase/lib/tests/phpunit/MockRepository.php |
<?php
namespace Wikibase\Lib\Tests;
use InvalidArgumentException;
use Status;
use User;
use Wikibase\DataModel\Entity\EntityDocument;
use Wikibase\DataModel\Entity\EntityId;
use Wikibase\DataModel\Entity\EntityRedirect;
use Wikibase\DataModel\Entity\Item;
use Wikibase\DataModel\Entity\ItemId;
use Wikibase\DataModel\Entity\Property;
use Wikibase\DataModel\Entity\PropertyId;
use Wikibase\DataModel\Entity\BasicEntityIdParser;
use Wikibase\DataModel\Services\Lookup\EntityLookup;
use Wikibase\DataModel\Services\Lookup\EntityRedirectLookup;
use Wikibase\DataModel\Services\Lookup\EntityRedirectLookupException;
use Wikibase\DataModel\Services\Lookup\PropertyDataTypeLookup;
use Wikibase\DataModel\Services\Lookup\PropertyDataTypeLookupException;
use Wikibase\DataModel\SiteLink;
use Wikibase\DataModel\Term\FingerprintProvider;
use Wikibase\EntityRevision;
use Wikibase\Lib\Store\EntityInfoBuilderFactory;
use Wikibase\Lib\Store\EntityRevisionLookup;
use Wikibase\Lib\Store\EntityStore;
use Wikibase\Lib\Store\GenericEntityInfoBuilder;
use Wikibase\Lib\Store\HashSiteLinkStore;
use Wikibase\Lib\Store\SiteLinkLookup;
use Wikibase\Lib\Store\SiteLinkStore;
use Wikibase\Lib\Store\StorageException;
use Wikibase\Lib\Store\RevisionedUnresolvedRedirectException;
use Wikibase\RedirectRevision;
/**
* @deprecated Try to use a simpler fake. The complexity and coupling of this
* test double are very high, so it is good to avoid binding to it.
*
* Mock repository for use in tests.
*
* @since 0.4
*
* @license GPL-2.0+
* @author Daniel Kinzler
* @author Thiemo Mättig
*/
class MockRepository implements
EntityInfoBuilderFactory,
EntityLookup,
EntityRevisionLookup,
EntityRedirectLookup,
EntityStore,
PropertyDataTypeLookup,
SiteLinkLookup
{
/**
* @var SiteLinkStore
*/
private $siteLinkStore;
/**
* Entity id serialization => array of EntityRevision
*
* @var array[]
*/
private $entities = array();
/**
* Log entries. Each entry has the following fields:
* revision, entity, summary, user
*
* @var array[]
*/
private $log = array();
/**
* Entity id serialization => EntityRedirect
*
* @var RedirectRevision[]
*/
private $redirects = array();
/**
* User ID + Entity Id -> bool
*
* @var bool[]
*/
private $watchlist = array();
/**
* @var int
*/
private $maxEntityId = 0;
/**
* @var int
*/
private $maxRevisionId = 0;
public function __construct() {
$this->siteLinkStore = new HashSiteLinkStore();
}
/**
* @see EntityLookup::getEntity
*
* @param EntityId $entityId
*
* @return EntityDocument|null
* @throws StorageException
*/
public function getEntity( EntityId $entityId ) {
$revision = $this->getEntityRevision( $entityId );
return $revision === null ? null : $revision->getEntity()->copy();
}
/**
* @since 0.4
* @see EntityRevisionLookup::getEntityRevision
*
* @param EntityId $entityId
* @param int|string $revisionId The desired revision id, or LATEST_FROM_SLAVE or LATEST_FROM_MASTER.
*
* @throws RevisionedUnresolvedRedirectException
* @throws StorageException
* @return EntityRevision|null
*/
public function getEntityRevision( EntityId $entityId, $revisionId = self::LATEST_FROM_SLAVE ) {
$key = $entityId->getSerialization();
if ( isset( $this->redirects[$key] ) ) {
$redirRev = $this->redirects[$key];
throw new RevisionedUnresolvedRedirectException(
$entityId,
$redirRev->getRedirect()->getTargetId(),
$redirRev->getRevisionId(),
$redirRev->getTimestamp()
);
}
if ( empty( $this->entities[$key] ) ) {
return null;
}
// default changed from false to 0 and then to LATEST_FROM_SLAVE
if ( $revisionId === false || $revisionId === 0 ) {
wfWarn( 'getEntityRevision() called with $revisionId = false or 0, ' .
'use EntityRevisionLookup::LATEST_FROM_SLAVE or EntityRevisionLookup::LATEST_FROM_MASTER instead.' );
$revisionId = self::LATEST_FROM_SLAVE;
}
/** @var EntityRevision[] $revisions */
$revisions = $this->entities[$key];
if ( !is_int( $revisionId ) ) {
$revisionIds = array_keys( $revisions );
$revisionId = end( $revisionIds );
} elseif ( !isset( $revisions[$revisionId] ) ) {
throw new StorageException( "no such revision for entity $key: $revisionId" );
}
$revision = $revisions[$revisionId];
$revision = new EntityRevision( // return a copy!
$revision->getEntity()->copy(), // return a copy!
$revision->getRevisionId(),
$revision->getTimestamp()
);
return $revision;
}
/**
* See EntityLookup::hasEntity()
*
* @since 0.4
*
* @param EntityId $entityId
*
* @return bool
*/
public function hasEntity( EntityId $entityId ) {
return $this->getEntity( $entityId ) !== null;
}
/**
* @see SiteLinkLookup::getItemIdForLink
*
* @param string $globalSiteId
* @param string $pageTitle
*
* @return ItemId|null
*/
public function getItemIdForLink( $globalSiteId, $pageTitle ) {
return $this->siteLinkStore->getItemIdForLink( $globalSiteId, $pageTitle );
}
/**
* @see SiteLinkLookup::getItemIdForSiteLink
*
* @param SiteLink $siteLink
*
* @return ItemId|null
*/
public function getItemIdForSiteLink( SiteLink $siteLink ) {
return $this->siteLinkStore->getItemIdForSiteLink( $siteLink );
}
/**
* Registers the sitelinks of the given Item so they can later be found with getLinks, etc
*
* @param Item $item
*/
private function registerSiteLinks( Item $item ) {
$this->siteLinkStore->saveLinksOfItem( $item );
}
/**
* Unregisters the sitelinks of the given Item so they are no longer found with getLinks, etc
*
* @param ItemId $itemId
*/
private function unregisterSiteLinks( ItemId $itemId ) {
$this->siteLinkStore->deleteLinksOfItem( $itemId );
}
/**
* Puts an entity into the mock repository. If there already is an entity with the same ID
* in the mock repository, it is not removed, but replaced as the current one. If a revision
* ID is given, the entity with the highest revision ID is considered the current one.
*
* @param EntityDocument $entity
* @param int $revisionId
* @param int|string $timestamp
* @param User|string|null $user
*
* @return EntityRevision
*/
public function putEntity( EntityDocument $entity, $revisionId = 0, $timestamp = 0, $user = null ) {
if ( $entity->getId() === null ) {
$this->assignFreshId( $entity );
}
$oldEntity = $this->getEntity( $entity->getId() );
if ( $oldEntity && ( $oldEntity instanceof Item ) ) {
// clean up old sitelinks
$this->unregisterSiteLinks( $entity->getId() );
}
if ( $entity instanceof Item ) {
// add new sitelinks
$this->registerSiteLinks( $entity );
}
if ( $revisionId === 0 ) {
$revisionId = ++$this->maxRevisionId;
}
$this->updateMaxNumericId( $entity->getId() );
$this->maxRevisionId = max( $this->maxRevisionId, $revisionId );
$revision = new EntityRevision(
$entity->copy(), // note: always clone
$revisionId,
wfTimestamp( TS_MW, $timestamp )
);
if ( $user !== null ) {
if ( $user instanceof User ) {
$user = $user->getName();
}
// just glue the user on here...
$revision->user = $user;
}
$key = $entity->getId()->getSerialization();
unset( $this->redirects[$key] );
if ( !array_key_exists( $key, $this->entities ) ) {
$this->entities[$key] = array();
}
$this->entities[$key][$revisionId] = $revision;
ksort( $this->entities[$key] );
return $revision;
}
/**
* Puts a redirect into the mock repository. If there already is an entity with the same ID
* in the mock repository, it is replaced with the redirect.
*
* @param EntityRedirect $redirect
* @param int $revisionId
* @param string|int $timestamp
*/
public function putRedirect( EntityRedirect $redirect, $revisionId = 0, $timestamp = 0 ) {
$key = $redirect->getEntityId()->getSerialization();
if ( isset( $this->entities[$key] ) ) {
$this->removeEntity( $redirect->getEntityId() );
}
if ( $revisionId === 0 ) {
$revisionId = ++$this->maxRevisionId;
}
$this->updateMaxNumericId( $redirect->getTargetId() );
$this->maxRevisionId = max( $this->maxRevisionId, $revisionId );
$this->redirects[$key] = new RedirectRevision(
$redirect, // EntityRedirect is immutable
$revisionId,
wfTimestamp( TS_MW, $timestamp )
);
}
/**
* Removes an entity from the mock repository.
*
* @param EntityId $entityId
*
* @return EntityDocument
*/
public function removeEntity( EntityId $entityId ) {
try {
$oldEntity = $this->getEntity( $entityId );
if ( $oldEntity && ( $oldEntity instanceof Item ) ) {
// clean up old sitelinks
$this->unregisterSiteLinks( $entityId );
}
} catch ( StorageException $ex ) {
$oldEntity = null; // ignore
}
$key = $entityId->getSerialization();
unset( $this->entities[$key] );
unset( $this->redirects[$key] );
return $oldEntity;
}
/**
* @see SiteLinkLookup::getLinks
*
* @param int[] $numericIds Numeric (unprefixed) item ids
* @param string[] $siteIds
* @param string[] $pageNames
*
* @return array[]
*/
public function getLinks( array $numericIds = array(), array $siteIds = array(), array $pageNames = array() ) {
return $this->siteLinkStore->getLinks( $numericIds, $siteIds, $pageNames );
}
/**
* Fetches the entities with provided ids and returns them.
* The result array contains the prefixed entity ids as keys.
* The values are either an Entity or null, if there is no entity with the associated id.
*
* The revisions can be specified as an array holding an integer element for each
* id in the $entityIds array or false for latest. If all should be latest, false
* can be provided instead of an array.
*
* @since 0.4
*
* @param EntityId[] $entityIds
*
* @return EntityDocument|null[]
*/
public function getEntities( array $entityIds ) {
$entities = array();
foreach ( $entityIds as $entityId ) {
if ( is_string( $entityId ) ) {
$entityId = $this->parseId( $entityId );
}
$entities[$entityId->getSerialization()] = $this->getEntity( $entityId );
}
return $entities;
}
/**
* @see SiteLinkLookup::getSiteLinksForItem
*
* @param ItemId $itemId
*
* @return SiteLink[]
*/
public function getSiteLinksForItem( ItemId $itemId ) {
return $this->siteLinkStore->getSiteLinksForItem( $itemId );
}
/**
* @param string $propertyLabel
* @param string $languageCode
*
* @return EntityDocument|null
*/
public function getPropertyByLabel( $propertyLabel, $languageCode ) {
foreach ( array_keys( $this->entities ) as $idString ) {
$propertyId = $this->parseId( $idString );
if ( !( $propertyId instanceof PropertyId ) ) {
continue;
}
$property = $this->getEntity( $propertyId );
if ( !( $property instanceof FingerprintProvider ) ) {
continue;
}
$labels = $property->getFingerprint()->getLabels();
if ( $labels->hasTermForLanguage( $languageCode )
&& $labels->getByLanguage( $languageCode )->getText() === $propertyLabel
) {
return $property;
}
}
return null;
}
/**
* @param EntityId[] $entityIds
*
* @return GenericEntityInfoBuilder
*/
public function newEntityInfoBuilder( array $entityIds ) {
return new GenericEntityInfoBuilder( $entityIds, new BasicEntityIdParser(), $this );
}
/**
* @see PropertyDataTypeLookup::getDataTypeIdForProperty
*
* @since 0.5
*
* @param PropertyId $propertyId
*
* @return string
* @throws PropertyDataTypeLookupException
*/
public function getDataTypeIdForProperty( PropertyId $propertyId ) {
$entity = $this->getEntity( $propertyId );
if ( $entity instanceof Property ) {
return $entity->getDataTypeId();
}
throw new PropertyDataTypeLookupException( $propertyId );
}
/**
* @see EntityRevisionLookup::getLatestRevisionId
*
* @param EntityId $entityId
* @param string $mode
*
* @return int|false
*/
public function getLatestRevisionId( EntityId $entityId, $mode = self::LATEST_FROM_SLAVE ) {
try {
$revision = $this->getEntityRevision( $entityId, $mode );
} catch ( RevisionedUnresolvedRedirectException $e ) {
return false;
}
return $revision === null ? false : $revision->getRevisionId();
}
/**
* Stores the given Entity.
*
* @param EntityDocument $entity the entity to save.
* @param string $summary ignored
* @param User $user ignored
* @param int $flags EDIT_XXX flags, as defined for WikiPage::doEditContent.
* @param int|bool $baseRevisionId the revision ID $entity is based on. Saving should fail if
* $baseRevId is no longer the current revision.
*
* @see WikiPage::doEditContent
*
* @return EntityRevision
* @throws StorageException
*/
public function saveEntity( EntityDocument $entity, $summary, User $user, $flags = 0, $baseRevisionId = false ) {
$entityId = $entity->getId();
$status = Status::newGood();
if ( ( $flags & EDIT_NEW ) > 0 && $entityId && $this->hasEntity( $entityId ) ) {
$status->fatal( 'edit-already-exists' );
}
if ( ( $flags & EDIT_UPDATE ) > 0 && !$this->hasEntity( $entityId ) ) {
$status->fatal( 'edit-gone-missing' );
}
if ( $baseRevisionId !== false && !$this->hasEntity( $entityId ) ) {
//TODO: find correct message key to use with status??
throw new StorageException( 'No base revision found for ' . $entityId->getSerialization() );
}
if ( $baseRevisionId !== false && $this->getEntityRevision( $entityId )->getRevisionId() !== $baseRevisionId ) {
$status->fatal( 'edit-conflict' );
}
if ( !$status->isOK() ) {
throw new StorageException( $status );
}
$revision = $this->putEntity( $entity, 0, 0, $user );
$this->putLog( $revision->getRevisionId(), $entity->getId(), $summary, $user->getName() );
return $revision;
}
/**
* @see EntityStore::saveRedirect
*
* @param EntityRedirect $redirect
* @param string $summary
* @param User $user
* @param int $flags
* @param int|bool $baseRevisionId
*
* @throws StorageException If the given type of entity does not support redirects
* @return int The revision id created by storing the redirect
*/
public function saveRedirect( EntityRedirect $redirect, $summary, User $user, $flags = 0, $baseRevisionId = false ) {
if ( !( $redirect->getEntityId() instanceof ItemId ) ) {
throw new StorageException( 'Entity type does not support redirects: ' . $redirect->getEntityId()->getEntityType() );
}
$this->putRedirect( $redirect );
$revisionId = ++$this->maxRevisionId;
$this->putLog( $revisionId, $redirect->getEntityId(), $summary, $user->getName() );
return $revisionId;
}
/**
* Deletes the given entity in some underlying storage mechanism.
*
* @param EntityId $entityId
* @param string $reason the reason for deletion
* @param User $user
*/
public function deleteEntity( EntityId $entityId, $reason, User $user ) {
$this->removeEntity( $entityId );
}
/**
* Check if no edits were made by other users since the given revision.
* This makes the assumption that revision ids are monotonically increasing.
*
* @see EditPage::userWasLastToEdit
*
* @param User $user the user
* @param EntityId $entityId the entity to check
* @param int $lastRevisionId the revision to check from
*
* @return bool
*/
public function userWasLastToEdit( User $user, EntityId $entityId, $lastRevisionId ) {
$key = $entityId->getSerialization();
if ( !isset( $this->entities[$key] ) ) {
return false;
}
/** @var EntityRevision $revision */
foreach ( $this->entities[$key] as $revision ) {
if ( $revision->getRevisionId() >= $lastRevisionId ) {
if ( isset( $revision->user ) && $revision->user !== $user->getName() ) {
return false;
}
}
}
return true;
}
/**
* Watches or unwatches the entity.
*
* @param User $user
* @param EntityId $entityId the entity to watch
* @param bool $watch whether to watch or unwatch the page.
*/
public function updateWatchlist( User $user, EntityId $entityId, $watch ) {
if ( $watch ) {
$this->watchlist[ $user->getName() ][ $entityId->getSerialization() ] = true;
} else {
unset( $this->watchlist[ $user->getName() ][ $entityId->getSerialization() ] );
}
}
/**
* Determines whether the given user is watching the given item
*
* @param User $user
* @param EntityId $entityId the entity to watch
*
* @return bool
*/
public function isWatching( User $user, EntityId $entityId ) {
return isset( $this->watchlist[ $user->getName() ][ $entityId->getSerialization() ] );
}
private function updateMaxNumericId( EntityId $id ) {
if ( method_exists( $id, 'getNumericId' ) ) {
$numericId = $id->getNumericId();
} else {
// FIXME: This is a generic implementation of getNumericId for entities without.
$numericId = (int)preg_replace( '/^\D+/', '', $id->getSerialization() );
}
$this->maxEntityId = max( $this->maxEntityId, $numericId );
}
/**
* @see EntityStore::assignFreshId
*
* @param EntityDocument $entity
*
* @throws InvalidArgumentException when the entity type does not support setting numeric ids.
*/
public function assignFreshId( EntityDocument $entity ) {
//TODO: Find a canonical way to generate an EntityId from the maxId number.
//XXX: Using setId() with an integer argument is deprecated!
$numericId = ++$this->maxEntityId;
$entity->setId( $numericId );
}
/**
* @param string $idString
*
* @return ItemId|PropertyId
*/
private function parseId( $idString ) {
$parser = new BasicEntityIdParser();
return $parser->parse( $idString );
}
/**
* @param int $revisionId
* @param EntityId|string $entityId
* @param string $summary
* @param User|string $user
*/
private function putLog( $revisionId, $entityId, $summary, $user ) {
if ( $entityId instanceof EntityId ) {
$entityId = $entityId->getSerialization();
}
if ( $user instanceof User ) {
$user = $user->getName();
}
$this->log[$revisionId] = array(
'revision' => $revisionId,
'entity' => $entityId,
'summary' => $summary,
'user' => $user,
);
}
/**
* Returns the log entry for the given revision Id.
*
* @param int $revisionId
*
* @return array|null An associative array containing the fields
* 'revision', 'entity', 'summary', and 'user'.
*/
public function getLogEntry( $revisionId ) {
return array_key_exists( $revisionId, $this->log ) ? $this->log[$revisionId] : null;
}
/**
* Returns the newest (according to the revision id) log entry
* for the given entity.
*
* @param EntityId|string $entityId
*
* @return array|null An associative array containing the fields
* 'revision', 'entity', 'summary', and 'user'.
*/
public function getLatestLogEntryFor( $entityId ) {
if ( $entityId instanceof EntityId ) {
$entityId = $entityId->getSerialization();
}
// log entries by revision id, largest id first.
$log = $this->log;
krsort( $log );
foreach ( $log as $entry ) {
if ( $entry['entity'] === $entityId ) {
return $entry;
}
}
return null;
}
/**
* Returns the IDs that redirect to (are aliases of) the given target entity.
*
* @since 0.5
*
* @param EntityId $targetId
*
* @return EntityId[]
*/
public function getRedirectIds( EntityId $targetId ) {
$redirects = array();
foreach ( $this->redirects as $redirRev ) {
$redir = $redirRev->getRedirect();
if ( $redir->getTargetId()->equals( $targetId ) ) {
$redirects[] = $redir->getEntityId();
}
}
return $redirects;
}
/**
* Returns the redirect target associated with the given redirect ID.
*
* @since 0.5
*
* @param EntityId $entityId
* @param string $forUpdate
*
* @return EntityId|null The ID of the redirect target, or null if $entityId
* does not refer to a redirect
* @throws EntityRedirectLookupException
*/
public function getRedirectForEntityId( EntityId $entityId, $forUpdate = '' ) {
$key = $entityId->getSerialization();
if ( isset( $this->redirects[$key] ) ) {
return $this->redirects[$key]->getRedirect()->getTargetId();
}
if ( isset( $this->entities[$key] ) ) {
return null;
}
throw new EntityRedirectLookupException( $entityId );
}
}