Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
EZP-28783: As a Developer I want to search using REST FieldCriterion (e…
…zsystems#2239)

* [REST] Implemented Field Criterion

* Allowed empty FieldCriterion values as long as key exists

* [EZP-28683] Basic functionality for content query logic query

* [EZP-28683] Fix and tests for queries containing ezkeyword fieldtype

* [EZP-28683] Changing payload structure, Functional test refactoring

* [EZP-28783] Allow logicalOperator as xml array
  • Loading branch information
ViniTou authored and alongosz committed Feb 22, 2018
1 parent 0b53cc2 commit a259cb7
Show file tree
Hide file tree
Showing 5 changed files with 341 additions and 2 deletions.
277 changes: 277 additions & 0 deletions eZ/Bundle/EzPublishRestBundle/Tests/Functional/SearchViewTest.php
@@ -0,0 +1,277 @@
<?php

/**
* File containing the Functional\SearchViewTest class.
*
* @copyright Copyright (C) eZ Systems AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
namespace eZ\Bundle\EzPublishRestBundle\Tests\Functional;

use eZ\Bundle\EzPublishRestBundle\Tests\Functional\TestCase as RESTFunctionalTestCase;
use eZ\Publish\API\Repository\Values\Content\Query\Criterion\Operator;

class SearchViewTest extends RESTFunctionalTestCase
{
/**
* @var string
*/
protected $contentTypeHref;

/**
* @var string[]
*/
protected $contentHrefList;

protected function setUp()
{
parent::setUp();
$this->contentTypeHref = $this->createTestContentType();
$this->contentHrefList[] = $this->createTestContentWithTags('test-name', ['foo', 'bar']);
$this->contentHrefList[] = $this->createTestContentWithTags('fancy-name', ['baz', 'foobaz']);
$this->contentHrefList[] = $this->createTestContentWithTags('even-fancier', ['bar', 'bazfoo']);
}

protected function tearDown()
{
parent::tearDown();
array_map([$this, 'deleteContent'], $this->contentHrefList);
$this->deleteContent($this->contentTypeHref);
}

/**
* @dataProvider xmlProvider
* Covers POST with ContentQuery Logic on /api/ezp/v2/views.
*/
public function testSimpleContentQuery(string $xmlQueryBody, int $expectedCount)
{
$request = $this->createHttpRequest('POST', '/api/ezp/v2/views', 'ViewInput+xml; version=1.1', 'ContentInfo+json');
$body = <<< XML
<?xml version="1.0" encoding="UTF-8"?>
<ViewInput>
<identifier>your-query-id</identifier>
<public>false</public>
<ContentQuery>
<Query>
$xmlQueryBody
</Query>
<limit>10</limit>
<offset>0</offset>
</ContentQuery>
</ViewInput>
XML;

$request->setContent($body);

$response = $this->sendHttpRequest($request);
self::assertHttpResponseCodeEquals($response, 200);
$jsonResponse = json_decode($response->getContent());
self::assertEquals($expectedCount, $jsonResponse->View->Result->count);
}

private function createTestContentType(): string
{
$body = <<< XML
<?xml version="1.0" encoding="UTF-8"?>
<ContentTypeCreate>
<identifier>tags-test</identifier>
<names>
<value languageCode="eng-GB">testContentQueryWithTags</value>
</names>
<remoteId>testContentQueryWithTags</remoteId>
<urlAliasSchema>&lt;title&gt;</urlAliasSchema>
<nameSchema>&lt;title&gt;</nameSchema>
<isContainer>true</isContainer>
<mainLanguageCode>eng-GB</mainLanguageCode>
<defaultAlwaysAvailable>true</defaultAlwaysAvailable>
<defaultSortField>PATH</defaultSortField>
<defaultSortOrder>ASC</defaultSortOrder>
<FieldDefinitions>
<FieldDefinition>
<identifier>title</identifier>
<fieldType>ezstring</fieldType>
<fieldGroup>content</fieldGroup>
<position>1</position>
<isTranslatable>true</isTranslatable>
<isRequired>true</isRequired>
<isInfoCollector>false</isInfoCollector>
<defaultValue>New Title</defaultValue>
<isSearchable>true</isSearchable>
<names>
<value languageCode="eng-GB">Title</value>
</names>
<descriptions>
<value languageCode="eng-GB">This is the title</value>
</descriptions>
</FieldDefinition>
<FieldDefinition>
<identifier>tags</identifier>
<fieldType>ezkeyword</fieldType>
<fieldGroup>content</fieldGroup>
<position>2</position>
<isTranslatable>true</isTranslatable>
<isRequired>true</isRequired>
<isInfoCollector>false</isInfoCollector>
<isSearchable>true</isSearchable>
<names>
<value languageCode="eng-GB">Tags</value>
</names>
<descriptions>
<value languageCode="eng-GB">Those are searchable tags</value>
</descriptions>
</FieldDefinition>
</FieldDefinitions>
</ContentTypeCreate>
XML;

$request = $this->createHttpRequest(
'POST',
'/api/ezp/v2/content/typegroups/1/types?publish=true',
'ContentTypeCreate+xml',
'ContentType+json'
);
$request->setContent($body);
$response = $this->sendHttpRequest($request);

return $response->getHeader('Location');
}

private function createTestContentWithTags(string $name, array $tags): string
{
$request = $this->createHttpRequest('POST', '/api/ezp/v2/content/objects', 'ContentCreate+xml', 'ContentInfo+json');
$tagsString = implode(',', $tags);
$body = <<< XML
<?xml version="1.0" encoding="UTF-8"?>
<ContentCreate>
<ContentType href="$this->contentTypeHref" />
<mainLanguageCode>eng-GB</mainLanguageCode>
<LocationCreate>
<ParentLocation href="/api/ezp/v2/content/locations/1" />
<priority>0</priority>
<hidden>false</hidden>
<sortField>PATH</sortField>
<sortOrder>ASC</sortOrder>
</LocationCreate>
<Section href="/api/ezp/v2/content/sections/1" />
<alwaysAvailable>true</alwaysAvailable>
<remoteId>$name</remoteId>
<User href="/api/ezp/v2/user/users/14" />
<modificationDate>2018-01-30T18:30:00</modificationDate>
<fields>
<field>
<fieldDefinitionIdentifier>title</fieldDefinitionIdentifier>
<languageCode>eng-GB</languageCode>
<fieldValue>$name</fieldValue>
</field>
<field>
<fieldDefinitionIdentifier>tags</fieldDefinitionIdentifier>
<languageCode>eng-GB</languageCode>
<fieldValue>$tagsString</fieldValue>
</field>
</fields>
</ContentCreate>
XML;
$request->setContent($body);

$response = $this->sendHttpRequest($request);
$href = $response->getHeader('Location');
$this->sendHttpRequest(
$this->createHttpRequest('PUBLISH', "$href/versions/1")
);

return $href;
}

private function deleteContent($href)
{
$this->sendHttpRequest(
$this->createHttpRequest('DELETE', $href)
);
}

public function xmlProvider()
{
$fooTag = $this->buildFieldXml('tags', Operator::CONTAINS, 'foo');
$barTag = $this->buildFieldXml('tags', Operator::CONTAINS, 'bar');
$bazTag = $this->buildFieldXml('tags', Operator::CONTAINS, 'baz');
$foobazTag = $this->buildFieldXml('tags', Operator::CONTAINS, 'foobaz');

return [
[
$this->getXmlString(
$this->wrapIn('AND', [$fooTag, $barTag])
),
1,
],
[
$this->getXmlString(
$this->wrapIn('OR', [
$this->wrapIn('AND', [$fooTag, $barTag]),
$this->wrapIn('AND', [$bazTag, $foobazTag]),
])
),
2,
],
[
$this->getXmlString(
$this->wrapIn('AND', [
$this->wrapIn('NOT', [$fooTag]),
$barTag,
])
),
1,
],
];
}

/**
* @param string $name
* @param string $operator
* @param string|string[] $value
* @return \DomElement
*/
private function buildFieldXml(string $name, string $operator, $value): \DomElement
{
$xml = new \DOMDocument();
$element = $xml->createElement('Field');
$element->appendChild(new \DOMElement('name', $name));
$element->appendChild(new \DOMElement('operator', $operator));
if (is_array($value)) {
$valueWrapper = $xml->createElement('value');
foreach ($value as $key => $singleValue) {
$valueWrapper->appendChild(new \DOMElement('value', $singleValue));
}
$element->appendChild($valueWrapper);

return $element;
}

$element->appendChild(new \DOMElement('value', $value));

return $element;
}

/**
* @param string $logicalOperator
* @param \DomElement|\DomElement[] $toWrap
* @return \DomElement
*/
private function wrapIn(string $logicalOperator, array $toWrap): \DomElement
{
$xml = new \DOMDocument();
$wrapper = $xml->createElement($logicalOperator);

foreach ($toWrap as $key => $field) {
$innerWrapper = $xml->createElement($logicalOperator);
$innerWrapper->appendChild($xml->importNode($field, true));
$wrapper->appendChild($innerWrapper);
}

return $wrapper;
}

private function getXmlString(\DomElement $simpleXMLElement): string
{
return $simpleXMLElement->ownerDocument->saveXML($simpleXMLElement);
}
}
57 changes: 56 additions & 1 deletion eZ/Publish/Core/REST/Server/Input/Parser/Criterion/Field.php
Expand Up @@ -8,14 +8,29 @@
*/
namespace eZ\Publish\Core\REST\Server\Input\Parser\Criterion;

