diff --git a/module/VuFind/src/VuFind/Hierarchy/TreeDataSource/Factory.php b/module/VuFind/src/VuFind/Hierarchy/TreeDataSource/Factory.php index 403360853e1c37192b83d8d6aa09ca31ad5f4877..ca21f2b09721e0984e0715ad02aceeebd291bb0b 100644 --- a/module/VuFind/src/VuFind/Hierarchy/TreeDataSource/Factory.php +++ b/module/VuFind/src/VuFind/Hierarchy/TreeDataSource/Factory.php @@ -59,9 +59,12 @@ class Factory $filters = isset($hierarchyFilters->HierarchyTree->filterQueries) ? $hierarchyFilters->HierarchyTree->filterQueries->toArray() : []; + $solr = $sm->getServiceLocator()->get('VuFind\Search\BackendManager') + ->get('Solr')->getConnector(); + $formatterManager = $sm->getServiceLocator() + ->get('VuFind\HierarchyTreeDataFormatterPluginManager'); return new Solr( - $sm->getServiceLocator()->get('VuFind\Search'), - rtrim($cacheDir, '/') . '/hierarchy', + $solr, $formatterManager, rtrim($cacheDir, '/') . '/hierarchy', $filters ); } diff --git a/module/VuFind/src/VuFind/Hierarchy/TreeDataSource/Solr.php b/module/VuFind/src/VuFind/Hierarchy/TreeDataSource/Solr.php index bff8f2fc3b3a98d0589ee27b900e885cbd5706e2..80097c1ae3b3f68d2b9be3c269e9df71e5954052 100644 --- a/module/VuFind/src/VuFind/Hierarchy/TreeDataSource/Solr.php +++ b/module/VuFind/src/VuFind/Hierarchy/TreeDataSource/Solr.php @@ -26,8 +26,9 @@ * @link http://vufind.org/wiki/vufind2:hierarchy_components Wiki */ namespace VuFind\Hierarchy\TreeDataSource; +use VuFind\Hierarchy\TreeDataFormatter\PluginManager as FormatterManager; use VuFindSearch\Query\Query; -use VuFindSearch\Service as SearchService; +use VuFindSearch\Backend\Solr\Connector; use VuFindSearch\ParamBag; /** @@ -46,9 +47,16 @@ class Solr extends AbstractBase /** * Search service * - * @var SearchService + * @var Connector */ - protected $searchService; + protected $solrConnector; + + /** + * Formatter manager + * + * @var FormatterManager + */ + protected $formatterManager; /** * Cache directory @@ -67,14 +75,16 @@ class Solr extends AbstractBase /** * Constructor. * - * @param SearchService $search Search service - * @param string $cacheDir Directory to hold cache results (optional) - * @param array $filters Filters to apply to Solr tree queries + * @param Connector $connector Solr connector + * @param FormatterManager $fm Formatter manager + * @param string $cacheDir Directory to hold cache results (optional) + * @param array $filters Filters to apply to Solr tree queries */ - public function __construct(SearchService $search, $cacheDir = null, - $filters = [] + public function __construct(Connector $connector, FormatterManager $fm, + $cacheDir = null, $filters = [] ) { - $this->searchService = $search; + $this->solrConnector = $connector; + $this->formatterManager = $fm; if (null !== $cacheDir) { $this->cacheDir = rtrim($cacheDir, '/'); } @@ -94,102 +104,97 @@ class Solr extends AbstractBase */ public function getXML($id, $options = []) { - $top = $this->searchService->retrieve('Solr', $id)->getRecords(); - if (!isset($top[0])) { - return ''; - } - $top = $top[0]; - $cacheFile = (null !== $this->cacheDir) - ? $this->cacheDir . '/hierarchyTree_' . urlencode($id) . '.xml' - : false; + return $this->getFormattedData($id, 'xml', $options, 'hierarchyTree_%s.xml'); + } - $useCache = isset($options['refresh']) ? !$options['refresh'] : true; - $cacheTime = $this->getHierarchyDriver()->getTreeCacheTime(); + /** + * Search Solr. + * + * @param string $q Search query + * @param int $rows Max rows to retrieve (default = int max) + * + * @return array + */ + protected function searchSolr($q, $rows = 2147483647) + { + $params = new ParamBag( + [ + 'q' => [$q], + 'fq' => $this->filters, + 'hl' => ['false'], + 'fl' => ['title,id,hierarchy_parent_id,hierarchy_top_id,' + . 'is_hierarchy_id,hierarchy_sequence,title_in_hierarchy'], + 'wt' => ['json'], + 'json.nl' => ['arrarr'], + 'rows' => [$rows], // Integer max + 'start' => [0] + ] + ); + $response = $this->solrConnector->search($params); + return json_decode($response); + } - if ($useCache && file_exists($cacheFile) - && ($cacheTime < 0 || filemtime($cacheFile) > (time() - $cacheTime)) - ) { - $this->debug("Using cached data from $cacheFile"); - $xml = file_get_contents($cacheFile); - } else { - $starttime = microtime(true); - $isCollection = $top->isCollection() ? "true" : "false"; - $xml = '<root><item id="' . - htmlspecialchars($id) . - '" isCollection="' . $isCollection . '">' . - '<content><name>' . htmlspecialchars($top->getTitle()) . - '</name></content>'; - $count = 0; - $xml .= $this->getChildren($id, $count); - $xml .= '</item></root>'; - if ($cacheFile) { - if (!file_exists($this->cacheDir)) { - mkdir($this->cacheDir); + /** + * Retrieve a map of children for the provided hierarchy. + * + * @param string $id Record ID + * + * @return array + */ + protected function getMapForHierarchy($id) + { + // Static cache of last map; if the user requests the same map twice + // in a row (as when generating XML and JSON in sequence) this will + // save a Solr hit. + static $map; + static $lastId = null; + if ($id === $lastId) { + return $map; + } + $lastId = $id; + + $results = $this->searchSolr('hierarchy_top_id:"' . $id . '"'); + if ($results->response->numFound < 1) { + return []; + } + $map = [$id => []]; + foreach ($results->response->docs as $current) { + $parents = isset($current->hierarchy_parent_id) + ? $current->hierarchy_parent_id : []; + foreach ($parents as $parentId) { + if (!isset($map[$parentId])) { + $map[$parentId] = [$current]; + } else { + $map[$parentId][] = $current; } - file_put_contents($cacheFile, $xml); } - $this->debug( - "Hierarchy of $count records built in " . - abs(microtime(true) - $starttime) - ); } - return $xml; + return $map; } /** - * Get Solr Children + * Get a record from Solr (return false if not found). * - * @param string $parentID The starting point for the current recursion - * (equivlent to Solr field hierarchy_parent_id) - * @param string $count The total count of items in the tree - * before this recursion + * @param string $id ID to fetch. * - * @return string + * @return array|bool */ - protected function getChildren($parentID, &$count) + protected function getRecord($id) { - $query = new Query( - 'hierarchy_parent_id:"' . addcslashes($parentID, '"') . '"' - ); - $results = $this->searchService->search( - 'Solr', $query, 0, 10000, - new ParamBag(['fq' => $this->filters, 'hl' => 'false']) - ); - if ($results->getTotal() < 1) { - return ''; - } - $xml = []; - $sorting = $this->getHierarchyDriver()->treeSorting(); - - foreach ($results->getRecords() as $current) { - ++$count; - - $titles = $current->getTitlesInHierarchy(); - $title = isset($titles[$parentID]) - ? $titles[$parentID] : $current->getTitle(); - - $this->debug("$parentID: " . $current->getUniqueID()); - $xmlNode = ''; - $isCollection = $current->isCollection() ? "true" : "false"; - $xmlNode .= '<item id="' . htmlspecialchars($current->getUniqueID()) . - '" isCollection="' . $isCollection . '"><content><name>' . - htmlspecialchars($title) . '</name></content>'; - $xmlNode .= $this->getChildren($current->getUniqueID(), $count); - $xmlNode .= '</item>'; - - // If we're in sorting mode, we need to create key-value arrays; - // otherwise, we can just collect flat strings. - if ($sorting) { - $positions = $current->getHierarchyPositionsInParents(); - $sequence = isset($positions[$parentID]) ? $positions[$parentID] : 0; - $xml[] = [$sequence, $xmlNode]; - } else { - $xml[] = $xmlNode; - } + // Static cache of last record; if the user requests the same map twice + // in a row (as when generating XML and JSON in sequence) this will + // save a Solr hit. + static $record; + static $lastId = null; + if ($id === $lastId) { + return $record; } + $lastId = $id; - // Assemble the XML, sorting it first if necessary: - return implode('', $sorting ? $this->sortNodes($xml) : $xml); + $recordResults = $this->searchSolr('id:"' . $id . '"', 1); + $record = isset($recordResults->response->docs[0]) + ? $recordResults->response->docs[0] : false; + return $record; } /** @@ -205,13 +210,26 @@ class Solr extends AbstractBase */ public function getJSON($id, $options = []) { - $top = $this->searchService->retrieve('Solr', $id)->getRecords(); - if (!isset($top[0])) { - return ''; - } - $top = $top[0]; + return $this->getFormattedData($id, 'json', $options, 'tree_%s.json'); + } + + /** + * Get formatted data for the specified hierarchy ID. + * + * @param string $id Hierarchy ID. + * @param string $format Name of formatter service to use. + * @param array $options Additional options for JSON generation. + * (Currently one option is supported: 'refresh' may be set to true to + * bypass caching). + * @param string $cacheTemplate Template for cache filenames + * + * @return string + */ + public function getFormattedData($id, $format, $options = [], + $cacheTemplate = 'tree_%s' + ) { $cacheFile = (null !== $this->cacheDir) - ? $this->cacheDir . '/tree_' . urlencode($id) . '.json' + ? $this->cacheDir . '/' . sprintf($cacheTemplate, urlencode($id)) : false; $useCache = isset($options['refresh']) ? !$options['refresh'] : true; @@ -225,19 +243,23 @@ class Solr extends AbstractBase return $json; } else { $starttime = microtime(true); - $json = [ - 'id' => $id, - 'type' => $top->isCollection() - ? 'collection' - : 'record', - 'title' => $top->getTitle() - ]; - $children = $this->getChildrenJson($id, $count); - if (!empty($children)) { - $json['children'] = $children; + $map = $this->getMapForHierarchy($id); + if (empty($map)) { + return ''; } + // Get top record's info + $formatter = $this->formatterManager->get($format); + $formatter->setRawData( + $this->getRecord($id), $map, + $this->getHierarchyDriver()->treeSorting(), + $this->getHierarchyDriver()->getCollectionLinkType() + ); + $encoded = $formatter->getData(); + $count = $formatter->getCount(); + + $this->debug('Done: ' . abs(microtime(true) - $starttime)); + if ($cacheFile) { - $encoded = json_encode($json); // Write file if (!file_exists($this->cacheDir)) { mkdir($this->cacheDir); @@ -245,101 +267,13 @@ class Solr extends AbstractBase file_put_contents($cacheFile, $encoded); } $this->debug( - "Hierarchy of $count records built in " . + "Hierarchy of {$count} records built in " . abs(microtime(true) - $starttime) ); return $encoded; } } - /** - * Get Solr Children for JSON - * - * @param string $parentID The starting point for the current recursion - * (equivlent to Solr field hierarchy_parent_id) - * @param string $count The total count of items in the tree - * before this recursion - * - * @return string - */ - protected function getChildrenJson($parentID, &$count) - { - $query = new Query( - 'hierarchy_parent_id:"' . addcslashes($parentID, '"') . '"' - ); - $results = $this->searchService->search( - 'Solr', $query, 0, 10000, - new ParamBag(['fq' => $this->filters, 'hl' => 'false']) - ); - if ($results->getTotal() < 1) { - return ''; - } - $json = []; - $sorting = $this->getHierarchyDriver()->treeSorting(); - - foreach ($results->getRecords() as $current) { - ++$count; - - $titles = $current->getTitlesInHierarchy(); - $title = isset($titles[$parentID]) - ? $titles[$parentID] : $current->getTitle(); - - $this->debug("$parentID: " . $current->getUniqueID()); - $childNode = [ - 'id' => $current->getUniqueID(), - 'type' => $current->isCollection() - ? 'collection' - : 'record', - 'title' => $title - ]; - if ($current->isCollection()) { - $children = $this->getChildrenJson( - $current->getUniqueID(), - $count - ); - if (!empty($children)) { - $childNode['children'] = $children; - } - } - - // If we're in sorting mode, we need to create key-value arrays; - // otherwise, we can just collect flat values. - if ($sorting) { - $positions = $current->getHierarchyPositionsInParents(); - $sequence = isset($positions[$parentID]) ? $positions[$parentID] : 0; - $json[] = [$sequence, $childNode]; - } else { - $json[] = $childNode; - } - } - - return $sorting ? $this->sortNodes($json) : $json; - } - - /** - * Sort Nodes - * Convert an unsorted array of [ key, value ] pairs into a sorted array - * of values. - * - * @param array $array The array of arrays to sort - * - * @return array - */ - protected function sortNodes($array) - { - // Sort arrays based on first element - $sorter = function ($a, $b) { - return strcmp($a[0], $b[0]); - }; - usort($array, $sorter); - - // Collapse array to remove sort values - $mapper = function ($i) { - return $i[1]; - }; - return array_map($mapper, $array); - } - /** * Does this data source support the specified hierarchy ID? * @@ -354,10 +288,7 @@ class Solr extends AbstractBase if (!isset($settings['checkAvailability']) || $settings['checkAvailability'] == 1 ) { - $results = $this->searchService->retrieve( - 'Solr', $id, new ParamBag(['fq' => $this->filters]) - ); - if ($results->getTotal() < 1) { + if (!$this->getRecord($id)) { return false; } }