Daniel Werner has submitted this change and it was merged. Change subject: (Bug 42063, bug 44577) RDF for Special:EntityData ......................................................................
(Bug 42063, bug 44577) RDF for Special:EntityData Special:EntityData supports RDF/XML output when using the .rdf postfix and N-Triples output when using the .nt postfix. EasyRdf ( http://www.easyrdf.org ) is introduced for easy modeling and exporting of the data model as RDF. Patch by Anja Jentzsch, Thomas Pellissier Tanon, Daniel Kinzler Change-Id: Ie9f2f8944a65750077152a69923c0a0b18a49d5e --- A .gitmodules M lib/includes/specials/SpecialWikibasePage.php M repo/Wikibase.classes.php M repo/Wikibase.php M repo/includes/WikibaseRepo.php A repo/includes/content/RdfSerializer.php A repo/includes/content/easyRdf M repo/includes/specials/SpecialEntityData.php M repo/tests/phpunit/includes/WikibaseRepoTest.php A repo/tests/phpunit/includes/content/RdfSerializerTest.php M repo/tests/phpunit/includes/specials/SpecialEntityDataTest.php 11 files changed, 870 insertions(+), 48 deletions(-) Approvals: Daniel Werner: Verified; Looks good to me, approved jenkins-bot: Verified diff --git a/.gitmodules b/.gitmodules new file mode 100755 index 0000000..794faa1 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "repo/includes/content/easyRdf"] + path = repo/includes/content/easyRdf + url = https://github.com/Wikidata/easyrdf_lite.git diff --git a/lib/includes/specials/SpecialWikibasePage.php b/lib/includes/specials/SpecialWikibasePage.php index e254315..74d4815 100644 --- a/lib/includes/specials/SpecialWikibasePage.php +++ b/lib/includes/specials/SpecialWikibasePage.php @@ -67,8 +67,6 @@ * @since 0.1 * * @param string|null $subPage - * - * @return boolean */ public function execute( $subPage ) { $subPage = is_null( $subPage ) ? '' : $subPage; @@ -81,10 +79,7 @@ // If the user is authorized, display the page, if not, show an error. if ( !$this->userCanExecute( $this->getUser() ) ) { $this->displayRestrictionError(); - return false; } - - return true; } } diff --git a/repo/Wikibase.classes.php b/repo/Wikibase.classes.php index 20394f4..3b8136c 100644 --- a/repo/Wikibase.classes.php +++ b/repo/Wikibase.classes.php @@ -96,6 +96,30 @@ 'Wikibase\ItemHandler' => 'includes/content/ItemHandler.php', 'Wikibase\PropertyContent' => 'includes/content/PropertyContent.php', 'Wikibase\PropertyHandler' => 'includes/content/PropertyHandler.php', + 'Wikibase\RdfSerializer' => 'includes/content/RdfSerializer.php', + + // EasyRdf + 'EasyRdf_Exception' => 'includes/content/easyRdf/EasyRdf/Exception.php', + 'EasyRdf_Format' => 'includes/content/easyRdf/EasyRdf/Format.php', + 'EasyRdf_Graph' => 'includes/content/easyRdf/EasyRdf/Graph.php', + 'EasyRdf_Namespace' => 'includes/content/easyRdf/EasyRdf/Namespace.php', + 'EasyRdf_Literal' => 'includes/content/easyRdf/EasyRdf/Literal.php', + 'EasyRdf_Literal_Boolean' => 'includes/content/easyRdf/EasyRdf/Literal/Boolean.php', + 'EasyRdf_Literal_Date' => 'includes/content/easyRdf/EasyRdf/Literal/Date.php', + 'EasyRdf_Literal_DateTime' => 'includes/content/easyRdf/EasyRdf/Literal/DateTime.php', + 'EasyRdf_Literal_Decimal' => 'includes/content/easyRdf/EasyRdf/Literal/Decimal.php', + 'EasyRdf_Literal_HexBinary' => 'includes/content/easyRdf/EasyRdf/Literal/HexBinary.php', + 'EasyRdf_Literal_Integer' => 'includes/content/easyRdf/EasyRdf/Literal/Integer.php', + 'EasyRdf_Resource' => 'includes/content/easyRdf/EasyRdf/Resource.php', + 'EasyRdf_Serialiser' => 'includes/content/easyRdf/EasyRdf/Serialiser.php', + 'EasyRdf_Serialiser_GraphViz' => 'includes/content/easyRdf/EasyRdf/Serialiser/GraphViz.php', + 'EasyRdf_Serialiser_RdfPhp' => 'includes/content/easyRdf/EasyRdf/Serialiser/RdfPhp.php', + 'EasyRdf_Serialiser_Ntriples' => 'includes/content/easyRdf/EasyRdf/Serialiser/Ntriples.php', + 'EasyRdf_Serialiser_Json' => 'includes/content/easyRdf/EasyRdf/Serialiser/Json.php', + 'EasyRdf_Serialiser_RdfXml' => 'includes/content/easyRdf/EasyRdf/Serialiser/RdfXml.php', + 'EasyRdf_Serialiser_Turtle' => 'includes/content/easyRdf/EasyRdf/Serialiser/Turtle.php', + 'EasyRdf_TypeMapper' => 'includes/content/easyRdf/EasyRdf/TypeMapper.php', + 'EasyRdf_Utils' => 'includes/content/easyRdf/EasyRdf/Utils.php', // includes/specials 'SpecialNewEntity' => 'includes/specials/SpecialNewEntity.php', diff --git a/repo/Wikibase.php b/repo/Wikibase.php old mode 100644 new mode 100755 diff --git a/repo/includes/WikibaseRepo.php b/repo/includes/WikibaseRepo.php index e3245b6..da21dc7 100644 --- a/repo/includes/WikibaseRepo.php +++ b/repo/includes/WikibaseRepo.php @@ -76,6 +76,19 @@ } /** + * Returns the base to use when generating URIs for use in RDF output. + * + * @return string + */ + public function getRdfBaseURI() { + global $wgServer; //TODO: make this configurable + + $uri = $wgServer; + $uri = preg_replace( '!^//!', 'http://', $uri ); + return $uri; + } + + /** * @since 0.4 * * @return EntityIdParser diff --git a/repo/includes/content/RdfSerializer.php b/repo/includes/content/RdfSerializer.php new file mode 100755 index 0000000..01c3e36 --- /dev/null +++ b/repo/includes/content/RdfSerializer.php @@ -0,0 +1,272 @@ +<?php + +namespace Wikibase; + +/** + * RDF mapping for wikibase data model. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @since 0.4 + * + * @file + * @ingroup WikibaseRepo + * @ingroup Content + * + * @licence GNU GPL v2+ + * @author Anja Jentzsch < [email protected] > + * @author Thomas Pellissier Tanon + * @author Daniel Kinzler + */ + +use DataTypes\DataTypeFactory; +use EasyRdf_Exception; +use EasyRdf_Format; +use EasyRdf_Graph; +use EasyRdf_Namespace; + +class RdfSerializer { + + const ontologyBaseUri = 'http://www.wikidata.org'; //XXX: Denny made me put the "www" there... + + /** + * @var DataTypeFactory + */ + protected $dataTypeFactory; + + /** + * @var EntityLookup + */ + protected $entityLookup; + + /** + * @var EasyRdf_Format + */ + protected $format; + + /** + * Map of gnames to namespace URIs + * + * @var array + */ + protected $namespaces = array(); + + /** + * @param EasyRdf_Format $format + * @param string $uriBase + * @param EntityLookup $entityLookup + * @param DataTypeFactory $dataTypeFactory + */ + public function __construct( + EasyRdf_Format $format, + $uriBase, + EntityLookup $entityLookup, + DataTypeFactory $dataTypeFactory + ) { + $this->uriBase = $uriBase; + $this->format = $format; + $this->entityLookup = $entityLookup; + $this->dataTypeFactory = $dataTypeFactory; + + $this->namespaces = array( + 'wikibase_ontology' => self::ontologyBaseUri . '/ontology/', + 'wikibase_entity' => $this->uriBase . '/entity/', + 'wikibase_data' => $this->uriBase . '/data/', + 'wikibase_property' => $this->uriBase . '/property/', + 'wikibase_value' => $this->uriBase . '/value/', + 'wikibase_qualifier' => $this->uriBase . '/qualifier/', + 'wikibase_statement' => $this->uriBase . '/statement/', + ); + } + + /** + * Checks whether the necessary libraries for RDF serialization are installed. + */ + public static function isSupported() { + // check that the submodule is present + $file = __DIR__ . '/easyRdf/EasyRdf.php'; + return file_exists( $file ) && class_exists( 'EasyRdf_Graph' ); + } + + /** + * Returns an EasyRdf_Format object for the given format name. + * The name may be a MIME type or a file extension (or a format URI + * or canonical name). + * + * If no format is found for $name, or EasyRdf is not installed, + * this method returns null. + * + * @param $name the name (file extension, mime type) of the desired format. + * + * @return EasyRdf_Format|null the format object, or null if not found. + */ + public static function getFormat( $name ) { + if ( !self::isSupported() ) { + wfWarn( "EasyRdf not found" ); + return null; + } + + try { + $format = EasyRdf_Format::getFormat( $name ); + return $format; + } catch ( EasyRdf_Exception $ex ) { + // noop + } + + return null; + } + + public function getNamespaces() { + return $this->namespaces; + } + + /** + * Inits a new Graph + * + * @return EasyRdf_Graph + */ + protected function newRdfGraph() { + //register namespaces (ugh, static crap) + + foreach ( $this->getNamespaces() as $gname => $uri ) { + EasyRdf_Namespace::set( $gname, $uri ); + } + + return new EasyRdf_Graph(); + } + + /** + * Add an entity to the RDF graph + * + * @param Entity $entity the entity to output. + * @param \Revision $revision for meta data (optional) + * + * @return EasyRdf_Graph + */ + public function buildGraphForEntity( Entity $entity, \Revision $revision = null ) { + $graph = $this->newRdfGraph(); + + $entityUri = 'wikibase_id:' . ucfirst( $entity->getId()->getPrefixedId() ); + $entityResource = $graph->resource( $entityUri ); + + //TODO: filter language + + foreach ( $entity->getLabels() as $languageCode => $labelText ) { + //TODO: also skos:prefLabel + $entityResource->addLiteral( 'rdfs:label', $labelText, $languageCode ); + } + + foreach ( $entity->getDescriptions() as $languageCode => $description ) { + //TODO: use skos:note + $entityResource->addLiteral( 'rdfs:comment', $description, $languageCode ); + } + + foreach ( $entity->getAllAliases() as $languageCode => $aliases ) { + foreach ( $aliases as $alias ) { + //TODO: use skos:altLabel + $entityResource->addLiteral( 'wikibase_ontology:knownAs', $alias, $languageCode ); + } + } + + $claims = $entity->getClaims(); + $claimsByProperty = array(); + foreach( $claims as $claim ) { + $propertyId = $claim->getMainSnak()->getPropertyId(); + $claimsByProperty[$propertyId->getNumericId()][] = $claim; + } + + //TODO: (optionally?) find referenced entities (items and properties) + // and include basic info about them + + /* @var Claim[] $claims */ + foreach( $claimsByProperty as $claims ) { + $counter = 1; + $propertyId = $claims[0]->getMainSnak()->getPropertyId(); + $propertyUri = 'wikibase_property:' . strval( $propertyId ); + + /* @var Property $property */ + $property = $this->entityLookup->getEntity( $propertyId ); + + //$property->getEntity()->getLabel( $languageCode ); + $valueUri = 'wikibase_value:' . strval( $propertyId ); + + foreach( $claims as $claim ) { + $snak = $claim->getMainSnak(); + if ( $snak instanceof PropertyValueSnak ) { + $value = $claim->getMainSnak()->getDataValue()->getValue(); + + $statementUri = 'wikibase_statement:' . ucfirst( $claim->getGuid() ); + $statementResource = $graph->resource( $statementUri, array( 'wikibase_ontology:Statement' ) ); + + $entityResource->addResource( $propertyUri, $statementResource ); + if ( $property->getType() == $this->dataTypeFactory->getType( 'wikibase-item' ) ) { + $value = 'wikibase_id:' . ucfirst( $value ); + $statementResource->addResource( $valueUri, $value ); + } + if ( $property->getType() == $this->dataTypeFactory->getType( 'commonsMedia' ) ) { + $statementResource->addResource( $valueUri, $value ); + } + + //TODO: sane mechanism for extending this? Some kind of expert logic? + + $counter += 1; + } + + //TODO: handle NoValueSnak and SomeValueSnak! + } + } + + // TODO: sitelinks, use skos:isPrimarySubjectOf or foaf:primaryTopic? + // + state language of resource + // TODO: use rdf:about (or what?!) to link subject URI to document URI + + return $graph; + } + + /** + * Returns the serialized graph + * + * @param EasyRdf_Graph $graph the graph to serialize + * + * @return string + */ + public function serializeRdf( EasyRdf_Graph $graph ) { + $serialiser = $this->format->newSerialiser(); + $data = $serialiser->serialise( $graph, $this->format->getName() ); + + assert( is_string( $data ) ); + return $data; + } + + /** + * Returns the serialized entity. + * Shorthand for $this->serializeRdf( $this->buildGraphForEntity( $entity ) ). + * + * @param Entity $entity the entity to serialize + * @param \Revision $revision for meta data (optional) + * + * @return string + */ + public function serializeEntity( Entity $entity, \Revision $revision = null ) { + $graph = $this->buildGraphForEntity( $entity, $revision ); + $data = $this->serializeRdf( $graph ); + return $data; + } + + public function getDefaultMimeType() { + return $this->format->getDefaultMimeType(); + } +} diff --git a/repo/includes/content/easyRdf b/repo/includes/content/easyRdf new file mode 160000 index 0000000..f74a880 --- /dev/null +++ b/repo/includes/content/easyRdf +Subproject commit f74a88084d3a0f8e15a6e5d5a311e8d4876cc7c5 diff --git a/repo/includes/specials/SpecialEntityData.php b/repo/includes/specials/SpecialEntityData.php old mode 100644 new mode 100755 index d5a3b84..7b35bc1 --- a/repo/includes/specials/SpecialEntityData.php +++ b/repo/includes/specials/SpecialEntityData.php @@ -1,10 +1,11 @@ <?php +use DataTypes\DataTypeFactory; +use \Wikibase\Entity; use \Wikibase\EntityContent; use \Wikibase\EntityContentFactory; use \Wikibase\EntityId; - -//XXX: our special pages are not in the wikibase namespace. why? some crappy internal magic going on? +use \Wikibase\RdfSerializer; /** * Special page to act as a data endpoint for the linked data web. @@ -18,13 +19,15 @@ * representation of data entities. Using the ContentHandler to serialize the entity would expose * internal implementation details. * - * @since 0.3 + * @since 0.4 * * @file * @ingroup WikibaseRepo * * @licence GNU GPL v2+ * @author Daniel Kinzler + * @author Thomas Pellissier Tanon + * @author Anja Jentzsch < [email protected] > */ class SpecialEntityData extends SpecialWikibasePage { @@ -45,9 +48,24 @@ ); /** + * @var string + */ + protected $rdfBaseURI = null; + + /** + * @var Wikibase\EntityLookup + */ + protected $entityLookup = null; + + /** + * @var DataTypeFactory + */ + protected $dataTypeFactory = null; + + /** * Constructor. * - * @since 0.1 + * @since 0.4 */ public function __construct() { parent::__construct( 'EntityData' ); @@ -56,11 +74,9 @@ /** * Main method. * - * @since 0.1 + * @since 0.4 * * @param string|null $subPage - * - * @return boolean */ public function execute( $subPage ) { $revision = 0; @@ -92,13 +108,35 @@ return; } + $repo = \Wikibase\Repo\WikibaseRepo::getDefaultInstance(); + $this->rdfBaseURI = $repo->getRdfBaseURI(); + $this->entityLookup = \Wikibase\StoreFactory::getStore()->getEntityLookup(); + $this->dataTypeFactory = $repo->getDataTypeFactory(); + + + $this->showData( $format, $id, $revision ); + } + + /** + * Output entity data. + * + * @param string $format The name (mime type of file extension) of the format to use + * @param string $id The entity ID + * @param int|string $revision The entity revision + * + * @throws HttpError + */ + public function showData( $format, $id, $revision ) { + //TODO: handle IfModifiedSince! + $serializer = $this->createApiSerializer( $format ); - $apiFormat = self::getApiFormatName( $format ); - $printer = $apiFormat === null ? null : $this->createApiPrinter( $apiFormat ); + if ( !$serializer ) { + $serializer = $this->createRdfSerializer( $format ); + } - if ( !$printer ) { + if ( !$serializer ) { throw new \HttpError( 415, wfMessage( 'wikibase-entitydata-unsupported-format' )->params( $format ) ); } @@ -140,7 +178,16 @@ $rev = $page->getRevision(); } - $this->showData( $entity, $printer, $rev ); + + if( $serializer instanceof \Wikibase\RdfSerializer ) { + $data = $serializer->serializeEntity( $entity->getEntity(), $rev ); + $contentType = $serializer->getDefaultMimeType(); + } else { + $data = $this->apiSerialize( $entity->getEntity(), $serializer, $rev ); + $contentType = $serializer->getIsHtml() ? 'text/html' : $serializer->getMimeType(); + } + + $this->outputData( $data, $contentType, $rev ); } /** @@ -203,24 +250,51 @@ } /** - * Creates a printer that can generate the given output format. + * Creates an API printer that can generate the given output format. * * @param String $format The desired serialization format, - * as a format name understood by ApiBase. + * as a format name understood by ApiBase or EasyRdf_Format * * @return ApiFormatBase|null A suitable result printer, or null + * if the given format is not supported by the API. + */ + protected function createApiSerializer( $format ) { + //MediaWiki formats + $api = $this->getApiMain( $format ); + $formats = $api->getFormats(); + $formatName = self::getApiFormatName( $format ); + if ( $formatName !== null && array_key_exists( $formatName, $formats ) ) { + return $api->createPrinterByName( $formatName ); + } + + return null; + } + + /** + * Creates an Rdf Serializer that can generate the given output format. + * + * @param String $format The desired serialization format, + * as a format name understood by ApiBase or EasyRdf_Format + * + * @return RdfSerializer|null A suitable result printer, or null * if the given format is not supported. */ - protected function createApiPrinter( $format ) { - $api = $this->getApiMain( $format ); + protected function createRdfSerializer( $format ) { + //MediaWiki formats + $rdfFormat = \Wikibase\RdfSerializer::getFormat( $format ); - $formats = $api->getFormats(); - if ( !array_key_exists( $format, $formats ) ) { + if ( !$rdfFormat ) { return null; } - $printer = $api->createPrinterByName( $format ); - return $printer; + $serializer = new RdfSerializer( + $rdfFormat, + $this->rdfBaseURI, + $this->entityLookup, + $this->dataTypeFactory + ); + + return $serializer; } /** @@ -229,19 +303,19 @@ * result, if $printer was generated from that same ApiMain module, as * createApiPrinter() does. * - * @param Wikibase\EntityContent $entityContent The entity to convert ot an ApiResult + * @param Wikibase\Entity $entity The entity to convert ot an ApiResult * @param ApiFormatBase $printer The output printer that will be used for serialization. * Used to provide context for generating the ApiResult, and may also be manipulated * to fine-tune the output. * * @return ApiResult */ - protected function generateApiResult( EntityContent $entityContent, ApiFormatBase $printer ) { + protected function generateApiResult( Entity $entity, ApiFormatBase $printer ) { wfProfileIn( __METHOD__ ); $format = strtolower( $printer->getFormat() ); //XXX: hack! - $entityKey = 'entity'; //XXX: perhaps better: $entity->getEntity()->getType(); + $entityKey = 'entity'; //XXX: perhaps better: $entity->getType(); $basePath = array(); $res = $this->getApiMain( $format )->getResult(); @@ -260,14 +334,14 @@ } $serializerFactory = new \Wikibase\Lib\Serializers\SerializerFactory(); - $serializer =$serializerFactory->newSerializerForObject( $entityContent->getEntity() ); + $serializer =$serializerFactory->newSerializerForObject( $entity ); $opt = new \Wikibase\Lib\Serializers\EntitySerializationOptions(); $opt->setIndexTags( $res->getIsRawMode() ); //FIXME: $res->rawMode doesn't seem to be set to what we want. $opt->setProps( self::$fieldsToShow ); //FIXME: someone does not know how to write clear FIXMEs $serializer->setOptions( $opt ); - $arr = $serializer->getSerialized( $entityContent->getEntity() ); + $arr = $serializer->getSerialized( $entity ); // we want the entity to *be* the result, not *in* the result foreach ( $arr as $key => $value ) { @@ -279,17 +353,19 @@ } /** - * Serialize the entity data using the provided format and send it as the HTTP response body. + * Serialize the entity data using the provided format. * * Note that we are using the API's serialization facility to ensure a consistent external * representation of data entities. Using the ContentHandler to serialize the entity would * expose internal implementation details. * - * @param Wikibase\EntityContent $entity the entity to output. + * @param Wikibase\Entity $entity the entity to output. * @param ApiFormatBase $printer the printer to use to generate the output * @param Revision $revision the entity's revision (optional) + * + * @return string the serialized data */ - public function output( EntityContent $entity, ApiFormatBase $printer, Revision $revision ) { + public function apiSerialize( Entity $entity, ApiFormatBase $printer, Revision $revision = null ) { // NOTE: The way the ApiResult is provided to $printer is somewhat // counter-intuitive. Basically, the relevant ApiResult object // is owned by the ApiMain module provided by getApiMain(). @@ -300,34 +376,37 @@ //XXX: really inject meta-info? where else should we put it? $basePath = array(); - $res->addValue( $basePath , '_revision_', intval( $revision->getId() ) ); - $res->addValue( $basePath , '_modified_', wfTimestamp( TS_ISO_8601, $revision->getTimestamp() ) ); + if ( $revision ) { + $res->addValue( $basePath , '_revision_', intval( $revision->getId() ) ); + $res->addValue( $basePath , '_modified_', wfTimestamp( TS_ISO_8601, $revision->getTimestamp() ) ); + } $this->getOutput()->disable(); // don't generate HTML $printer->profileIn(); $printer->initPrinter( false ); + $printer->setBufferResult( true ); // Outputs the ApiResult held by the ApiMain module, which is hopefully the same as $res + //NOTE: this can and will mess with the HTTP response! $printer->execute(); + $data = $printer->getBuffer(); $printer->closePrinter(); $printer->profileOut(); - //FIXME: Content-Length is bogus! Where does it come from?! + return $data; } /** * Output the entity data and set the appropriate HTTP response headers. * - * @param Wikibase\EntityContent $entity the entity to output. - * @param ApiFormatBase $printer the printer to use to generate the output - * @param Revision $revision the entity's revision (optional) - * - * @throws HttpError If an unsupported format is requested. + * @param string $data the data to output + * @param string $contentType the data's mime type + * @param Revision $revision */ - public function showData( EntityContent $entity, ApiFormatBase $printer, Revision $revision = null ) { + public function outputData( $data, $contentType, Revision $revision = null ) { global $wgSquidMaxage; // NOTE: similar code as in RawAction::onView, keep in sync. @@ -342,18 +421,30 @@ $maxage = max( 0, min( 60 * 60 * 24 * 30, $maxage ) ); $smaxage = max( 0, min( 60 * 60 * 24 * 30, $smaxage ) ); - //TODO: set Last-Modified header? Why doesn't mediawiki set that for article pages? - - // make sure we are reporting the correct content type - $contentType = $printer->getIsHtml() ? 'text/html' : $printer->getMimeType(); $response->header( 'Content-Type: ' . $contentType . '; charset=UTF-8' ); + $response->header( 'Content-Length: ' . strlen( $data ) ); + + if ( $revision ) { + $response->header( 'Last-Modified: ' . wfTimestamp( TS_ISO_8601, $revision->getTimestamp() ) ); + } + + //Set X-Frame-Options API results (bug 39180) + global $wgApiFrameOptions; + if ( $wgApiFrameOptions ) { + $response->header( "X-Frame-Options: $wgApiFrameOptions" ); + } // allow the client to cache this $mode = 'public'; $response->header( 'Cache-Control: ' . $mode . ', s-maxage=' . $smaxage . ', max-age=' . $maxage ); - //FIXME: Cache-Control and Expires headers are apparently overwritten later! - $this->output( $entity, $printer, $revision ); + $this->getOutput()->disable(); // don't generate HTML + ob_clean(); + + print $data; + flush(); + + //die(); //FIXME: figure out how to best shut down here. } } diff --git a/repo/tests/phpunit/includes/WikibaseRepoTest.php b/repo/tests/phpunit/includes/WikibaseRepoTest.php index a4ad7c9..f8171c9 100644 --- a/repo/tests/phpunit/includes/WikibaseRepoTest.php +++ b/repo/tests/phpunit/includes/WikibaseRepoTest.php @@ -34,8 +34,9 @@ * * @licence GNU GPL v2+ * @author Jeroen De Dauw < [email protected] > + * @author Daniel Kinzler */ -class WikibaseRepoTest extends \PHPUnit_Framework_TestCase { +class WikibaseRepoTest extends \MediaWikiTestCase { /** * @return WikibaseRepo @@ -59,4 +60,21 @@ $this->assertInstanceOf( 'Wikibase\Lib\EntityIdParser', $returnValue ); } + public static function provideGetRdfBaseURI() { + return array( + array ( 'http://acme.test', 'http://acme.test' ), + array ( 'https://acme.test', 'https://acme.test' ), + array ( '//acme.test', 'http://acme.test' ), + ); + } + + /** + * @dataProvider provideGetRdfBaseURI + */ + public function testGetRdfBaseURI( $server, $expected ) { + $this->setMwGlobals( 'wgServer', $server ); + + $returnValue = $this->newInstance()->getRdfBaseURI(); + $this->assertEquals( $expected, $returnValue ); + } } diff --git a/repo/tests/phpunit/includes/content/RdfSerializerTest.php b/repo/tests/phpunit/includes/content/RdfSerializerTest.php new file mode 100644 index 0000000..7de00d5 --- /dev/null +++ b/repo/tests/phpunit/includes/content/RdfSerializerTest.php @@ -0,0 +1,384 @@ +<?php + +namespace Wikibase\Test; +use DataTypes\DataTypeFactory; +use EasyRdf_Namespace; +use MediaWikiSite; +use Wikibase\Entity; +use Wikibase\EntityId; +use Wikibase\Item; +use Wikibase\RdfSerializer; +use Wikibase\SiteLink; + +/** + * Tests for the Wikibase\RdfSerializer class. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @since 0.4 + * + * @ingroup WikibaseRepoTest + * @ingroup Test + * + * @group Wikibase + * @group WikibaseRepo + * @group WikibaseProperty + * @group WikibaseEntity + * @group WikibaseEntityHandler + * + * @licence GNU GPL v2+ + * @author Daniel Kinzler + */ +class RdfSerializerTest extends \MediaWikiTestCase { + + protected static $formats = array( + 'rdf', + 'application/rdf+xml', + 'n3', + 'text/n3', + 'nt', + 'ntriples', + 'turtle', + ); + + protected static $dataTypes = array( + 'commonsMedia' => array( + 'datavalue' => 'string', + ), + 'string' => array( + 'datavalue' => 'string', + ), + 'geo-coordinate' => array( + 'datavalue' => 'geocoordinate', + ), + 'quantity' => array( + 'datavalue' => 'quantity', + ), + 'monolingual-text' => array( + 'datavalue' => 'monolingualtext', + ), + 'multilingual-text' => array( + 'datavalue' => 'multilingualtext', + ), + 'time' => array( + 'datavalue' => 'time', + ), + ); + + protected static $uriBase = 'http://acme.test'; + + public function setUp() { + parent::setUp(); + + if ( !RdfSerializer::isSupported() ) { + $this->markTestSkipped( "RDF library not found" ); + } + } + + /** + * @return Entity[] + */ + protected static function getTestEntities() { + static $entities = array(); + + if ( !empty( $entities ) ) { + return $entities; + } + + $entity = Item::newEmpty(); + $entities['empty'] = $entity; + + + $entity = Item::newEmpty(); + $entities['terms'] = $entity; + + $entity->setLabel( 'en', 'Berlin' ); + $entity->setLabel( 'ru', 'Берлин' ); + + $entity->setDescription( 'en', 'German city' ); + $entity->setDescription( 'ru', 'столица и одновременно земля Германии' ); + + $entity->addAliases( 'en', array( 'Berlin, Germany', 'Land Berlin' ) ); + $entity->addAliases( 'ru', array( 'Berlin' ) ); + + // not yet supported + /* + $entity = Item::newEmpty(); + $entities['links'] = $entity; + + $entity->setLabel( 'en', 'Berlin' ); + + $entity->addSiteLink( new SiteLink( MediaWikiSite::newFromGlobalId( 'enwiki' ), 'Berlin' ) ); + $entity->addSiteLink( new SiteLink( MediaWikiSite::newFromGlobalId( 'dewiki' ), 'Berlin' ) ); + $entity->addSiteLink( new SiteLink( MediaWikiSite::newFromGlobalId( 'ruwiki' ), 'Берлин' ) ); + */ + + $i = 1; + foreach ( $entities as $entity ) { + $entity->setId( new EntityId( Item::ENTITY_TYPE, $i++ ) ); + } + + return $entities; + } + + /** + * @param \Wikibase\EntityId $entityId + * @param array $properties + * + * @return \EasyRdf_Graph + */ + protected static function makeEntityGraph( EntityId $entityId, $properties ) { + $graph = new \EasyRdf_Graph(); + + $entityUri = 'wikibase_id:' . ucfirst( $entityId->getPrefixedId() ); + $entityResource = $graph->resource( $entityUri ); + + /* @var \EasyRdf_Resource $entityResource */ + foreach ( $properties as $prop => $values ) { + if ( is_scalar( $values ) ) { + $values = array( $values ); + } + + foreach ( $values as $val ) { + $entityResource->add( $prop, $val ); + } + } + + return $graph; + } + + /** + * @return \EasyRdf_Graph[] + */ + protected static function getTestGraphs() { + static $graphs = array(); + + if ( !empty( $graphs ) ) { + return $graphs; + } + + $serializer = self::newRdfSerializer( 'rdf' ); // dummy + + foreach ( $serializer->getNamespaces() as $gname => $uri ) { + EasyRdf_Namespace::set( $gname, $uri ); + } + + $entities = self::getTestEntities(); + + $graphs['empty'] = self::makeEntityGraph( + $entities['empty']->getId(), + array() + ); + + $graphs['terms'] = self::makeEntityGraph( + $entities['terms']->getId(), + array( + 'rdfs:label' => array( + new \EasyRdf_Literal( 'Berlin', 'en' ), + new \EasyRdf_Literal( 'Берлин', 'ru' ) + ), + 'rdfs:comment' => array( + new \EasyRdf_Literal( 'German city', 'en' ), + new \EasyRdf_Literal( 'столица и одновременно земля Германии', 'ru' ) + ), + 'wikibase_ontology:knownAs' => array( + new \EasyRdf_Literal( 'Berlin, Germany', 'en' ), + new \EasyRdf_Literal( 'Land Berlin', 'en' ), + new \EasyRdf_Literal( 'Berlin', 'ru' ) + ), + ) + ); + + // not yet supportted + /* + $graphs['links'] = self::makeEntityGraph( + $entities['links']->getId(), + array( + //'rdfs:label', new \EasyRdf_Literal( '', '' ) + ) + ); + */ + + return $graphs; + } + + protected static function getTestDataPatterns() { + static $patterns = array(); + + if ( !empty( $patterns ) ) { + return $patterns; + } + + $patterns['empty']['rdf'] = '!<rdf:RDF.*</rdf:RDF>!s'; + $patterns['empty']['n3'] = '!!s'; + + $patterns['terms']['rdf'] = '!<rdf:RDF.*' + . '<rdf:Description.*rdf:about="wikibase_id:Q2".*' + . '<rdfs:label xml:lang="en">Berlin</rdfs:label>.*' + . '<rdfs:comment xml:lang="en">German city</rdfs:comment>.*' + . '<wikibase_ontology:knownAs xml:lang="en">Berlin, Germany</wikibase_ontology:knownAs>.*' + . '</rdf:RDF>!s'; + $patterns['terms']['n3'] = '!<wikibase_id:Q2>.*' + . 'rdfs:label +"Berlin"@en,.*' + . 'rdfs:comment +"German city"@en,.*' + . 'wikibase_ontology:knownAs +"Berlin, Germany"@en,.*' + . '!s'; + + // links not yet supported + /* + $patterns['links']['rdf'] = '!xxx!s'; + $patterns['links']['n3'] = '!xxx!s'; + */ + + return $patterns; + } + + + protected static function newRdfSerializer( $formatName ) { + $format = RdfSerializer::getFormat( $formatName ); + + $dataTypes = new DataTypeFactory( self::$dataTypes ); + + $mockRepo = new MockRepository(); + + foreach( self::getTestEntities() as $entity ) { + $mockRepo->putEntity( $entity ); + } + + return new RdfSerializer( + $format, + self::$uriBase, + $mockRepo, + $dataTypes + ); + } + + public static function provideGetFormat() { + return array_map( + function ( $format ) { + return array( $format ); + }, + self::$formats + ); + } + + /** + * @dataProvider provideGetFormat + */ + public function testGetFormat( $name ) { + $format = RdfSerializer::getFormat( $name ); + + $this->assertNotNull( $format, $name ); + } + + public static function provideBuildGraphForEntity() { + $entities = self::getTestEntities(); + $graphs = self::getTestGraphs(); + + $cases = array(); + + foreach ( $entities as $name => $entity ) { + $cases[] = array( + $entity, + $graphs[$name], + ); + } + + return $cases; + } + + /** + * @dataProvider provideBuildGraphForEntity + */ + public function testBuildGraphForEntity( Entity $entity, \EasyRdf_Graph $expectedGraph ) { + $serializer = self::newRdfSerializer( 'rdf' ); + + $graph = $serializer->buildGraphForEntity( $entity ); + //TODO: meta-info from Revision + + foreach ( $expectedGraph->resources() as $rc ) { + foreach ( $expectedGraph->properties( $rc ) as $prop ) { + $expectedValues = $expectedGraph->all( $rc, $prop ); + $actualValues = $graph->all( $rc, $prop ); + $this->assertArrayEquals( $expectedValues, $actualValues ); + } + } + } + + public static function provideSerializeRdf() { + $graphs = self::getTestGraphs(); + $patterns = self::getTestDataPatterns(); + + $cases = array(); + + foreach ( $graphs as $name => $graph ) { + foreach ( self::$formats as $format ) { + if ( isset( $patterns[$name][$format] ) ) { + $cases[] = array( + $graph, + $format, + $patterns[$name][$format], + ); + } + } + } + + return $cases; + } + + /** + * @dataProvider provideSerializeRdf + */ + public function testSerializeRdf( \EasyRdf_Graph $graph, $format, $regex ) { + $serializer = self::newRdfSerializer( $format ); + + $data = $serializer->serializeRdf( $graph ); + $this->assertRegExp( $regex, $data ); + } + + public static function provideSerializeEntity() { + $entities = self::getTestEntities(); + $patterns = self::getTestDataPatterns(); + + $cases = array(); + + foreach ( $entities as $name => $entity ) { + foreach ( self::$formats as $format ) { + if ( isset( $patterns[$name][$format] ) ) { + $cases[] = array( + $entity, + $format, + $patterns[$name][$format], + ); + } + } + } + + return $cases; + } + + /** + * @dataProvider provideSerializeEntity + */ + public function TestSerializeEntity( Entity $entity, $format, $regex ) { + $serializer = self::newRdfSerializer( $format ); + + $data = $serializer->serializeEntity( $entity ); + $this->assertRegExp( $regex, $data ); + } + +} diff --git a/repo/tests/phpunit/includes/specials/SpecialEntityDataTest.php b/repo/tests/phpunit/includes/specials/SpecialEntityDataTest.php index a4d4c2c..a5e091e 100644 --- a/repo/tests/phpunit/includes/specials/SpecialEntityDataTest.php +++ b/repo/tests/phpunit/includes/specials/SpecialEntityDataTest.php @@ -115,6 +115,9 @@ ), '!^a:\d+.*Raarr!', // output regex 200, // http code + array( // headers + 'Content-Type' => 'application/vnd.php.serialized; charset=UTF-8' + ) ); $cases[] = array( // #6: mime type @@ -125,6 +128,9 @@ ), '!^\{.*Raarr!', // output regex 200, // http code + array( // headers + 'Content-Type' => 'application/json; charset=UTF-8' + ) ); $cases[] = array( // #7: bad format @@ -145,6 +151,9 @@ ), '!<entity!', // output regex 200, // http code + array( // headers + 'Content-Type' => 'text/xml; charset=UTF-8' + ) ); $cases[] = array( // #9: evil stuff @@ -158,6 +167,19 @@ 404, // http code ); + $cases[] = array( // #10: RDF+XML + '', // subpage + array( // parameters + 'id' => '{testitemid}', + 'format' => 'rdf', + ), + '!<rdf:RDF.*rdf:about.*</rdf:RDF>!s', // output regex + 200, // http code + array( // headers + 'Content-Type' => 'application/rdf+xml; charset=UTF-8' + ) + ); + $subpageCases = array(); foreach ( $cases as $c ) { -- To view, visit https://gerrit.wikimedia.org/r/51164 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: merged Gerrit-Change-Id: Ie9f2f8944a65750077152a69923c0a0b18a49d5e Gerrit-PatchSet: 24 Gerrit-Project: mediawiki/extensions/Wikibase Gerrit-Branch: master Gerrit-Owner: Anja Jentzsch <[email protected]> Gerrit-Reviewer: Anja Jentzsch <[email protected]> Gerrit-Reviewer: Daniel Kinzler <[email protected]> Gerrit-Reviewer: Daniel Werner <[email protected]> Gerrit-Reviewer: Denny Vrandecic <[email protected]> Gerrit-Reviewer: Jeroen De Dauw <[email protected]> Gerrit-Reviewer: John Erling Blad <[email protected]> Gerrit-Reviewer: Tpt <[email protected]> Gerrit-Reviewer: jenkins-bot _______________________________________________ MediaWiki-commits mailing list [email protected] https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits
