<?php

/**
 * (c) Kitodo. Key to digital objects e.V. <contact@kitodo.org>
 *
 * This file is part of the Kitodo and TYPO3 projects.
 *
 * @license GNU General Public License version 3 or later.
 * For the full copyright and license information, please read the
 * LICENSE.txt file that was distributed with this source code.
 */

namespace Kitodo\Dlf\Common;

use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use Ubl\Iiif\Presentation\Common\Model\Resources\IiifResourceInterface;
use Ubl\Iiif\Tools\IiifHelper;

/**
 * Document class for the 'dlf' extension
 *
 * @author Sebastian Meyer <sebastian.meyer@slub-dresden.de>
 * @author Henrik Lochmann <dev@mentalmotive.com>
 * @package TYPO3
 * @subpackage dlf
 * @access public
 * @property-write int $cPid This holds the PID for the configuration
 * @property-read bool $hasFulltext Are there any fulltext files available?
 * @property-read string $location This holds the documents location
 * @property-read array $metadataArray This holds the documents' parsed metadata array
 * @property-read int $numPages The holds the total number of pages
 * @property-read int $parentId This holds the UID of the parent document or zero if not multi-volumed
 * @property-read array $physicalStructure This holds the physical structure
 * @property-read array $physicalStructureInfo This holds the physical structure metadata
 * @property-read int $pid This holds the PID of the document or zero if not in database
 * @property-read bool $ready Is the document instantiated successfully?
 * @property-read string $recordId The METS file's / IIIF manifest's record identifier
 * @property-read int $rootId This holds the UID of the root document or zero if not multi-volumed
 * @property-read array $smLinks This holds the smLinks between logical and physical structMap
 * @property-read array $tableOfContents This holds the logical structure
 * @property-read string $thumbnail This holds the document's thumbnail location
 * @property-read string $toplevelId This holds the toplevel structure's @ID (METS) or the manifest's @id (IIIF)
 * @property-read mixed $uid This holds the UID or the URL of the document
 * @abstract
 */
abstract class Document
{
    /**
     * This holds the PID for the configuration
     *
     * @var int
     * @access protected
     */
    protected $cPid = 0;

    /**
     * The extension key
     *
     * @var string
     * @access public
     */
    public static $extKey = 'dlf';

    /**
     * This holds the configuration for all supported metadata encodings
     * @see loadFormats()
     *
     * @var array
     * @access protected
     */
    protected $formats = [
        'OAI' => [
            'rootElement' => 'OAI-PMH',
            'namespaceURI' => 'http://www.openarchives.org/OAI/2.0/',
        ],
        'METS' => [
            'rootElement' => 'mets',
            'namespaceURI' => 'http://www.loc.gov/METS/',
        ],
        'XLINK' => [
            'rootElement' => 'xlink',
            'namespaceURI' => 'http://www.w3.org/1999/xlink',
        ]
    ];

    /**
     * Are the available metadata formats loaded?
     * @see $formats
     *
     * @var bool
     * @access protected
     */
    protected $formatsLoaded = false;

    /**
     * Are there any fulltext files available? This also includes IIIF text annotations
     * with motivation 'painting' if Kitodo.Presentation is configured to store text
     * annotations as fulltext.
     *
     * @var bool
     * @access protected
     */
    protected $hasFulltext = false;

    /**
     * Last searched logical and physical page
     *
     * @var array
     * @access protected
     */
    protected $lastSearchedPhysicalPage = ['logicalPage' => null, 'physicalPage' => null];

    /**
     * This holds the documents location
     *
     * @var string
     * @access protected
     */
    protected $location = '';

    /**
     * This holds the logical units
     *
     * @var array
     * @access protected
     */
    protected $logicalUnits = [];

    /**
     * This holds the documents' parsed metadata array with their corresponding
     * structMap//div's ID (METS) or Range / Manifest / Sequence ID (IIIF) as array key
     *
     * @var array
     * @access protected
     */
    protected $metadataArray = [];

    /**
     * Is the metadata array loaded?
     * @see $metadataArray
     *
     * @var bool
     * @access protected
     */
    protected $metadataArrayLoaded = false;

    /**
     * The holds the total number of pages
     *
     * @var int
     * @access protected
     */
    protected $numPages = 0;

    /**
     * This holds the UID of the parent document or zero if not multi-volumed
     *
     * @var int
     * @access protected
     */
    protected $parentId = 0;

    /**
     * This holds the physical structure
     *
     * @var array
     * @access protected
     */
    protected $physicalStructure = [];

    /**
     * This holds the physical structure metadata
     *
     * @var array
     * @access protected
     */
    protected $physicalStructureInfo = [];

    /**
     * Is the physical structure loaded?
     * @see $physicalStructure
     *
     * @var bool
     * @access protected
     */
    protected $physicalStructureLoaded = false;

    /**
     * This holds the PID of the document or zero if not in database
     *
     * @var int
     * @access protected
     */
    protected $pid = 0;

    /**
     * This holds the documents' raw text pages with their corresponding
     * structMap//div's ID (METS) or Range / Manifest / Sequence ID (IIIF) as array key
     *
     * @var array
     * @access protected
     */
    protected $rawTextArray = [];

    /**
     * Is the document instantiated successfully?
     *
     * @var bool
     * @access protected
     */
    protected $ready = false;

    /**
     * The METS file's / IIIF manifest's record identifier
     *
     * @var string
     * @access protected
     */
    protected $recordId;

    /**
     * This holds the singleton object of the document
     *
     * @var array (\Kitodo\Dlf\Common\Document)
     * @static
     * @access protected
     */
    protected static $registry = [];

    /**
     * This holds the UID of the root document or zero if not multi-volumed
     *
     * @var int
     * @access protected
     */
    protected $rootId = 0;

    /**
     * Is the root id loaded?
     * @see $rootId
     *
     * @var bool
     * @access protected
     */
    protected $rootIdLoaded = false;

    /**
     * This holds the smLinks between logical and physical structMap
     *
     * @var array
     * @access protected
     */
    protected $smLinks = ['l2p' => [], 'p2l' => []];

    /**
     * Are the smLinks loaded?
     * @see $smLinks
     *
     * @var bool
     * @access protected
     */
    protected $smLinksLoaded = false;

    /**
     * This holds the logical structure
     *
     * @var array
     * @access protected
     */
    protected $tableOfContents = [];

    /**
     * Is the table of contents loaded?
     * @see $tableOfContents
     *
     * @var bool
     * @access protected
     */
    protected $tableOfContentsLoaded = false;

