my changes
This commit is contained in:
parent
28301e4312
commit
8dc1f1b048
263 changed files with 36882 additions and 4453 deletions
68
app/Library/Search/Navigation.php
Normal file
68
app/Library/Search/Navigation.php
Normal file
|
@ -0,0 +1,68 @@
|
|||
<?php
|
||||
namespace App\Library\Search;
|
||||
|
||||
use App\Library\Util\QueryBuilder;
|
||||
use App\Library\Util\Searchtypes;
|
||||
use App\Library\Util\SolrSearchQuery;
|
||||
|
||||
/**
|
||||
* Class for navigation in search results.
|
||||
*/
|
||||
class Navigation
|
||||
{
|
||||
|
||||
/**
|
||||
* Builds query for Solr search.
|
||||
* @return SolrSearchQuery|void
|
||||
* @throws Application_Exception, Application_Util_BrowsingParamsException, Application_Util_QueryBuilderException
|
||||
*/
|
||||
public static function getQueryUrl(\Illuminate\Http\Request $request) : SolrSearchQuery
|
||||
{
|
||||
$queryBuilder = new QueryBuilder();
|
||||
$queryBuilderInput = $queryBuilder->createQueryBuilderInputFromRequest($request);
|
||||
|
||||
if (is_null($request->input('sortfield')) &&
|
||||
($request->input('browsing') === 'true' || $request->input('searchtype') === 'collection')) {
|
||||
$queryBuilderInput['sortField'] = 'server_date_published';
|
||||
}
|
||||
|
||||
if ($request->input('searchtype') === Searchtypes::LATEST_SEARCH) {
|
||||
return $queryBuilder->createSearchQuery(self::validateInput($queryBuilderInput, 10, 100));
|
||||
}
|
||||
|
||||
$solrSearchQuery = $queryBuilder->createSearchQuery(self::validateInput($queryBuilderInput, 1, 100));
|
||||
return $solrSearchQuery;
|
||||
//$queryBuilder->createSearchQuery(self::validateInput($queryBuilderInput,1, 100));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust the actual rows parameter value if it is not between $min
|
||||
* and $max (inclusive). In case the actual value is smaller (greater)
|
||||
* than $min ($max) it is adjusted to $min ($max).
|
||||
*
|
||||
* Sets the actual start parameter value to 0 if it is negative.
|
||||
*
|
||||
* @param array $data An array that contains the request parameters.
|
||||
* @param int $lowerBoundInclusive The lower bound.
|
||||
* @param int $upperBoundInclusive The upper bound.
|
||||
* @return int Returns the actual rows parameter value or an adjusted value if
|
||||
* it is not in the interval [$lowerBoundInclusive, $upperBoundInclusive].
|
||||
*
|
||||
*/
|
||||
private static function validateInput(array $input, $min = 1, $max = 100) : array
|
||||
{
|
||||
if ($input['rows'] > $max) {
|
||||
// $logger->warn("Values greater than $max are currently not allowed for the rows paramter.");
|
||||
$input['rows'] = $max;
|
||||
}
|
||||
if ($input['rows'] < $min) {
|
||||
// $logger->warn("rows parameter is smaller than $min: adjusting to $min.");
|
||||
$input['rows'] = $min;
|
||||
}
|
||||
if ($input['start'] < 0) {
|
||||
// $logger->warn("A negative start parameter is ignored.");
|
||||
$input['start'] = 0;
|
||||
}
|
||||
return $input;
|
||||
}
|
||||
}
|
313
app/Library/Search/SearchResult.php
Normal file
313
app/Library/Search/SearchResult.php
Normal file
|
@ -0,0 +1,313 @@
|
|||
<?php
|
||||
namespace App\Library\Search;
|
||||
|
||||
/**
|
||||
* Implements API for describing successful response to search query.
|
||||
*/
|
||||
|
||||
use App\Library\Util\SearchResultMatch;
|
||||
|
||||
class SearchResult
|
||||
{
|
||||
protected $data = array(
|
||||
'matches' => null,
|
||||
'count' => null,
|
||||
'querytime' => null,
|
||||
'facets' => null,
|
||||
);
|
||||
protected $validated = false;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @return SearchResult
|
||||
*/
|
||||
public static function create()
|
||||
{
|
||||
return new static();
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns matches returned in response to search query.
|
||||
*
|
||||
* @param mixed $documentId ID of document considered match of related search query
|
||||
* @return SearchResultMatch
|
||||
*/
|
||||
public function addMatch($documentId)
|
||||
{
|
||||
if (!is_array($this->data['matches'])) {
|
||||
$this->data['matches'] = array();
|
||||
}
|
||||
$match = SearchResultMatch::create($documentId);
|
||||
$this->data['matches'][] = $match;
|
||||
|
||||
return $match;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets number of all matching documents.
|
||||
*
|
||||
* @note This may include documents not listed as matches here due to using
|
||||
* paging parameters on query.
|
||||
*
|
||||
* @param int $allMatchesCount number of all matching documents
|
||||
* @return $this fluent interface
|
||||
*/
|
||||
public function setAllMatchesCount($allMatchesCount)
|
||||
{
|
||||
if (!is_null($this->data['count'])) {
|
||||
throw new RuntimeException('must not set count of all matches multiple times');
|
||||
}
|
||||
|
||||
if (!ctype_digit(trim($allMatchesCount))) {
|
||||
throw new InvalidArgumentException('invalid number of overall matches');
|
||||
}
|
||||
$this->data['count'] = intval($allMatchesCount);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets information on time taken for querying search engine.
|
||||
*
|
||||
* @param string $time
|
||||
* @return $this fluent interface
|
||||
*/
|
||||
public function setQueryTime($time)
|
||||
{
|
||||
if (!is_null($this->data['querytime'])) {
|
||||
throw new RuntimeException('must not set query time multiple times');
|
||||
}
|
||||
if (!is_null($time)) {
|
||||
$this->data['querytime'] = trim($time);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds another result of faceted search to current result set.
|
||||
*
|
||||
* @param string $facetField name of field result of faceted search is related to
|
||||
* @param string $text description on particular faceted result on field (e.g. single value in field)
|
||||
* @param int $count number of occurrences of facet on field in all matches
|
||||
* @return $this fluent interface
|
||||
*
|
||||
* TODO special year_inverted facet handling should be moved to separate class
|
||||
*/
|
||||
public function addFacet($facetField, $text, $count)
|
||||
{
|
||||
$facetField = strval($facetField);
|
||||
|
||||
// remove inverted sorting prefix from year values
|
||||
if ($facetField === 'year_inverted') {
|
||||
$text = explode(':', $text, 2)[1];
|
||||
|
||||
// treat 'year_inverted' as if it was 'year'
|
||||
$facetField = 'year';
|
||||
}
|
||||
|
||||
// treat 'year_inverted' as if it was 'year'
|
||||
if ($facetField === 'year_inverted') {
|
||||
$facetField = 'year';
|
||||
}
|
||||
|
||||
if (!is_array($this->data['facets'])) {
|
||||
$this->data['facets'] = array();
|
||||
}
|
||||
|
||||
if (!array_key_exists($facetField, $this->data['facets'])) {
|
||||
$this->data['facets'][$facetField] = array();
|
||||
}
|
||||
|
||||
$this->data['facets'][$facetField][] = new Opus_Search_Result_Facet($text, $count);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves results of faceted search.
|
||||
*
|
||||
* @return Opus_Search_Result_Facet[][] map of fields' names into sets of facet result per field
|
||||
*/
|
||||
public function getFacets()
|
||||
{
|
||||
return is_null($this->data['facets']) ? array() : $this->data['facets'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves set of facet results on single field selected by name.
|
||||
*
|
||||
* @param string $fieldName name of field returned facet result is related to
|
||||
* @return Opus_Search_Result_Facet[] set of facet results on selected field
|
||||
*/
|
||||
public function getFacet($fieldName)
|
||||
{
|
||||
if ($this->data['facets'] && array_key_exists($fieldName, $this->data['facets'])) {
|
||||
return $this->data['facets'][$fieldName];
|
||||
}
|
||||
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves set of matching and locally existing documents returned in
|
||||
* response to some search query.
|
||||
*
|
||||
* @return Opus_Search_Result_Match[]
|
||||
*/
|
||||
public function getReturnedMatches()
|
||||
{
|
||||
if (is_null($this->data['matches'])) {
|
||||
return array();
|
||||
}
|
||||
|
||||
// map AND FILTER set of returned matches ensuring to list related
|
||||
// documents existing locally, only
|
||||
$matches = array();
|
||||
|
||||
foreach ($this->data['matches'] as $match) {
|
||||
try {
|
||||
/** @var SearchResultMatch $match */
|
||||
// $match->getDocument();
|
||||
$matches[] = $match;
|
||||
} catch (Opus_Document_Exception $e) {
|
||||
Opus_Log::get()->warn('skipping matching but locally missing document #' . $match->getId());
|
||||
}
|
||||
}
|
||||
return $matches;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves set of matching documents' IDs returned in response to some
|
||||
* search query.
|
||||
*
|
||||
* @note If query was requesting to retrieve non-qualified matches this set
|
||||
* might include IDs of documents that doesn't exist locally anymore.
|
||||
*
|
||||
* @return int[]
|
||||
*/
|
||||
public function getReturnedMatchingIds()
|
||||
{
|
||||
if (is_null($this->data['matches'])) {
|
||||
return array();
|
||||
}
|
||||
|
||||
return array_map(function ($match) {
|
||||
/** @var SearchResultMatch $match */
|
||||
return $match->getId();
|
||||
}, $this->data['matches']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves set of matching documents.
|
||||
*
|
||||
* @note This is provided for downward compatibility, though it's signature
|
||||
* has changed in that it's returning set of Opus_Document instances
|
||||
* rather than set of Opus_SolrSearch_Result instances.
|
||||
*
|
||||
* @note The wording is less specific in that all information in response to
|
||||
* search query may considered results of search. Thus this new API
|
||||
* prefers "matches" over "results".
|
||||
*
|
||||
* @deprecated
|
||||
* @return Opus_Document[]
|
||||
*/
|
||||
public function getResults()
|
||||
{
|
||||
return $this->getReturnedMatches();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all returned matches referring to Opus documents missing in local
|
||||
* database.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function dropLocallyMissingMatches()
|
||||
{
|
||||
if (!$this->validated) {
|
||||
$finder = new Opus_DocumentFinder();
|
||||
|
||||
$returnedIds = $this->getReturnedMatchingIds();
|
||||
$existingIds = $finder
|
||||
->setServerState('published')
|
||||
->setIdSubset($returnedIds)
|
||||
->ids();
|
||||
|
||||
if (count($returnedIds) !== count($existingIds)) {
|
||||
Opus_Log::get()->err(sprintf(
|
||||
"inconsistency between db and search index: index returns %d documents, but only %d found in db",
|
||||
count($returnedIds),
|
||||
count($existingIds)
|
||||
));
|
||||
|
||||
// update set of returned matches internally
|
||||
$this->data['matches'] = array();
|
||||
foreach ($existingIds as $id) {
|
||||
$this->addMatch($id);
|
||||
}
|
||||
// set mark to prevent validating matches again
|
||||
$this->validated = true;
|
||||
}
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves overall number of matches.
|
||||
*
|
||||
* @note This number includes matches not included in fetched subset of
|
||||
* matches.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getAllMatchesCount()
|
||||
{
|
||||
if (is_null($this->data['count'])) {
|
||||
throw new RuntimeException('count of matches have not been provided yet');
|
||||
}
|
||||
return $this->data['count'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves overall number of matches.
|
||||
*
|
||||
* @note This is provided for downward compatibility.
|
||||
*
|
||||
* @deprecated
|
||||
* @return int
|
||||
*/
|
||||
public function getNumberOfHits()
|
||||
{
|
||||
return $this->getAllMatchesCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves information on search query's processing time.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getQueryTime()
|
||||
{
|
||||
return $this->data['querytime'];
|
||||
}
|
||||
|
||||
public function __get($name)
|
||||
{
|
||||
switch (strtolower(trim($name))) {
|
||||
case 'matches':
|
||||
return $this->getReturnedMatches();
|
||||
|
||||
case 'allmatchescount':
|
||||
return $this->getAllMatchesCount();
|
||||
|
||||
case 'querytime':
|
||||
return $this->getQueryTime();
|
||||
default:
|
||||
throw new RuntimeException('invalid request for property ' . $name);
|
||||
}
|
||||
}
|
||||
}
|
218
app/Library/Search/SolariumAdapter.php
Normal file
218
app/Library/Search/SolariumAdapter.php
Normal file
|
@ -0,0 +1,218 @@
|
|||
<?php
|
||||
|
||||
namespace App\Library\Search;
|
||||
|
||||
//use App\Library\Util\SolrSearchQuery;
|
||||
use App\Library\Util\SearchParameter;
|
||||
use App\Library\Search\SearchResult;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class SolariumAdapter
|
||||
{
|
||||
protected $options;
|
||||
|
||||
/**
|
||||
* @var \Solarium\Core\Client\Client
|
||||
*/
|
||||
protected $client;
|
||||
|
||||
public function __construct($serviceName, $options)
|
||||
{
|
||||
$this->options = $options;
|
||||
$this->client = new \Solarium\Client($options);
|
||||
|
||||
// ensure service is basically available
|
||||
$ping = $this->client->createPing();
|
||||
$this->execute($ping, 'failed pinging service ' . $serviceName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps name of field returned by search engine into name of asset to use
|
||||
* on storing field's value in context of related match.
|
||||
*
|
||||
* This mapping relies on runtime configuration. Mapping is defined per
|
||||
* service in
|
||||
*
|
||||
* @param string $fieldName
|
||||
* @return string
|
||||
*/
|
||||
protected function mapResultFieldToAsset($fieldName)
|
||||
{
|
||||
//if ( $this->options->fieldToAsset instanceof Zend_Config )
|
||||
//{
|
||||
// return $this->options->fieldToAsset->get( $fieldName, $fieldName );
|
||||
//}
|
||||
return $fieldName;
|
||||
}
|
||||
|
||||
public function getDomain()
|
||||
{
|
||||
return 'solr';
|
||||
}
|
||||
|
||||
public function createQuery() : SearchParameter
|
||||
{
|
||||
return new SearchParameter();
|
||||
}
|
||||
|
||||
public function customSearch(SearchParameter $queryParameter)
|
||||
{
|
||||
$search = $this->client->createSelect();
|
||||
$solariumQuery = $this->applyParametersToSolariumQuery($search, $queryParameter, false);
|
||||
$searchResult = $this->processQuery($solariumQuery);
|
||||
return $searchResult;
|
||||
}
|
||||
|
||||
protected function applyParametersToSolariumQuery(\Solarium\QueryType\Select\Query\Query $query, SearchParameter $parameters = null, $preferOriginalQuery = false)
|
||||
{
|
||||
if ($parameters) {
|
||||
//$subfilters = $parameters->getSubFilters();
|
||||
//if ( $subfilters !== null ) {
|
||||
// foreach ( $subfilters as $name => $subfilter ) {
|
||||
// if ( $subfilter instanceof Opus_Search_Solr_Filter_Raw || $subfilter instanceof Opus_Search_Solr_Solarium_Filter_Complex ) {
|
||||
// $query->createFilterQuery( $name )
|
||||
// ->setQuery( $subfilter->compile( $query ) );
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
// $filter = $parameters->getFilter();//"aa"
|
||||
// if ( $filter instanceof Opus_Search_Solr_Filter_Raw || $filter instanceof Opus_Search_Solr_Solarium_Filter_Complex ) {
|
||||
// if ( !$query->getQuery() || !$preferOriginalQuery ) {
|
||||
// $compiled = $filter->compile( $query );
|
||||
// if ( $compiled !== null ) {
|
||||
// // compile() hasn't implicitly assigned query before
|
||||
// $query->setQuery( $compiled );
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
$filter = $parameters->getFilter();//"aa" all: '*:*'
|
||||
if ($filter !== null) {
|
||||
//$query->setStart( intval( $start ) );
|
||||
//$query->setQuery('%P1%', array($filter));
|
||||
$query->setQuery($filter);
|
||||
}
|
||||
|
||||
|
||||
$start = $parameters->getStart();
|
||||
if ($start !== null) {
|
||||
$query->setStart(intval($start));
|
||||
}
|
||||
|
||||
$rows = $parameters->getRows();
|
||||
if ($rows !== null) {
|
||||
$query->setRows(intval($rows));
|
||||
}
|
||||
|
||||
$union = $parameters->getUnion();
|
||||
if ($union !== null) {
|
||||
$query->setQueryDefaultOperator($union ? 'OR' : 'AND');
|
||||
}
|
||||
|
||||
$fields = $parameters->getFields();
|
||||
if ($fields !== null) {
|
||||
$query->setFields($fields);
|
||||
}
|
||||
|
||||
$sortings = $parameters->getSort();
|
||||
if ($sortings !== null) {
|
||||
$query->setSorts($sortings);
|
||||
}
|
||||
|
||||
$facet = $parameters->getFacet();
|
||||
if ($facet !== null) {
|
||||
$facetSet = $query->getFacetSet();
|
||||
foreach ($facet->getFields() as $field) {
|
||||
$facetSet->createFacetField($field->getName())
|
||||
->setField($field->getName())
|
||||
->setMinCount($field->getMinCount())
|
||||
->setLimit($field->getLimit())
|
||||
->setSort($field->getSort() ? 'index' : null);
|
||||
}
|
||||
if ($facet->isFacetOnly()) {
|
||||
$query->setFields(array());
|
||||
}
|
||||
}
|
||||
}
|
||||
return $query;
|
||||
}
|
||||
|
||||
protected function execute($query, $actionText)
|
||||
{
|
||||
$result = null;
|
||||
try {
|
||||
$result = $this->client->execute($query);
|
||||
} catch (\Solarium\Exception\HttpException $e) {
|
||||
sprintf('%s: %d %s', $actionText, $e->getCode(), $e->getStatusMessage());
|
||||
} finally {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// if ( $result->getStatus() ) {
|
||||
// throw new Opus_Search_Exception( $actionText, $result->getStatus() );
|
||||
// }
|
||||
}
|
||||
|
||||
protected function processQuery(\Solarium\QueryType\Select\Query\Query $query) : SearchResult
|
||||
{
|
||||
// send search query to service
|
||||
$request = $this->execute($query, 'failed querying search engine');
|
||||
|
||||
//$count = $request->getDocuments();
|
||||
// create result descriptor
|
||||
$result = SearchResult::create()
|
||||
->setAllMatchesCount($request->getNumFound())
|
||||
->setQueryTime($request->getQueryTime());
|
||||
|
||||
// add description on every returned match
|
||||
$excluded = 0;
|
||||
foreach ($request->getDocuments() as $document) {
|
||||
/** @var \Solarium\QueryType\Select\Result\Document $document */
|
||||
$fields = $document->getFields();
|
||||
|
||||
if (array_key_exists('id', $fields)) {
|
||||
$match = $result->addMatch($fields['id']);
|
||||
|
||||
foreach ($fields as $fieldName => $fieldValue) {
|
||||
switch ($fieldName) {
|
||||
case 'id':
|
||||
break;
|
||||
|
||||
case 'score':
|
||||
$match->setScore($fieldValue);
|
||||
break;
|
||||
|
||||
case 'server_date_modified':
|
||||
$match->setServerDateModified($fieldValue);
|
||||
break;
|
||||
|
||||
case 'fulltext_id_success':
|
||||
$match->setFulltextIDsSuccess($fieldValue);
|
||||
break;
|
||||
|
||||
case 'fulltext_id_failure':
|
||||
$match->setFulltextIDsFailure($fieldValue);
|
||||
break;
|
||||
|
||||
default:
|
||||
$match->setAsset($fieldName, $fieldValue);
|
||||
//$match->setAsset( $this->mapResultFieldToAsset( $fieldName ), $fieldValue );
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$excluded++;
|
||||
}
|
||||
}
|
||||
|
||||
if ($excluded > 0) {
|
||||
Log::warning(sprintf(
|
||||
'search yielded %d matches not available in result set for missing ID of related document',
|
||||
$excluded
|
||||
));
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
257
app/Library/Util/QueryBuilder.php
Normal file
257
app/Library/Util/QueryBuilder.php
Normal file
|
@ -0,0 +1,257 @@
|
|||
<?php
|
||||
namespace App\Library\Util;
|
||||
|
||||
use App\Library\Util\Searchtypes;
|
||||
use App\Library\Util\SolrSearchQuery;
|
||||
|
||||
class QueryBuilder
|
||||
{
|
||||
private $_logger;
|
||||
private $_filterFields;
|
||||
private $_searchFields;
|
||||
private $_export = false;
|
||||
|
||||
const SEARCH_MODIFIER_CONTAINS_ALL = "contains_all";
|
||||
const SEARCH_MODIFIER_CONTAINS_ANY = "contains_any";
|
||||
const SEARCH_MODIFIER_CONTAINS_NONE = "contains_none";
|
||||
|
||||
const MAX_ROWS = 2147483647;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param boolean $export
|
||||
*/
|
||||
public function __construct($export = false)
|
||||
{
|
||||
$this->_filterFields = array();
|
||||
|
||||
// $filters = Opus_Search_Config::getFacetFields();
|
||||
// if ( !count( $filters ) ) {
|
||||
// $this->_logger->debug( 'key searchengine.solr.facets is not present in config. skipping filter queries' );
|
||||
// } else {
|
||||
// $this->_logger->debug( 'searchengine.solr.facets is set to ' . implode( ',', $filters ) );
|
||||
// }
|
||||
|
||||
// foreach ($filters as $filterfield) {
|
||||
// if ($filterfield == 'year_inverted') {
|
||||
// $filterfield = 'year';
|
||||
// }
|
||||
// array_push($this->_filterFields, trim($filterfield));
|
||||
// }
|
||||
|
||||
$this->_searchFields = array('author', 'title', 'persons', 'referee', 'abstract', 'fulltext', 'year');
|
||||
$this->_export = $export;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param $request
|
||||
* @return array
|
||||
*/
|
||||
public function createQueryBuilderInputFromRequest($request) : array
|
||||
{
|
||||
if (is_null($request->all())) {
|
||||
throw new Application_Util_QueryBuilderException('Unable to read request data.Search cannot be performed.');
|
||||
}
|
||||
|
||||
if (is_null($request->input('searchtype'))) {
|
||||
throw new Application_Util_QueryBuilderException('Unspecified search type: unable to create query.');
|
||||
}
|
||||
|
||||
if (!Searchtypes::isSupported($request->input('searchtype'))) {
|
||||
throw new Application_Util_QueryBuilderException(
|
||||
'Unsupported search type ' . $request->input('searchtype') . ' : unable to create query.'
|
||||
);
|
||||
}
|
||||
|
||||
$this->validateParamsType($request);
|
||||
|
||||
if ($request->input('sortfield')) {
|
||||
$sorting = array($request->input('sortfield'), 'asc');
|
||||
} else {
|
||||
//$sorting = Opus_Search_Query::getDefaultSorting();
|
||||
$sorting = array('score', 'desc' );
|
||||
}
|
||||
|
||||
$input = array(
|
||||
'searchtype' => $request->input('searchtype'),
|
||||
'start' => $request->input('start'),//, Opus_Search_Query::getDefaultStart()),
|
||||
'rows' => $request->input('rows'),// Opus_Search_Query::getDefaultRows()),
|
||||
'sortField' => $sorting[0],
|
||||
'sortOrder' => $request->input('sortorder', $sorting[1]),
|
||||
'docId' => $request->input('docId'),
|
||||
'query' => $request->input('query', '*:*')
|
||||
);
|
||||
|
||||
//if ($this->_export) {
|
||||
// $maxRows = self::MAX_ROWS;
|
||||
// // pagination within export was introduced in OPUS 4.2.2
|
||||
// $startParam = $request->input('start', 0);
|
||||
// $rowsParam = $request->input('rows', $maxRows);
|
||||
// $start = intval($startParam);
|
||||
// $rows = intval($rowsParam);
|
||||
// $input['start'] = $start > 0 ? $start : 0;
|
||||
// $input['rows'] = $rows > 0 || ($rows == 0 && $rowsParam == '0') ? $rows : $maxRows;
|
||||
// if ($input['start'] > $maxRows) {
|
||||
// $input['start'] = $maxRows;
|
||||
// }
|
||||
// if ($input['rows'] + $input['start'] > $maxRows) {
|
||||
// $input['rows'] = $maxRows - $start;
|
||||
// }
|
||||
//}
|
||||
|
||||
foreach ($this->_searchFields as $searchField) {
|
||||
$input[$searchField] = $request->input($searchField, '');
|
||||
$input[$searchField . 'modifier'] = $request->input(
|
||||
$searchField . 'modifier',
|
||||
self::SEARCH_MODIFIER_CONTAINS_ALL
|
||||
);
|
||||
}
|
||||
|
||||
// foreach ($this->_filterFields as $filterField) {
|
||||
// $param = $filterField . 'fq';
|
||||
// $input[$param] = $request->getParam($param, '');
|
||||
// }
|
||||
|
||||
|
||||
// if ($request->getParam('searchtype') === Searchtypes::COLLECTION_SEARCH
|
||||
// || $request->input('searchtype') === Searchtypes::SERIES_SEARCH)
|
||||
// {
|
||||
// $searchParams = new Application_Util_BrowsingParams($request, $this->_logger);
|
||||
// switch ($request->input('searchtype')) {
|
||||
// case Searchtypes::COLLECTION_SEARCH:
|
||||
// $input['collectionId'] = $searchParams->getCollectionId();
|
||||
// break;
|
||||
// case Searchtypes::SERIES_SEARCH:
|
||||
// $input['seriesId'] = $searchParams->getSeriesId();
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
|
||||
return $input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if all given parameters are of type string. Otherwise, throws Application_Util_QueryBuilderException.
|
||||
*
|
||||
* @throws //Application_Util_QueryBuilderException
|
||||
*/
|
||||
private function validateParamsType($request)
|
||||
{
|
||||
$paramNames = array(
|
||||
'searchtype',
|
||||
'start',
|
||||
'rows',
|
||||
'sortField',
|
||||
'sortOrder',
|
||||
'search',
|
||||
'collectionId',
|
||||
'seriesId'
|
||||
);
|
||||
foreach ($this->_searchFields as $searchField) {
|
||||
array_push($paramNames, $searchField, $searchField . 'modifier');
|
||||
}
|
||||
foreach ($this->_filterFields as $filterField) {
|
||||
array_push($paramNames, $filterField . 'fq');
|
||||
}
|
||||
|
||||
foreach ($paramNames as $paramName) {
|
||||
$paramValue = $request->input($paramName, null);
|
||||
if (!is_null($paramValue) && !is_string($paramValue)) {
|
||||
throw new Application_Util_QueryBuilderException('Parameter ' . $paramName . ' is not of type string');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param array $input
|
||||
* @return SolrSearchQuery
|
||||
*/
|
||||
public function createSearchQuery($input) : SolrSearchQuery
|
||||
{
|
||||
if ($input['searchtype'] === Searchtypes::SIMPLE_SEARCH) {
|
||||
return $this->createSimpleSearchQuery($input);
|
||||
//return $this->createAllSearchQuery($input);
|
||||
}
|
||||
|
||||
if ($input['searchtype'] === Searchtypes::ALL_SEARCH) {
|
||||
return $this->createAllSearchQuery($input);
|
||||
}
|
||||
return $this->createSimpleSearchQuery($input);
|
||||
}
|
||||
|
||||
// private function createIdSearchQuery($input) {
|
||||
// $this->_logger->debug("Constructing query for id search.");
|
||||
|
||||
// if (is_null($input['docId'])) {
|
||||
// throw new Application_Exception("No id provided.", 404);
|
||||
// }
|
||||
|
||||
// $query = new Opus_SolrSearch_Query(Opus_SolrSearch_Query::DOC_ID);
|
||||
// $query->setField('id', $input['docId']);
|
||||
|
||||
// if ($this->_export) {
|
||||
// $query->setReturnIdsOnly(true);
|
||||
// }
|
||||
|
||||
// $this->_logger->debug("Query $query complete");
|
||||
// return $query;
|
||||
// }
|
||||
|
||||
private function createAllSearchQuery($input)
|
||||
{
|
||||
//$this->_logger->debug("Constructing query for all search.");
|
||||
|
||||
$query = new SolrSearchQuery(SolrSearchQuery::ALL_DOCS);
|
||||
$query->setStart("0");//$input['start']);
|
||||
//$query->setRows($input['rows']);
|
||||
$query->setRows("10");
|
||||
$query->setSortField($input['sortField']);
|
||||
$query->setSortOrder($input['sortOrder']);
|
||||
|
||||
//$this->addFiltersToQuery($query, $input);
|
||||
|
||||
//if ($this->_export) {
|
||||
// $query->setReturnIdsOnly(true);
|
||||
//}
|
||||
|
||||
//$this->_logger->debug("Query $query complete");
|
||||
return $query;
|
||||
}
|
||||
|
||||
private function createSimpleSearchQuery($input) : SolrSearchQuery
|
||||
{
|
||||
// $this->_logger->debug("Constructing query for simple search.");
|
||||
|
||||
$solrQuery = new SolrSearchQuery(SolrSearchQuery::SIMPLE);
|
||||
$solrQuery->setStart($input['start']);
|
||||
$solrQuery->setRows("10");//$input['rows']);
|
||||
$solrQuery->setSortField($input['sortField']);
|
||||
$solrQuery->setSortOrder($input['sortOrder']);
|
||||
|
||||
$solrQuery->setCatchAll($input['query']);
|
||||
//$this->addFiltersToQuery($solrQuery, $input);
|
||||
|
||||
// if ($this->_export) {
|
||||
// $solrQuery->setReturnIdsOnly(true);
|
||||
// }
|
||||
|
||||
// $this->_logger->debug("Query $solrQuery complete");
|
||||
return $solrQuery;
|
||||
}
|
||||
|
||||
private function addFiltersToQuery($query, $input)
|
||||
{
|
||||
foreach ($this->_filterFields as $filterField) {
|
||||
$facetKey = $filterField . 'fq';
|
||||
$facetValue = $input[$facetKey];
|
||||
if ($facetValue !== '') {
|
||||
$this->_logger->debug(
|
||||
"request has facet key: $facetKey - value is: $facetValue - corresponding facet is: $filterField"
|
||||
);
|
||||
$query->addFilterQuery($filterField, $facetValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
452
app/Library/Util/SearchParameter.php
Normal file
452
app/Library/Util/SearchParameter.php
Normal file
|
@ -0,0 +1,452 @@
|
|||
<?php
|
||||
namespace App\Library\Util;
|
||||
|
||||
/**
|
||||
* Implements API for describing search queries.
|
||||
*
|
||||
* @note This part of Opus search API differs from Solr in terminology in that
|
||||
* all requests for searching documents are considered "queries" with a
|
||||
* "filter" used to describe conditions matching documents has to met.
|
||||
* In opposition to Solr's "filter queries" this API supports "subfilters"
|
||||
* to reduce confusions on differences between filters, queries and
|
||||
* filter queries. Thus wording is mapped like this
|
||||
*
|
||||
* Solr --> Opus
|
||||
* "request" --> "query"
|
||||
* "query" --> "filter"
|
||||
* "filter query" --> "subfilter"
|
||||
*
|
||||
* @method int getStart( int $default = null )
|
||||
* @method int getRows( int $default = null )
|
||||
* @method string[] getFields( array $default = null )
|
||||
* @method array getSort( array $default = null )
|
||||
* @method bool getUnion( bool $default = null )
|
||||
* @method Opus_Search_Filter_Base getFilter( Opus_Search_Filter_Base $default = null )
|
||||
* @method Opus_Search_Facet_Set getFacet( Opus_Search_Facet_Set $default = null )
|
||||
* @method $this setStart( int $offset )
|
||||
* @method $this setRows( int $count )
|
||||
* @method $this setFields( $fields )
|
||||
* @method $this setSort( $sorting )
|
||||
* @method $this setUnion( bool $isUnion )
|
||||
* @method $this setFilter( Opus_Search_Filter_Base $filter ) assigns condition to be met by resulting documents
|
||||
* @method $this setFacet( Opus_Search_Facet_Set $facet )
|
||||
* @method $this addFields( string $fields )
|
||||
* @method $this addSort( $sorting )
|
||||
*/
|
||||
class SearchParameter
|
||||
{
|
||||
protected $_data;
|
||||
|
||||
public function reset()
|
||||
{
|
||||
$this->_data = array(
|
||||
'start' => null,
|
||||
'rows' => null,
|
||||
'fields' => null,
|
||||
'sort' => null,
|
||||
'union' => null,
|
||||
'filter' => null,
|
||||
'facet' => null,
|
||||
'subfilters' => null,
|
||||
);
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if provided name is actually name of known parameter normalizing it
|
||||
* on return.
|
||||
*
|
||||
* @throws InvalidArgumentException unless providing name of existing parameter
|
||||
* @param string $name name of parameter to access
|
||||
* @return string normalized name of existing parameter
|
||||
*/
|
||||
protected function isValidParameter($name)
|
||||
{
|
||||
if (!array_key_exists(strtolower(trim($name)), $this->_data)) {
|
||||
throw new InvalidArgumentException('invalid query parameter: ' . $name);
|
||||
}
|
||||
|
||||
return strtolower(trim($name));
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes one or more field names or set of comma-separated field names
|
||||
* into set of field names.
|
||||
*
|
||||
* @param string|string[] $input one or more field names or comma-separated lists of fields' names
|
||||
* @return string[] list of field names
|
||||
*/
|
||||
protected function normalizeFields($input)
|
||||
{
|
||||
if (!is_array($input)) {
|
||||
$input = array($input);
|
||||
}
|
||||
$output = array();
|
||||
|
||||
foreach ($input as $field) {
|
||||
if (!is_string($field)) {
|
||||
throw new InvalidArgumentException('invalid type of field selector');
|
||||
}
|
||||
|
||||
$fieldNames = preg_split('/[\s,]+/', $field, null, PREG_SPLIT_NO_EMPTY);
|
||||
foreach ($fieldNames as $name) {
|
||||
if (!preg_match('/^(?:\*|[a-z_][a-z0-9_]*)$/i', $name)) {
|
||||
throw new InvalidArgumentException('malformed field selector: ' . $name);
|
||||
}
|
||||
$output[] = $name;
|
||||
}
|
||||
}
|
||||
|
||||
if (!count($input)) {
|
||||
throw new InvalidArgumentException('missing field selector');
|
||||
}
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses provided parameter for describing some sorting direction.
|
||||
*
|
||||
* @param string|bool $ascending one out of true, false, "asc" or "desc"
|
||||
* @return bool true if parameter is considered requesting to sort in ascending order
|
||||
*/
|
||||
protected function normalizeDirection($ascending)
|
||||
{
|
||||
if (!strcasecmp($ascending, 'asc')) {
|
||||
$ascending = true;
|
||||
} elseif (!strcasecmp($ascending, 'desc')) {
|
||||
$ascending = false;
|
||||
} elseif ($ascending !== false && $ascending !== true) {
|
||||
throw new InvalidArgumentException('invalid sorting direction selector');
|
||||
}
|
||||
return $ascending;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves value of selected query parameter.
|
||||
*
|
||||
* @param string $name name of parameter to read
|
||||
* @param mixed $defaultValue value to retrieve if parameter hasn't been set internally
|
||||
* @return mixed value of selected parameter, default if missing internally
|
||||
*/
|
||||
public function get($name, $defaultValue = null)
|
||||
{
|
||||
$name = $this->isValidParameter($name);
|
||||
|
||||
return is_null($this->_data[$name]) ? $defaultValue : $this->_data[$name];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets value of selected query parameter.
|
||||
*
|
||||
* @throws InvalidArgumentException in case of invalid arguments (e.g. on trying to add value to single-value param)
|
||||
* @param string $name name of query parameter to adjust
|
||||
* @param string[]|array|string|int $value value of query parameter to write
|
||||
* @param bool $adding true for adding given parameter to any existing one
|
||||
* @return $this
|
||||
*/
|
||||
public function set($name, $value, $adding = false) //filter, "aa", false
|
||||
{
|
||||
$name = $this->isValidParameter($name);
|
||||
|
||||
switch ($name) {
|
||||
case 'start':
|
||||
case 'rows':
|
||||
if ($adding) {
|
||||
throw new InvalidArgumentException('invalid parameter access on ' . $name);
|
||||
}
|
||||
|
||||
if (!is_scalar($value) || !ctype_digit(trim($value))) {
|
||||
throw new InvalidArgumentException('invalid parameter value on ' . $name);
|
||||
}
|
||||
|
||||
$this->_data[$name] = intval($value);
|
||||
break;
|
||||
|
||||
case 'fields':
|
||||
$fields = $this->normalizeFields($value);
|
||||
|
||||
if ($adding && is_null($this->_data['fields'])) {
|
||||
$adding = false;
|
||||
}
|
||||
|
||||
if ($adding) {
|
||||
$this->_data['fields'] = array_merge($this->_data['fields'], $fields);
|
||||
} else {
|
||||
if (!count($fields)) {
|
||||
throw new InvalidArgumentException('setting empty set of fields rejected');
|
||||
}
|
||||
$this->_data['fields'] = $fields;
|
||||
}
|
||||
|
||||
$this->_data['fields'] = array_unique($this->_data['fields']);
|
||||
break;
|
||||
|
||||
case 'sort':
|
||||
if (!is_array($value)) {
|
||||
$value = array($value, true);
|
||||
}
|
||||
|
||||
switch (count($value)) {
|
||||
case 2:
|
||||
$fields = array_shift($value);
|
||||
$ascending = array_shift($value);
|
||||
break;
|
||||
case 1:
|
||||
$fields = array_shift($value);
|
||||
$ascending = true;
|
||||
break;
|
||||
default:
|
||||
throw new InvalidArgumentException('invalid sorting selector');
|
||||
}
|
||||
|
||||
$this->addSorting($fields, $ascending, !$adding);
|
||||
break;
|
||||
|
||||
case 'union':
|
||||
if ($adding) {
|
||||
throw new InvalidArgumentException('invalid parameter access on ' . $name);
|
||||
}
|
||||
$this->_data[$name] = !!$value;
|
||||
break;
|
||||
|
||||
case 'filter':
|
||||
if ($adding) {
|
||||
throw new InvalidArgumentException('invalid parameter access on ' . $name);
|
||||
}
|
||||
|
||||
// if ( !( $value instanceof Opus_Search_Filter_Base ) ) {
|
||||
// throw new InvalidArgumentException( 'invalid filter' );
|
||||
// }
|
||||
|
||||
$this->_data[$name] = $value;
|
||||
break;
|
||||
|
||||
case 'facet':
|
||||
if ($adding) {
|
||||
throw new InvalidArgumentException('invalid parameter access on ' . $name);
|
||||
}
|
||||
|
||||
if (!($value instanceof Opus_Search_Facet_Set)) {
|
||||
throw new InvalidArgumentException('invalid facet options');
|
||||
}
|
||||
$this->_data[$name] = $value;
|
||||
break;
|
||||
|
||||
case 'subfilters':
|
||||
throw new RuntimeException('invalid access on sub filters');
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function __get($name)
|
||||
{
|
||||
return $this->get($name);
|
||||
}
|
||||
|
||||
public function __isset($name)
|
||||
{
|
||||
return !is_null($this->get($name));
|
||||
}
|
||||
|
||||
public function __set($name, $value)
|
||||
{
|
||||
$this->set($name, $value, false);
|
||||
}
|
||||
|
||||
public function __call($method, $arguments)
|
||||
{
|
||||
if (preg_match('/^(get|set|add)([a-z]+)$/i', $method, $matches)) {
|
||||
$property = $this->isValidParameter($matches[2]);
|
||||
switch (strtolower($matches[1])) {
|
||||
case 'get':
|
||||
return $this->get($property, @$arguments[0]);
|
||||
|
||||
case 'set':
|
||||
$this->set($property, @$arguments[0], false);
|
||||
return $this;
|
||||
|
||||
case 'add':
|
||||
$this->set($property, @$arguments[0], true);
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
throw new RuntimeException('invalid method: ' . $method);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds request for sorting by some field in desired order.
|
||||
*
|
||||
* @param string|string[] $field one or more field names to add sorting (as array and/or comma-separated string)
|
||||
* @param bool $ascending true or "asc" for ascending by all given fields
|
||||
* @param bool $reset true for dropping previously declared sorting
|
||||
* @return $this fluent interface
|
||||
*/
|
||||
public function addSorting($field, $ascending = true, $reset = false)
|
||||
{
|
||||
$fields = $this->normalizeFields($field);
|
||||
$ascending = $this->normalizeDirection($ascending);
|
||||
|
||||
if (!count($fields)) {
|
||||
throw new InvalidArgumentException('missing field for sorting result');
|
||||
}
|
||||
|
||||
if ($reset || !is_array($this->_data['sort'])) {
|
||||
$this->_data['sort'] = array();
|
||||
}
|
||||
|
||||
foreach ($fields as $field) {
|
||||
if ($field === '*') {
|
||||
throw new InvalidArgumentException('invalid request for sorting by all fields (*)');
|
||||
}
|
||||
$this->_data['sort'][$field] = $ascending ? 'asc' : 'desc';
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Declares some subfilter.
|
||||
*
|
||||
* @note In Solr a search includes a "query" and optionally one or more
|
||||
* "filter query". This API intends different terminology for the
|
||||
* whole search request is considered a "query" with a "filter" used
|
||||
* to select actually desired documents by matching conditions. In
|
||||
* context with this terminology "subfilter" was used to describe what
|
||||
* is "filter query" in Solr world: some named query to be included on
|
||||
* selecting documents in database with some benefits regarding
|
||||
* performance, server-side result caching and non-affecting score.
|
||||
*
|
||||
* @see http://wiki.apache.org/solr/CommonQueryParameters#fq
|
||||
*
|
||||
* @param string $name name of query (used for server-side caching)
|
||||
* @param Opus_Search_Filter_Base $subFilter filter to be satisfied by all matching documents in addition
|
||||
* @return $this fluent interface
|
||||
*/
|
||||
public function setSubFilter($name, Opus_Search_Filter_Base $subFilter)
|
||||
{
|
||||
if (!is_string($name) || !$name) {
|
||||
throw new InvalidArgumentException('invalid sub filter name');
|
||||
}
|
||||
|
||||
if (!is_array($this->_data['subfilters'])) {
|
||||
$this->_data['subfilters'] = array($name => $subFilter);
|
||||
} else {
|
||||
$this->_data['subfilters'][$name] = $subFilter;
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes some previously defined subfilter from current query again.
|
||||
*
|
||||
* @note This isn't affecting server-side caching of selected filter but
|
||||
* reverting some parts of query compiled on client-side.
|
||||
*
|
||||
* @see Opus_Search_Query::setSubFilter()
|
||||
*
|
||||
* @param string $name name of filter to remove from query again
|
||||
* @return $this fluent interface
|
||||
*/
|
||||
public function removeSubFilter($name)
|
||||
{
|
||||
if (!is_string($name) || !$name) {
|
||||
throw new InvalidArgumentException('invalid sub filter name');
|
||||
}
|
||||
|
||||
if (is_array($this->_data['subfilters'])) {
|
||||
if (array_key_exists($name, $this->_data['subfilters'])) {
|
||||
unset($this->_data['subfilters'][$name]);
|
||||
}
|
||||
if (!count($this->_data['subfilters'])) {
|
||||
$this->_data['subfilters'] = null;
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves named map of subfilters to include on querying search engine.
|
||||
*
|
||||
* @return Opus_Search_Filter_Base[]
|
||||
*/
|
||||
public function getSubFilters()
|
||||
{
|
||||
return $this->_data['subfilters'];
|
||||
}
|
||||
|
||||
public static function getParameterDefault($name, $fallbackIfMissing, $oldName = null)
|
||||
{
|
||||
$config = Opus_Search_Config::getDomainConfiguration();
|
||||
$defaults = $config->parameterDefaults;
|
||||
|
||||
if ($defaults instanceof Zend_Config) {
|
||||
return $defaults->get($name, $fallbackIfMissing);
|
||||
}
|
||||
if ($oldName) {
|
||||
return $config->get($oldName, $fallbackIfMissing);
|
||||
}
|
||||
|
||||
return $fallbackIfMissing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves configured default offset for paging results.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function getDefaultStart()
|
||||
{
|
||||
return static::getParameterDefault('start', 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves configured default number of rows to show (per page).
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function getDefaultRows()
|
||||
{
|
||||
return static::getParameterDefault('rows', 10, 'numberOfDefaultSearchResults');
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves configured default sorting.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public static function getDefaultSorting()
|
||||
{
|
||||
$sorting = static::getParameterDefault('sortField', 'score desc');
|
||||
|
||||
$parts = preg_split('/[\s,]+/', trim($sorting), null, PREG_SPLIT_NO_EMPTY);
|
||||
|
||||
$sorting = array(array_shift($parts));
|
||||
|
||||
if (!count($parts)) {
|
||||
$sorting[] = 'desc';
|
||||
} else {
|
||||
$dir = array_shift($parts);
|
||||
if (strcasecmp($dir, 'asc') || strcasecmp($dir, 'desc')) {
|
||||
$dir = 'desc';
|
||||
}
|
||||
$sorting[] = strtolower($dir);
|
||||
}
|
||||
return $sorting;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves configured name of field to use for sorting results by default.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getDefaultSortingField()
|
||||
{
|
||||
$sorting = static::getDefaultSorting();
|
||||
return $sorting[0];
|
||||
}
|
||||
}
|
290
app/Library/Util/SearchResultMatch.php
Normal file
290
app/Library/Util/SearchResultMatch.php
Normal file
|
@ -0,0 +1,290 @@
|
|||
<?php
|
||||
namespace App\Library\Util;
|
||||
|
||||
use Carbon\Carbon;
|
||||
|
||||
/**
|
||||
* Describes local document as a match in context of a related search query.
|
||||
*/
|
||||
class SearchResultMatch
|
||||
{
|
||||
|
||||
/**
|
||||
* @var mixed
|
||||
*/
|
||||
protected $id = null;
|
||||
|
||||
/**
|
||||
* @var Opus_Document
|
||||
*/
|
||||
protected $doc = null;
|
||||
|
||||
/**
|
||||
* @var float
|
||||
*/
|
||||
protected $score = null;
|
||||
|
||||
/**
|
||||
* @var Opus_Date
|
||||
*/
|
||||
protected $serverDateModified = null;
|
||||
|
||||
/**
|
||||
* @var
|
||||
*/
|
||||
protected $fulltextIdSuccess = null;
|
||||
|
||||
/**
|
||||
* @var
|
||||
*/
|
||||
protected $fulltextIdFailure = null;
|
||||
|
||||
/**
|
||||
* Caches current document's mapping of containing serieses into document's
|
||||
* number in either series.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected $seriesNumbers = null;
|
||||
|
||||
/**
|
||||
* Collects all additional information related to current match.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $data = array();
|
||||
|
||||
public function __construct($matchId)
|
||||
{
|
||||
$this->id = $matchId;
|
||||
}
|
||||
|
||||
public static function create($matchId)
|
||||
{
|
||||
return new static($matchId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves ID of document matching related search query.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves instance of Opus_Document related to current match.
|
||||
*
|
||||
* @throws Opus_Model_NotFoundException
|
||||
* @return Opus_Document
|
||||
*/
|
||||
public function getDocument()
|
||||
{
|
||||
if (is_null($this->doc)) {
|
||||
$this->doc = new Opus_Document($this->id);
|
||||
}
|
||||
return $this->doc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns score of match in context of related search.
|
||||
*
|
||||
* @param $score
|
||||
* @return $this
|
||||
*/
|
||||
public function setScore($score)
|
||||
{
|
||||
if (!is_null($this->score)) {
|
||||
throw new RuntimeException('score has been set before');
|
||||
}
|
||||
$this->score = floatval($score);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves score of match in context of related search.
|
||||
*
|
||||
* @return float|null null if score was not set
|
||||
*/
|
||||
public function getScore()
|
||||
{
|
||||
return $this->score;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves matching document's number in series selected by its ID.
|
||||
*
|
||||
* This method is provided for downward compatibility. You are advised to
|
||||
* inspect document's model for this locally available information rather
|
||||
* than relying on search engine returning it.
|
||||
*
|
||||
* @deprecated
|
||||
* @return string
|
||||
*/
|
||||
public function getSeriesNumber($seriesId)
|
||||
{
|
||||
if (!$seriesId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!is_array($this->seriesNumbers)) {
|
||||
$this->seriesNumbers = array();
|
||||
|
||||
foreach ($this->getDocument()->getSeries() as $linkedSeries) {
|
||||
$id = $linkedSeries->getModel()->getId();
|
||||
$number = $linkedSeries->getNumber();
|
||||
|
||||
$this->seriesNumbers[$id] = $number;
|
||||
}
|
||||
}
|
||||
|
||||
return array_key_exists($seriesId, $this->seriesNumbers) ? $this->seriesNumbers[$seriesId] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns timestamp of last modification to document as tracked in search
|
||||
* index.
|
||||
*
|
||||
* @note This information is temporarily overloading related timestamp in
|
||||
* local document.
|
||||
*
|
||||
* @param {int} $timestamp Unix timestamp of last modification tracked in search index
|
||||
* @return $this fluent interface
|
||||
*/
|
||||
public function setServerDateModified($timestamp)
|
||||
{
|
||||
if (!is_null($this->serverDateModified)) {
|
||||
throw new RuntimeException('timestamp of modification has been set before');
|
||||
}
|
||||
|
||||
//$this->serverDateModified = new Opus_Date();
|
||||
//$this->serverDateModified = Carbon::createFromTimestamp($timestamp);
|
||||
$this->serverDateModified = Carbon::createFromTimestamp($timestamp)->toDateTimeString();
|
||||
|
||||
// if ( ctype_digit( $timestamp = trim( $timestamp ) ) ) {
|
||||
// $this->serverDateModified->setUnixTimestamp( intval( $timestamp ) );
|
||||
// } else {
|
||||
// $this->serverDateModified->setFromString( $timestamp );
|
||||
// }
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides timestamp of last modification preferring value provided by
|
||||
* search engine over value stored locally in document.
|
||||
*
|
||||
* @note This method is used by Opus to detect outdated records in search
|
||||
* index.
|
||||
*
|
||||
* @return string //old Opusdate
|
||||
*/
|
||||
public function getServerDateModified()
|
||||
{
|
||||
if (!is_null($this->serverDateModified)) {
|
||||
return $this->serverDateModified;
|
||||
}
|
||||
|
||||
return $this->getDocument()->getServerDateModified();
|
||||
}
|
||||
|
||||
public function setFulltextIDsSuccess($value)
|
||||
{
|
||||
if (!is_null($this->fulltextIdSuccess)) {
|
||||
throw new RuntimeException('successful fulltext IDs have been set before');
|
||||
}
|
||||
$this->fulltextIdSuccess = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getFulltextIDsSuccess()
|
||||
{
|
||||
if (!is_null($this->fulltextIdSuccess)) {
|
||||
return $this->fulltextIdSuccess;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function setFulltextIDsFailure($value)
|
||||
{
|
||||
if (!is_null($this->fulltextIdFailure)) {
|
||||
throw new RuntimeException('failed fulltext IDs have been set before');
|
||||
}
|
||||
$this->fulltextIdFailure = $value;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getFulltextIDsFailure()
|
||||
{
|
||||
if (!is_null($this->fulltextIdFailure)) {
|
||||
return $this->fulltextIdFailure;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Passes all unknown method invocations to related instance of
|
||||
* Opus_Document.
|
||||
*
|
||||
* @param string $method name of locally missing/protected method
|
||||
* @param mixed[] $args arguments used on invoking that method
|
||||
* @return mixed
|
||||
*/
|
||||
public function __call($method, $args)
|
||||
{
|
||||
return call_user_func_array(array($this->getDocument(), $method), $args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Passes access on locally missing/protected property to related instance
|
||||
* of Opus_Document.
|
||||
*
|
||||
* @param string $name name of locally missing/protected property
|
||||
* @return mixed value of property
|
||||
*/
|
||||
public function __get($name)
|
||||
{
|
||||
return $this->getDocument()->{$name};
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches named asset to current match.
|
||||
*
|
||||
* Assets are additional information on match provided by search engine.
|
||||
*
|
||||
* @param string $name
|
||||
* @param mixed $value
|
||||
* @return $this fluent interface
|
||||
*/
|
||||
public function setAsset($name, $value)
|
||||
{
|
||||
$this->data[$name] = $value;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves selected asset attached to current match or null if asset was
|
||||
* not assigned to match.
|
||||
*
|
||||
* @param string $name
|
||||
* @return mixed|null
|
||||
*/
|
||||
public function getAsset($name)
|
||||
{
|
||||
return isset($this->data[$name]) ? $this->data[$name] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if selected asset has been attached to current match.
|
||||
*
|
||||
* @param string $name name of asset to test
|
||||
* @return bool true if asset was assigned to current match
|
||||
*/
|
||||
public function hasAsset($name) : bool
|
||||
{
|
||||
return array_key_exists($name, $this->data);
|
||||
}
|
||||
}
|
30
app/Library/Util/Searchtypes.php
Normal file
30
app/Library/Util/Searchtypes.php
Normal file
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
namespace App\Library\Util;
|
||||
|
||||
class Searchtypes
|
||||
{
|
||||
|
||||
const SIMPLE_SEARCH = 'simple';
|
||||
const ADVANCED_SEARCH = 'advanced';
|
||||
const AUTHOR_SEARCH = 'authorsearch';
|
||||
const COLLECTION_SEARCH = 'collection';
|
||||
const LATEST_SEARCH = 'latest';
|
||||
const ALL_SEARCH = 'all';
|
||||
const SERIES_SEARCH = 'series';
|
||||
const ID_SEARCH = 'id';
|
||||
|
||||
public static function isSupported($searchtype)
|
||||
{
|
||||
$supportedTypes = array (
|
||||
self::SIMPLE_SEARCH,
|
||||
self::ADVANCED_SEARCH,
|
||||
self::AUTHOR_SEARCH,
|
||||
self::COLLECTION_SEARCH,
|
||||
self::LATEST_SEARCH,
|
||||
self::ALL_SEARCH,
|
||||
self::SERIES_SEARCH,
|
||||
self::ID_SEARCH
|
||||
);
|
||||
return in_array($searchtype, $supportedTypes);
|
||||
}
|
||||
}
|
413
app/Library/Util/SolrSearchQuery.php
Normal file
413
app/Library/Util/SolrSearchQuery.php
Normal file
|
@ -0,0 +1,413 @@
|
|||
<?php
|
||||
namespace App\Library\Util;
|
||||
|
||||
/**
|
||||
* Encapsulates all parameter values needed to build the Solr query URL.
|
||||
*/
|
||||
class SolrSearchQuery
|
||||
{
|
||||
// currently available search types
|
||||
const SIMPLE = 'simple';
|
||||
const ADVANCED = 'advanced';
|
||||
const FACET_ONLY = 'facet_only';
|
||||
const LATEST_DOCS = 'latest';
|
||||
const ALL_DOCS = 'all_docs';
|
||||
const DOC_ID = 'doc_id';
|
||||
|
||||
const DEFAULT_START = 0;
|
||||
const DEFAULT_ROWS = 10;
|
||||
// java.lang.Integer.MAX_VALUE
|
||||
const MAX_ROWS = 2147483647;
|
||||
const DEFAULT_SORTFIELD = 'score';
|
||||
const DEFAULT_SORTORDER = 'desc';
|
||||
const SEARCH_MODIFIER_CONTAINS_ALL = "contains_all";
|
||||
const SEARCH_MODIFIER_CONTAINS_ANY = "contains_any";
|
||||
const SEARCH_MODIFIER_CONTAINS_NONE = "contains_none";
|
||||
|
||||
private $start = self::DEFAULT_START;
|
||||
private $rows = self::DEFAULT_ROWS;
|
||||
private $sortField = self::DEFAULT_SORTFIELD;
|
||||
private $sortOrder = self::DEFAULT_SORTORDER;
|
||||
private $filterQueries = array();
|
||||
private $catchAll;
|
||||
private $searchType;
|
||||
private $modifier;
|
||||
private $fieldValues = array();
|
||||
private $escapingEnabled = true;
|
||||
private $q;
|
||||
private $facetField;
|
||||
private $returnIdsOnly = false;
|
||||
private $seriesId = null;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param string $searchType
|
||||
*/
|
||||
public function __construct($searchType = self::SIMPLE)
|
||||
{
|
||||
//$this->invalidQCache();
|
||||
$this->q = null;
|
||||
|
||||
if ($searchType === self::SIMPLE || $searchType === self::ADVANCED || $searchType === self::ALL_DOCS) {
|
||||
$this->searchType = $searchType;
|
||||
return;
|
||||
}
|
||||
|
||||
if ($searchType === self::FACET_ONLY) {
|
||||
$this->searchType = self::FACET_ONLY;
|
||||
$this->setRows(0);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($searchType === self::LATEST_DOCS) {
|
||||
$this->searchType = self::LATEST_DOCS;
|
||||
$this->sortField = 'server_date_published';
|
||||
$this->sortOrder = 'desc';
|
||||
return;
|
||||
}
|
||||
|
||||
if ($searchType === self::DOC_ID) {
|
||||
$this->searchType = self::DOC_ID;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public function getSearchType()
|
||||
{
|
||||
return $this->searchType;
|
||||
}
|
||||
|
||||
public function getFacetField()
|
||||
{
|
||||
return $this->facetField;
|
||||
}
|
||||
|
||||
public function setFacetField($facetField)
|
||||
{
|
||||
$this->facetField = $facetField;
|
||||
}
|
||||
|
||||
public function getStart()
|
||||
{
|
||||
return $this->start;
|
||||
}
|
||||
|
||||
public function setStart($start)
|
||||
{
|
||||
$this->start = $start;
|
||||
}
|
||||
|
||||
public static function getDefaultRows()
|
||||
{
|
||||
return SolrSearchQuery::getDefaultRows();
|
||||
}
|
||||
|
||||
public function getRows()
|
||||
{
|
||||
return $this->rows;
|
||||
}
|
||||
|
||||
public function setRows($rows)
|
||||
{
|
||||
$this->rows = $rows;
|
||||
}
|
||||
|
||||
public function getSortField()
|
||||
{
|
||||
return $this->sortField;
|
||||
}
|
||||
|
||||
public function setSortField($sortField)
|
||||
{
|
||||
if ($sortField === self::DEFAULT_SORTFIELD) {
|
||||
if ($this->searchType === self::ALL_DOCS) {
|
||||
// change the default sortfield for searchtype all
|
||||
// since sorting by relevance does not make any sense here
|
||||
$this->sortField = 'server_date_published';
|
||||
} else {
|
||||
$this->sortField = self::DEFAULT_SORTFIELD;
|
||||
}
|
||||
return;
|
||||
}
|
||||
$this->sortField = $sortField;
|
||||
if (strpos($sortField, 'doc_sort_order_for_seriesid_') !== 0 && strpos($sortField, 'server_date_published') !== 0) {
|
||||
// add _sort to the end of $sortField if not already done
|
||||
$suffix = '_sort';
|
||||
if (substr($sortField, strlen($sortField) - strlen($suffix)) !== $suffix) {
|
||||
$this->sortField .= $suffix;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getSortOrder()
|
||||
{
|
||||
return $this->sortOrder;
|
||||
}
|
||||
|
||||
public function setSortOrder($sortOrder)
|
||||
{
|
||||
$this->sortOrder = $sortOrder;
|
||||
}
|
||||
|
||||
public function getSeriesId()
|
||||
{
|
||||
return $this->seriesId;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return array An array that contains all specified filter queries.
|
||||
*/
|
||||
public function getFilterQueries()
|
||||
{
|
||||
return $this->filterQueries;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param string $filterField The field that should be used in a filter query.
|
||||
* @param string $filterValue The field value that should be used in a filter query.
|
||||
*/
|
||||
public function addFilterQuery($filterField, $filterValue)
|
||||
{
|
||||
if ($filterField == 'has_fulltext') {
|
||||
$filterQuery = $filterField . ':' . $filterValue;
|
||||
} else {
|
||||
$filterQuery = '{!raw f=' . $filterField . '}' . $filterValue;
|
||||
}
|
||||
array_push($this->filterQueries, $filterQuery);
|
||||
|
||||
// we need to store the ID of the requested series here,
|
||||
// since we need it later to build the index field name
|
||||
if ($filterField === 'series_ids') {
|
||||
$this->seriesId = $filterValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param array $filterQueries An array of queries that should be used as filter queries.
|
||||
*/
|
||||
public function setFilterQueries($filterQueries)
|
||||
{
|
||||
$this->filterQueries = $filterQueries;
|
||||
}
|
||||
|
||||
public function getCatchAll()
|
||||
{
|
||||
return $this->catchAll;
|
||||
}
|
||||
|
||||
public function setCatchAll($catchAll)
|
||||
{
|
||||
$this->catchAll = $catchAll;
|
||||
$this->invalidQCache();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param string $name
|
||||
* @param string $value
|
||||
* @param string $modifier
|
||||
*/
|
||||
public function setField($name, $value, $modifier = self::SEARCH_MODIFIER_CONTAINS_ALL)
|
||||
{
|
||||
if (!empty($value)) {
|
||||
$this->fieldValues[$name] = $value;
|
||||
$this->modifier[$name] = $modifier;
|
||||
$this->invalidQCache();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param string $name
|
||||
* @return Returns null if no values was specified for the given field name.
|
||||
*/
|
||||
public function getField($name)
|
||||
{
|
||||
if (array_key_exists($name, $this->fieldValues)) {
|
||||
return $this->fieldValues[$name];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param string $fieldname
|
||||
* @return returns null if no modifier was specified for the given field name.
|
||||
*/
|
||||
public function getModifier($fieldname)
|
||||
{
|
||||
if (array_key_exists($fieldname, $this->modifier)) {
|
||||
return $this->modifier[$fieldname];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getQ()
|
||||
{
|
||||
if (is_null($this->q)) {
|
||||
// earlier cached query was marked as invalid: perform new setup of query cache
|
||||
$this->q = $this->setupQCache();
|
||||
}
|
||||
|
||||
// return cached result (caching is done here since building q is an expensive operation)
|
||||
return $this->q;
|
||||
}
|
||||
|
||||
private function setupQCache()
|
||||
{
|
||||
if ($this->searchType === self::SIMPLE) {
|
||||
if ($this->getCatchAll() === '*:*') {
|
||||
return $this->catchAll;
|
||||
}
|
||||
return $this->escape($this->getCatchAll());
|
||||
}
|
||||
if ($this->searchType === self::FACET_ONLY || $this->searchType === self::LATEST_DOCS || $this->searchType === self::ALL_DOCS) {
|
||||
return '*:*';
|
||||
}
|
||||
if ($this->searchType === self::DOC_ID) {
|
||||
return 'id:' . $this->fieldValues['id'];
|
||||
}
|
||||
return $this->buildAdvancedQString();
|
||||
}
|
||||
|
||||
private function invalidQCache()
|
||||
{
|
||||
$this->q = null;
|
||||
}
|
||||
|
||||
private function buildAdvancedQString()
|
||||
{
|
||||
$q = "{!lucene q.op=AND}";
|
||||
$first = true;
|
||||
foreach ($this->fieldValues as $fieldname => $fieldvalue) {
|
||||
if ($first) {
|
||||
$first = false;
|
||||
} else {
|
||||
$q .= ' ';
|
||||
}
|
||||
|
||||
if ($this->modifier[$fieldname] === self::SEARCH_MODIFIER_CONTAINS_ANY) {
|
||||
$q .= $this->combineSearchTerms($fieldname, $fieldvalue, 'OR');
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($this->modifier[$fieldname] === self::SEARCH_MODIFIER_CONTAINS_NONE) {
|
||||
$q .= '-' . $this->combineSearchTerms($fieldname, $fieldvalue, 'OR');
|
||||
continue;
|
||||
}
|
||||
|
||||
// self::SEARCH_MODIFIER_CONTAINS_ALL
|
||||
$q .= $this->combineSearchTerms($fieldname, $fieldvalue);
|
||||
}
|
||||
return $q;
|
||||
}
|
||||
|
||||
private function combineSearchTerms($fieldname, $fieldvalue, $conjunction = null)
|
||||
{
|
||||
$result = $fieldname . ':(';
|
||||
$firstTerm = true;
|
||||
$queryTerms = preg_split("/[\s]+/", $this->escape($fieldvalue), null, PREG_SPLIT_NO_EMPTY);
|
||||
foreach ($queryTerms as $queryTerm) {
|
||||
if ($firstTerm) {
|
||||
$firstTerm = false;
|
||||
} else {
|
||||
$result .= is_null($conjunction) ? " " : " $conjunction ";
|
||||
}
|
||||
$result .= $queryTerm;
|
||||
}
|
||||
$result .= ')';
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function disableEscaping()
|
||||
{
|
||||
$this->invalidQCache();
|
||||
$this->escapingEnabled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape Lucene's special query characters specified in
|
||||
* http://lucene.apache.org/java/3_0_2/queryparsersyntax.html#Escaping%20Special%20Characters
|
||||
* Escaping currently ignores * and ? which are used as wildcard operators.
|
||||
* Additionally, double-quotes are not escaped and a double-quote is added to
|
||||
* the end of $query in case it contains an odd number of double-quotes.
|
||||
* @param string $query The query which needs to be escaped.
|
||||
*/
|
||||
private function escape($query)
|
||||
{
|
||||
if (!$this->escapingEnabled) {
|
||||
return $query;
|
||||
}
|
||||
$query = trim($query);
|
||||
// add one " to the end of $query if it contains an odd number of "
|
||||
if (substr_count($query, '"') % 2 == 1) {
|
||||
$query .= '"';
|
||||
}
|
||||
// escape special characters (currently ignore " \* \?) outside of ""
|
||||
$insidePhrase = false;
|
||||
$result = '';
|
||||
foreach (explode('"', $query) as $phrase) {
|
||||
if ($insidePhrase) {
|
||||
$result .= '"' . $phrase . '"';
|
||||
} else {
|
||||
$result .= preg_replace(
|
||||
'/(\+|-|&&|\|\||!|\(|\)|\{|}|\[|]|\^|~|:|\\\)/',
|
||||
'\\\$1',
|
||||
$this->lowercaseWildcardQuery($phrase)
|
||||
);
|
||||
}
|
||||
$insidePhrase = !$insidePhrase;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function lowercaseWildcardQuery($query)
|
||||
{
|
||||
// check if $query is a wildcard query
|
||||
if (strpos($query, '*') === false && strpos($query, '?') === false) {
|
||||
return $query;
|
||||
}
|
||||
// lowercase query
|
||||
return strtolower($query);
|
||||
}
|
||||
|
||||
public function __toString()
|
||||
{
|
||||
if ($this->searchType === self::SIMPLE) {
|
||||
return 'simple search with query ' . $this->getQ();
|
||||
}
|
||||
if ($this->searchType === self::FACET_ONLY) {
|
||||
return 'facet only search with query *:*';
|
||||
}
|
||||
if ($this->searchType === self::LATEST_DOCS) {
|
||||
return 'search for latest documents with query *:*';
|
||||
}
|
||||
if ($this->searchType === self::ALL_DOCS) {
|
||||
return 'search for all documents';
|
||||
}
|
||||
if ($this->searchType === self::DOC_ID) {
|
||||
return 'search for document id ' . $this->getQ();
|
||||
}
|
||||
return 'advanced search with query ' . $this->getQ();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param boolean $returnIdsOnly
|
||||
*/
|
||||
public function setReturnIdsOnly($returnIdsOnly)
|
||||
{
|
||||
$this->returnIdsOnly = $returnIdsOnly;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return boolean
|
||||
*/
|
||||
public function isReturnIdsOnly()
|
||||
{
|
||||
return $this->returnIdsOnly;
|
||||
}
|
||||
}
|
94
app/Library/Util/SolrSearchSearcher.php
Normal file
94
app/Library/Util/SolrSearchSearcher.php
Normal file
|
@ -0,0 +1,94 @@
|
|||
<?php
|
||||
namespace App\Library\Util;
|
||||
|
||||
use App\Library\Search\SolariumAdapter;
|
||||
use App\Library\Search\SearchResult;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class SolrSearchSearcher
|
||||
{
|
||||
/*
|
||||
* Holds numbers of facets
|
||||
*/
|
||||
private $facetArray;
|
||||
|
||||
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param SolrSearchQuery $query
|
||||
* @param bool $validateDocIds check document IDs coming from Solr index against database
|
||||
* @return SearchResult
|
||||
* @throws //Opus_SolrSearch_Exception If Solr server responds with an error or the response is empty.
|
||||
*/
|
||||
public function search(SolrSearchQuery $query, bool $validateDocIds = true) : SearchResult
|
||||
{
|
||||
|
||||
try {
|
||||
//Opus_Log::get()->debug("query: " . $query->getQ());
|
||||
|
||||
// get service adapter for searching
|
||||
// $service = SearchService::selectSearchingService( null, 'solr' );
|
||||
$service = new SolariumAdapter("solr", config('solarium'));
|
||||
|
||||
$filterText = $query->getQ();//"*:*"
|
||||
// basically create query
|
||||
$requestParameter = $service->createQuery()
|
||||
->setFilter($filterText)
|
||||
->setStart($query->getStart())
|
||||
->setRows($query->getRows());
|
||||
//start:0
|
||||
// rows:1
|
||||
// fields:null
|
||||
// sort:null
|
||||
// union:null
|
||||
// filter:"aa"
|
||||
// facet:null
|
||||
// subfilters:null
|
||||
|
||||
$requestParameter->setFields(array('*', 'score'));
|
||||
|
||||
$searchResult = $service->customSearch($requestParameter);
|
||||
|
||||
//if ( $validateDocIds )
|
||||
//{
|
||||
// $searchResult->dropLocallyMissingMatches();
|
||||
//}
|
||||
|
||||
return $searchResult;
|
||||
} catch (Exception $e) {
|
||||
return $this->mapException(null, $e);
|
||||
}
|
||||
// catch ( Opus_Search_InvalidServiceException $e ) {
|
||||
// return $this->mapException( Opus_SolrSearch_Exception::SERVER_UNREACHABLE, $e );
|
||||
// }
|
||||
// catch( Opus_Search_InvalidQueryException $e ) {
|
||||
// return $this->mapException( Opus_SolrSearch_Exception::INVALID_QUERY, $e );
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $type
|
||||
* @param //Exception $previousException
|
||||
* @throws //Opus_SolrSearch_Exception
|
||||
* @return no-return
|
||||
*/
|
||||
private function mapException($type, Exception $previousException)
|
||||
{
|
||||
$msg = 'Solr server responds with an error ' . $previousException->getMessage();
|
||||
//Opus_Log::get()->err($msg);
|
||||
Log::error($msg);
|
||||
|
||||
//throw new Opus_SolrSearch_Exception($msg, $type, $previousException);
|
||||
}
|
||||
|
||||
public function setFacetArray($array)
|
||||
{
|
||||
$this->facetArray = $array;
|
||||
}
|
||||
}
|
58
app/Library/Xml/Conf.php
Normal file
58
app/Library/Xml/Conf.php
Normal file
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
namespace App\Library\Xml;
|
||||
|
||||
use App\Library\Util\Searchtypes;
|
||||
use App\Dataset;
|
||||
|
||||
/**
|
||||
* Conf short summary.
|
||||
*
|
||||
* Conf description.
|
||||
*
|
||||
* @version 1.0
|
||||
* @author kaiarn
|
||||
*/
|
||||
class Conf
|
||||
{
|
||||
/**
|
||||
* Holds the current model either directly set or deserialized from XML.
|
||||
*
|
||||
* @var Dataset
|
||||
*/
|
||||
public $model = null;
|
||||
|
||||
/**
|
||||
* Holds the current DOM representation.
|
||||
*
|
||||
* @var \DOMDocument
|
||||
*/
|
||||
public $dom = null;
|
||||
|
||||
/**
|
||||
* List of fields to skip on serialization.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $excludeFields = array();
|
||||
|
||||
/**
|
||||
* True, if empty fields get excluded from serialization.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $excludeEmpty = false;
|
||||
|
||||
/**
|
||||
* Base URI for xlink:ref elements
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $baseUri = '';
|
||||
|
||||
/**
|
||||
* Map of model class names to resource names for URI generation.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $resourceNameMap = array();
|
||||
}
|
284
app/Library/Xml/DatasetExtension.php
Normal file
284
app/Library/Xml/DatasetExtension.php
Normal file
|
@ -0,0 +1,284 @@
|
|||
<?php
|
||||
namespace App\Library\Xml;
|
||||
|
||||
/**
|
||||
* DatasetExtension short summary.
|
||||
*
|
||||
* DatasetExtension description.
|
||||
*
|
||||
* @version 1.0
|
||||
* @author kaiarn
|
||||
*/
|
||||
trait DatasetExtension
|
||||
{
|
||||
protected $_externalFields = array(
|
||||
'TitleMain' => array(
|
||||
'model' => 'App\Title',
|
||||
'options' => array('type' => 'main'),
|
||||
'fetch' => 'eager'
|
||||
),
|
||||
'TitleAbstract' => array(
|
||||
'model' => 'App\Title',
|
||||
'options' => array('type' => 'abstract'),
|
||||
'fetch' => 'eager'
|
||||
),
|
||||
'Licence' => array(
|
||||
'model' => 'App\License',
|
||||
'through' => 'link_documents_licences',
|
||||
'relation' => 'licenses',
|
||||
'fetch' => 'eager'
|
||||
),
|
||||
'PersonAuthor' => array(
|
||||
'model' => 'App\Person',
|
||||
'through' => 'link_documents_persons',
|
||||
'pivot' => array('role' => 'author'),
|
||||
//'sort_order' => array('sort_order' => 'ASC'), // <-- We need a sorted authors list.
|
||||
//'sort_field' => 'SortOrder',
|
||||
'relation' => 'authors',
|
||||
'fetch' => 'eager'
|
||||
),
|
||||
'PersonContributor' => array(
|
||||
'model' => 'App\Person',
|
||||
'through' => 'link_documents_persons',
|
||||
'pivot' => array('role' => 'contributor'),
|
||||
// 'sort_order' => array('sort_order' => 'ASC'), // <-- We need a sorted authors list.
|
||||
//'sort_field' => 'SortOrder',
|
||||
'relation' => 'contributors',
|
||||
'fetch' => 'eager'
|
||||
),
|
||||
'File' => array(
|
||||
'model' => 'App\File',
|
||||
'relation' => 'files',
|
||||
'fetch' => 'eager'
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
protected $_internalFields = array();
|
||||
|
||||
protected $_fields = array();
|
||||
|
||||
protected function _initFields()
|
||||
{
|
||||
$fields = array(
|
||||
"Id",
|
||||
"CompletedDate", "CompletedYear",
|
||||
"ContributingCorporation",
|
||||
"CreatingCorporation",
|
||||
"ThesisDateAccepted", "ThesisYearAccepted",
|
||||
"Edition",
|
||||
"Issue",
|
||||
"Language",
|
||||
"PageFirst", "PageLast", "PageNumber",
|
||||
"PublishedDate", "PublishedYear",
|
||||
"PublisherName", "PublisherPlace",
|
||||
"PublicationState",
|
||||
"ServerDateCreated",
|
||||
"ServerDateModified",
|
||||
"ServerDatePublished",
|
||||
"ServerDateDeleted",
|
||||
"ServerState",
|
||||
"Type",
|
||||
"Volume",
|
||||
"BelongsToBibliography",
|
||||
"EmbargoDate"
|
||||
);
|
||||
|
||||
foreach ($fields as $fieldname) {
|
||||
$field = new Field($fieldname);
|
||||
$this->addField($field);
|
||||
}
|
||||
|
||||
foreach (array_keys($this->_externalFields) as $fieldname) {
|
||||
$field = new Field($fieldname);
|
||||
$field->setMultiplicity('*');
|
||||
$this->addField($field);
|
||||
}
|
||||
|
||||
// Initialize available date fields and set up date validator
|
||||
// if the particular field is present
|
||||
$dateFields = array(
|
||||
'ServerDateCreated', 'CompletedDate', 'PublishedDate',
|
||||
'ServerDateModified', 'ServerDatePublished', 'ServerDateDeleted', 'EmbargoDate'
|
||||
);
|
||||
foreach ($dateFields as $fieldName) {
|
||||
$this->getField($fieldName)
|
||||
->setValueModelClass('Carbon');
|
||||
}
|
||||
|
||||
// $this->_fetchValues();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of all fields attached to the model. Filters all fieldnames
|
||||
* that are defined to be inetrnal in $_internalFields.
|
||||
*
|
||||
* @see Opus_Model_Abstract::_internalFields
|
||||
* @return array List of fields
|
||||
*/
|
||||
public function describe()
|
||||
{
|
||||
return array_diff(array_keys($this->_fields), $this->_internalFields);
|
||||
}
|
||||
|
||||
public function addField(Field $field)
|
||||
{
|
||||
$fieldname = $field->getName();
|
||||
if (isset($fieldname, $this->_externalFields[$fieldname])) {
|
||||
$options = $this->_externalFields[$fieldname];
|
||||
|
||||
// set ValueModelClass if a through option is given
|
||||
if (isset($options['model'])) {
|
||||
$field->setValueModelClass($options['model']);
|
||||
}
|
||||
// set LinkModelClass if a through option is given
|
||||
//if (isset($options['through']))
|
||||
//{
|
||||
// $field->setLinkModelClass($options['through']);
|
||||
//}
|
||||
}
|
||||
|
||||
$this->_fields[$field->getName()] = $field;
|
||||
$field->setOwningModelClass(get_class($this));
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getField($name)
|
||||
{
|
||||
return $this->_getField($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a reference to an actual field.
|
||||
*
|
||||
* @param string $name Name of the requested field.
|
||||
* @return Field The requested field instance. If no such instance can be found, null is returned.
|
||||
*/
|
||||
protected function _getField($name)
|
||||
{
|
||||
if (isset($this->_fields[$name])) {
|
||||
return $this->_fields[$name];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public function fetchValues()
|
||||
{
|
||||
$this->_initFields();
|
||||
foreach ($this->_fields as $fieldname => $field) {
|
||||
if (isset($this->_externalFields[$fieldname]) === true) {
|
||||
$fetchmode = 'lazy';
|
||||
if (isset($this->_externalFields[$fieldname]['fetch']) === true) {
|
||||
$fetchmode = $this->_externalFields[$fieldname]['fetch'];
|
||||
}
|
||||
|
||||
if ($fetchmode === 'lazy') {
|
||||
// Remember the field to be fetched later.
|
||||
$this->_pending[] = $fieldname;
|
||||
// Go to next field
|
||||
continue;
|
||||
} else {
|
||||
// Immediately load external field if fetching mode is set to 'eager'
|
||||
$this->_loadExternal($fieldname);
|
||||
}
|
||||
} else {
|
||||
// Field is not external an gets handled by simply reading
|
||||
$property_name = self::convertFieldnameToColumn($fieldname);
|
||||
//$test = $this->server_date_created;
|
||||
$fieldval = $this->{$property_name};
|
||||
|
||||
// explicitly set null if the field represents a model except for dates
|
||||
if (null !== $field->getValueModelClass()) {
|
||||
if (true === empty($fieldval)) {
|
||||
$fieldval = null;
|
||||
} else {
|
||||
$fieldval = new \Carbon\Carbon($fieldval);
|
||||
}
|
||||
}
|
||||
|
||||
$field->setValue($fieldval);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function convertFieldnameToColumn($fieldname)
|
||||
{
|
||||
return strtolower(preg_replace('/(?!^)[[:upper:]]/', '_\0', $fieldname));
|
||||
}
|
||||
|
||||
protected function _loadExternal($fieldname)
|
||||
{
|
||||
$field = $this->_fields[$fieldname];
|
||||
|
||||
$modelclass = $field->getLinkModelClass();
|
||||
if (!isset($modelclass)) {
|
||||
// For handling a value model, see 'model' option.
|
||||
$modelclass = $field->getValueModelClass();
|
||||
}
|
||||
|
||||
$tableclass = new $modelclass();//::getTableGatewayClass();
|
||||
// $table = Opus_Db_TableGateway::getInstance($tableclass);
|
||||
$select = $tableclass->query();//->where("document_id", $this->id);;
|
||||
|
||||
// If any declared constraints, add them to query
|
||||
if (isset($this->_externalFields[$fieldname]['options'])) {
|
||||
$options = $this->_externalFields[$fieldname]['options'];
|
||||
foreach ($options as $column => $value) {
|
||||
$select = $select->where($column, $value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Get dependent rows
|
||||
$result = array();
|
||||
$datasetId = $this->id;
|
||||
|
||||
$rows = array();
|
||||
if (isset($this->_externalFields[$fieldname]['through'])) {
|
||||
$relation = $this->_externalFields[$fieldname]['relation'];
|
||||
//$rows = $select->datasets
|
||||
////->orderBy('name')
|
||||
//->get();
|
||||
//$licenses = $select->with('datasets')->get();
|
||||
//$rows = $supplier->datasets;
|
||||
$rows = $this->{$relation};
|
||||
//if (isset($this->_externalFields[$fieldname]['pivot']))
|
||||
//{
|
||||
// $pivArray = $this->_externalFields[$fieldname]['pivot'];
|
||||
// $rows = $rows->wherePivot('role', $pivArray['role']);
|
||||
//}
|
||||
} else {
|
||||
$rows = $select->whereHas('dataset', function ($q) use ($datasetId) {
|
||||
$q->where('id', $datasetId);
|
||||
})->get();
|
||||
}
|
||||
|
||||
foreach ($rows as $row) {
|
||||
// //$newModel = new $modelclass($row);
|
||||
// $result[] = $row;//->value;
|
||||
|
||||
$attributes = array_keys($row->getAttributes());
|
||||
$objArray = [];
|
||||
foreach ($attributes as $property_name) {
|
||||
$fieldName = self::convertColumnToFieldname($property_name);
|
||||
// $field =new Field($fieldName);
|
||||
$fieldval = $row->{$property_name};
|
||||
// $field->setValue($fieldval);
|
||||
// $this->_mapField($field, $dom, $rootNode);
|
||||
$objArray[$fieldName] = $fieldval;
|
||||
}
|
||||
$result[] = $objArray;
|
||||
}
|
||||
|
||||
// Set the field value
|
||||
$field->setValue($result);
|
||||
}
|
||||
|
||||
//snakeToCamel
|
||||
public static function convertColumnToFieldname($columnname)
|
||||
{
|
||||
//return lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $columnname))));
|
||||
return str_replace(' ', '', ucwords(str_replace(['-', '_'], ' ', $columnname)));
|
||||
}
|
||||
}
|
213
app/Library/Xml/Field.php
Normal file
213
app/Library/Xml/Field.php
Normal file
|
@ -0,0 +1,213 @@
|
|||
<?php
|
||||
namespace App\Library\Xml;
|
||||
|
||||
/**
|
||||
* Field short summary.
|
||||
*
|
||||
* Field description.
|
||||
*
|
||||
* @version 1.0
|
||||
* @author kaiarn
|
||||
*/
|
||||
class Field
|
||||
{
|
||||
/**
|
||||
* Hold multiplicity constraint.
|
||||
*
|
||||
* @var Integer|String
|
||||
*/
|
||||
protected $_multiplicity = 1;
|
||||
|
||||
/**
|
||||
* Simple check for multiple values.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $_hasMultipleValues = false;
|
||||
|
||||
/**
|
||||
* Holds the classname for external fields.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $_valueModelClass = null;
|
||||
|
||||
/**
|
||||
* Holds the classname for link fields.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $_linkModelClass = null;
|
||||
|
||||
/**
|
||||
* Holds the classname of the model that the field belongs to.
|
||||
*/
|
||||
protected $_owningModelClass = null;
|
||||
|
||||
/**
|
||||
* Hold the fields value.
|
||||
*
|
||||
* @var mixed
|
||||
*/
|
||||
protected $_value = null;
|
||||
|
||||
/**
|
||||
* Create an new field instance and set the given name.
|
||||
*
|
||||
* @param string $name Internal name of the field.
|
||||
*/
|
||||
public function __construct($name)
|
||||
{
|
||||
$this->_name = $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the name of model class if the field holds model instances.
|
||||
*
|
||||
* @return string Class name or null if the value is not a model.
|
||||
*/
|
||||
public function getValueModelClass()
|
||||
{
|
||||
return $this->_valueModelClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the name of model class if the field holds model instances.
|
||||
*
|
||||
* @param string $classname The name of the class that is used as model for this field or null.
|
||||
* @return Field Fluent interface.
|
||||
*/
|
||||
public function setValueModelClass($classname)
|
||||
{
|
||||
$this->_valueModelClass = $classname;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getName()
|
||||
{
|
||||
return $this->_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the name of the model class that owns the field.
|
||||
* @param string $classname The name of the class that owns the field.
|
||||
* @return Field Fluent interface.
|
||||
*/
|
||||
public function setOwningModelClass($classname)
|
||||
{
|
||||
$this->_owningModelClass = $classname;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setValue($value)
|
||||
{
|
||||
// If the fields value is not going to change, leave.
|
||||
if (is_object($value) === true) {
|
||||
// weak comparison for objects
|
||||
// TODO: DateTimeZone == DateTimeZone always returns true in weak equal check! Why?
|
||||
if ($value == $this->_value) {
|
||||
return $this;
|
||||
}
|
||||
} else {
|
||||
// strong comparison for other values
|
||||
if ($value === $this->_value) {
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
// if (true === is_array($value) and 1 === count($value)) {
|
||||
// //$value = array_pop($value);
|
||||
// }
|
||||
if (true === is_array($value) and 0 === count($value)) {
|
||||
$value = null;
|
||||
} elseif (is_bool($value)) {
|
||||
// make sure 'false' is not converted to '' (empty string), but 0 for database
|
||||
$value = (int)$value;
|
||||
}
|
||||
|
||||
// if null is given, delete dependent objects
|
||||
if (null === $value) {
|
||||
//$this->_deleteDependentModels();
|
||||
} else {
|
||||
$multiValueCondition = $this->hasMultipleValues();
|
||||
$arrayCondition = is_array($value);
|
||||
|
||||
// arrayfy value
|
||||
$values = $value;
|
||||
if (false === $arrayCondition) {
|
||||
$values = array($value);
|
||||
}
|
||||
// remove wrapper array if multivalue condition is not given
|
||||
if (false === $multiValueCondition) {
|
||||
$value = $values[0];
|
||||
}
|
||||
|
||||
$this->_value = $value;
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
public function getValue($index = null)
|
||||
{
|
||||
|
||||
// wrap start value in array if multivalue option is set for this field
|
||||
$this->_value = $this->_wrapValueInArrayIfRequired($this->_value);
|
||||
|
||||
// Caller requested a specific array index
|
||||
//if (!is_null($index)) {
|
||||
// if (true === is_array($this->_value)) {
|
||||
// if (true === isset($this->_value[$index])) {
|
||||
// return $this->_value[$index];
|
||||
// }
|
||||
// else {
|
||||
// throw new \Exception('Unvalid index: ' . $index);
|
||||
// }
|
||||
// }
|
||||
// else {
|
||||
// throw new \Exception('Invalid index (' . $index . '). Requested value is not an array.');
|
||||
// }
|
||||
//}
|
||||
|
||||
return $this->_value;
|
||||
}
|
||||
|
||||
private function _wrapValueInArrayIfRequired($value)
|
||||
{
|
||||
if (is_array($value) or !$this->hasMultipleValues()) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if (is_null($value)) {
|
||||
return array();
|
||||
}
|
||||
|
||||
return array($value);
|
||||
}
|
||||
|
||||
public function hasMultipleValues()
|
||||
{
|
||||
return $this->_hasMultipleValues;
|
||||
}
|
||||
|
||||
public function setMultiplicity($max)
|
||||
{
|
||||
if ($max !== '*') {
|
||||
if ((is_int($max) === false) or ($max < 1)) {
|
||||
throw new \Exception('Only integer values > 1 or "*" allowed.');
|
||||
}
|
||||
}
|
||||
$this->_multiplicity = $max;
|
||||
$this->_hasMultipleValues = (($max > 1) or ($max === '*'));
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the name of model class if the field holds link model instances.
|
||||
*
|
||||
* @return string Class name or null if the value is not a model.
|
||||
*/
|
||||
public function getLinkModelClass()
|
||||
{
|
||||
return $this->_linkModelClass;
|
||||
}
|
||||
}
|
232
app/Library/Xml/Strategy.php
Normal file
232
app/Library/Xml/Strategy.php
Normal file
|
@ -0,0 +1,232 @@
|
|||
<?php
|
||||
namespace App\Library\Xml;
|
||||
|
||||
use App\Dataset;
|
||||
use DOMDocument;
|
||||
|
||||
/**
|
||||
* Strategy short summary.
|
||||
*
|
||||
* Strategy description.
|
||||
*
|
||||
* @version 1.0
|
||||
* @author kaiarn
|
||||
*/
|
||||
class Strategy
|
||||
{
|
||||
/**
|
||||
* Holds current configuration.
|
||||
*
|
||||
* @var Conf
|
||||
*/
|
||||
private $_config;
|
||||
|
||||
/**
|
||||
* Holds current representation version.
|
||||
*
|
||||
* @var double
|
||||
*/
|
||||
protected $_version = null;
|
||||
|
||||
|
||||
/**
|
||||
* Initiate class with a valid config object.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->_version = 1.0;
|
||||
$this->_config = new Conf();
|
||||
}
|
||||
|
||||
/**
|
||||
* (non-PHPdoc)
|
||||
* see library/Opus/Model/Xml/Opus_Model_Xml_Strategy#setDomDocument()
|
||||
*/
|
||||
public function setup(Conf $conf)
|
||||
{
|
||||
$this->_config = $conf;
|
||||
}
|
||||
|
||||
/**
|
||||
* If a model has been set this method generates and returnes
|
||||
* DOM representation of it.
|
||||
*
|
||||
* @throws \Exception Thrown if no Model is given.
|
||||
* @return \DOMDocument DOM representation of the current Model.
|
||||
*/
|
||||
public function getDomDocument()
|
||||
{
|
||||
if (null === $this->_config->model) {
|
||||
throw new \Exception('No Model given for serialization.');
|
||||
}
|
||||
$this->_config->dom = new DOMDocument('1.0', 'UTF-8');
|
||||
$root = $this->_config->dom->createElement('Opus');
|
||||
$root->setAttribute('version', $this->getVersion());
|
||||
$this->_config->dom->appendChild($root);
|
||||
$root->setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
|
||||
|
||||
$this->_mapModel($this->_config->model, $this->_config->dom, $root);
|
||||
|
||||
return $this->_config->dom;
|
||||
}
|
||||
|
||||
protected function _mapModel(Dataset $model, \DOMDocument $dom, \DOMNode $rootNode)
|
||||
{
|
||||
$fields = $model->describe();
|
||||
$excludeFields = $this->getConfig()->excludeFields;
|
||||
if (count($excludeFields) > 0) {
|
||||
$fieldsDiff = array_diff($fields, $excludeFields);
|
||||
} else {
|
||||
$fieldsDiff = $fields;
|
||||
}
|
||||
|
||||
$childNode = $this->createModelNode($dom, $model);
|
||||
$rootNode->appendChild($childNode);
|
||||
|
||||
foreach ($fieldsDiff as $fieldname) {
|
||||
$field = $model->getField($fieldname);
|
||||
$this->_mapField($field, $dom, $childNode);
|
||||
}
|
||||
}
|
||||
|
||||
protected function _mapField(Field $field, DOMDocument $dom, \DOMNode $rootNode)
|
||||
{
|
||||
$modelClass = $field->getValueModelClass();
|
||||
$fieldValues = $field->getValue();
|
||||
|
||||
if (true === $this->getConfig()->excludeEmpty) {
|
||||
if (true === is_null($fieldValues)
|
||||
or (is_string($fieldValues) && trim($fieldValues) == '')
|
||||
or (is_array($fieldValues) && empty($fieldValues))) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (null === $modelClass) {
|
||||
$this->mapSimpleField($dom, $rootNode, $field);
|
||||
} else {
|
||||
$fieldName = $field->getName();
|
||||
|
||||
if (!is_array($fieldValues)) {
|
||||
$fieldValues = array($fieldValues);
|
||||
}
|
||||
|
||||
foreach ($fieldValues as $value) {
|
||||
$childNode = $this->createFieldElement($dom, $fieldName);
|
||||
//$childNode->setAttribute("Value", $value);
|
||||
$rootNode->appendChild($childNode);
|
||||
|
||||
|
||||
// if a field has no value then is nothing more to do
|
||||
// TODO maybe must be there an other solution
|
||||
// FIXME remove code duplication (duplicates Opus_Model_Xml_Version*)
|
||||
if (is_null($value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($value instanceof \Illuminate\Database\Eloquent\Model) {
|
||||
$this->_mapModelAttributes($value, $dom, $childNode);
|
||||
} elseif ($value instanceof \Carbon\Carbon) {
|
||||
$this->_mapDateAttributes($value, $dom, $childNode);
|
||||
} elseif (is_array($value)) {
|
||||
$this->_mapArrayAttributes($value, $dom, $childNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function mapSimpleField(DOMDocument $dom, \DOMNode $rootNode, Field $field)
|
||||
{
|
||||
$fieldName = $field->getName();
|
||||
$fieldValues = $this->getFieldValues($field);
|
||||
|
||||
// Replace invalid XML-1.0-Characters by UTF-8 replacement character.
|
||||
$fieldValues = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', "\xEF\xBF\xBD ", $fieldValues);
|
||||
$rootNode->setAttribute($fieldName, $fieldValues);
|
||||
}
|
||||
|
||||
protected function createFieldElement(DOMDocument $dom, $fieldName)
|
||||
{
|
||||
return $dom->createElement($fieldName);
|
||||
}
|
||||
|
||||
protected function _mapDateAttributes(\Carbon\Carbon $model, DOMDocument $dom, \DOMNode $rootNode)
|
||||
{
|
||||
$rootNode->setAttribute("Year", $model->year);
|
||||
$rootNode->setAttribute("Month", $model->month);
|
||||
$rootNode->setAttribute("Day", $model->day);
|
||||
$rootNode->setAttribute("Hour", $model->hour);
|
||||
$rootNode->setAttribute("Minute", $model->minute);
|
||||
$rootNode->setAttribute("Second", $model->second);
|
||||
$rootNode->setAttribute("UnixTimestamp", $model->timestamp);
|
||||
$rootNode->setAttribute("Timezone", $model->tzName);
|
||||
}
|
||||
|
||||
protected function _mapArrayAttributes(array $attributes, DOMDocument $dom, \DOMNode $rootNode)
|
||||
{
|
||||
//$attributes = array_keys($model->getAttributes());
|
||||
foreach ($attributes as $property_name => $value) {
|
||||
$fieldName = $property_name;
|
||||
$field = new Field($fieldName);
|
||||
$fieldval = $value;
|
||||
$field->setValue($fieldval);
|
||||
$this->_mapField($field, $dom, $rootNode);
|
||||
}
|
||||
}
|
||||
|
||||
protected function _mapModelAttributes(\Illuminate\Database\Eloquent\Model $model, DOMDocument $dom, \DOMNode $rootNode)
|
||||
{
|
||||
$attributes = array_keys($model->getAttributes());
|
||||
foreach ($attributes as $property_name) {
|
||||
$fieldName = self::convertColumnToFieldname($property_name);
|
||||
$field = new Field($fieldName);
|
||||
$fieldval = $model->{$property_name};
|
||||
$field->setValue($fieldval);
|
||||
$this->_mapField($field, $dom, $rootNode);
|
||||
}
|
||||
}
|
||||
|
||||
//snakeToCamel
|
||||
public static function convertColumnToFieldname($columnname)
|
||||
{
|
||||
//return lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $columnname))));
|
||||
return str_replace(' ', '', ucwords(str_replace(['-', '_'], ' ', $columnname)));
|
||||
}
|
||||
|
||||
public function getFieldValues($field)
|
||||
{
|
||||
$fieldValues = $field->getValue();
|
||||
|
||||
// workaround for simple fields with multiple values
|
||||
if (true === $field->hasMultipleValues()) {
|
||||
$fieldValues = implode(',', $fieldValues);
|
||||
}
|
||||
//if ($fieldValues instanceOf DateTimeZone) {
|
||||
// $fieldValues = $fieldValues->getName();
|
||||
//}
|
||||
|
||||
return trim($fieldValues);
|
||||
}
|
||||
|
||||
|
||||
protected function createModelNode(DOMDocument $dom, Dataset $model)
|
||||
{
|
||||
$classname = "Rdr_" . substr(strrchr(get_class($model), '\\'), 1);
|
||||
return $dom->createElement($classname);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return version value of current xml representation.
|
||||
*
|
||||
* @see library/Opus/Model/Xml/Opus_Model_Xml_Strategy#getVersion()
|
||||
*/
|
||||
public function getVersion()
|
||||
{
|
||||
return floor($this->_version);
|
||||
}
|
||||
|
||||
public function getConfig()
|
||||
{
|
||||
return $this->_config;
|
||||
}
|
||||
}
|
180
app/Library/Xml/XmlModel.php
Normal file
180
app/Library/Xml/XmlModel.php
Normal file
|
@ -0,0 +1,180 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Footer
|
||||
*
|
||||
* Main footer file for the theme.
|
||||
*
|
||||
* @category Components
|
||||
* @package ResearchRepository
|
||||
* @subpackage Publish
|
||||
* @author Your Name <yourname@example.com>
|
||||
* @license https://www.gnu.org/licenses/gpl-3.0.txt GNU/GPLv3
|
||||
* @link https://gisgba.geologie.ac.at
|
||||
* @since 1.0.0
|
||||
*/
|
||||
namespace App\Library\Xml;
|
||||
|
||||
use App\XmlCache;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class XmlModel
|
||||
{
|
||||
/**
|
||||
* Holds current configuration.
|
||||
* @var Conf
|
||||
*/
|
||||
private $_config = null;
|
||||
|
||||
/**
|
||||
* Holds current xml strategy object.
|
||||
* @var Strategy
|
||||
*/
|
||||
private $_strategy = null;
|
||||
|
||||
/**
|
||||
* TODO
|
||||
* @var XmlCache
|
||||
*/
|
||||
private $_cache = null;
|
||||
|
||||
|
||||
/**
|
||||
* Do some initial stuff like setting of a XML version and an empty
|
||||
* configuration.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->_strategy = new Strategy();// Opus_Model_Xml_Version1;
|
||||
$this->_config = new Conf();
|
||||
$this->_strategy->setup($this->_config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a new XML version with current configuration up.
|
||||
*
|
||||
* @param Strategy $strategy Version of Xml to process
|
||||
*
|
||||
* @return XmlModel fluent interface.
|
||||
*/
|
||||
public function setStrategy(Strategy $strategy)
|
||||
{
|
||||
$this->_strategy = $strategy;
|
||||
$this->_strategy->setup($this->_config);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a new XML version with current configuration up.
|
||||
*
|
||||
* @param XmlCache $cache cach table
|
||||
*
|
||||
* @return XmlModel fluent interface.
|
||||
*/
|
||||
public function setXmlCache(XmlCache $cache)
|
||||
{
|
||||
$this->_cache = $cache;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return cache table.
|
||||
*
|
||||
* @return XmlCache
|
||||
*/
|
||||
public function getXmlCache()
|
||||
{
|
||||
return $this->_cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Model for XML generation.
|
||||
*
|
||||
* @param \App\Dataset $model Model to serialize.
|
||||
*
|
||||
* @return XmlModel Fluent interface.
|
||||
*/
|
||||
public function setModel($model)
|
||||
{
|
||||
$this->_config->model = $model;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define that empty fields (value===null) shall be excluded.
|
||||
*
|
||||
* @return XmlModel Fluent interface
|
||||
*/
|
||||
public function excludeEmptyFields()
|
||||
{
|
||||
$this->_config->excludeEmpty = true;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* If a model has been set this method generates and returnes
|
||||
* DOM representation of it.
|
||||
*
|
||||
* @return \DOMDocument DOM representation of the current Model.
|
||||
*/
|
||||
public function getDomDocument()
|
||||
{
|
||||
$dataset = $this->_config->model;
|
||||
|
||||
$domDocument = $this->getDomDocumentFromXmlCache();
|
||||
if (!is_null($domDocument)) {
|
||||
return $domDocument;
|
||||
}
|
||||
|
||||
//create xml:
|
||||
$domDocument = $this->_strategy->getDomDocument();
|
||||
//if caching is not desired, return domDocument
|
||||
if (is_null($this->_cache)) {
|
||||
return $domDocument;
|
||||
} else {
|
||||
//create cache relation
|
||||
$this->_cache->fill(array(
|
||||
'document_id' => $dataset->id,
|
||||
'xml_version' => (int)$this->_strategy->getVersion(),
|
||||
'server_date_modified' => $dataset->server_date_modified,
|
||||
'xml_data' => $domDocument->saveXML()
|
||||
));
|
||||
$this->_cache->save();
|
||||
|
||||
Log::debug(__METHOD__ . ' cache refreshed for ' . get_class($dataset) . '#' . $dataset->id);
|
||||
return $domDocument;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method tries to load the current model from the xml cache. Returns
|
||||
* null in case of an error/cache miss/cache disabled. Returns DOMDocument
|
||||
* otherwise.
|
||||
*
|
||||
* @return \DOMDocument DOM representation of the current Model.
|
||||
*/
|
||||
private function getDomDocumentFromXmlCache()
|
||||
{
|
||||
$dataset = $this->_config->model;
|
||||
if (null === $this->_cache) {
|
||||
//$logger->debug(__METHOD__ . ' skipping cache for ' . get_class($model));
|
||||
Log::debug(__METHOD__ . ' skipping cache for ' . get_class($dataset));
|
||||
return null;
|
||||
}
|
||||
//$cached = $this->_cache->hasValidEntry(
|
||||
// $dataset->id,
|
||||
// (int) $this->_strategy->getVersion(),
|
||||
// $dataset->server_date_modified
|
||||
//);
|
||||
|
||||
//$cached = false;
|
||||
$cache = XmlCache::where('document_id', $dataset->id)
|
||||
->first();// model or null
|
||||
if (!$cache) {
|
||||
Log::debug(__METHOD__ . ' cache miss for ' . get_class($dataset) . '#' . $dataset->id);
|
||||
return null;
|
||||
} else {
|
||||
return $cache->getDomDocument();
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
editor.link_modal.header
Reference in a new issue