SMWSemanticData. * * @since 1.8 * @var SubSemanticData */ protected $subSemanticData; /** * @var array */ protected $errors = []; /** * Cache the hash to ensure a minimal impact in case of repeated usage. Any * removal or insert action will reset the hash to null to ensure it is * recreated in corresponds to changed nature of the data. * * @var string|null */ private $hash = null; /** * @var Options */ protected $options; /** * @var array */ protected $extensionData = []; /** * @var array[] */ protected $sequenceMap = []; /** * @var array of int|array */ protected $countMap = []; /** * This is kept public to keep track of the depth during a recursive processing * when accessed through the SubSemanticData instance. * * @var int */ public $subContainerDepthCounter = 0; /** * Constructor. * * @param DIWikiPage $subject to which this data refers * @param bool $noDuplicates stating if duplicate data should be avoided */ public function __construct( DIWikiPage $subject, $noDuplicates = true ) { $this->clear(); $this->mSubject = $subject; $this->mNoDuplicates = $noDuplicates; $this->subSemanticData = new SubSemanticData( $subject, $noDuplicates ); } /** * This object is added to the parser output of MediaWiki, but it is * not useful to have all its data as part of the parser cache since * the data is already stored in more accessible format in SMW. Hence * this implementation of __sleep() makes sure only the subject is * serialised, yielding a minimal stub data container after * unserialisation. This is a little safer than serialising nothing: * if, for any reason, SMW should ever access an unserialised parser * output, then the Semdata container will at least look as if properly * initialised (though empty). * * @return array */ public function __sleep() { return [ 'mSubject', 'mPropVals', 'mProperties', 'subSemanticData', 'mHasVisibleProps', 'mHasVisibleSpecs', 'options', 'extensionData', 'sequenceMap', 'countMap' ]; } /** * @since 3.2 * * @return bool */ public function isStub(): bool { return false; } /** * Return subject to which the stored semantic annotations refer to. * * @return DIWikiPage subject */ public function getSubject() { return $this->mSubject; } /** * Returns a hashed value map * * @since 3.1 * * @return array[] */ public function getSequenceMap() { return $this->sequenceMap; } /** * Returns a map of property value counts * * @since 3.2 * * @return array */ public function getCountMap() { return $this->countMap; } /** * Get the array of all properties that have stored values. * * @return DIProperty[] */ public function getProperties() { ksort( $this->mProperties, SORT_STRING ); return $this->mProperties; } /** * @since 2.4 * * @param DIProperty $property * * @return bool */ public function hasProperty( DIProperty $property ) { return isset( $this->mProperties[$property->getKey()] ) || array_key_exists( $property->getKey(), $this->mProperties ); } /** * Get the array of all stored values for some property. * * @param DIProperty $property * @return SMWDataItem[] */ public function getPropertyValues( DIProperty $property ) { if ( $property->isInverse() ) { // we never have any data for inverses return []; } if ( array_key_exists( $property->getKey(), $this->mPropVals ) ) { return array_values( $this->mPropVals[$property->getKey()] ); } return []; } /** * @since 3.0 * * @param string $key * @param mixed $value */ public function setExtensionData( $key, $value ) { $this->extensionData[$key] = $value; } /** * @since 3.0 * * @param string $key * * @return mixed|null */ public function getExtensionData( $key ) { if ( !isset( $this->extensionData[$key] ) ) { return null; } return $this->extensionData[$key]; } /** * @since 2.5 * * @param string $key * @param mixed $default * * @return mixed */ public function getOption( $key, $default = null ) { if ( !$this->options instanceof Options ) { $this->options = new Options(); } if ( $this->options->has( $key ) ) { return $this->options->get( $key ); } return $default; } /** * @since 2.5 * * @param string $key * @param string $value */ public function setOption( $key, $value ) { if ( !$this->options instanceof Options ) { $this->options = new Options(); } $this->options->set( $key, $value ); } /** * Returns collected errors occurred during processing * * @since 1.9 * * @return array */ public function getErrors() { return $this->errors; } /** * Adds an error array * * @since 1.9 */ public function addError( $error ) { $this->errors = array_merge( $this->errors, (array)$error ); } /** * Generate a hash value to simplify the comparison of this data * container with other containers. Subdata is taken into account. * * The hash uses PHP's md5 implementation, which is among the fastest * hash algorithms that PHP offers. * * @note This function may be used to obtain keys for SemanticData * objects or to do simple equality tests. Equal hashes with very * high probability indicate equal data. * * @return string */ public function getHash() { if ( $this->hash !== null ) { return $this->hash; } return $this->hash = HashBuilder::createFromSemanticData( $this ); } /** * @see SubSemanticData::getSubSemanticData * * @since 1.8 * * @return SemanticData[] */ public function getSubSemanticData() { // Remove the check in 3.0 $subSemanticData = $this->subSemanticData; // Avoids an issue where the serialized array from a previous usage is // returned from a __wakeup, where now a SubSemanticData (#2177) is expected. if ( !$subSemanticData instanceof SubSemanticData ) { $this->subSemanticData = new SubSemanticData( $this->mSubject, $this->mNoDuplicates ); $this->subSemanticData->copyDataFrom( $subSemanticData ); } return $this->subSemanticData->getSubSemanticData(); } /** * @since 2.5 */ public function clearSubSemanticData() { if ( $this->subContainerDepthCounter > 0 ) { $this->subContainerDepthCounter--; } if ( $this->subSemanticData !== null ) { $this->subSemanticData->clear(); } } /** * Return true if there are any visible properties. * * @note While called "visible" this check actually refers to the * function DIProperty::isShown(). The name is kept for * compatibility. * * @return bool */ public function hasVisibleProperties() { return $this->mHasVisibleProps; } /** * Return true if there are any special properties that can * be displayed. * * @note While called "visible" this check actually refers to the * function DIProperty::isShown(). The name is kept for * compatibility. * * @return bool */ public function hasVisibleSpecialProperties() { return $this->mHasVisibleSpecs; } /** * Store a value for a property identified by its SMWDataItem object. * * @note There is no check whether the type of the given data item * agrees with the type of the property. Since property types can * change, all parts of SMW are prepared to handle mismatched data item * types anyway. * * @param $property DIProperty * @param $dataItem SMWDataItem */ public function addPropertyObjectValue( DIProperty $property, SMWDataItem $dataItem ) { $this->hash = null; if ( $dataItem instanceof SMWDIContainer ) { $this->addSubSemanticData( $dataItem->getSemanticData() ); $dataItem = $dataItem->getSemanticData()->getSubject(); } if ( $property->getKey() === DIProperty::TYPE_MODIFICATION_DATE ) { $this->setOption( self::OPT_LAST_MODIFIED, $dataItem->getMwTimestamp() ); } if ( $property->isInverse() ) { // inverse properties cannot be used for annotation return; } $key = $property->getKey(); if ( !isset( $this->countMap[$key] ) ) { $this->countMap[$key] = $key === '_INST' ? [] : 0; } if ( !array_key_exists( $property->getKey(), $this->mPropVals ) ) { $this->mPropVals[$property->getKey()] = []; $this->mProperties[$property->getKey()] = $property; if ( SequenceMap::canMap( $property ) ) { $this->sequenceMap[$property->getKey()] = []; } } $hash = md5( $dataItem->getHash() ); // Only store a map for values that are allowed to if ( $this->mNoDuplicates && isset( $this->sequenceMap[$property->getKey()] ) && !isset( $this->mPropVals[$property->getKey()][$hash] ) ) { $this->sequenceMap[$property->getKey()][] = $hash; } if ( !isset( $this->mPropVals[$property->getKey()][$hash] ) ) { // Count categories differently if ( $key === '_INST' ) { $this->countMap[$key][$dataItem->getDBKey()] = 1; } else { $this->countMap[$key]++; } } if ( $this->mNoDuplicates ) { $this->mPropVals[$property->getKey()][$hash] = $dataItem; } else { $this->mPropVals[$property->getKey()][] = $dataItem; } if ( !$property->isUserDefined() ) { if ( $property->isShown() ) { $this->mHasVisibleSpecs = true; $this->mHasVisibleProps = true; } } else { $this->mHasVisibleProps = true; } // Account for things like DISPLAYTITLE or DEFAULTSORT which are only set // after #subobject has been processed therefore keep them in-memory // for a post process if ( $this->mSubject->getSubobjectName() === '' && $property->getKey() === DIProperty::TYPE_SORTKEY ) { foreach ( $this->getSubSemanticData() as $subSemanticData ) { $subSemanticData->setExtensionData( 'sort.extension', $dataItem->getString() ); } } } /** * Store a value for a given property identified by its text label * (without namespace prefix). * * @param $propertyName string * @param $dataItem SMWDataItem */ public function addPropertyValue( $propertyName, SMWDataItem $dataItem ) { $propertyKey = smwfNormalTitleDBKey( $propertyName ); if ( array_key_exists( $propertyKey, $this->mProperties ) ) { $property = $this->mProperties[$propertyKey]; } else { if ( self::$mPropertyPrefix === '' ) { self::$mPropertyPrefix = Localizer::getInstance()->getNsText( SMW_NS_PROPERTY ) . ':'; } // explicitly use prefix to cope with things like [[Property:User:Stupid::somevalue]] $propertyDV = DataValueFactory::getInstance()->newPropertyValueByLabel( self::$mPropertyPrefix . $propertyName ); if ( !$propertyDV->isValid() ) { // error, maybe illegal title text return; } $property = $propertyDV->getDataItem(); } $this->addPropertyObjectValue( $property, $dataItem ); } /** * @since 1.9 * * @param SMWDataValue $dataValue */ public function addDataValue( SMWDataValue $dataValue ) { if ( !$dataValue->getProperty() instanceof DIProperty || !$dataValue->isValid() ) { $processingErrorMsgHandler = new ProcessingErrorMsgHandler( $this->getSubject() ); $processingErrorMsgHandler->addToSemanticData( $this, $processingErrorMsgHandler->newErrorContainerFromDataValue( $dataValue ) ); $this->addError( $dataValue->getErrors() ); return; } $this->addPropertyObjectValue( $dataValue->getProperty(), $dataValue->getDataItem() ); } /** * @since 2.1 * * @param Subobject $subobject */ public function addSubobject( Subobject $subobject ) { $this->addPropertyObjectValue( $subobject->getProperty(), $subobject->getContainer() ); } /** * Remove a value for a property identified by its SMWDataItem object. * This method removes a property-value specified by the property and * dataitem. If there are no more property-values for this property it * also removes the property from the mProperties. * * @note There is no check whether the type of the given data item * agrees with the type of the property. Since property types can * change, all parts of SMW are prepared to handle mismatched data item * types anyway. * * @param $property DIProperty * @param $dataItem SMWDataItem * * @since 1.8 */ public function removePropertyObjectValue( DIProperty $property, SMWDataItem $dataItem ) { $this->hash = null; // delete associated subSemanticData if ( $dataItem instanceof SMWDIContainer ) { $this->removeSubSemanticData( $dataItem->getSemanticData() ); $dataItem = $dataItem->getSemanticData()->getSubject(); } if ( $property->isInverse() ) { // inverse properties cannot be used for annotation return; } $key = $property->getKey(); if ( !array_key_exists( $property->getKey(), $this->mPropVals ) || !array_key_exists( $property->getKey(), $this->mProperties ) ) { return; } if ( $this->mNoDuplicates ) { // this didn't get checked for my tests, but should work unset( $this->mPropVals[$property->getKey()][md5( $dataItem->getHash() )] ); if ( isset( $this->countMap[$key] ) && $key === '_INST' ) { unset( $this->countMap[$key][$dataItem->getDBKey()] ); } elseif ( isset( $this->countMap[$key] ) ) { $this->countMap[$key]--; } } else { foreach ( $this->mPropVals[$property->getKey()] as $index => $di ) { if ( $di->equals( $dataItem ) ) { unset( $this->mPropVals[$property->getKey()][$index] ); } if ( isset( $this->countMap[$key] ) && $key === '_INST' ) { unset( $this->countMap[$key][$dataItem->getDBKey()] ); } elseif ( isset( $this->countMap[$key] ) ) { $this->countMap[$key]--; } } $this->mPropVals[$property->getKey()] = array_values( $this->mPropVals[$property->getKey()] ); } if ( isset( $this->countMap[$key] ) && $this->countMap[$key] == 0 ) { unset( $this->countMap[$key] ); } if ( $this->mPropVals[$property->getKey()] === [] ) { unset( $this->mProperties[$property->getKey()] ); unset( $this->mPropVals[$property->getKey()] ); } } /** * Removes a property and all the values associated with this property. * * @since 2.5 * * @param $property DIProperty */ public function removeProperty( DIProperty $property ) { $this->hash = null; $key = $property->getKey(); // Inverse properties cannot be used for an annotation if ( $property->isInverse() ) { return; } if ( !isset( $this->mProperties[$key] ) || !isset( $this->mPropVals[$key] ) ) { return; } // Find and remove associated assignments (e.g. _ASK as subobject // contains _ASKSI ...) foreach ( $this->mPropVals[$key] as $dataItem ) { $this->removePropertyObjectValue( $property, $dataItem ); } unset( $this->mPropVals[$key] ); unset( $this->mProperties[$key] ); unset( $this->sequenceMap[$key] ); unset( $this->countMap[$key] ); } /** * Delete all data other than the subject. */ public function clear() { $this->mPropVals = []; $this->mProperties = []; $this->mHasVisibleProps = false; $this->mHasVisibleSpecs = false; $this->stubObject = false; $this->clearSubSemanticData(); $this->hash = null; $this->options = null; } /** * Return true if this SemanticData is empty. * This is the case when the subject has neither property values nor * data for subobjects. * * @since 1.8 */ public function isEmpty(): bool { return $this->getProperties() === [] && $this->getSubSemanticData() === []; } /** * Add all data from the given SMWSemanticData. * Only works if the imported SMWSemanticData has the same subject as * this SMWSemanticData; an exception is thrown otherwise. * * @since 1.7 * * @param SemanticData $semanticData object to copy from * * @throws SemanticDataImportException */ public function importDataFrom( SemanticData $semanticData ) { if ( !$this->mSubject->equals( $semanticData->getSubject() ) ) { throw new SemanticDataImportException( "SemanticData can only represent data about one subject. Importing data for another subject is not possible." ); } $this->hash = null; // Shortcut when copying into empty objects that don't ask for // more duplicate elimination: if ( count( $this->mProperties ) == 0 && ( $semanticData->mNoDuplicates >= $this->mNoDuplicates ) ) { $this->mProperties = $semanticData->getProperties(); $this->sequenceMap = $semanticData->getSequenceMap(); $this->countMap = $semanticData->getCountMap(); $this->mPropVals = []; foreach ( $this->mProperties as $property ) { $key = $property->getKey(); $this->mPropVals[$key] = $semanticData->getPropertyValues( $property ); if ( isset( $this->sequenceMap[$key] ) && SequenceMap::canMap( $property ) ) { $sequenceMap = array_flip( $this->sequenceMap[$key] ); usort( $this->mPropVals[$key], static function ( $a, $b ) use( $sequenceMap ) { $pos_a = $sequenceMap[md5( $a->getHash() )]; $pos_b = $sequenceMap[md5( $b->getHash() )]; return ( $pos_a < $pos_b ) ? -1 : 1; } ); } } $this->mHasVisibleProps = $semanticData->hasVisibleProperties(); $this->mHasVisibleSpecs = $semanticData->hasVisibleSpecialProperties(); } else { foreach ( $semanticData->getProperties() as $property ) { $values = $semanticData->getPropertyValues( $property ); foreach ( $values as $dataItem ) { $this->addPropertyObjectValue( $property, $dataItem ); } } } // Postpone the import and avoid resolving `SubSemanticData` for // a `StubSemanticData` objects that would otherwise create the entire // object graph recursively even though it might not be necessary as in // case when retrieving data via `Special:Browse` // // Subobject references are part of the value representation and assigned // to the relevant property which may be resolved at a later point if ( !$semanticData->isStub() ) { foreach ( $semanticData->getSubSemanticData() as $subSemanticData ) { $this->addSubSemanticData( $subSemanticData ); } } } /** * Removes data from the given SMWSemanticData. * If the subject of the data that is to be removed is not equal to the * subject of this SMWSemanticData, it will just be ignored (nothing to * remove). Likewise, removing data that is not present does not change * anything. * * @since 1.8 * * @param SemanticData $semanticData */ public function removeDataFrom( SemanticData $semanticData ) { if ( !$this->mSubject->equals( $semanticData->getSubject() ) ) { return; } foreach ( $semanticData->getProperties() as $property ) { $this->removeProperty( $property ); } foreach ( $semanticData->getSubSemanticData() as $semData ) { $this->removeSubSemanticData( $semData ); } } /** * @see SubSemanticData::hasSubSemanticData * @since 1.9 * * @param string|null $subobjectName * * @return bool */ public function hasSubSemanticData( $subobjectName = null ) { return $this->subSemanticData->hasSubSemanticData( $subobjectName ); } /** * @see SubSemanticData::findSubSemanticData * @since 1.9 * * @param string $subobjectName * * @return ContainerSemanticData|null */ public function findSubSemanticData( $subobjectName ) { return $this->subSemanticData->findSubSemanticData( $subobjectName ); } /** * @see SubSemanticData::addSubSemanticData * @since 1.8 * * @param SemanticData $semanticData * @throws SubSemanticDataException */ public function addSubSemanticData( SemanticData $semanticData ) { $this->hash = null; $this->subSemanticData->addSubSemanticData( $semanticData ); } /** * @see SubSemanticData::removeSubSemanticData * @since 1.8 * * @param SemanticData $semanticData */ public function removeSubSemanticData( SemanticData $semanticData ) { $this->hash = null; $this->subSemanticData->removeSubSemanticData( $semanticData ); } /** * Implements \JsonSerializable. * * @since 4.0.0 * * @return array */ public function jsonSerialize(): array { # T312589 explicitly calling jsonSerialize() will be unnecessary # in the future. $json = [ 'stubObject' => $this->stubObject, 'mPropVals' => array_map( static function ( $x ) { return array_map( static function ( $y ) { return $y->jsonSerialize(); }, $x ); }, $this->mPropVals ), 'mProperties' => array_map( static function ( $x ) { return $x->jsonSerialize(); }, $this->mProperties ), 'mHasVisibleProps' => $this->mHasVisibleProps, 'mHasVisibleSpecs' => $this->mHasVisibleSpecs, 'mNoDuplicates' => $this->mNoDuplicates, 'mSubject' => $this->mSubject ? $this->mSubject->jsonSerialize() : null, 'subSemanticData' => $this->subSemanticData ? $this->subSemanticData->jsonSerialize() : null, 'errors' => $this->errors, 'hash' => $this->hash, 'options' => $this->options ? $this->options->jsonSerialize() : null, 'extensionData' => $this->extensionData, 'sequenceMap' => $this->sequenceMap, 'countMap' => $this->countMap, '_type_' => get_class( $this ), ]; return $json; } public static function maybeUnserialize( $unserializer, $value ) { # Compatibility thunk for MW versions with T312589 fixed/unfixed return is_object( $value ) ? $value : $unserializer->unserialize( $value ); } public static function maybeUnserializeArray( $unserializer, array $value ) { # Compatibility thunk for MW versions with T312589 fixed/unfixed $result = []; foreach ( $value as $k => $v ) { $result[$k] = self::maybeUnserialize( $unserializer, $v ); } return $result; } /** * Implements JsonUnserializable. * * @since 4.0.0 * * @param JsonUnserializer $unserializer Unserializer * @param array $json JSON to be unserialized * * @return self */ public static function newFromJsonArray( JsonUnserializer $unserializer, array $json ) { # T312589: In the future JsonCodec will take care of unserializing # the values in the $json array itself. $obj = new self( self::maybeUnserialize( $unserializer, $json['mSubject'] ), $json['mNoDuplicates'] ); $obj->stubObject = $json['stubObject']; $obj->mPropVals = array_map( static function ( $x ) use( $unserializer ) { return self::maybeUnserializeArray( $unserializer, $x ); }, $json['mPropVals'] ); $obj->mProperties = self::maybeUnserializeArray( $unserializer, $json['mProperties'] ); $obj->mHasVisibleProps = $json['mHasVisibleProps']; $obj->mHasVisibleSpecs = $json['mHasVisibleSpecs']; $obj->subSemanticData = $json['subSemanticData'] ? self::maybeUnserialize( $unserializer, $json['subSemanticData'] ) : null; $obj->errors = $json['errors']; $obj->hash = $json['hash']; $obj->options = $json['options'] ? self::maybeUnserialize( $unserializer, $json['options'] ) : null; $obj->extensionData = $json['extensionData']; $obj->sequenceMap = $json['sequenceMap']; $obj->countMap = $json['countMap']; return $obj; } }