use eZ\Publish\API\Repository\Values\Content\Query\Criterion\Field as FieldCriterion;
use eZ\Publish\API\Repository\Values\Content\Query\Criterion\Operator;
use eZ\Publish\Core\REST\Common\Input\BaseParser;
use eZ\Publish\Core\REST\Common\Input\ParsingDispatcher;
use eZ\Publish\Core\REST\Common\Exceptions;

/**
* Parser for Field Criterion.
*/
class Field extends BaseParser
{
const OPERATORS = [
'IN' => Operator::IN,
'EQ' => Operator::EQ,
'GT' => Operator::GT,
'GTE' => Operator::GTE,
'LT' => Operator::LT,
'LTE' => Operator::LTE,
'LIKE' => Operator::LIKE,
'BETWEEN' => Operator::BETWEEN,
'CONTAINS' => Operator::CONTAINS,
];

/**
* Parses input structure to a Criterion object.
*
Expand All @@ -28,6 +43,46 @@ class Field extends BaseParser
*/
public function parse(array $data, ParsingDispatcher $parsingDispatcher)
{
throw new \Exception('@todo implement');
if (!array_key_exists('Field', $data)) {
throw new Exceptions\Parser('Invalid <Field> format');
}

$fieldData = $data['Field'];
if (empty($fieldData['name']) || empty($fieldData['operator']) || !array_key_exists('value', $fieldData)) {
throw new Exceptions\Parser('<Field> format expects name, operator and value keys');
}

$operator = $this->getOperator($fieldData['operator']);

return new FieldCriterion(
$fieldData['name'],
$operator,
$fieldData['value']
);
}

/**
* Get operator for the given literal name.
*
* For the full list of supported operators:
* @see \eZ\Publish\Core\REST\Server\Input\Parser\Criterion\Field::OPERATORS
*
* @param string $operatorName operator literal operator name
*
* @return string
*/
private function getOperator($operatorName)
{
$operatorName = strtoupper($operatorName);
if (!isset(self::OPERATORS[$operatorName])) {
throw new Exceptions\Parser(
sprintf(
'Unexpected Field operator, expected one of the following: %s',
implode(', ', array_keys(self::OPERATORS))
)
);
}

return self::OPERATORS[$operatorName];
}
}
Expand Up @@ -36,6 +36,10 @@ public function parse(array $data, ParsingDispatcher $parsingDispatcher)

