/*
* (C) Copyright 2014 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:
* Thierry Delprat
*/
package org.nuxeo.elasticsearch.test.nxql;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
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.api.PathRef;
import org.nuxeo.ecm.core.api.VersioningOption;
import org.nuxeo.ecm.core.api.model.PropertyNotFoundException;
import org.nuxeo.ecm.core.test.annotations.Granularity;
import org.nuxeo.ecm.core.test.annotations.RepositoryConfig;
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.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;
@RunWith(FeaturesRunner.class)
@Features({ RepositoryElasticSearchFeature.class })
@LocalDeploy("org.nuxeo.elasticsearch.core:elasticsearch-test-contrib.xml")
@RepositoryConfig(cleanup = Granularity.METHOD)
public class TestCompareCoreWithES {
@Inject
protected CoreSession session;
@Inject
protected ElasticSearchService ess;
@Inject
protected ElasticSearchAdmin esa;
@Inject
protected ElasticSearchIndexing esi;
private String proxyPath;
@Before
public void initWorkingDocuments() throws Exception {
if (!TransactionHelper.isTransactionActive()) {
TransactionHelper.startTransaction();
}
for (int i = 0; i < 5; i++) {
String name = "file" + 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.setPropertyValue("dc:subjects",
(i % 2 == 0) ? new String[] { "Subjects1" } : new String[] { "Subjects1", "Subjects2" });
doc.setPropertyValue("relatedtext:relatedtextresources",
(Serializable) Arrays.asList(Collections.singletonMap("relatedtextid", "123")));
doc = session.createDocument(doc);
}
for (int i = 5; i < 10; i++) {
String name = "note" + i;
DocumentModel doc = session.createDocumentModel("/", name, "Note");
doc.setPropertyValue("dc:title", "Note" + i);
doc.setPropertyValue("note:note", "Content" + i);
doc.setPropertyValue("dc:nature", "Nature" + i);
doc.setPropertyValue("dc:rights", "Rights" + i % 2);
doc = session.createDocument(doc);
}
DocumentModel doc = session.createDocumentModel("/", "hidden", "HiddenFolder");
doc.setPropertyValue("dc:title", "HiddenFolder");
doc = session.createDocument(doc);
DocumentModel folder = session.createDocumentModel("/", "folder", "Folder");
folder.setPropertyValue("dc:title", "Folder");
folder = session.createDocument(folder);
DocumentModel file = session.getDocument(new PathRef("/file3"));
DocumentModel proxy = session.publishDocument(file, folder);
proxyPath = proxy.getPathAsString();
session.followTransition(new PathRef("/file1"), "delete");
session.followTransition(new PathRef("/note5"), "delete");
session.checkIn(new PathRef("/file2"), VersioningOption.MINOR, "for testing");
TransactionHelper.commitOrRollbackTransaction();
// wait for async jobs
Framework.getLocalService(WorkManager.class).awaitCompletion(20, TimeUnit.SECONDS);
esa.prepareWaitForIndexing().get(20, TimeUnit.SECONDS);
esa.refresh();
Assert.assertEquals(0, esa.getPendingWorkerCount());
TransactionHelper.startTransaction();
}
@Before
public void setupIndex() throws Exception {
esa.initIndexes(true);
}
@After
public void cleanWorkingDocuments() throws Exception {
// prevent NXP-14686 bug that prevent cleanupSession to remove version
session.removeDocument(new PathRef(proxyPath));
}
protected String getDigest(DocumentModelList docs) {
StringBuilder sb = new StringBuilder();
for (DocumentModel doc : docs) {
String nameOrTitle = doc.getName();
if (nameOrTitle == null || nameOrTitle.isEmpty()) {
nameOrTitle = doc.getTitle();
}
sb.append(nameOrTitle);
sb.append("[" + doc.getPropertyValue("dc:nature") + "]");
sb.append("[" + doc.getPropertyValue("dc:rights") + "]");
sb.append(",");
}
return sb.toString();
}
protected void assertSameDocumentLists(DocumentModelList expected, DocumentModelList actual) throws Exception {
Assert.assertEquals(expected.size(), actual.size());
// quick check for some props for better failure messages
for (int i = 0; i < expected.size(); i++) {
DocumentModel expecteDdoc = expected.get(i);
DocumentModel actualDoc = actual.get(i);
for (String xpath : Arrays.asList("dc:title", "dc:nature", "dc:rights", "dc:subjects",
"relatedtext:relatedtextresources")) {
Serializable expectedValue = getProperty(expecteDdoc, xpath);
Serializable actualValue = getProperty(actualDoc, xpath);
Assert.assertEquals(xpath, expectedValue, actualValue);
}
}
Assert.assertEquals(getDigest(expected), getDigest(actual));
}
protected Serializable getProperty(DocumentModel doc, String xpath) {
Serializable value;
try {
value = doc.getPropertyValue(xpath);
} catch (PropertyNotFoundException e) {
value = "__NOTFOUND__";
}
if (value instanceof Object[]) {
value = (Serializable) Arrays.asList(((Object[]) value));
}
if (value instanceof List && ((List<?>) value).isEmpty()) {
value = null;
}
return value;
}
protected void dump(DocumentModelList docs) {
for (DocumentModel doc : docs) {
System.out.println(doc);
}
}
protected void compareESAndCore(String nxql) throws Exception {
DocumentModelList coreResult = session.query(nxql);
NxQueryBuilder nxQueryBuilder = new NxQueryBuilder(session).nxql(nxql).limit(20);
for (int i = 0; i < 2; i++) {
if (i == 1) {
nxQueryBuilder = nxQueryBuilder.fetchFromElasticsearch();
}
DocumentModelList esResult = ess.query(nxQueryBuilder);
try {
assertSameDocumentLists(coreResult, esResult);
} catch (AssertionError e) {
// System.out.println("Error while executing " + nxql);
// System.out.println("Core result : ");
// dump(coreResult);
// System.out.println("elasticsearch result : ");
// dump(esResult);
// e.printStackTrace();
throw e;
}
}
}
protected void testQueries(String[] testQueries) throws Exception {
for (String nxql : testQueries) {
// System.out.println("test " + nxql);
compareESAndCore(nxql);
}
}
@Test
public void testSimpleSearchWithSort() throws Exception {
testQueries(new String[] { "select * from Document order by dc:title, dc:created",
"select * from Document where ecm:currentLifeCycleState != 'deleted' order by dc:title",
"select * from File order by dc:title", });
}
@Test
public void testSearchOnProxies() throws Exception {
testQueries(new String[] { "select * from Document where ecm:isProxy=0 order by dc:title",
"select * from Document where ecm:isProxy=1 order by dc:title", });
}
@Test
public void testSearchOnVersions() throws Exception {
testQueries(new String[] { "select * from Document where ecm:isVersion = 0 order by dc:title",
"select * from Document where ecm:isVersion = 1 order by dc:title",
"select * from Document where ecm:isCheckedInVersion = 0 order by dc:title",
"select * from Document where ecm:isCheckedInVersion = 1 order by dc:title",
// TODO: fix, ES results sounds correct
// "select * from Document where ecm:isCheckedIn = 0 order by dc:title",
// "select * from Document where ecm:isCheckedIn = 1 order by dc:title"
});
}
@Test
public void testSearchOnTypes() throws Exception {
testQueries(new String[] { "select * from File order by dc:title", "select * from Folder order by dc:title",
"select * from Note order by dc:title",
"select * from Note where ecm:primaryType IN ('Note', 'Folder') order by dc:title",
"select * from Document where ecm:mixinType = 'Folderish' order by dc:title",
"select * from Document where ecm:mixinType != 'Folderish' order by dc:title", });
}
@Test
public void testSearchWithLike() throws Exception {
// Validate that NXP-14338 is fixed
testQueries(new String[] { "SELECT * FROM Document WHERE dc:title LIKE 'nomatch%'",
"SELECT * from Document WHERE dc:title LIKE 'File%' ORDER BY dc:title",
"SELECT * from Document WHERE dc:title LIKE '%ile%' ORDER BY dc:title",
"SELECT * from Document WHERE dc:title NOT LIKE '%ile%' ORDER BY dc:title",
"SELECT * from Document WHERE dc:title NOT LIKE '%i%e%' ORDER BY dc:title", });
}
@Test
public void testSearchWithStartsWith() throws Exception {
testQueries(new String[] {
// Note that there are differnces between ES and VCS:
// ES version document has a path and is searchable with startswith
// ES match the root document, VCS only the children
"SELECT * from Document WHERE ecm:path STARTSWITH '/nomatch' ORDER BY dc:title",
"SELECT * from Document WHERE ecm:path STARTSWITH '/folder' AND ecm:path != '/folder' ORDER BY dc:title",
"SELECT * FROM Document WHERE ecm:path STARTSWITH '/' AND ecm:isVersion = 0 ORDER BY dc:title",
});
}
@Test
public void testSearchWithAncestorId() throws Exception {
DocumentModel folder = session.getDocument(new PathRef("/folder"));
Assert.assertNotNull(folder);
String fid = folder.getId();
testQueries(new String[]{
"SELECT * from Document WHERE ecm:ancestorId = 'non-esisting-id' ORDER BY dc:title",
"SELECT * from Document WHERE ecm:ancestorId != 'non-existing-id' ORDER BY dc:title",
"SELECT * FROM Document WHERE ecm:ancestorId = '" + fid + "' ORDER BY dc:title",
});
}
}