    /**
     * This holds the document's thumbnail location
     *
     * @var string
     * @access protected
     */
    protected $thumbnail = '';

    /**
     * Is the document's thumbnail location loaded?
     * @see $thumbnail
     *
     * @var bool
     * @access protected
     */
    protected $thumbnailLoaded = false;

    /**
     * This holds the toplevel structure's @ID (METS) or the manifest's @id (IIIF)
     *
     * @var string
     * @access protected
     */
    protected $toplevelId = '';

    /**
     * This holds the UID or the URL of the document
     *
     * @var mixed
     * @access protected
     */
    protected $uid = 0;

    /**
     * This holds the whole XML file as \SimpleXMLElement object
     *
     * @var \SimpleXMLElement
     * @access protected
     */
    protected $xml;

    /**
     * This clears the static registry to prevent memory exhaustion
     *
     * @access public
     *
     * @static
     *
     * @return void
     */
    public static function clearRegistry()
    {
        // Reset registry array.
        self::$registry = [];
    }

    /**
     * This ensures that the recordId, if existent, is retrieved from the document
     *
     * @access protected
     *
     * @abstract
     *
     * @param int $pid: ID of the configuration page with the recordId config
     *
     */
    protected abstract function establishRecordId($pid);

    /**
     * Source document PHP object which is represented by a Document instance
     *
     * @access protected
     *
     * @abstract
     *
     * @return \SimpleXMLElement|IiifResourceInterface An PHP object representation of
     * the current document. SimpleXMLElement for METS, IiifResourceInterface for IIIF
     */
    protected abstract function getDocument();

    /**
     * This gets the location of a downloadable file for a physical page or track
     *
     * @access public
     *
     * @abstract
     *
     * @param string $id: The @ID attribute of the file node (METS) or the @id property of the IIIF resource
     *
     * @return string    The file's location as URL
     */
    public abstract function getDownloadLocation($id);

    /**
     * This gets the location of a file representing a physical page or track
     *
     * @access public
     *
     * @abstract
     *
     * @param string $id: The @ID attribute of the file node (METS) or the @id property of the IIIF resource
     *
     * @return string The file's location as URL
     */
    public abstract function getFileLocation($id);

    /**
     * This gets the MIME type of a file representing a physical page or track
     *
     * @access public
     *
     * @abstract
     *
     * @param string $id: The @ID attribute of the file node
     *
     * @return string The file's MIME type
     */
    public abstract function getFileMimeType($id);