$criteria = array();
foreach ($data['AND'] as $criterionName => $criterionData) {
if (is_array($criterionData) && !array_key_exists(0, $criterionData)) {
$criterionName = key($criterionData);
$criterionData = current($criterionData);
}
$criteria[] = $this->dispatchCriterion($criterionName, $criterionData, $parsingDispatcher);
}

Expand Down
Expand Up @@ -36,6 +36,10 @@ public function parse(array $data, ParsingDispatcher $parsingDispatcher)

$criteria = array();
foreach ($data['OR'] as $criterionName => $criterionData) {
if (is_array($criterionData) && !array_key_exists(0, $criterionData)) {
$criterionName = key($criterionData);
$criterionData = current($criterionData);
}
$criteria[] = $this->dispatchCriterion($criterionName, $criterionData, $parsingDispatcher);
}

Expand Down
Expand Up @@ -14,7 +14,6 @@
use eZ\Publish\Core\Persistence\Database\DatabaseHandler;
use eZ\Publish\API\Repository\Values\Content\Query\Criterion;
use eZ\Publish\Core\Persistence\Legacy\Content\FieldValue\ConverterRegistry as Registry;
use eZ\Publish\Core\Persistence\Legacy\Content\FieldValue\Converter;
use eZ\Publish\Core\Base\Exceptions\InvalidArgumentException;
use eZ\Publish\Core\Persistence\TransformationProcessor;
use eZ\Publish\Core\Persistence\Database\SelectQuery;
Expand Down

0 comments on commit a259cb7

Please sign in to comment.