/*
* (C) Copyright 2014-2016 Nuxeo SA (http://nuxeo.com/) and others.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Contributors:
* Nuxeo
*/
package org.nuxeo.elasticsearch.test.nxql;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import org.apache.commons.lang.SystemUtils;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.action.search.SearchType;
import org.elasticsearch.index.query.QueryBuilders;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.nuxeo.ecm.core.api.CoreSession;
import org.nuxeo.ecm.core.api.DocumentModel;
import org.nuxeo.ecm.core.api.DocumentModelList;
import org.nuxeo.ecm.core.work.api.WorkManager;
import org.nuxeo.elasticsearch.api.ElasticSearchAdmin;
import org.nuxeo.elasticsearch.api.ElasticSearchIndexing;
import org.nuxeo.elasticsearch.api.ElasticSearchService;
import org.nuxeo.elasticsearch.query.NxQueryBuilder;
import org.nuxeo.elasticsearch.query.NxqlQueryConverter;
import org.nuxeo.elasticsearch.test.RepositoryElasticSearchFeature;
import org.nuxeo.runtime.api.Framework;
import org.nuxeo.runtime.test.runner.Features;
import org.nuxeo.runtime.test.runner.FeaturesRunner;
import org.nuxeo.runtime.test.runner.LocalDeploy;
import org.nuxeo.runtime.transaction.TransactionHelper;
/**
* Test that NXQL can be used to generate ES queries
*
* @author <a href="mailto:tdelprat@nuxeo.com">Tiry</a>
*/
@RunWith(FeaturesRunner.class)
@Features({ RepositoryElasticSearchFeature.class })
@LocalDeploy({ "org.nuxeo.elasticsearch.core:elasticsearch-test-contrib.xml" })
public class TestNxqlConversion {
private static final String IDX_NAME = "nxutest";
private static final String TYPE_NAME = "doc";
@Inject
protected CoreSession session;
@Inject
protected ElasticSearchService ess;
@Inject
protected ElasticSearchAdmin esa;
@Inject
protected ElasticSearchIndexing esi;
protected void buildDocs() throws Exception {
for (int i = 0; i < 10; i++) {
String name = "doc" + i;
DocumentModel doc = session.createDocumentModel("/", name, "File");
doc.setPropertyValue("dc:title", "File" + i);
doc.setPropertyValue("dc:nature", "Nature" + i);
doc.setPropertyValue("dc:rights", "Rights" + i % 2);
doc = session.createDocument(doc);
}
TransactionHelper.commitOrRollbackTransaction();
// wait for async jobs
WorkManager wm = Framework.getLocalService(WorkManager.class);
Assert.assertTrue(wm.awaitCompletion(20, TimeUnit.SECONDS));
Assert.assertEquals(0, esa.getPendingWorkerCount());
esa.refresh();
TransactionHelper.startTransaction();
}
@Test
public void testQuery() throws Exception {
buildDocs();
SearchResponse searchResponse = esa.getClient()
.prepareSearch(IDX_NAME)
.setTypes(TYPE_NAME)
.setSearchType(SearchType.DFS_QUERY_THEN_FETCH)
.setQuery(QueryBuilders.queryStringQuery(
" dc\\:nature:\"Nature1\" AND dc\\:title:\"File1\""))
.setFrom(0)
.setSize(60)
.execute()
.actionGet();
Assert.assertEquals(1, searchResponse.getHits().getTotalHits());
searchResponse = esa.getClient()
.prepareSearch(IDX_NAME)
.setTypes(TYPE_NAME)
.setSearchType(SearchType.DFS_QUERY_THEN_FETCH)
.setQuery(
QueryBuilders.queryStringQuery(" dc\\:nature:\"Nature2\" AND dc\\:title:\"File1\""))
.setFrom(0)
.setSize(60)
.execute()
.actionGet();
Assert.assertEquals(0, searchResponse.getHits().getTotalHits());
searchResponse = esa.getClient()
.prepareSearch(IDX_NAME)
.setTypes(TYPE_NAME)
.setSearchType(SearchType.DFS_QUERY_THEN_FETCH)
.setQuery(QueryBuilders.queryStringQuery(" NOT " + "dc\\:nature:\"Nature2\""))
.setFrom(0)
.setSize(60)
.execute()
.actionGet();
Assert.assertEquals(9, searchResponse.getHits().getTotalHits());
checkNXQL("select * from Document where dc:nature='Nature2' and dc:title='File2'", 1);
checkNXQL("select * from Document where dc:nature='Nature2' and dc:title='File1'", 0);
checkNXQL("select * from Document where dc:nature='Nature2' or dc:title='File1'", 2);
}
@Test
public void testQueryLimits() throws Exception {
buildDocs();
// limit does not change the total size, only the returned number of docs
DocumentModelList docs = ess.query(new NxQueryBuilder(session).nxql("select * from Document").limit(1));
Assert.assertEquals(10, docs.totalSize());
Assert.assertEquals(1, docs.size());
// default is 10
docs = ess.query(new NxQueryBuilder(session).nxql("select * from Document"));
Assert.assertEquals(10, docs.totalSize());
Assert.assertEquals(10, docs.size());
// only interested about totalSize
docs = ess.query(new NxQueryBuilder(session).nxql("select * from Document").limit(0));
Assert.assertEquals(10, docs.totalSize());
Assert.assertEquals(0, docs.size());
}
@Test
public void testQueryWithSpecialCharacters() throws Exception {
// special character should not raise syntax error
String specialChars = "^..*+ - && || ! ( ) { } [ ] )^ \" (~ * ? : \\ / \\t$";
checkNXQL("select * from Document where dc:title = '" + specialChars + "'", 0);
checkNXQL("select * from Document where ecm:fulltext.dc:title = '" + specialChars + "'", 0);
checkNXQL("select * from Document where dc:title LIKE '" + specialChars + "'", 0);
checkNXQL("select * from Document where dc:title IN ('" + specialChars + "')", 0);
checkNXQL("select * from Document where dc:title STARTSWITH '" + specialChars + "'", 0);
}
protected void checkNXQL(String nxql, int expectedNumberOfHis) {
// System.out.println(NXQLQueryConverter.toESQueryString(nxql));
DocumentModelList docs = ess.query(new NxQueryBuilder(session).nxql(nxql).limit(0));
Assert.assertEquals(expectedNumberOfHis, docs.totalSize());
}
@Test
public void testConverterSelect() throws Exception {
String es = NxqlQueryConverter.toESQueryBuilder("select * from Document").toString();
assertEqualsEvenUnderWindows("{\n" + //
" \"match_all\" : { }\n" + //
"}", es);
es = NxqlQueryConverter.toESQueryBuilder("select * from File, Document").toString();
assertEqualsEvenUnderWindows("{\n" + //
" \"match_all\" : { }\n" + //
"}", es);
es = NxqlQueryConverter.toESQueryBuilder("select * from File").toString();
assertEqualsEvenUnderWindows("{\n" + //
" \"bool\" : {\n" + //
" \"must\" : {\n" + //
" \"match_all\" : { }\n" + //
" },\n" + //
" \"filter\" : {\n" + //
" \"terms\" : {\n" + //
" \"ecm:primaryType\" : [ \"File\" ]\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
"}", es);
es = NxqlQueryConverter.toESQueryBuilder("select * from File, Note").toString();
assertEqualsEvenUnderWindows("{\n" + //
" \"bool\" : {\n" + //
" \"must\" : {\n" + //
" \"match_all\" : { }\n" + //
" },\n" + //
" \"filter\" : {\n" + //
" \"terms\" : {\n" + //
" \"ecm:primaryType\" : [ \"File\", \"Note\" ]\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
"}", es);
}
@Test
public void testConverterEQUALS() throws Exception {
String es = NxqlQueryConverter.toESQueryBuilder("select * from Document where f1=1").toString();
assertEqualsEvenUnderWindows("{\n" + //
" \"constant_score\" : {\n" + //
" \"filter\" : {\n" + //
" \"term\" : {\n" + //
" \"f1\" : \"1\"\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
"}", es);
es = NxqlQueryConverter.toESQueryBuilder("select * from Document where f1 != 1").toString();
assertEqualsEvenUnderWindows("{\n" + //
" \"constant_score\" : {\n" + //
" \"filter\" : {\n" + //
" \"bool\" : {\n" + //
" \"must_not\" : {\n" + //
" \"term\" : {\n" + //
" \"f1\" : \"1\"\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
"}", es);
es = NxqlQueryConverter.toESQueryBuilder("select * from Document where f1 <> 1").toString();
assertEqualsEvenUnderWindows("{\n" + //
" \"constant_score\" : {\n" + //
" \"filter\" : {\n" + //
" \"bool\" : {\n" + //
" \"must_not\" : {\n" + //
" \"term\" : {\n" + //
" \"f1\" : \"1\"\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
"}", es);
}
@Test
public void testConverterIN() throws Exception {
String es = NxqlQueryConverter.toESQueryBuilder("select * from Document where f1 IN (1)").toString();
assertEqualsEvenUnderWindows("{\n" + //
" \"constant_score\" : {\n" + //
" \"filter\" : {\n" + //
" \"terms\" : {\n" + //
" \"f1\" : [ \"1\" ]\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
"}", es);
es = NxqlQueryConverter.toESQueryBuilder("select * from Document where f1 NOT IN (1, '2', 3)").toString();
assertEqualsEvenUnderWindows("{\n" + //
" \"constant_score\" : {\n" + //
" \"filter\" : {\n" + //
" \"bool\" : {\n" + //
" \"must_not\" : {\n" + //
" \"terms\" : {\n" + //
" \"f1\" : [ \"1\", \"2\", \"3\" ]\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
"}", es);
}
@Test
public void testConverterLIKE() throws Exception {
String es = NxqlQueryConverter.toESQueryBuilder("select * from Document where f1 LIKE 'foo%'").toString();
assertEqualsEvenUnderWindows("{\n" + //
" \"match\" : {\n" + //
" \"f1\" : {\n" + //
" \"query\" : \"foo\",\n" + //
" \"type\" : \"phrase_prefix\"\n" + //
" }\n" + //
" }\n" + //
"}", es);
es = NxqlQueryConverter.toESQueryBuilder("select * from Document where f1 LIKE '%Foo%'").toString();
assertEqualsEvenUnderWindows("{\n" + //
" \"wildcard\" : {\n" + //
" \"f1\" : \"*Foo*\"\n" + //
" }\n" + //
"}", es);
es = NxqlQueryConverter.toESQueryBuilder("select * from Document where f1 NOT LIKE 'Foo%'").toString();
assertEqualsEvenUnderWindows("{\n" + //
" \"constant_score\" : {\n" + //
" \"filter\" : {\n" + //
" \"bool\" : {\n" + //
" \"must_not\" : {\n" + //
" \"match\" : {\n" + //
" \"f1\" : {\n" + //
" \"query\" : \"Foo\",\n" + //
" \"type\" : \"phrase_prefix\"\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
"}", es);
// invalid input
NxqlQueryConverter.toESQueryBuilder("select * from Document where f1 LIKE '(foo.*$#@^'").toString();
}
@Test
public void testConverterLIKEWildcard() throws Exception {
String es;
es = NxqlQueryConverter.toESQueryBuilder("SELECT * FROM Document WHERE f1 LIKE '%foo'").toString();
assertEqualsEvenUnderWindows("{\n" + //
" \"wildcard\" : {\n" + //
" \"f1\" : \"*foo\"\n" + //
" }\n" + //
"}", es);
es = NxqlQueryConverter.toESQueryBuilder("SELECT * FROM Document WHERE f1 LIKE '_foo'").toString();
assertEqualsEvenUnderWindows("{\n" + //
" \"wildcard\" : {\n" + //
" \"f1\" : \"?foo\"\n" + //
" }\n" + //
"}", es);
es = NxqlQueryConverter.toESQueryBuilder("SELECT * FROM Document WHERE f1 LIKE '?foo'").toString();
assertEqualsEvenUnderWindows("{\n" + //
" \"wildcard\" : {\n" + //
" \"f1\" : \"\\\\?foo\"\n" // backslash escaped for JSON
+ " }\n" + //
"}", es);
// * is also accepted as a wildcard (compat)
es = NxqlQueryConverter.toESQueryBuilder("SELECT * FROM Document WHERE f1 LIKE '*foo'").toString();
assertEqualsEvenUnderWindows("{\n" + //
" \"wildcard\" : {\n" + //
" \"f1\" : \"*foo\"\n" + //
" }\n" + //
"}", es);
// NXQL escaping
es = NxqlQueryConverter.toESQueryBuilder("SELECT * FROM Document WHERE f1 LIKE 'foo\\_bar\\%'").toString();
assertEqualsEvenUnderWindows("{\n" + //
" \"wildcard\" : {\n" + //
" \"f1\" : \"foo_bar%\"\n" + //
" }\n" + //
"}", es);
}
@Test
public void testConverterILIKE() throws Exception {
String es = NxqlQueryConverter.toESQueryBuilder("select * from Document where f1 ILIKE 'Foo%'").toString();
assertEqualsEvenUnderWindows("{\n" + //
" \"match\" : {\n" + //
" \"f1.lowercase\" : {\n" + //
" \"query\" : \"foo\",\n" + //
" \"type\" : \"phrase_prefix\"\n" + //
" }\n" + //
" }\n" + //
"}", es);
es = NxqlQueryConverter.toESQueryBuilder("select * from Document where f1 ILIKE '%Foo%'").toString();
assertEqualsEvenUnderWindows("{\n" + //
" \"wildcard\" : {\n" + //
" \"f1.lowercase\" : \"*foo*\"\n" + //
" }\n" + //
"}", es);
es = NxqlQueryConverter.toESQueryBuilder("select * from Document where f1 NOT ILIKE 'Foo%'").toString();
assertEqualsEvenUnderWindows("{\n" + //
" \"constant_score\" : {\n" + //
" \"filter\" : {\n" + //
" \"bool\" : {\n" + //
" \"must_not\" : {\n" + //
" \"match\" : {\n" + //
" \"f1.lowercase\" : {\n" + //
" \"query\" : \"foo\",\n" + //
" \"type\" : \"phrase_prefix\"\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
"}", es);
}
@Test
public void testConverterIsNULL() throws Exception {
String es = NxqlQueryConverter.toESQueryBuilder("select * from Document where f1 IS NULL").toString();
assertEqualsEvenUnderWindows("{\n" + //
" \"constant_score\" : {\n" + //
" \"filter\" : {\n" + //
" \"bool\" : {\n" + //
" \"must_not\" : {\n" + //
" \"exists\" : {\n" + //
" \"field\" : \"f1\"\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
"}", es);
es = NxqlQueryConverter.toESQueryBuilder("select * from Document where f1 IS NOT NULL").toString();
assertEqualsEvenUnderWindows("{\n" + //
" \"constant_score\" : {\n" + //
" \"filter\" : {\n" + //
" \"exists\" : {\n" + //
" \"field\" : \"f1\"\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
"}", es);
}
@Test
public void testConverterBETWEEN() throws Exception {
String es = NxqlQueryConverter.toESQueryBuilder("select * from Document where f1 BETWEEN 1 AND 2").toString();
assertEqualsEvenUnderWindows("{\n" + //
" \"constant_score\" : {\n" + //
" \"filter\" : {\n" + //
" \"range\" : {\n" + //
" \"f1\" : {\n" + //
" \"from\" : \"1\",\n" + //
" \"to\" : \"2\",\n" + //
" \"include_lower\" : true,\n" + //
" \"include_upper\" : true\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
"}", es);
es = NxqlQueryConverter.toESQueryBuilder("select * from Document where f1 NOT BETWEEN 1 AND 2").toString();
assertEqualsEvenUnderWindows("{\n" + //
" \"constant_score\" : {\n" + //
" \"filter\" : {\n" + //
" \"bool\" : {\n" + //
" \"must_not\" : {\n" + //
" \"range\" : {\n" + //
" \"f1\" : {\n" + //
" \"from\" : \"1\",\n" + //
" \"to\" : \"2\",\n" + //
" \"include_lower\" : true,\n" + //
" \"include_upper\" : true\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
"}", es);
}
@Test
public void testConverterSTARTSWITH() throws Exception {
String es = NxqlQueryConverter.toESQueryBuilder("select * from Document where ecm:path STARTSWITH '/the/path'")
.toString();
assertEqualsEvenUnderWindows("{\n" + //
" \"constant_score\" : {\n" + //
" \"filter\" : {\n" + //
" \"bool\" : {\n" + //
" \"must\" : {\n" + //
" \"term\" : {\n" + //
" \"ecm:path.children\" : \"/the/path\"\n" + //
" }\n" + //
" },\n" + //
" \"must_not\" : {\n" + //
" \"term\" : {\n" + //
" \"ecm:path\" : \"/the/path\"\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
"}", es);
es = NxqlQueryConverter.toESQueryBuilder("select * from Document where ecm:path STARTSWITH '/'").toString();
assertEqualsEvenUnderWindows("{\n" + //
" \"constant_score\" : {\n" + //
" \"filter\" : {\n" + //
" \"exists\" : {\n" + //
" \"field\" : \"ecm:path.children\"\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
"}", es);
es = NxqlQueryConverter.toESQueryBuilder("select * from Document where ecm:path STARTSWITH '/the/path/'")
.toString();
assertEqualsEvenUnderWindows("{\n" + //
" \"constant_score\" : {\n" + //
" \"filter\" : {\n" + //
" \"bool\" : {\n" + //
" \"must\" : {\n" + //
" \"term\" : {\n" + //
" \"ecm:path.children\" : \"/the/path\"\n" + //
" }\n" + //
" },\n" + //
" \"must_not\" : {\n" + //
" \"term\" : {\n" + //
" \"ecm:path\" : \"/the/path/\"\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
"}", es);
// for other field than ecm:path we want to match the root
es = NxqlQueryConverter.toESQueryBuilder("select * from Document where dc:coverage STARTSWITH 'Europe/France'")
.toString();
assertEqualsEvenUnderWindows("{\n" + //
" \"constant_score\" : {\n" + //
" \"filter\" : {\n" + //
" \"term\" : {\n" + //
" \"dc:coverage.children\" : \"Europe/France\"\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
"}", es);
}
@Test
public void testConverterAncestorId() throws Exception {
String es = NxqlQueryConverter.toESQueryBuilder(
"select * from Document where ecm:ancestorId = 'c5904f77-299a-411e-8477-81d3102a81f9'").toString();
assertEqualsEvenUnderWindows("{\n" + //
" \"constant_score\" : {\n" + //
" \"filter\" : {\n" + //
" \"exists\" : {\n" + //
" \"field\" : \"ancestorid-without-session\"\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
"}", es);
es = NxqlQueryConverter.toESQueryBuilder(
"select * from Document where ecm:ancestorId != 'c5904f77-299a-411e-8477-81d3102a81f9'", session)
.toString();
assertEqualsEvenUnderWindows("{\n" + //
" \"constant_score\" : {\n" + //
" \"filter\" : {\n" + //
" \"bool\" : {\n" + //
" \"must_not\" : {\n" + //
" \"exists\" : {\n" + //
" \"field\" : \"ancestorid-not-found\"\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
"}", es);
}
@Test
public void testConverterIsVersion() throws Exception {
String es = NxqlQueryConverter.toESQueryBuilder("select * from Document where ecm:isVersion = 1").toString();
assertEqualsEvenUnderWindows("{\n" + //
" \"constant_score\" : {\n" + //
" \"filter\" : {\n" + //
" \"term\" : {\n" + //
" \"ecm:isVersion\" : \"1\"\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
"}", es);
String es2 = NxqlQueryConverter.toESQueryBuilder("select * from Document where ecm:isCheckedInVersion = 1")
.toString();
assertEqualsEvenUnderWindows(es, es2);
}
@Test
public void testConverterFulltext() throws Exception {
// Given a search on a fulltext field
String es = NxqlQueryConverter.toESQueryBuilder("select * from Document where ecm:fulltext='+foo -bar'")
.toString();
// then we have a simple query text and not a filter
assertEqualsEvenUnderWindows("{\n" + //
" \"simple_query_string\" : {\n" + //
" \"query\" : \"+foo -bar\",\n" + //
" \"fields\" : [ \"_all\" ],\n" + //
" \"analyzer\" : \"fulltext\",\n" + //
" \"default_operator\" : \"and\"\n" + //
" }\n" + //
"}", es);
es = NxqlQueryConverter.toESQueryBuilder("select * from Document where ecm:fulltext_someindex LIKE '+foo -bar'")
.toString();
// don't handle nxql fulltext index definition, match to _all field
assertEqualsEvenUnderWindows("{\n" + //
" \"simple_query_string\" : {\n" + //
" \"query\" : \"+foo -bar\",\n" + //
" \"fields\" : [ \"_all\" ],\n" + //
" \"analyzer\" : \"fulltext\",\n" + //
" \"default_operator\" : \"and\"\n" + //
" }\n" + //
"}", es);
es = NxqlQueryConverter.toESQueryBuilder("select * from Document where ecm:fulltext.dc:title!='+foo -bar'")
.toString();
// request on field match field.fulltext
assertEqualsEvenUnderWindows("{\n" + //
" \"constant_score\" : {\n" + //
" \"filter\" : {\n" + //
" \"bool\" : {\n" + //
" \"must_not\" : {\n" + //
" \"simple_query_string\" : {\n" + //
" \"query\" : \"+foo -bar\",\n" + //
" \"fields\" : [ \"dc:title.fulltext\" ],\n" + //
" \"analyzer\" : \"fulltext\",\n" + //
" \"default_operator\" : \"and\"\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
"}", es);
}
@Test
public void testConverterFulltextElasticsearchPrefix() throws Exception {
// Given a search on a fulltext field with the
// elasticsearch-specific prefix
String es = NxqlQueryConverter.toESQueryBuilder("SELECT * FROM Document WHERE ecm:fulltext = 'es: foo bar'")
.toString();
// then we have a simple query text and not a filter
// and we have the OR operator
assertEqualsEvenUnderWindows("{\n" + //
" \"simple_query_string\" : {\n" + //
" \"query\" : \"foo bar\",\n" + //
" \"fields\" : [ \"_all\" ],\n" + //
" \"analyzer\" : \"fulltext\",\n" + //
" \"default_operator\" : \"or\"\n" + //
" }\n" + //
"}", es);
}
@Test
public void testConverterWhereCombination() throws Exception {
String es = NxqlQueryConverter.toESQueryBuilder("select * from Document where f1=1 AND f2=2").toString();
assertEqualsEvenUnderWindows("{\n" + //
" \"bool\" : {\n" + //
" \"must\" : [ {\n" + //
" \"constant_score\" : {\n" + //
" \"filter\" : {\n" + //
" \"term\" : {\n" + //
" \"f1\" : \"1\"\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
" }, {\n" + //
" \"constant_score\" : {\n" + //
" \"filter\" : {\n" + //
" \"term\" : {\n" + //
" \"f2\" : \"2\"\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
" } ]\n" + //
" }\n" + //
"}", es);
es = NxqlQueryConverter.toESQueryBuilder("select * from Document where f1=1 OR f2=2").toString();
assertEqualsEvenUnderWindows("{\n" + //
" \"bool\" : {\n" + //
" \"should\" : [ {\n" + //
" \"constant_score\" : {\n" + //
" \"filter\" : {\n" + //
" \"term\" : {\n" + //
" \"f1\" : \"1\"\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
" }, {\n" + //
" \"constant_score\" : {\n" + //
" \"filter\" : {\n" + //
" \"term\" : {\n" + //
" \"f2\" : \"2\"\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
" } ]\n" + //
" }\n" + //
"}", es);
es = NxqlQueryConverter.toESQueryBuilder("select * from Document where f1=1 AND f2=2 AND f3=3").toString();
// Assert.assertEquals("foo", es);
es = NxqlQueryConverter.toESQueryBuilder("select * from Document where f1=1 OR f2=2 OR f3=3").toString();
// Assert.assertEquals("foo", es);
es = NxqlQueryConverter.toESQueryBuilder("select * from Document where f1=1 OR f2 LIKE 'foo' OR f3=3")
.toString();
// Assert.assertEquals("foo", es);
es = NxqlQueryConverter.toESQueryBuilder("select * from Document where (f1=1 OR f2=2) AND f3=3").toString();
assertEqualsEvenUnderWindows("{\n" + //
" \"bool\" : {\n" + //
" \"must\" : [ {\n" + //
" \"bool\" : {\n" + //
" \"should\" : [ {\n" + //
" \"constant_score\" : {\n" + //
" \"filter\" : {\n" + //
" \"term\" : {\n" + //
" \"f1\" : \"1\"\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
" }, {\n" + //
" \"constant_score\" : {\n" + //
" \"filter\" : {\n" + //
" \"term\" : {\n" + //
" \"f2\" : \"2\"\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
" } ]\n" + //
" }\n" + //
" }, {\n" + //
" \"constant_score\" : {\n" + //
" \"filter\" : {\n" + //
" \"term\" : {\n" + //
" \"f3\" : \"3\"\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
" } ]\n" + //
" }\n" + //
"}", es);
}
@Test
public void testConverterComplex() throws Exception {
String es = NxqlQueryConverter.toESQueryBuilder(
"select * from Document where (f1 LIKE '1%' OR f2 LIKE '2%') AND f3=3").toString();
assertEqualsEvenUnderWindows("{\n" + //
" \"bool\" : {\n" + //
" \"must\" : [ {\n" + //
" \"bool\" : {\n" + //
" \"should\" : [ {\n" + //
" \"match\" : {\n" + //
" \"f1\" : {\n" + //
" \"query\" : \"1\",\n" + //
" \"type\" : \"phrase_prefix\"\n" + //
" }\n" + //
" }\n" + //
" }, {\n" + //
" \"match\" : {\n" + //
" \"f2\" : {\n" + //
" \"query\" : \"2\",\n" + //
" \"type\" : \"phrase_prefix\"\n" + //
" }\n" + //
" }\n" + //
" } ]\n" + //
" }\n" + //
" }, {\n" + //
" \"constant_score\" : {\n" + //
" \"filter\" : {\n" + //
" \"term\" : {\n" + //
" \"f3\" : \"3\"\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
" } ]\n" + //
" }\n" + //
"}", es);
// Assert.assertEquals("foo", es);
es = NxqlQueryConverter.toESQueryBuilder(
"select * from Document where ecm:fulltext='foo bar' AND ecm:path STARTSWITH '/foo/bar' OR ecm:path='/foo/'")
.toString();
// Assert.assertEquals("foo", es);
es = NxqlQueryConverter.toESQueryBuilder(
"select * from File, Note, Workspace where f1 IN ('foo', 'bar', 'foo') AND NOT f2>=3").toString();
assertEqualsEvenUnderWindows("{\n" + //
" \"bool\" : {\n" + //
" \"must\" : {\n" + //
" \"bool\" : {\n" + //
" \"must\" : [ {\n" + //
" \"constant_score\" : {\n" + //
" \"filter\" : {\n" + //
" \"terms\" : {\n" + //
" \"f1\" : [ \"foo\", \"bar\", \"foo\" ]\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
" }, {\n" + //
" \"bool\" : {\n" + //
" \"must_not\" : {\n" + //
" \"constant_score\" : {\n" + //
" \"filter\" : {\n" + //
" \"range\" : {\n" + //
" \"f2\" : {\n" + //
" \"from\" : \"3\",\n" + //
" \"to\" : null,\n" + //
" \"include_lower\" : true,\n" + //
" \"include_upper\" : true\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
" } ]\n" + //
" }\n" + //
" },\n" + //
" \"filter\" : {\n" + //
" \"terms\" : {\n" + //
" \"ecm:primaryType\" : [ \"File\", \"Note\", \"Workspace\" ]\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
"}", es);
}
@Test
public void testConverterWhereWithoutSelect() throws Exception {
String es = NxqlQueryConverter.toESQueryBuilder("f1=1").toString();
assertEqualsEvenUnderWindows("{\n" + //
" \"constant_score\" : {\n" + //
" \"filter\" : {\n" + //
" \"term\" : {\n" + //
" \"f1\" : \"1\"\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
"}", es);
es = NxqlQueryConverter.toESQueryBuilder(null).toString();
assertEqualsEvenUnderWindows("{\n" + //
" \"match_all\" : { }\n" + //
"}", es);
es = NxqlQueryConverter.toESQueryBuilder("").toString();
assertEqualsEvenUnderWindows("{\n" + //
" \"match_all\" : { }\n" + //
"}", es);
}
@Test
public void testConvertComplexProperties() throws Exception {
String es = NxqlQueryConverter.toESQueryBuilder("select * from Document where file:content/name = 'foo'")
.toString();
assertEqualsEvenUnderWindows("{\n" + //
" \"constant_score\" : {\n" + //
" \"filter\" : {\n" + //
" \"term\" : {\n" + //
" \"file:content.name\" : \"foo\"\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
"}", es);
}
@Test
public void testConvertComplexListProperties() throws Exception {
String es = NxqlQueryConverter.toESQueryBuilder("select * from Document where dc:subjects/* = 'foo'")
.toString();
// this is supported and match any element of the list
assertEqualsEvenUnderWindows("{\n" + //
" \"constant_score\" : {\n" + //
" \"filter\" : {\n" + //
" \"term\" : {\n" + //
" \"dc:subjects\" : \"foo\"\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
"}", es);
es = NxqlQueryConverter.toESQueryBuilder("select * from Document where files:files/*/file/length=123")
.toString();
assertEqualsEvenUnderWindows("{\n" + //
" \"constant_score\" : {\n" + //
" \"filter\" : {\n" + //
" \"term\" : {\n" + //
" \"files:files.file.length\" : \"123\"\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
"}", es);
}
@Test
public void testConvertComplexListPropertiesUnsupported() throws Exception {
String es = NxqlQueryConverter.toESQueryBuilder("select * from Document where dc:subjects/3 = 'foo'")
.toString();
// This is not supported and generate query that is going to match nothing
assertEqualsEvenUnderWindows("{\n" + //
" \"constant_score\" : {\n" + //
" \"filter\" : {\n" + //
" \"term\" : {\n" + //
" \"dc:subjects.3\" : \"foo\"\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
"}", es);
es = NxqlQueryConverter.toESQueryBuilder("select * from Document where dc:subjects/*1 = 'foo'").toString();
// This is not supported and generate query that is going to match nothing
assertEqualsEvenUnderWindows("{\n" + //
" \"constant_score\" : {\n" + //
" \"filter\" : {\n" + //
" \"term\" : {\n" + //
" \"dc:subjects1\" : \"foo\"\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
"}", es);
es = NxqlQueryConverter.toESQueryBuilder("select * from Document where files:files/*1/file/length=123")
.toString();
// This is not supported and generate query that is going to match nothing
assertEqualsEvenUnderWindows("{\n" + //
" \"constant_score\" : {\n" + //
" \"filter\" : {\n" + //
" \"term\" : {\n" + //
" \"files:files1.file.length\" : \"123\"\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
"}", es);
}
@Test
public void testOrderByFromNxql() throws Exception {
NxQueryBuilder qb = new NxQueryBuilder(session).nxql("name='foo' ORDER BY name DESC");
String es = qb.makeQuery().toString();
assertEqualsEvenUnderWindows("{\n" + //
" \"constant_score\" : {\n" + //
" \"filter\" : {\n" + //
" \"term\" : {\n" + //
" \"name\" : \"foo\"\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
"}", es);
Assert.assertEquals(1, qb.getSortInfos().size());
Assert.assertEquals("SortInfo [sortColumn=name, sortAscending=false]", qb.getSortInfos().get(0).toString());
}
@Test
public void testOrderByWithComplexProperties() throws Exception {
NxQueryBuilder qb = new NxQueryBuilder(session).nxql("SELECT * FROM File ORDER BY file:content/name DESC");
String es = qb.makeQuery().toString();
assertEqualsEvenUnderWindows("{\n" + //
" \"bool\" : {\n" + //
" \"must\" : {\n" + //
" \"match_all\" : { }\n" + //
" },\n" + //
" \"filter\" : {\n" + //
" \"terms\" : {\n" + //
" \"ecm:primaryType\" : [ \"File\" ]\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
"}", es);
Assert.assertEquals(1, qb.getSortInfos().size());
Assert.assertEquals("SortInfo [sortColumn=file:content.name, sortAscending=false]",
qb.getSortInfos().get(0).toString());
}
@Test
public void testConvertHint() throws Exception {
String es = NxqlQueryConverter.toESQueryBuilder(
"select * from Document where /*+ES: INDEX(some:field) */ dc:title = 'foo'").toString();
assertEqualsEvenUnderWindows("{\n" + //
" \"constant_score\" : {\n" + //
" \"filter\" : {\n" + //
" \"term\" : {\n" + //
" \"some:field\" : \"foo\"\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
"}", es);
es = NxqlQueryConverter.toESQueryBuilder(
"select * from Document where /*+ES: INDEX(some:field) */ dc:title != 'foo'").toString();
assertEqualsEvenUnderWindows("{\n" + //
" \"constant_score\" : {\n" + //
" \"filter\" : {\n" + //
" \"bool\" : {\n" + //
" \"must_not\" : {\n" + //
" \"term\" : {\n" + //
" \"some:field\" : \"foo\"\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
"}", es);
}
@Test
public void testConvertHintOperator() throws Exception {
String es = NxqlQueryConverter.toESQueryBuilder(
"select * from Document where /*+ES: INDEX(some:field) ANALYZER(my_analyzer) OPERATOR(match) */ dc:subjects = 'foo'")
.toString();
assertEqualsEvenUnderWindows("{\n" + " \"match\" : {\n" + //
" \"some:field\" : {\n" + //
" \"query\" : \"foo\",\n" + //
" \"type\" : \"boolean\",\n" + //
" \"analyzer\" : \"my_analyzer\"\n" + //
" }\n" + //
" }\n" + //
"}", es);
es = NxqlQueryConverter.toESQueryBuilder(
"select * from Document where /*+ES: OPERATOR(match_phrase) */ dc:title = 'foo'").toString();
assertEqualsEvenUnderWindows("{\n" + //
" \"match\" : {\n" + //
" \"dc:title\" : {\n" + //
" \"query\" : \"foo\",\n" + //
" \"type\" : \"phrase\"\n" + //
" }\n" + //
" }\n" + //
"}", es);
es = NxqlQueryConverter.toESQueryBuilder(
"select * from Document where /*+ES: OPERATOR(match_phrase_prefix) */ dc:title = 'this is a test'")
.toString();
assertEqualsEvenUnderWindows("{\n" + //
" \"match\" : {\n" + //
" \"dc:title\" : {\n" + //
" \"query\" : \"this is a test\",\n" + //
" \"type\" : \"phrase_prefix\"\n" + //
" }\n" + //
" }\n" + //
"}", es);
es = NxqlQueryConverter.toESQueryBuilder(
"select * from Document where /*+ES: INDEX(dc:title^3,dc:description) OPERATOR(multi_match) */ dc:title = 'this is a test'")
.toString();
// fields are not ordered
assertIn(es,
"{\n" + //
" \"multi_match\" : {\n" + //
" \"query\" : \"this is a test\",\n" + //
" \"fields\" : [ \"dc:title^3\", \"dc:description\" ]\n" + //
" }\n" + //
"}",
"{\n" + //
" \"multi_match\" : {\n" + //
" \"query\" : \"this is a test\",\n" + //
" \"fields\" : [ \"dc:description\", \"dc:title^3\" ]\n" + //
" }\n" + //
"}");
es = NxqlQueryConverter.toESQueryBuilder(
"select * from Document where /*+ES: OPERATOR(regex) */ dc:title = 's.*y'").toString();
assertEqualsEvenUnderWindows("{\n" + //
" \"regexp\" : {\n" + //
" \"dc:title\" : {\n" + //
" \"value\" : \"s.*y\",\n" + //
" \"flags_value\" : 65535\n" + //
" }\n" + //
" }\n" + //
"}", es);
es = NxqlQueryConverter.toESQueryBuilder(
"select * from Document where /*+ES: OPERATOR(fuzzy) */ dc:title = 'ki'").toString();
assertEqualsEvenUnderWindows("{\n" + //
" \"fuzzy\" : {\n" + //
" \"dc:title\" : {\n" + //
" \"value\" : \"ki\"\n" + //
" }\n" + //
" }\n" + //
"}", es);
es = NxqlQueryConverter.toESQueryBuilder(
"select * from Document where /*+ES: OPERATOR(wildcard) */ dc:title = 'ki*y'").toString();
assertEqualsEvenUnderWindows("{\n" + //
" \"wildcard\" : {\n" + //
" \"dc:title\" : \"ki*y\"\n" + //
" }\n" + //
"}", es);
es = NxqlQueryConverter.toESQueryBuilder(
"select * from Document where /*+ES: OPERATOR(simple_query_string) */ dc:title = '\"fried eggs\" +(eggplant | potato) -frittata'")
.toString();
assertEqualsEvenUnderWindows("{\n" + //
" \"simple_query_string\" : {\n" + //
" \"query\" : \"\\\"fried eggs\\\" +(eggplant | potato) -frittata\",\n" + //
" \"fields\" : [ \"dc:title\" ]\n" + //
" }\n" + //
"}", es);
es = NxqlQueryConverter.toESQueryBuilder(
"select * from Document where /*+ES: INDEX(dc:title,dc:description) ANALYZER(fulltext) OPERATOR(query_string) */ dc:title = 'this AND that OR thus'")
.toString();
// fields are not ordered
assertIn(es,
"{\n" + //
" \"query_string\" : {\n" + //
" \"query\" : \"this AND that OR thus\",\n" + //
" \"fields\" : [ \"dc:title\", \"dc:description\" ],\n" + //
" \"analyzer\" : \"fulltext\"\n" + //
" }\n" + //
"}",
"{\n" + //
" \"query_string\" : {\n" + //
" \"query\" : \"this AND that OR thus\",\n" + //
" \"fields\" : [ \"dc:description\", \"dc:title\" ],\n" + //
" \"analyzer\" : \"fulltext\"\n" + //
" }\n" + //
"}");
es = NxqlQueryConverter.toESQueryBuilder(
"select * from Document where /*+ES: OPERATOR(common) */ dc:title = 'this is bonsai cool'").toString();
assertEqualsEvenUnderWindows("{\n" + //
" \"common\" : {\n" + //
" \"dc:title\" : {\n" + //
" \"query\" : \"this is bonsai cool\"\n" + //
" }\n" + //
" }\n" + //
"}", es);
}
@Test
public void testConvertHintLike() throws Exception {
String es = NxqlQueryConverter.toESQueryBuilder(
"select * from Document where /*+ES: INDEX(some:field) ANALYZER(my_analyzer) */ dc:subjects LIKE 'foo*'")
.toString();
assertEqualsEvenUnderWindows("{\n" + //
" \"match\" : {\n" + //
" \"some:field\" : {\n" + //
" \"query\" : \"foo\",\n" + //
" \"type\" : \"phrase_prefix\",\n" + //
" \"analyzer\" : \"my_analyzer\"\n" + //
" }\n" + //
" }\n" + //
"}", es);
es = NxqlQueryConverter.toESQueryBuilder(
"select * from Document where /*+ES: INDEX(some:field) */ dc:subjects LIKE '%foo%'").toString();
assertEqualsEvenUnderWindows("{\n" + //
" \"wildcard\" : {\n" + //
" \"some:field\" : \"*foo*\"\n" + //
" }\n" + //
"}", es);
}
@Test
public void testConvertHintFulltext() throws Exception {
// search on title and description, boost title
String es = NxqlQueryConverter.toESQueryBuilder(
"select * from Document where /*+ES: INDEX(dc:title.fulltext^3,dc:description.fulltext) */ ecm:fulltext = 'foo'")
.toString();
// fields are not ordered
assertIn(es,
"{\n" + //
" \"simple_query_string\" : {\n" + //
" \"query\" : \"foo\",\n" + //
" \"fields\" : [ \"dc:title.fulltext^3\", \"dc:description.fulltext\" ],\n" + //
" \"analyzer\" : \"fulltext\",\n" + //
" \"default_operator\" : \"and\"\n" + //
" }\n" + //
"}",
"{\n" + //
" \"simple_query_string\" : {\n" + //
" \"query\" : \"foo\",\n" + //
" \"fields\" : [ \"dc:description.fulltext\", \"dc:title.fulltext^3\" ],\n" + //
" \"analyzer\" : \"fulltext\",\n" + //
" \"default_operator\" : \"and\"\n" + //
" }\n" + //
"}");
}
protected void assertEqualsEvenUnderWindows(String expected, String actual) {
expected = normalizeString(expected);
actual = normalizeString(actual);
Assert.assertEquals(expected, actual);
}
private String normalizeString(String str) {
if (SystemUtils.IS_OS_WINDOWS) {
str = str.trim();
str = str.replace("\n", "");
str = str.replace("\r", "");
}
return str;
}
protected void assertIn(String actual, String... expected) {
actual = normalizeString(actual);
for (String exp : expected) {
exp = normalizeString(exp);
if (exp.equals(actual)) {
return;
}
}
// fail
Assert.assertEquals(expected[0], actual);
}
@Test
public void testConvertHintGeo() throws Exception {
String es = NxqlQueryConverter.toESQueryBuilder(
"select * from Document where /*+ES: OPERATOR(geo_bounding_box) */ osm:location IN ('40.73, -74.1', '40.01, -71.12')")
.toString();
String response = "{\n" + //
" \"constant_score\" : {\n" + //
" \"filter\" : {\n" + //
" \"geo_bbox\" : {\n" + //
" \"osm:location\" : {\n" + //
" \"top_left\" : [ -74.1, 40.01 ],\n" + //
" \"bottom_right\" : [ -71.12, 40.73 ]\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
"}";
assertEqualsEvenUnderWindows(response, es);
es = NxqlQueryConverter.toESQueryBuilder(
"select * from Document where /*+ES: OPERATOR(geo_bounding_box) */ osm:location IN ('dr5r9y', 'drj7tee')")
.toString();
// we can not do this because lat and lon are not rounded to match the input
// assertTruEqualsEvenUnderWindows(response, es);
Assert.assertTrue(es.contains("geo_bbox"));
Assert.assertTrue(es.contains("bottom_right"));
es = NxqlQueryConverter.toESQueryBuilder("select * from Document where /*+ES: OPERATOR(geo_distance) */ "
+ "osm:location IN ('40.73, -74.1', '20km')").toString();
assertEqualsEvenUnderWindows("{\n" + //
" \"constant_score\" : {\n" + //
" \"filter\" : {\n" + //
" \"geo_distance\" : {\n" + //
" \"osm:location\" : [ -74.1, 40.73 ],\n" + //
" \"distance\" : \"20km\"\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
"}", es);
es = NxqlQueryConverter.toESQueryBuilder("select * from Document where /*+ES: OPERATOR(geo_distance_range) */"
+ "osm:location IN ('40.73, -74.1', '500m', '20km')").toString();
assertEqualsEvenUnderWindows("{\n" + //
" \"constant_score\" : {\n" + //
" \"filter\" : {\n" + //
" \"geo_distance_range\" : {\n" + //
" \"osm:location\" : [ -74.1, 40.73 ],\n" + //
" \"from\" : \"500m\",\n" + //
" \"to\" : \"20km\",\n" + //
" \"include_lower\" : true,\n" + //
" \"include_upper\" : true\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
"}", es);
es = NxqlQueryConverter.toESQueryBuilder("select * from Document where /*+ES: OPERATOR(geo_distance_range) */"
+ "osm:location IN ('40.73, -74.1', '500m', '20km')").toString();
assertEqualsEvenUnderWindows("{\n" + //
" \"constant_score\" : {\n" + //
" \"filter\" : {\n" + //
" \"geo_distance_range\" : {\n" + //
" \"osm:location\" : [ -74.1, 40.73 ],\n" + //
" \"from\" : \"500m\",\n" + //
" \"to\" : \"20km\",\n" + //
" \"include_lower\" : true,\n" + //
" \"include_upper\" : true\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
"}", es);
es = NxqlQueryConverter.toESQueryBuilder("select * from Document where /*+ES: OPERATOR(geo_hash_cell) */"
+ "osm:location IN ('40.73, -74.1', '2')").toString();
assertEqualsEvenUnderWindows("{\n" + //
" \"constant_score\" : {\n" + //
" \"filter\" : {\n" + //
" \"geohash_cell\" : {\n" + //
" \"precision\" : 10,\n" + //
" \"osm:location\" : \"dr5r9ydj2y73\"\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
"}", es);
es = NxqlQueryConverter.toESQueryBuilder("select * from Document where /*+ES: OPERATOR(geo_shape) */"
+ "osm:location IN ('FRA', 'countries', 'shapes', 'location')").toString();
assertEqualsEvenUnderWindows("{\n" + //
" \"constant_score\" : {\n" + //
" \"filter\" : {\n" + //
" \"geo_shape\" : {\n" + //
" \"osm:location\" : {\n" + //
" \"indexed_shape\" : {\n" + //
" \"id\" : \"FRA\",\n" + //
" \"type\" : \"countries\",\n" + //
" \"index\" : \"shapes\",\n" + //
" \"path\" : \"location\"\n" + //
" },\n" + //
" \"relation\" : \"within\"\n" + //
" },\n" + //
" \"_name\" : null\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
"}", es);
}
}