    /**
     * This is a singleton class, thus an instance must be created by this method
     *
     * @access public
     *
     * @static
     *
     * @param mixed $uid: The unique identifier of the document to parse, the URL of XML file or the IRI of the IIIF resource
     * @param int $pid: If > 0, then only document with this PID gets loaded
     * @param bool $forceReload: Force reloading the document instead of returning the cached instance
     *
     * @return \Kitodo\Dlf\Common\Document Instance of this class, either MetsDocument or IiifManifest
     */
    public static function &getInstance($uid, $pid = 0, $forceReload = false)
    {
        // Sanitize input.
        $pid = max(intval($pid), 0);
        if (!$forceReload) {
            $regObj = Helper::digest($uid);
            if (
                is_object(self::$registry[$regObj])
                && self::$registry[$regObj] instanceof self
            ) {
                // Check if instance has given PID.
                if (
                    !$pid
                    || !self::$registry[$regObj]->pid
                    || $pid == self::$registry[$regObj]->pid
                ) {
                    // Return singleton instance if available.
                    return self::$registry[$regObj];
                }
            } else {
                // Check the user's session...
                $sessionData = Helper::loadFromSession(get_called_class());
                if (
                    is_object($sessionData[$regObj])
                    && $sessionData[$regObj] instanceof self
                ) {
                    // Check if instance has given PID.
                    if (
                        !$pid
                        || !$sessionData[$regObj]->pid
                        || $pid == $sessionData[$regObj]->pid
                    ) {
                        // ...and restore registry.
                        self::$registry[$regObj] = $sessionData[$regObj];
                        return self::$registry[$regObj];
                    }
                }
            }
        }
        // Create new instance depending on format (METS or IIIF) ...
        $instance = null;
        $documentFormat = null;
        $xml = null;
        $iiif = null;
        // Try to get document format from database
        if (MathUtility::canBeInterpretedAsInteger($uid)) {
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
                ->getQueryBuilderForTable('tx_dlf_documents');

            $queryBuilder
                ->select(
                    'tx_dlf_documents.location AS location',
                    'tx_dlf_documents.document_format AS document_format'
                )
                ->from('tx_dlf_documents');

            // Get UID of document with given record identifier.
            if ($pid) {
                $queryBuilder
                    ->where(
                        $queryBuilder->expr()->eq('tx_dlf_documents.uid', intval($uid)),
                        $queryBuilder->expr()->eq('tx_dlf_documents.pid', intval($pid)),
                        Helper::whereExpression('tx_dlf_documents')
                    );
            } else {
                $queryBuilder
                    ->where(
                        $queryBuilder->expr()->eq('tx_dlf_documents.uid', intval($uid)),
                        Helper::whereExpression('tx_dlf_documents')
                    );
            }

            $result = $queryBuilder
                ->setMaxResults(1)
                ->execute();

            if ($resArray = $result->fetch()) {
                $documentFormat = $resArray['document_format'];
            }
        } else {
            // Get document format from content of remote document
            // Cast to string for safety reasons.
            $location = (string) $uid;
            // Try to load a file from the url
            if (GeneralUtility::isValidUrl($location)) {
                // Load extension configuration
                $extConf = unserialize($GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf']['dlf']);
                // Set user-agent to identify self when fetching XML data.
                if (!empty($extConf['useragent'])) {
                    @ini_set('user_agent', $extConf['useragent']);
                }
                $content = GeneralUtility::getUrl($location);
                if ($content !== false) {
                    // TODO use single place to load xml
                    // Turn off libxml's error logging.
                    $libxmlErrors = libxml_use_internal_errors(true);
                    // Disables the functionality to allow external entities to be loaded when parsing the XML, must be kept
                    $previousValueOfEntityLoader = libxml_disable_entity_loader(true);
                    // Try to load XML from file.
                    $xml = simplexml_load_string($content);
                    // reset entity loader setting
                    libxml_disable_entity_loader($previousValueOfEntityLoader);
                    // Reset libxml's error logging.
                    libxml_use_internal_errors($libxmlErrors);
                    if ($xml !== false) {
                        /* @var $xml \SimpleXMLElement */
                        $xml->registerXPathNamespace('mets', 'http://www.loc.gov/METS/');
                        $xpathResult = $xml->xpath('//mets:mets');
                        $documentFormat = !empty($xpathResult) ? 'METS' : null;
                    } else {
                        // Try to load file as IIIF resource instead.
                        $contentAsJsonArray = json_decode($content, true);
                        if ($contentAsJsonArray !== null) {
                            // Load plugin configuration.
                            $conf = unserialize($GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf'][self::$extKey]);
                            IiifHelper::setUrlReader(IiifUrlReader::getInstance());
                            IiifHelper::setMaxThumbnailHeight($conf['iiifThumbnailHeight']);
                            IiifHelper::setMaxThumbnailWidth($conf['iiifThumbnailWidth']);
                            $iiif = IiifHelper::loadIiifResource($contentAsJsonArray);
                            if ($iiif instanceof IiifResourceInterface) {
                                $documentFormat = 'IIIF';
                            }
                        }
                    }
                }
            }
        }
        // Sanitize input.
        $pid = max(intval($pid), 0);
        if ($documentFormat == 'METS') {
            $instance = new MetsDocument($uid, $pid, $xml);
        } elseif ($documentFormat == 'IIIF') {
            $instance = new IiifManifest($uid, $pid, $iiif);
        }
        // Save instance to registry.
        if (
            $instance instanceof self
            && $instance->ready) {
            self::$registry[Helper::digest($instance->uid)] = $instance;
            if ($instance->uid != $instance->location) {
                self::$registry[Helper::digest($instance->location)] = $instance;
            }
            // Load extension configuration
            $extConf = unserialize($GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf']['dlf']);
            // Save registry to session if caching is enabled.
            if (!empty($extConf['caching'])) {
                Helper::saveToSession(self::$registry, get_class($instance));
            }
        }
        // Return new instance.
        return $instance;
    }

    /**
     * This gets details about a logical structure element
     *
     * @access public
     *
     * @abstract
     *
     * @param string $id: The @ID attribute of the logical structure node (METS) or
     * the @id property of the Manifest / Range (IIIF)
     * @param bool $recursive: Whether to include the child elements / resources
     *
     * @return array Array of the element's id, label, type and physical page indexes/mptr link
     */
    public abstract function getLogicalStructure($id, $recursive = false);

    /**
     * This extracts all the metadata for a logical structure node
     *
     * @access public
     *
     * @abstract
     *
     * @param string $id: The @ID attribute of the logical structure node (METS) or the @id property
     * of the Manifest / Range (IIIF)
     * @param int $cPid: The PID for the metadata definitions
     *                       (defaults to $this->cPid or $this->pid)
     *
     * @return array The logical structure node's / the IIIF resource's parsed metadata array
     */
    public abstract function getMetadata($id, $cPid = 0);

    /**
     * This returns the first corresponding physical page number of a given logical page label
     *
     * @access public
     *
     * @param string $logicalPage: The label (or a part of the label) of the logical page
     *
     * @return int The physical page number
     */
    public function getPhysicalPage($logicalPage)
    {
        if (
            !empty($this->lastSearchedPhysicalPage['logicalPage'])
            && $this->lastSearchedPhysicalPage['logicalPage'] == $logicalPage
        ) {
            return $this->lastSearchedPhysicalPage['physicalPage'];
        } else {
            $physicalPage = 0;
            foreach ($this->physicalStructureInfo as $page) {
                if (strpos($page['orderlabel'], $logicalPage) !== false) {
                    $this->lastSearchedPhysicalPage['logicalPage'] = $logicalPage;
                    $this->lastSearchedPhysicalPage['physicalPage'] = $physicalPage;
                    return $physicalPage;
                }
                $physicalPage++;
            }
        }
        return 1;
    }

    /**
     * This extracts the raw text for a physical structure node / IIIF Manifest / Canvas. Text might be
     * given as ALTO for METS or as annotations or ALTO for IIIF resources. If IIIF plain text annotations
     * with the motivation "painting" should be treated as full text representations, the extension has to be
     * configured accordingly.
     *
     * @access public
     *
     * @abstract
     *
     * @param string $id: The @ID attribute of the physical structure node (METS) or the @id property
     * of the Manifest / Range (IIIF)
     *
     * @return string The physical structure node's / IIIF resource's raw text
     */
    public abstract function getRawText($id);

    /**
     * This extracts the raw text for a physical structure node / IIIF Manifest / Canvas from an
     * XML fulltext representation (currently only ALTO). For IIIF manifests, ALTO documents have
     * to be given in the Canvas' / Manifest's "seeAlso" property.
     *
     * @param string $id: The @ID attribute of the physical structure node (METS) or the @id property
     * of the Manifest / Range (IIIF)
     *
     * @return string The physical structure node's / IIIF resource's raw text from XML
     */
    protected function getRawTextFromXml($id)
    {
        $rawText = '';
        // Load available text formats, ...
        $this->loadFormats();
        // ... physical structure ...
        $this->_getPhysicalStructure();
        // ... and extension configuration.
        $extConf = unserialize($GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf'][self::$extKey]);
        $fileGrpsFulltext = GeneralUtility::trimExplode(',', $extConf['fileGrpFulltext']);
        if (!empty($this->physicalStructureInfo[$id])) {
            while ($fileGrpFulltext = array_shift($fileGrpsFulltext)) {
                if (!empty($this->physicalStructureInfo[$id]['files'][$fileGrpFulltext])) {
                    // Get fulltext file.
                    $file = GeneralUtility::getUrl($this->getFileLocation($this->physicalStructureInfo[$id]['files'][$fileGrpFulltext]));
                    if ($file !== false) {
                        // Turn off libxml's error logging.
                        $libxmlErrors = libxml_use_internal_errors(true);
                        // Disables the functionality to allow external entities to be loaded when parsing the XML, must be kept.
                        $previousValueOfEntityLoader = libxml_disable_entity_loader(true);
                        // Load XML from file.
                        $rawTextXml = simplexml_load_string($file);
                        // Reset entity loader setting.
                        libxml_disable_entity_loader($previousValueOfEntityLoader);
                        // Reset libxml's error logging.
                        libxml_use_internal_errors($libxmlErrors);
                        // Get the root element's name as text format.
                        $textFormat = strtoupper($rawTextXml->getName());
                    } else {
                        Helper::devLog('Couln\'t load fulltext file for structure node @ID "' . $id . '"', DEVLOG_SEVERITY_WARNING);
                        return $rawText;
                    }
                    break;
                }
            }
        } else {
            Helper::devLog('Invalid structure node @ID "' . $id . '"', DEVLOG_SEVERITY_WARNING);
            return $rawText;
        }
        // Is this text format supported?
        if (
            !empty($rawTextXml)
            && !empty($this->formats[$textFormat])
        ) {
            if (!empty($this->formats[$textFormat]['class'])) {
                $class = $this->formats[$textFormat]['class'];
                // Get the raw text from class.
                if (
                    class_exists($class)
                    && ($obj = GeneralUtility::makeInstance($class)) instanceof FulltextInterface
                ) {
                    $rawText = $obj->getRawText($rawTextXml);
                    $this->rawTextArray[$id] = $rawText;
                } else {
                    Helper::devLog('Invalid class/method "' . $class . '->getRawText()" for text format "' . $textFormat . '"', DEVLOG_SEVERITY_WARNING);
                }
            }
        } else {
            Helper::devLog('Unsupported text format "' . $textFormat . '" in physical node with @ID "' . $id . '"', DEVLOG_SEVERITY_WARNING);
        }
        return $rawText;
    }

    /**
     * This determines a title for the given document
     *
     * @access public
     *
     * @static
     *
     * @param int $uid: The UID of the document
     * @param bool $recursive: Search superior documents for a title, too?
     *
     * @return string The title of the document itself or a parent document
     */
    public static function getTitle($uid, $recursive = false)
    {
        $title = '';
        // Sanitize input.
        $uid = max(intval($uid), 0);
        if ($uid) {
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
                ->getQueryBuilderForTable('tx_dlf_documents');

            $result = $queryBuilder
                ->select(
                    'tx_dlf_documents.title',
                    'tx_dlf_documents.partof'
                )
                ->from('tx_dlf_documents')
                ->where(
                    $queryBuilder->expr()->eq('tx_dlf_documents.uid', $uid),
                    Helper::whereExpression('tx_dlf_documents')
                )
                ->setMaxResults(1)
                ->execute();

            if ($resArray = $result->fetch()) {
                // Get title information.
                $title = $resArray['title'];
                $partof = $resArray['partof'];
                // Search parent documents recursively for a title?
                if (
                    $recursive
                    && empty($title)
                    && intval($partof)
                    && $partof != $uid
                ) {
                    $title = self::getTitle($partof, true);
                }
            } else {
                Helper::devLog('No document with UID ' . $uid . ' found or document not accessible', DEVLOG_SEVERITY_WARNING);
            }
        } else {
            Helper::devLog('Invalid UID ' . $uid . ' for document', DEVLOG_SEVERITY_ERROR);
        }
        return $title;
    }

    /**
     * This extracts all the metadata for the toplevel logical structure node / resource
     *
     * @access public
     *
     * @param int $cPid: The PID for the metadata definitions
     *
     * @return array The logical structure node's / resource's parsed metadata array
     */
    public function getTitledata($cPid = 0)
    {
        $titledata = $this->getMetadata($this->_getToplevelId(), $cPid);
        // Add information from METS structural map to titledata array.
        if ($this instanceof MetsDocument) {
            $this->addMetadataFromMets($titledata, $this->_getToplevelId());
        }
        // Set record identifier for METS file / IIIF manifest if not present.
        if (
            is_array($titledata)
            && array_key_exists('record_id', $titledata)
        ) {
            if (
                !empty($this->recordId)
                && !in_array($this->recordId, $titledata['record_id'])
            ) {
                array_unshift($titledata['record_id'], $this->recordId);
            }
        }
        return $titledata;
    }

    /**
     * Traverse a logical (sub-) structure tree to find the structure with the requested logical id and return it's depth.
     *
     * @access protected
     *
     * @param array $structure: logical structure array
     * @param int $depth: current tree depth
     * @param string $logId: ID of the logical structure whose depth is requested
     *
     * @return int|bool: false if structure with $logId is not a child of this substructure,
     * or the actual depth.
     */
    protected function getTreeDepth($structure, $depth, $logId)
    {
        foreach ($structure as $element) {
            if ($element['id'] == $logId) {
                return $depth;
            } elseif (array_key_exists('children', $element)) {
                $foundInChildren = $this->getTreeDepth($element['children'], $depth + 1, $logId);
                if ($foundInChildren !== false) {
                    return $foundInChildren;
                }
            }
        }
        return false;
    }

    /**
     * Get the tree depth of a logical structure element within the table of content
     *
     * @access public
     *
     * @param string $logId: The id of the logical structure element whose depth is requested
     * @return int|bool tree depth as integer or false if no element with $logId exists within the TOC.
     */
    public function getStructureDepth($logId)
    {
        return $this->getTreeDepth($this->_getTableOfContents(), 1, $logId);
    }

    /**
     * This sets some basic class properties
     *
     * @access protected
     *
     * @abstract
     *
     * @return void
     */
    protected abstract function init();

    /**
     * Reuse any document object that might have been already loaded to determine wether document is METS or IIIF
     *
     * @access protected
     *
     * @abstract
     *
     * @param \SimpleXMLElement|IiifResourceInterface $preloadedDocument: any instance that has already been loaded
     *
     * @return bool true if $preloadedDocument can actually be reused, false if it has to be loaded again
     */
    protected abstract function setPreloadedDocument($preloadedDocument);

    /**
     * METS/IIIF specific part of loading a location
     *
     * @access protected
     *
     * @abstract
     *
     * @param string $location: The URL of the file to load
     *
     * @return bool true on success or false on failure
     */
    protected abstract function loadLocation($location);

    /**
     * Load XML file / IIIF resource from URL
     *
     * @access protected
     *
     * @param string $location: The URL of the file to load
     *
     * @return bool true on success or false on failure
     */
    protected function load($location)
    {
        // Load XML / JSON-LD file.
        if (\TYPO3\CMS\Core\Utility\GeneralUtility::isValidUrl($location)) {
            // Load extension configuration
            $extConf = unserialize($GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf']['dlf']);
            // Set user-agent to identify self when fetching XML / JSON-LD data.
            if (!empty($extConf['useragent'])) {
                @ini_set('user_agent', $extConf['useragent']);
            }
            // the actual loading is format specific
            return $this->loadLocation($location);
        } else {
            Helper::devLog('Invalid file location "' . $location . '" for document loading', DEVLOG_SEVERITY_ERROR);
        }
        return false;
    }

    /**
     * Analyze the document if it contains any fulltext that needs to be indexed.
     *
     * @access protected
     *
     * @abstract
     */
    protected abstract function ensureHasFulltextIsSet();

    /**
     * Register all available data formats
     *
     * @access protected
     *
     * @return void
     */
    protected function loadFormats()
    {
        if (!$this->formatsLoaded) {
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
                ->getQueryBuilderForTable('tx_dlf_formats');

            // Get available data formats from database.
            $result = $queryBuilder
                ->select(
                    'tx_dlf_formats.type AS type',
                    'tx_dlf_formats.root AS root',
                    'tx_dlf_formats.namespace AS namespace',
                    'tx_dlf_formats.class AS class'
                )
                ->from('tx_dlf_formats')
                ->where(
                    $queryBuilder->expr()->eq('tx_dlf_formats.pid', 0)
                )
                ->execute();

            while ($resArray = $result->fetch()) {
                // Update format registry.
                $this->formats[$resArray['type']] = [
                    'rootElement' => $resArray['root'],
                    'namespaceURI' => $resArray['namespace'],
                    'class' => $resArray['class']
                ];
            }
            $this->formatsLoaded = true;
        }
    }

    /**
     * Register all available namespaces for a \SimpleXMLElement object
     *
     * @access public
     *
     * @param \SimpleXMLElement|\DOMXPath &$obj: \SimpleXMLElement or \DOMXPath object
     *
     * @return void
     */
    public function registerNamespaces(&$obj)
    {
        // TODO Check usage. XML specific method does not seem to be used anywhere outside this class within the project, but it is public and may be used by extensions.
        $this->loadFormats();
        // Do we have a \SimpleXMLElement or \DOMXPath object?
        if ($obj instanceof \SimpleXMLElement) {
            $method = 'registerXPathNamespace';
        } elseif ($obj instanceof \DOMXPath) {
            $method = 'registerNamespace';
        } else {
            Helper::devLog('Given object is neither a SimpleXMLElement nor a DOMXPath instance', DEVLOG_SEVERITY_ERROR);
            return;
        }
        // Register metadata format's namespaces.
        foreach ($this->formats as $enc => $conf) {
            $obj->$method(strtolower($enc), $conf['namespaceURI']);
        }
    }

    /**
     * This saves the document to the database and index
     *
     * @access public
     *
     * @param int $pid: The PID of the saved record
     * @param int $core: The UID of the Solr core for indexing
     *
     * @return bool true on success or false on failure
     */
    public function save($pid = 0, $core = 0)
    {
        if (\TYPO3_MODE !== 'BE') {
            Helper::devLog('Saving a document is only allowed in the backend', DEVLOG_SEVERITY_ERROR);
            return false;
        }
        // Make sure $pid is a non-negative integer.
        $pid = max(intval($pid), 0);
        // Make sure $core is a non-negative integer.
        $core = max(intval($core), 0);
        // If $pid is not given, try to get it elsewhere.
        if (
            !$pid
            && $this->pid
        ) {
            // Retain current PID.
            $pid = $this->pid;
        } elseif (!$pid) {
            Helper::devLog('Invalid PID ' . $pid . ' for document saving', DEVLOG_SEVERITY_ERROR);
            return false;
        }
        // Set PID for metadata definitions.
        $this->cPid = $pid;
        // Set UID placeholder if not updating existing record.
        if ($pid != $this->pid) {
            $this->uid = uniqid('NEW');
        }
        // Get metadata array.
        $metadata = $this->getTitledata($pid);
        // Check for record identifier.
        if (empty($metadata['record_id'][0])) {
            Helper::devLog('No record identifier found to avoid duplication', DEVLOG_SEVERITY_ERROR);
            return false;
        }
        // Load plugin configuration.
        $conf = unserialize($GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf'][self::$extKey]);

        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
            ->getQueryBuilderForTable('tx_dlf_structures');

        // Get UID for structure type.
        $result = $queryBuilder
            ->select('tx_dlf_structures.uid AS uid')
            ->from('tx_dlf_structures')
            ->where(
                $queryBuilder->expr()->eq('tx_dlf_structures.pid', intval($pid)),
                $queryBuilder->expr()->eq('tx_dlf_structures.index_name', $queryBuilder->expr()->literal($metadata['type'][0])),
                Helper::whereExpression('tx_dlf_structures')
            )
            ->setMaxResults(1)
            ->execute();

        if ($resArray = $result->fetch()) {
            $structure = $resArray['uid'];
        } else {
            Helper::devLog('Could not identify document/structure type "' . $queryBuilder->expr()->literal($metadata['type'][0]) . '"', DEVLOG_SEVERITY_ERROR);
            return false;
        }
        $metadata['type'][0] = $structure;

        // Remove appended "valueURI" from authors' names for storing in database.
        foreach ($metadata['author'] as $i => $author) {
            $splitName = explode(chr(31), $author);
            $metadata['author'][$i] = $splitName[0];
        }

        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
            ->getQueryBuilderForTable('tx_dlf_collections');

        // Get UIDs for collections.
        $result = $queryBuilder
            ->select(
                'tx_dlf_collections.index_name AS index_name',
                'tx_dlf_collections.uid AS uid'
            )
            ->from('tx_dlf_collections')
            ->where(
                $queryBuilder->expr()->eq('tx_dlf_collections.pid', intval($pid)),
                $queryBuilder->expr()->in('tx_dlf_collections.sys_language_uid', [-1, 0]),
                Helper::whereExpression('tx_dlf_collections')
            )
            ->execute();

        $collUid = [];
        while ($resArray = $result->fetch()) {
            $collUid[$resArray['index_name']] = $resArray['uid'];
        }
        $collections = [];
        foreach ($metadata['collection'] as $collection) {
            if (!empty($collUid[$collection])) {
                // Add existing collection's UID.
                $collections[] = $collUid[$collection];
            } else {
                // Insert new collection.
                $collNewUid = uniqid('NEW');
                $collData['tx_dlf_collections'][$collNewUid] = [
                    'pid' => $pid,
                    'label' => $collection,
                    'index_name' => $collection,
                    'oai_name' => (!empty($conf['publishNewCollections']) ? Helper::getCleanString($collection) : ''),
                    'description' => '',
                    'documents' => 0,
                    'owner' => 0,
                    'status' => 0,
                ];
                $substUid = Helper::processDBasAdmin($collData);
                // Prevent double insertion.
                unset($collData);
                // Add new collection's UID.
                $collections[] = $substUid[$collNewUid];
                if (!(\TYPO3_REQUESTTYPE & \TYPO3_REQUESTTYPE_CLI)) {
                    Helper::addMessage(
                        htmlspecialchars(sprintf(Helper::getMessage('flash.newCollection'), $collection, $substUid[$collNewUid])),
                        Helper::getMessage('flash.attention', true),
                        \TYPO3\CMS\Core\Messaging\FlashMessage::INFO,
                        true
                    );
                }
            }
        }
        $metadata['collection'] = $collections;

        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
            ->getQueryBuilderForTable('tx_dlf_libraries');

        // Get UID for owner.
        $owner = !empty($metadata['owner'][0]) ? $metadata['owner'][0] : 'default';

        $result = $queryBuilder
            ->select('tx_dlf_libraries.uid AS uid')
            ->from('tx_dlf_libraries')
            ->where(
                $queryBuilder->expr()->eq('tx_dlf_libraries.pid', intval($pid)),
                $queryBuilder->expr()->eq('tx_dlf_libraries.index_name', $queryBuilder->expr()->literal($owner)),
                Helper::whereExpression('tx_dlf_libraries')
            )
            ->setMaxResults(1)
            ->execute();

        if ($resArray = $result->fetch()) {
            $ownerUid = $resArray['uid'];
        } else {
            // Insert new library.
            $libNewUid = uniqid('NEW');
            $libData['tx_dlf_libraries'][$libNewUid] = [
                'pid' => $pid,
                'label' => $owner,
                'index_name' => $owner,
                'website' => '',
                'contact' => '',
                'image' => '',
                'oai_label' => '',
                'oai_base' => '',
                'opac_label' => '',
                'opac_base' => '',
                'union_label' => '',
                'union_base' => '',
            ];
            $substUid = Helper::processDBasAdmin($libData);
            // Add new library's UID.
            $ownerUid = $substUid[$libNewUid];
            if (!(\TYPO3_REQUESTTYPE & \TYPO3_REQUESTTYPE_CLI)) {
                Helper::addMessage(
                    htmlspecialchars(sprintf(Helper::getMessage('flash.newLibrary'), $owner, $ownerUid)),
                    Helper::getMessage('flash.attention', true),
                    \TYPO3\CMS\Core\Messaging\FlashMessage::INFO,
                    true
                );
            }
        }
        $metadata['owner'][0] = $ownerUid;
        // Get UID of parent document.
        $partof = $this->getParentDocumentUidForSaving($pid, $core);
        // Use the date of publication or title as alternative sorting metric for parts of multi-part works.
        if (!empty($partof)) {
            if (
                empty($metadata['volume'][0])
                && !empty($metadata['year'][0])
            ) {
                $metadata['volume'] = $metadata['year'];
            }
            if (empty($metadata['volume_sorting'][0])) {
                // If METS @ORDER is given it is preferred over year_sorting and year.
                if (!empty($metadata['mets_order'][0])) {
                    $metadata['volume_sorting'][0] = $metadata['mets_order'][0];
                } elseif (!empty($metadata['year_sorting'][0])) {
                    $metadata['volume_sorting'][0] = $metadata['year_sorting'][0];
                } elseif (!empty($metadata['year'][0])) {
                    $metadata['volume_sorting'][0] = $metadata['year'][0];
                }
            }
            // If volume_sorting is still empty, try to use title_sorting or METS @ORDERLABEL finally (workaround for newspapers)
            if (empty($metadata['volume_sorting'][0])) {
                if (!empty($metadata['title_sorting'][0])) {
                    $metadata['volume_sorting'][0] = $metadata['title_sorting'][0];
                } elseif (!empty($metadata['mets_orderlabel'][0])) {
                    $metadata['volume_sorting'][0] = $metadata['mets_orderlabel'][0];
                }
            }
        }

        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
            ->getQueryBuilderForTable('tx_dlf_metadata');

        // Get metadata for lists and sorting.
        $result = $queryBuilder
            ->select(
                'tx_dlf_metadata.index_name AS index_name',
                'tx_dlf_metadata.is_listed AS is_listed',
                'tx_dlf_metadata.is_sortable AS is_sortable'
            )
            ->from('tx_dlf_metadata')
            ->where(
                $queryBuilder->expr()->orX(
                    $queryBuilder->expr()->eq('tx_dlf_metadata.is_listed', 1),
                    $queryBuilder->expr()->eq('tx_dlf_metadata.is_sortable', 1)
                ),
                $queryBuilder->expr()->eq('tx_dlf_metadata.pid', intval($pid)),
                Helper::whereExpression('tx_dlf_metadata')
            )
            ->execute();

        $listed = [];
        $sortable = [];

        while ($resArray = $result->fetch()) {
            if (!empty($metadata[$resArray['index_name']])) {
                if ($resArray['is_listed']) {
                    $listed[$resArray['index_name']] = $metadata[$resArray['index_name']];
                }
                if ($resArray['is_sortable']) {
                    $sortable[$resArray['index_name']] = $metadata[$resArray['index_name']][0];
                }
            }
        }
        // Fill data array.
        $data['tx_dlf_documents'][$this->uid] = [
            'pid' => $pid,
            $GLOBALS['TCA']['tx_dlf_documents']['ctrl']['enablecolumns']['starttime'] => 0,
            $GLOBALS['TCA']['tx_dlf_documents']['ctrl']['enablecolumns']['endtime'] => 0,
            'prod_id' => $metadata['prod_id'][0],
            'location' => $this->location,
            'record_id' => $metadata['record_id'][0],
            'opac_id' => $metadata['opac_id'][0],
            'union_id' => $metadata['union_id'][0],
            'urn' => $metadata['urn'][0],
            'purl' => $metadata['purl'][0],
            'title' => $metadata['title'][0],
            'title_sorting' => $metadata['title_sorting'][0],
            'author' => implode('; ', $metadata['author']),
            'year' => implode('; ', $metadata['year']),
            'place' => implode('; ', $metadata['place']),
            'thumbnail' => $this->_getThumbnail(true),
            'metadata' => serialize($listed),
            'metadata_sorting' => serialize($sortable),
            'structure' => $metadata['type'][0],
            'partof' => $partof,
            'volume' => $metadata['volume'][0],
            'volume_sorting' => $metadata['volume_sorting'][0],
            'license' => $metadata['license'][0],
            'terms' => $metadata['terms'][0],
            'restrictions' => $metadata['restrictions'][0],
            'out_of_print' => $metadata['out_of_print'][0],
            'rights_info' => $metadata['rights_info'][0],
            'collections' => $metadata['collection'],
            'mets_label' => $metadata['mets_label'][0],
            'mets_orderlabel' => $metadata['mets_orderlabel'][0],
            'mets_order' => $metadata['mets_order'][0],
            'owner' => $metadata['owner'][0],
            'solrcore' => $core,
            'status' => 0,
            'document_format' => $metadata['document_format'][0],
        ];
        // Unhide hidden documents.
        if (!empty($conf['unhideOnIndex'])) {
            $data['tx_dlf_documents'][$this->uid][$GLOBALS['TCA']['tx_dlf_documents']['ctrl']['enablecolumns']['disabled']] = 0;
        }
        // Process data.
        $newIds = Helper::processDBasAdmin($data);
        // Replace placeholder with actual UID.
        if (strpos($this->uid, 'NEW') === 0) {
            $this->uid = $newIds[$this->uid];
            $this->pid = $pid;
            $this->parentId = $partof;
        }
        if (!(\TYPO3_REQUESTTYPE & \TYPO3_REQUESTTYPE_CLI)) {
            Helper::addMessage(
                htmlspecialchars(sprintf(Helper::getMessage('flash.documentSaved'), $metadata['title'][0], $this->uid)),
                Helper::getMessage('flash.done', true),
                \TYPO3\CMS\Core\Messaging\FlashMessage::OK,
                true
            );
        }
        // Add document to index.
        if ($core) {
            Indexer::add($this, $core);
        } else {
            Helper::devLog('Invalid UID "' . $core . '" for Solr core', DEVLOG_SEVERITY_NOTICE);
        }
        return true;
    }

    /**
     * Get the ID of the parent document if the current document has one. Also save a parent document
     * to the database and the Solr index if their $pid and the current $pid differ.
     * Currently only applies to METS documents.
     *
     * @access protected
     *
     * @abstract
     *
     * @return int The parent document's id.
     */
    protected abstract function getParentDocumentUidForSaving($pid, $core);

    /**
     * This returns $this->hasFulltext via __get()
     *
     * @access protected
     *
     * @return bool Are there any fulltext files available?
     */
    protected function _getHasFulltext()
    {
        $this->ensureHasFulltextIsSet();
        return $this->hasFulltext;
    }

    /**
     * This returns $this->location via __get()
     *
     * @access protected
     *
     * @return string The location of the document
     */
    protected function _getLocation()
    {
        return $this->location;
    }

    /**
     * Format specific part of building the document's metadata array
     *
     * @access protected
     *
     * @abstract
     *
     * @param int $cPid
     */
    protected abstract function prepareMetadataArray($cPid);

    /**
     * This builds an array of the document's metadata
     *
     * @access protected
     *
     * @return array Array of metadata with their corresponding logical structure node ID as key
     */
    protected function _getMetadataArray()
    {
        // Set metadata definitions' PID.
        $cPid = ($this->cPid ? $this->cPid : $this->pid);
        if (!$cPid) {
            Helper::devLog('Invalid PID ' . $cPid . ' for metadata definitions', DEVLOG_SEVERITY_ERROR);
            return [];
        }
        if (
            !$this->metadataArrayLoaded
            || $this->metadataArray[0] != $cPid
        ) {
            $this->prepareMetadataArray($cPid);
            $this->metadataArray[0] = $cPid;
            $this->metadataArrayLoaded = true;
        }
        return $this->metadataArray;
    }

    /**
     * This returns $this->numPages via __get()
     *
     * @access protected
     *
     * @return int The total number of pages and/or tracks
     */
    protected function _getNumPages()
    {
        $this->_getPhysicalStructure();
        return $this->numPages;
    }

    /**
     * This returns $this->parentId via __get()
     *
     * @access protected
     *
     * @return int The UID of the parent document or zero if not applicable
     */
    protected function _getParentId()
    {
        return $this->parentId;
    }

    /**
     * This builds an array of the document's physical structure
     *
     * @access protected
     *
     * @abstract
     *
     * @return array Array of physical elements' id, type, label and file representations ordered
     * by @ORDER attribute / IIIF Sequence's Canvases
     */
    protected abstract function _getPhysicalStructure();

    /**
     * This gives an array of the document's physical structure metadata
     *
     * @access protected
     *
     * @return array Array of elements' type, label and file representations ordered by @ID attribute / Canvas order
     */
    protected function _getPhysicalStructureInfo()
    {
        // Is there no physical structure array yet?
        if (!$this->physicalStructureLoaded) {
            // Build physical structure array.
            $this->_getPhysicalStructure();
        }
        return $this->physicalStructureInfo;
    }

    /**
     * This returns $this->pid via __get()
     *
     * @access protected
     *
     * @return int The PID of the document or zero if not in database
     */
    protected function _getPid()
    {
        return $this->pid;
    }

    /**
     * This returns $this->ready via __get()
     *
     * @access protected
     *
     * @return bool Is the document instantiated successfully?
     */
    protected function _getReady()
    {
        return $this->ready;
    }

    /**
     * This returns $this->recordId via __get()
     *
     * @access protected
     *
     * @return mixed The METS file's / IIIF manifest's record identifier
     */
    protected function _getRecordId()
    {
        return $this->recordId;
    }

    /**
     * This returns $this->rootId via __get()
     *
     * @access protected
     *
     * @return int The UID of the root document or zero if not applicable
     */
    protected function _getRootId()
    {
        if (!$this->rootIdLoaded) {
            if ($this->parentId) {
                $parent = self::getInstance($this->parentId, $this->pid);
                $this->rootId = $parent->rootId;
            }
            $this->rootIdLoaded = true;
        }
        return $this->rootId;
    }

    /**
     * This returns the smLinks between logical and physical structMap (METS) and models the
     * relation between IIIF Canvases and Manifests / Ranges in the same way
     *
     * @access protected
     *
     * @abstract
     *
     * @return array The links between logical and physical nodes / Range, Manifest and Canvas
     */
    protected abstract function _getSmLinks();

    /**
     * This builds an array of the document's logical structure
     *
     * @access protected
     *
     * @return array Array of structure nodes' id, label, type and physical page indexes/mptr / Canvas link with original hierarchy preserved
     */
    protected function _getTableOfContents()
    {
        // Is there no logical structure array yet?
        if (!$this->tableOfContentsLoaded) {
            // Get all logical structures.
            $this->getLogicalStructure('', true);
            $this->tableOfContentsLoaded = true;
        }
        return $this->tableOfContents;
    }

    /**
     * This returns the document's thumbnail location
     *
     * @access protected
     *
     * @abstract
     *
     * @param bool $forceReload: Force reloading the thumbnail instead of returning the cached value
     *
     * @return string The document's thumbnail location
     */
    protected abstract function _getThumbnail($forceReload = false);

    /**
     * This returns the ID of the toplevel logical structure node
     *
     * @access protected
     *
     * @abstract
     *
     * @return string The logical structure node's ID
     */
    protected abstract function _getToplevelId();

    /**
     * This returns $this->uid via __get()
     *
     * @access protected
     *
     * @return mixed The UID or the URL of the document
     */
    protected function _getUid()
    {
        return $this->uid;
    }

    /**
     * This sets $this->cPid via __set()
     *
     * @access protected
     *
     * @param int $value: The new PID for the metadata definitions
     *
     * @return void
     */
    protected function _setCPid($value)
    {
        $this->cPid = max(intval($value), 0);
    }

    /**
     * This magic method is invoked each time a clone is called on the object variable
     *
     * @access protected
     *
     * @return void
     */
    protected function __clone()
    {
        // This method is defined as protected because singleton objects should not be cloned.
    }

    /**
     * This is a singleton class, thus the constructor should be private/protected
     * (Get an instance of this class by calling \Kitodo\Dlf\Common\Document::getInstance())
     *
     * @access protected
     *
     * @param int $uid: The UID of the document to parse or URL to XML file
     * @param int $pid: If > 0, then only document with this PID gets loaded
     * @param \SimpleXMLElement|IiifResourceInterface $preloadedDocument: Either null or the \SimpleXMLElement
     * or IiifResourceInterface that has been loaded to determine the basic document format.
     *
     * @return void
     */
    protected function __construct($uid, $pid, $preloadedDocument)
    {
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
            ->getQueryBuilderForTable('tx_dlf_documents');
        $location = '';
        // Prepare to check database for the requested document.
        if (MathUtility::canBeInterpretedAsInteger($uid)) {
            $whereClause = $queryBuilder->expr()->andX(
                $queryBuilder->expr()->eq('tx_dlf_documents.uid', intval($uid)),
                Helper::whereExpression('tx_dlf_documents')
            );
        } else {
            // Try to load METS file / IIIF manifest.
            if ($this->setPreloadedDocument($preloadedDocument) || (GeneralUtility::isValidUrl($uid)
                && $this->load($uid))) {
                // Initialize core METS object.
                $this->init();
                if ($this->getDocument() !== null) {
                    // Cast to string for safety reasons.
                    $location = (string) $uid;
                    $this->establishRecordId($pid);
                } else {
                    // No METS / IIIF part found.
                    return;
                }
            } else {
                // Loading failed.
                return;
            }
            if (
                !empty($location)
                && !empty($this->recordId)
            ) {
                // Try to match record identifier or location (both should be unique).
                $whereClause = $queryBuilder->expr()->andX(
                    $queryBuilder->expr()->orX(
                        $queryBuilder->expr()->eq('tx_dlf_documents.location', $queryBuilder->expr()->literal($location)),
                        $queryBuilder->expr()->eq('tx_dlf_documents.record_id', $queryBuilder->expr()->literal($this->recordId))
                    ),
                    Helper::whereExpression('tx_dlf_documents')
                );
            } else {
                // Can't persistently identify document, don't try to match at all.
                $whereClause = '1=-1';
            }
        }
        // Check for PID if needed.
        if ($pid) {
            $whereClause = $queryBuilder->expr()->andX(
                $whereClause,
                $queryBuilder->expr()->eq('tx_dlf_documents.pid', intval($pid))
            );
        }
        // Get document PID and location from database.
        $result = $queryBuilder
            ->select(
                'tx_dlf_documents.uid AS uid',
                'tx_dlf_documents.pid AS pid',
                'tx_dlf_documents.record_id AS record_id',
                'tx_dlf_documents.partof AS partof',
                'tx_dlf_documents.thumbnail AS thumbnail',
                'tx_dlf_documents.location AS location'
            )
            ->from('tx_dlf_documents')
            ->where($whereClause)
            ->setMaxResults(1)
            ->execute();

        if ($resArray = $result->fetch()) {
            $this->uid = $resArray['uid'];
            $this->pid = $resArray['pid'];
            $this->recordId = $resArray['record_id'];
            $this->parentId = $resArray['partof'];
            $this->thumbnail = $resArray['thumbnail'];
            $this->location = $resArray['location'];
            $this->thumbnailLoaded = true;
            // Load XML file if necessary...
            if (
                $this->getDocument() === null
                && $this->load($this->location)
            ) {
                // ...and set some basic properties.
                $this->init();
            }
            // Do we have a METS / IIIF object now?
            if ($this->getDocument() !== null) {
                // Set new location if necessary.
                if (!empty($location)) {
                    $this->location = $location;
                }
                // Document ready!
                $this->ready = true;
            }
        } elseif ($this->getDocument() !== null) {
            // Set location as UID for documents not in database.
            $this->uid = $location;
            $this->location = $location;
            // Document ready!
            $this->ready = true;
        } else {
            Helper::devLog('No document with UID ' . $uid . ' found or document not accessible', DEVLOG_SEVERITY_ERROR);
        }
    }

    /**
     * This magic method is called each time an invisible property is referenced from the object
     *
     * @access public
     *
     * @param string $var: Name of variable to get
     *
     * @return mixed Value of $this->$var
     */
    public function __get($var)
    {
        $method = '_get' . ucfirst($var);
        if (
            !property_exists($this, $var)
            || !method_exists($this, $method)
        ) {
            Helper::devLog('There is no getter function for property "' . $var . '"', DEVLOG_SEVERITY_WARNING);
            return;
        } else {
            return $this->$method();
        }
    }

    /**
     * This magic method is called each time an invisible property is checked for isset() or empty()
     *
     * @access public
     *
     * @param string $var: Name of variable to check
     *
     * @return bool true if variable is set and not empty, false otherwise
     */
    public function __isset($var)
    {
        return !empty($this->__get($var));
    }

    /**
     * This magic method is called each time an invisible property is referenced from the object
     *
     * @access public
     *
     * @param string $var: Name of variable to set
     * @param mixed $value: New value of variable
     *
     * @return void
     */
    public function __set($var, $value)
    {
        $method = '_set' . ucfirst($var);
        if (
            !property_exists($this, $var)
            || !method_exists($this, $method)
        ) {
            Helper::devLog('There is no setter function for property "' . $var . '"', DEVLOG_SEVERITY_WARNING);
        } else {
            $this->$method($value);
        }
    }
}
