summaryrefslogtreecommitdiff
path: root/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/ConditionBuilder.php
diff options
context:
space:
mode:
Diffstat (limited to 'www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/ConditionBuilder.php')
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/ConditionBuilder.php629
1 files changed, 629 insertions, 0 deletions
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/ConditionBuilder.php b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/ConditionBuilder.php
new file mode 100644
index 00000000..0f8587e8
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/ConditionBuilder.php
@@ -0,0 +1,629 @@
+<?php
+
+namespace SMW\SPARQLStore\QueryEngine;
+
+use RuntimeException;
+use SMW\DataTypeRegistry;
+use SMW\DataValues\PropertyChainValue;
+use SMW\DIProperty;
+use SMW\DIWikiPage;
+use SMW\HierarchyLookup;
+use SMW\Message;
+use SMW\Query\DescriptionFactory;
+use SMW\Query\Language\Description;
+use SMW\SPARQLStore\HierarchyFinder;
+use SMW\SPARQLStore\QueryEngine\Condition\Condition;
+use SMW\SPARQLStore\QueryEngine\Condition\SingletonCondition;
+use SMW\SPARQLStore\QueryEngine\Condition\TrueCondition;
+use SMW\Utils\CircularReferenceGuard;
+use SMWDataItem as DataItem;
+use SMWExpElement as ExpElement;
+use SMWExpNsResource as ExpNsResource;
+use SMWExporter as Exporter;
+use SMWTurtleSerializer as TurtleSerializer;
+
+/**
+ * Build an internal representation for a SPARQL condition from individual query
+ * descriptions
+ *
+ * @license GNU GPL v2+
+ * @since 2.0
+ *
+ * @author Markus Krötzsch
+ * @author mwjames
+ */
+class ConditionBuilder {
+
+ /**
+ * @var EngineOptions
+ */
+ private $engineOptions;
+
+ /**
+ * @var DispatchingDescriptionInterpreter
+ */
+ private $dispatchingDescriptionInterpreter;
+
+ /**
+ * @var CircularReferenceGuard
+ */
+ private $circularReferenceGuard;
+
+ /**
+ * @var HierarchyLookup
+ */
+ private $hierarchyLookup;
+
+ /**
+ * @var DescriptionFactory
+ */
+ private $descriptionFactory;
+
+ /**
+ * @var array
+ */
+ private $errors = [];
+
+ /**
+ * Counter used to generate globally fresh variables.
+ * @var integer
+ */
+ private $variableCounter = 0;
+
+ /**
+ * sortKeys that are being used while building the query conditions
+ * @var array
+ */
+ private $sortKeys = [];
+
+ /**
+ * The name of the SPARQL variable that represents the query result
+ * @var string
+ */
+ private $resultVariable = 'result';
+
+ /**
+ * @var string
+ */
+ private $joinVariable;
+
+ /**
+ * @var DIProperty|null
+ */
+ private $orderByProperty;
+
+ /**
+ * @var array
+ */
+ private $redirectByVariableReplacementMap = [];
+
+ /**
+ * @since 2.2
+ *
+ * @param DescriptionInterpreterFactory $descriptionInterpreterFactory
+ * @param EngineOptions|null $engineOptions
+ */
+ public function __construct( DescriptionInterpreterFactory $descriptionInterpreterFactory, EngineOptions $engineOptions = null ) {
+ $this->dispatchingDescriptionInterpreter = $descriptionInterpreterFactory->newDispatchingDescriptionInterpreter( $this );
+ $this->engineOptions = $engineOptions;
+
+ if ( $this->engineOptions === null ) {
+ $this->engineOptions = new EngineOptions();
+ }
+
+ $this->descriptionFactory = new DescriptionFactory();
+ }
+
+ /**
+ * @since 2.0
+ *
+ * @param string $resultVariable
+ */
+ public function setResultVariable( $resultVariable ) {
+ $this->resultVariable = $resultVariable;
+ return $this;
+ }
+
+ /**
+ * Get a fresh unused variable name for building SPARQL conditions.
+ *
+ * @return string
+ */
+ public function getNextVariable( $prefix = 'v' ) {
+ return $prefix . ( ++$this->variableCounter );
+ }
+
+ /**
+ * @since 2.0
+ *
+ * @param array $sortKeys
+ */
+ public function setSortKeys( $sortKeys ) {
+ $this->sortKeys = $sortKeys;
+ return $this;
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @return array
+ */
+ public function getSortKeys() {
+ return $this->sortKeys;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return array
+ */
+ public function getErrors() {
+ return $this->errors;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param string $error
+ */
+ public function addError( $error, $type = Message::TEXT ) {
+ $this->errors[Message::getHash( $error, $type )] = Message::encode( $error, $type );
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param CircularReferenceGuard $circularReferenceGuard
+ */
+ public function setCircularReferenceGuard( CircularReferenceGuard $circularReferenceGuard ) {
+ $this->circularReferenceGuard = $circularReferenceGuard;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return CircularReferenceGuard
+ */
+ public function getCircularReferenceGuard() {
+ return $this->circularReferenceGuard;
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @param HierarchyLookup $hierarchyLookup
+ */
+ public function setHierarchyLookup( HierarchyLookup $hierarchyLookup ) {
+ $this->hierarchyLookup = $hierarchyLookup;
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @return HierarchyLookup
+ */
+ public function getHierarchyLookup() {
+ return $this->hierarchyLookup;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param string $joinVariable name of the variable that conditions
+ * will refer to
+ */
+ public function setJoinVariable( $joinVariable ) {
+ $this->joinVariable = $joinVariable;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return string
+ */
+ public function getJoinVariable() {
+ return $this->joinVariable;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param DIProperty|null $orderByProperty if given then
+ * this is the property the values of which this condition will refer
+ * to, and the condition should also enable ordering by this value
+ */
+ public function setOrderByProperty( $orderByProperty ) {
+ $this->orderByProperty = $orderByProperty;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return DIProperty|null
+ */
+ public function getOrderByProperty() {
+ return $this->orderByProperty;
+ }
+
+ /**
+ * Get a Condition object for a Description.
+ *
+ * This conversion is implemented by a number of recursive functions,
+ * and this is the main entry point for this recursion. In particular,
+ * it resets global variables that are used for the construction.
+ *
+ * If property value variables should be recorded for ordering results
+ * later on, the keys of the respective properties need to be given in
+ * sortKeys earlier.
+ *
+ * @param Description $description
+ *
+ * @return Condition
+ */
+ public function getConditionFrom( Description $description ) {
+ $this->variableCounter = 0;
+
+ $this->setJoinVariable( $this->resultVariable );
+ $this->setOrderByProperty( null );
+
+ $condition = $this->mapDescriptionToCondition( $description );
+
+ $this->addMissingOrderByConditions(
+ $condition
+ );
+
+ $this->addPropertyPathToMatchRedirectTargets(
+ $condition
+ );
+
+ $this->addFilterToRemoveEntitiesThatContainRedirectPredicate(
+ $condition
+ );
+
+ return $condition;
+ }
+
+ /**
+ * Recursively create a Condition from a Description
+ *
+ * @param Description $description
+ *
+ * @return Condition
+ */
+ public function mapDescriptionToCondition( Description $description ) {
+ return $this->dispatchingDescriptionInterpreter->interpretDescription( $description );
+ }
+
+ /**
+ * Build the condition (WHERE) string for a given Condition.
+ * The function also expresses the single value of
+ * SingletonCondition objects in the condition, which may
+ * lead to additional namespaces for serializing its URI.
+ *
+ * @param Condition $condition
+ *
+ * @return string
+ */
+ public function convertConditionToString( Condition &$condition ) {
+
+ $conditionAsString = $condition->getWeakConditionString();
+
+ if ( ( $conditionAsString === '' ) && !$condition->isSafe() ) {
+ $swivtPageResource = Exporter::getInstance()->getSpecialNsResource( 'swivt', 'page' );
+ $conditionAsString = '?' . $this->resultVariable . ' ' . $swivtPageResource->getQName() . " ?url .\n";
+ }
+
+ $conditionAsString .= $condition->getCondition();
+ $conditionAsString .= $condition->getCogentConditionString();
+
+ if ( $condition instanceof SingletonCondition ) { // prepare for ASK, maybe rather use BIND?
+
+ $matchElement = $condition->matchElement;
+
+ if ( $matchElement instanceof ExpElement ) {
+ $matchElementName = TurtleSerializer::getTurtleNameForExpElement( $matchElement );
+ } else {
+ $matchElementName = $matchElement;
+ }
+
+ if ( $matchElement instanceof ExpNsResource ) {
+ $condition->namespaces[$matchElement->getNamespaceId()] = $matchElement->getNamespace();
+ }
+
+ $conditionAsString = str_replace( '?' . $this->resultVariable . ' ', "$matchElementName ", $conditionAsString );
+ }
+
+ return $conditionAsString;
+ }
+
+ /**
+ * Create an Condition from an empty (true) description.
+ * May still require helper conditions for ordering.
+ *
+ * @param $joinVariable string name, see mapDescriptionToCondition()
+ * @param $orderByProperty mixed DIProperty or null, see mapDescriptionToCondition()
+ *
+ * @return Condition
+ */
+ public function newTrueCondition( $joinVariable, $orderByProperty ) {
+ $result = new TrueCondition();
+ $this->addOrderByDataForProperty( $result, $joinVariable, $orderByProperty );
+ return $result;
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @param DataItem|null $dataItem
+ *
+ * @return string|null
+ */
+ public function tryToFindRedirectVariableForDataItem( DataItem $dataItem = null ) {
+
+ if ( !$dataItem instanceof DIWikiPage || !$this->isSetFlag( SMW_SPARQL_QF_REDI ) ) {
+ return null;
+ }
+
+ // Maybe there is a better way to verify the "isRedirect" state other
+ // than by using the Title object
+ if ( $dataItem->getTitle() === null || !$dataItem->getTitle()->isRedirect() ) {
+ return null;
+ }
+
+ $redirectExpElement = Exporter::getInstance()->getResourceElementForWikiPage( $dataItem );
+
+ // If the resource was matched to an imported vocab then no redirect is required
+ if ( $redirectExpElement->isImported() ) {
+ return null;
+ }
+
+ $valueName = TurtleSerializer::getTurtleNameForExpElement( $redirectExpElement );
+
+ // Add unknow redirect target/variable for value
+ if ( !isset( $this->redirectByVariableReplacementMap[$valueName] ) ) {
+
+ $namespaces[$redirectExpElement->getNamespaceId()] = $redirectExpElement->getNamespace();
+ $redirectByVariable = '?' . $this->getNextVariable( 'r' );
+
+ $this->redirectByVariableReplacementMap[$valueName] = [
+ $redirectByVariable,
+ $namespaces
+ ];
+ }
+
+ // Reuse an existing variable for the value to allow to be used more than
+ // once when referring to the same property/value redirect
+ list( $redirectByVariable, $namespaces ) = $this->redirectByVariableReplacementMap[$valueName];
+
+ return $redirectByVariable;
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @param integer $featureFlag
+ *
+ * @return boolean
+ */
+ public function isSetFlag( $featureFlag ) {
+
+ $canUse = true;
+
+ // Adhere additional condition
+ if ( $featureFlag === SMW_SPARQL_QF_SUBP ) {
+ $canUse = $this->engineOptions->get( 'smwgQSubpropertyDepth' ) > 0;
+ }
+
+ if ( $featureFlag === SMW_SPARQL_QF_SUBC ) {
+ $canUse = $this->engineOptions->get( 'smwgQSubcategoryDepth' ) > 0;
+ }
+
+ return $this->engineOptions->get( 'smwgSparqlQFeatures' ) === ( (int)$this->engineOptions->get( 'smwgSparqlQFeatures' ) | (int)$featureFlag ) && $canUse;
+ }
+
+ /**
+ * Extend the given SPARQL condition by a suitable order by variable,
+ * if an order by property is set.
+ *
+ * @param Condition $sparqlCondition condition to modify
+ * @param string $mainVariable the variable that represents the value to be ordered
+ * @param mixed $orderByProperty DIProperty or null
+ * @param integer $diType DataItem type id if known, or DataItem::TYPE_NOTYPE to determine it from the property
+ */
+ public function addOrderByDataForProperty( Condition &$sparqlCondition, $mainVariable, $orderByProperty, $diType = DataItem::TYPE_NOTYPE ) {
+ if ( is_null( $orderByProperty ) ) {
+ return;
+ }
+
+ if ( $diType == DataItem::TYPE_NOTYPE ) {
+ $diType = DataTypeRegistry::getInstance()->getDataItemId( $orderByProperty->findPropertyTypeID() );
+ }
+
+ $this->addOrderByData( $sparqlCondition, $mainVariable, $diType );
+ }
+
+ /**
+ * Extend the given SPARQL condition by a suitable order by variable,
+ * possibly adding conditions if required for the type of data.
+ *
+ * @param Condition $sparqlCondition condition to modify
+ * @param string $mainVariable the variable that represents the value to be ordered
+ * @param integer $diType DataItem type id
+ */
+ public function addOrderByData( Condition &$condition, $mainVariable, $diType ) {
+
+ if ( $diType !== DataItem::TYPE_WIKIPAGE ) {
+ return $condition->orderByVariable = $mainVariable;
+ }
+
+ $condition->orderByVariable = $mainVariable . 'sk';
+
+ if ( $this->isSetFlag( SMW_SPARQL_QF_COLLATION ) ) {
+ $skeyExpElement = Exporter::getInstance()->getSpecialNsResource( 'swivt', 'sort' );
+ } else {
+ $skeyExpElement = Exporter::getInstance()->getSpecialPropertyResource( '_SKEY' );
+ }
+
+ $weakConditions = [
+ $condition->orderByVariable =>"?$mainVariable " . $skeyExpElement->getQName() . " ?{$condition->orderByVariable} .\n"
+ ];
+
+ $condition->weakConditions += $weakConditions;
+ }
+
+ /**
+ * Extend the given Condition with additional conditions to
+ * ensure that it can be ordered by all requested properties. After
+ * this operation, every key in sortKeys is assigned to a query
+ * variable by $sparqlCondition->orderVariables.
+ *
+ * @param Condition $condition condition to modify
+ */
+ protected function addMissingOrderByConditions( Condition &$condition ) {
+ foreach ( $this->sortKeys as $propertyKey => $order ) {
+
+ if ( !is_string( $propertyKey ) ) {
+ throw new RuntimeException( "Expected a string value as sortkey" );
+ }
+
+ if ( strpos( $propertyKey, " " ) !== false ) {
+ throw new RuntimeException( "Expected the canonical form of {$propertyKey} (without any whitespace)" );
+ }
+
+ if ( !array_key_exists( $propertyKey, $condition->orderVariables ) ) { // Find missing property to sort by.
+ $this->addOrderForUnknownPropertyKey( $condition, $propertyKey, $order );
+ }
+ }
+ }
+
+ private function addOrderForUnknownPropertyKey( Condition &$condition, $propertyKey, $order ) {
+
+ if ( $propertyKey === '' || $propertyKey === '#' ) { // order by result page sortkey
+
+ $this->addOrderByData(
+ $condition,
+ $this->resultVariable,
+ DataItem::TYPE_WIKIPAGE
+ );
+
+ $condition->orderVariables[$propertyKey] = $condition->orderByVariable;
+ return;
+ } elseif ( PropertyChainValue::isChained( $propertyKey ) ) { // Try to extend query.
+ $propertyChainValue = new PropertyChainValue();
+ $propertyChainValue->setUserValue( $propertyKey );
+
+ if ( !$propertyChainValue->isValid() ) {
+ return $description;
+ }
+
+ $lastDataItem = $propertyChainValue->getLastPropertyChainValue()->getDataItem();
+
+ $description = $this->descriptionFactory->newSomeProperty(
+ $lastDataItem,
+ $this->descriptionFactory->newThingDescription()
+ );
+
+ foreach ( $propertyChainValue->getPropertyChainValues() as $val ) {
+ $description = $this->descriptionFactory->newSomeProperty(
+ $val->getDataItem(),
+ $description
+ );
+ }
+
+ // Add and replace Foo.Bar=asc with Bar=asc as we ultimately only
+ // order to the result of the last element
+ $this->sortKeys[$lastDataItem->getKey()] = $order;
+ unset( $this->sortKeys[$propertyKey] );
+ $propertyKey = $lastDataItem->getKey();
+
+ $auxDescription = $description;
+ } else {
+ $auxDescription = $this->descriptionFactory->newSomeProperty(
+ new DIProperty( $propertyKey ),
+ $this->descriptionFactory->newThingDescription()
+ );
+ }
+
+ $this->setJoinVariable( $this->resultVariable );
+ $this->setOrderByProperty( null );
+
+ $auxCondition = $this->mapDescriptionToCondition(
+ $auxDescription
+ );
+
+ // orderVariables MUST be set for $propertyKey -- or there is a bug; let it show!
+ $condition->orderVariables[$propertyKey] = $auxCondition->orderVariables[$propertyKey];
+ $condition->weakConditions[$condition->orderVariables[$propertyKey]] = $auxCondition->getWeakConditionString() . $auxCondition->getCondition();
+ $condition->namespaces = array_merge( $condition->namespaces, $auxCondition->namespaces );
+ }
+
+ /**
+ * @see http://www.w3.org/TR/sparql11-query/#propertypaths
+ *
+ * Query of:
+ *
+ * SELECT DISTINCT ?result WHERE {
+ * ?result swivt:wikiPageSortKey ?resultsk .
+ * {
+ * ?result property:FOO ?v1 .
+ * FILTER( ?v1sk >= "=BAR" )
+ * ?v1 swivt:wikiPageSortKey ?v1sk .
+ * } UNION {
+ * ?result property:FOO ?v2 .
+ * }
+ * }
+ *
+ * results in:
+ *
+ * SELECT DISTINCT ?result WHERE {
+ * ?result swivt:wikiPageSortKey ?resultsk .
+ * ?r2 ^swivt:redirectsTo property:FOO .
+ * {
+ * ?result ?r2 ?v1 .
+ * FILTER( ?v1sk >= "=BAR" )
+ * ?v1 swivt:wikiPageSortKey ?v1sk .
+ * } UNION {
+ * ?result ?r2 ?v3 .
+ * }
+ * }
+ */
+ private function addPropertyPathToMatchRedirectTargets( Condition &$condition ) {
+
+ if ( $this->redirectByVariableReplacementMap === [] ) {
+ return;
+ }
+
+ $weakConditions = [];
+ $namespaces = [];
+
+ $rediExpElement = Exporter::getInstance()->getSpecialPropertyResource( '_REDI' );
+ $namespaces[$rediExpElement->getNamespaceId()] = $rediExpElement->getNamespace();
+
+ foreach ( $this->redirectByVariableReplacementMap as $valueName => $content ) {
+ list( $redirectByVariable, $ns ) = $content;
+ $weakConditions[] = "$redirectByVariable " . "^" . $rediExpElement->getQName() . " $valueName .\n";
+ $namespaces = array_merge( $namespaces, $ns );
+ }
+
+ $condition->namespaces = array_merge( $condition->namespaces, $namespaces );
+ $condition->weakConditions += $weakConditions;
+ }
+
+ /**
+ * @see https://www.w3.org/TR/rdf-sparql-query/#func-bound
+ *
+ * Remove entities that contain a "swivt:redirectsTo" predicate
+ */
+ private function addFilterToRemoveEntitiesThatContainRedirectPredicate( Condition &$condition ) {
+
+ $rediExpElement = Exporter::getInstance()->getSpecialPropertyResource( '_REDI' );
+ $namespaces[$rediExpElement->getNamespaceId()] = $rediExpElement->getNamespace();
+
+ $boundVariable = '?' . $this->getNextVariable( 'o' );
+ $cogentCondition = " OPTIONAL { ?$this->resultVariable " . $rediExpElement->getQName() . " $boundVariable } .\n FILTER ( !bound( $boundVariable ) ) .\n";
+
+ $condition->addNamespaces( $namespaces );
+ $condition->cogentConditions[$boundVariable] = $cogentCondition;
+ }
+
+}