2 * This document is a part of the source code and related artifacts
3 * for CollectionSpace, an open source collections management system
4 * for museums and related institutions:
6 * http://www.collectionspace.org
7 * http://wiki.collectionspace.org
9 * Copyright © 2009 Regents of the University of California
11 * Licensed under the Educational Community License (ECL), Version 2.0.
12 * You may not use this file except in compliance with this License.
14 * You may obtain a copy of the ECL 2.0 License at
15 * https://source.collectionspace.org/collection-space/LICENSE.txt
17 * Unless required by applicable law or agreed to in writing, software
18 * distributed under the License is distributed on an "AS IS" BASIS,
19 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20 * See the License for the specific language governing permissions and
21 * limitations under the License.
23 package org.collectionspace.services.client.test;
25 import java.util.Arrays;
26 import java.util.ArrayList;
27 import java.util.List;
28 import javax.ws.rs.core.MediaType;
29 import javax.ws.rs.core.Response;
31 import org.collectionspace.services.CollectionObjectJAXBSchema;
32 import org.collectionspace.services.client.AbstractCommonListUtils;
33 import org.collectionspace.services.client.CollectionObjectClient;
34 import org.collectionspace.services.client.CollectionSpaceClient;
35 import org.collectionspace.services.client.PayloadOutputPart;
36 import org.collectionspace.services.client.PoxPayloadOut;
37 import org.collectionspace.services.collectionobject.CollectionobjectsCommon;
38 import org.collectionspace.services.jaxb.AbstractCommonList;
40 import org.jboss.resteasy.client.ClientResponse;
41 import org.slf4j.Logger;
42 import org.slf4j.LoggerFactory;
43 import org.testng.Assert;
44 import org.testng.annotations.AfterClass;
45 import org.testng.annotations.BeforeClass;
46 import org.testng.annotations.Test;
49 * CollectionObjectSearchTest, carries out tests of keyword search functionality
50 * against a deployed and running CollectionObject Service.
52 * $LastChangedRevision: 1327 $ $LastChangedDate: 2010-02-12 10:35:11 -0800
53 * (Fri, 12 Feb 2010) $
55 public class CollectionObjectSearchTest extends BaseServiceTest<AbstractCommonList> {
58 private final String CLASS_NAME = CollectionObjectSearchTest.class
60 private final Logger logger = LoggerFactory.getLogger(CLASS_NAME);
61 final static String IDENTIFIER = getSystemTimeIdentifier();
62 final static String KEYWORD_SEPARATOR = " ";
63 final long numNoiseWordResources = 10;
64 final double pctNonNoiseWordResources = 0.5;
65 // Use this to keep track of resources to delete
66 private List<String> allResourceIdsCreated = new ArrayList<String>();
68 // Constants for data used in search testing
70 // Test keywords unlikely to be encountered in actual collections data,
71 // consisting of the names of mythical creatures in a 1970s role-playing
72 // game, which result in very few 'hits' in Google searches.
73 final static String KEYWORD = "Tsolyani" + IDENTIFIER;
74 final static List<String> TWO_KEYWORDS = Arrays.asList(new String[] {
75 "Cheggarra" + IDENTIFIER, "Ahoggya" + IDENTIFIER });
76 final static List<String> TWO_MORE_KEYWORDS = Arrays.asList(new String[] {
77 "Karihaya" + IDENTIFIER, "Hlikku" + IDENTIFIER });
78 final static String NOISE_WORD = "Mihalli + IDENTIFIER";
79 // Test Unicode UTF-8 term for keyword searching: a random sequence,
80 // unlikely to be encountered in actual collections data, of two USASCII
81 // characters followed by four non-USASCII range Unicode UTF-8 characters:
83 // Δ : Greek capital letter Delta (U+0394)
84 // Ж : Cyrillic capital letter Zhe with breve (U+04C1)
85 // Ŵ : Latin capital letter W with circumflex (U+0174)
86 // Ω : Greek capital letter Omega (U+03A9)
87 final String UTF8_KEYWORD = "to" + '\u0394' + '\u04C1' + '\u0174'
89 // Non-existent term unlikely to be encountered in actual collections
90 // data, consisting of two back-to-back sets of the first letters of
91 // each of the words in a short pangram for the English alphabet.
92 final static String NON_EXISTENT_KEYWORD = "jlmbsoqjlmbsoq";
95 protected String getServiceName() {
96 throw new UnsupportedOperationException(); // FIXME: REM - See
97 // http://issues.collectionspace.org/browse/CSPACE-3498
101 protected String getServicePathComponent() {
102 // TODO Auto-generated method stub
103 throw new UnsupportedOperationException(); // FIXME: REM - See
104 // http://issues.collectionspace.org/browse/CSPACE-3498
109 // org.collectionspace.services.client.test.BaseServiceTest#getServicePathComponent()
112 // protected String getServicePathComponent() {
113 // return new CollectionObjectClient().getServicePathComponent(); //FIXME:
114 // REM = Remove all refs to this method.
121 * org.collectionspace.services.client.test.BaseServiceTest#getClientInstance
125 protected CollectionSpaceClient getClientInstance() {
126 return new CollectionObjectClient();
130 * Creates one or more resources containing a "noise" keyword, which should
131 * NOT be retrieved by keyword searches.
133 * This also helps ensure that searches will not fail, due to a
134 * database-specific constraint or otherwise, if the number of records
135 * containing a particular keyword represent too high a proportion of the
136 * total number of records.
138 @BeforeClass(alwaysRun = true)
139 public void setup() {
140 if (logger.isDebugEnabled()) {
141 logger.debug("Creating " + numNoiseWordResources
142 + " 'noise word' resources ...");
144 createCollectionObjects(numNoiseWordResources, NOISE_WORD);
147 // ---------------------------------------------------------------
149 // ---------------------------------------------------------------
152 @Test(dataProvider = "testName", dataProviderClass = AbstractServiceTestImpl.class, groups = { "advancedSearch" })
153 public void advancedSearch(String testName) throws Exception {
154 // Create one or more keyword retrievable resources, each containing
155 // a specified keyword.
156 String theKeyword = KEYWORD + "COW";
157 long numKeywordRetrievableResources = 1;
158 createCollectionObjects(numKeywordRetrievableResources, theKeyword);
160 // Set the expected status code and group of valid status codes
161 testSetup(STATUS_OK, ServiceRequestType.SEARCH);
163 // Send the search request and receive a response
164 String propertyName = CollectionObjectClient.SERVICE_COMMON_PART_NAME + ":" +
165 CollectionObjectJAXBSchema.DISTINGUISHING_FEATURES;
166 String propertyValue = theKeyword;
167 ClientResponse<AbstractCommonList> res = doAdvancedSearch(propertyName, propertyValue, "=");
168 int statusCode = res.getStatus();
170 // Check the status code of the response: does it match
171 // the expected response(s)?
172 if (logger.isDebugEnabled()) {
173 logger.debug(testName + ": status = " + statusCode);
175 Assert.assertTrue(testRequestType.isValidStatusCode(statusCode),
176 invalidStatusCodeMessage(testRequestType, statusCode));
177 Assert.assertEquals(statusCode, testExpectedStatusCode);
179 // Verify that the number of resources matched by the search
180 // is identical to the expected result
181 long NUM_MATCHES_EXPECTED = numKeywordRetrievableResources;
182 long numMatched = getNumMatched(res, NUM_MATCHES_EXPECTED, testName);
183 Assert.assertEquals(numMatched, NUM_MATCHES_EXPECTED);
186 @Test(dataProvider = "testName", dataProviderClass = AbstractServiceTestImpl.class, groups = { "oneKeyword" })
187 public void searchWithOneKeyword(String testName) throws Exception {
188 // Create one or more keyword retrievable resources, each containing
189 // a specified keyword.
190 long numKeywordRetrievableResources = (long) (numNoiseWordResources * pctNonNoiseWordResources);
191 if (logger.isDebugEnabled()) {
192 logger.debug("Creating " + numKeywordRetrievableResources
193 + " keyword-retrievable resources ...");
195 createCollectionObjects(numKeywordRetrievableResources, KEYWORD);
197 // Set the expected status code and group of valid status codes
198 testSetup(STATUS_OK, ServiceRequestType.SEARCH);
200 // Send the search request and receive a response
201 ClientResponse<AbstractCommonList> res = doSearch(KEYWORD);
202 int statusCode = res.getStatus();
204 // Check the status code of the response: does it match
205 // the expected response(s)?
206 if (logger.isDebugEnabled()) {
207 logger.debug(testName + ": status = " + statusCode);
209 Assert.assertTrue(testRequestType.isValidStatusCode(statusCode),
210 invalidStatusCodeMessage(testRequestType, statusCode));
211 Assert.assertEquals(statusCode, testExpectedStatusCode);
213 // Verify that the number of resources matched by the search
214 // is identical to the expected result
215 long NUM_MATCHES_EXPECTED = numKeywordRetrievableResources;
216 long numMatched = getNumMatched(res, NUM_MATCHES_EXPECTED, testName);
217 Assert.assertEquals(numMatched, NUM_MATCHES_EXPECTED);
220 @Test(dataProvider = "testName", dataProviderClass = AbstractServiceTestImpl.class)
221 public void searchWithTwoKeywordsInSameField(String testName)
223 // Create one or more keyword retrievable resources, each containing
224 // two specified keywords.
225 long numKeywordRetrievableResources = (long) (numNoiseWordResources * pctNonNoiseWordResources);
226 if (logger.isDebugEnabled()) {
227 logger.debug("Creating " + numKeywordRetrievableResources
228 + " keyword-retrievable resources ...");
230 boolean keywordsInSameField = true;
231 createCollectionObjects(numKeywordRetrievableResources, TWO_KEYWORDS,
232 keywordsInSameField);
234 // Set the expected status code and group of valid status codes
235 testSetup(STATUS_OK, ServiceRequestType.SEARCH);
237 // Search using both terms
239 // Send the search request and receive a response
240 ClientResponse<AbstractCommonList> res = doSearch(TWO_KEYWORDS);
241 int statusCode = res.getStatus();
243 // Check the status code of the response: does it match
244 // the expected response(s)?
245 if (logger.isDebugEnabled()) {
246 logger.debug(testName + ": status = " + statusCode);
248 Assert.assertTrue(testRequestType.isValidStatusCode(statusCode),
249 invalidStatusCodeMessage(testRequestType, statusCode));
250 Assert.assertEquals(statusCode, testExpectedStatusCode);
252 // Verify that the number of resources matched by the search
253 // is identical to the expected result
254 long NUM_MATCHES_EXPECTED = numKeywordRetrievableResources;
255 long numMatched = getNumMatched(res, NUM_MATCHES_EXPECTED, testName);
256 Assert.assertEquals(numMatched, NUM_MATCHES_EXPECTED);
258 // Search using a single term
260 // Send the search request and receive a response
261 res = doSearch(TWO_KEYWORDS.get(0));
262 statusCode = res.getStatus();
264 // Check the status code of the response: does it match
265 // the expected response(s)?
266 if (logger.isDebugEnabled()) {
267 logger.debug(testName + ": status = " + statusCode);
269 Assert.assertTrue(testRequestType.isValidStatusCode(statusCode),
270 invalidStatusCodeMessage(testRequestType, statusCode));
271 Assert.assertEquals(statusCode, testExpectedStatusCode);
273 // Verify that the number of resources matched by the search
274 // is identical to the expected result
275 NUM_MATCHES_EXPECTED = numKeywordRetrievableResources;
276 numMatched = getNumMatched(res, NUM_MATCHES_EXPECTED, testName);
277 Assert.assertEquals(numMatched, NUM_MATCHES_EXPECTED);
281 @Test(dataProvider = "testName", dataProviderClass = AbstractServiceTestImpl.class)
282 public void searchWithTwoKeywordsAcrossTwoFields(String testName)
284 // Create one or more keyword retrievable resources, each containing
285 // two specified keywords.
286 long numKeywordRetrievableResources = 5;
287 if (logger.isDebugEnabled()) {
288 logger.debug("Creating " + numKeywordRetrievableResources
289 + " keyword-retrievable resources ...");
291 boolean keywordsInSameField = false;
292 createCollectionObjects(numKeywordRetrievableResources,
293 TWO_MORE_KEYWORDS, keywordsInSameField);
295 // Set the expected status code and group of valid status codes
296 testSetup(STATUS_OK, ServiceRequestType.SEARCH);
298 // Search using both terms
300 // Send the search request and receive a response
301 ClientResponse<AbstractCommonList> res = doSearch(TWO_MORE_KEYWORDS);
302 int statusCode = res.getStatus();
304 // Check the status code of the response: does it match
305 // the expected response(s)?
306 if (logger.isDebugEnabled()) {
307 logger.debug(testName + ": status = " + statusCode);
309 Assert.assertTrue(testRequestType.isValidStatusCode(statusCode),
310 invalidStatusCodeMessage(testRequestType, statusCode));
311 Assert.assertEquals(statusCode, testExpectedStatusCode);
313 // Verify that the number of resources matched by the search
314 // is identical to the expected result
315 long NUM_MATCHES_EXPECTED = numKeywordRetrievableResources;
316 long numMatched = getNumMatched(res, NUM_MATCHES_EXPECTED, testName);
317 Assert.assertEquals(numMatched, NUM_MATCHES_EXPECTED);
319 // Search using a single term
321 // Send the search request and receive a response
322 res = doSearch(TWO_MORE_KEYWORDS.get(0));
323 statusCode = res.getStatus();
325 // Check the status code of the response: does it match
326 // the expected response(s)?
327 if (logger.isDebugEnabled()) {
328 logger.debug(testName + ": status = " + statusCode);
330 Assert.assertTrue(testRequestType.isValidStatusCode(statusCode),
331 invalidStatusCodeMessage(testRequestType, statusCode));
332 Assert.assertEquals(statusCode, testExpectedStatusCode);
334 // Verify that the number of resources matched by the search
335 // is identical to the expected result
336 NUM_MATCHES_EXPECTED = numKeywordRetrievableResources;
337 numMatched = getNumMatched(res, NUM_MATCHES_EXPECTED, testName);
338 Assert.assertEquals(numMatched, NUM_MATCHES_EXPECTED);
342 // @Test(dataProvider="testName",
343 // dataProviderClass=AbstractServiceTestImpl.class)
344 // public void searchWithOneKeywordInRepeatableScalarField(String testName)
345 // throws Exception {
346 // BriefDescriptionList descriptionList = new BriefDescriptionList();
347 // List<String> descriptions = descriptionList.getBriefDescription();
348 // if (TWO_KEYWORDS.size() >= 2) {
349 // descriptions.add(TWO_KEYWORDS.get(0));
350 // descriptions.add(TWO_KEYWORDS.get(1));
353 @Test(dataProvider = "testName", dataProviderClass = AbstractServiceTestImpl.class, groups = { "utf8" })
354 public void searchWithUTF8Keyword(String testName) {
355 // Create one or more keyword retrievable resources, each containing
356 // two specified keywords.
357 long numKeywordRetrievableResources = 2;
358 if (logger.isDebugEnabled()) {
359 logger.debug("Creating " + numKeywordRetrievableResources
360 + " keyword-retrievable resources ...");
362 createCollectionObjects(numKeywordRetrievableResources, UTF8_KEYWORD);
364 // Set the expected status code and group of valid status codes
365 testSetup(STATUS_OK, ServiceRequestType.SEARCH);
367 // Send the search request and receive a response
368 ClientResponse<AbstractCommonList> res = doSearch(UTF8_KEYWORD);
369 int statusCode = res.getStatus();
371 // Check the status code of the response: does it match
372 // the expected response(s)?
373 if (logger.isDebugEnabled()) {
374 logger.debug(testName + ": status = " + statusCode);
376 Assert.assertTrue(testRequestType.isValidStatusCode(statusCode),
377 invalidStatusCodeMessage(testRequestType, statusCode));
378 Assert.assertEquals(statusCode, testExpectedStatusCode);
380 // Verify that the number of resources matched by the search
381 // is identical to the expected result
382 long NUM_MATCHES_EXPECTED = numKeywordRetrievableResources;
383 long numMatched = getNumMatched(res, NUM_MATCHES_EXPECTED, testName);
384 Assert.assertEquals(numMatched, NUM_MATCHES_EXPECTED);
388 // FIXME: Rename to searchWithNonExistentKeyword
389 @Test(dataProvider = "testName", dataProviderClass = AbstractServiceTestImpl.class)
390 public void keywordSearchNonExistentKeyword(String testName)
392 // Set the expected status code and group of valid status codes
393 testSetup(STATUS_OK, ServiceRequestType.SEARCH);
395 // Send the search request and receive a response
396 ClientResponse<AbstractCommonList> res = doSearch(NON_EXISTENT_KEYWORD);
397 int statusCode = res.getStatus();
399 // Check the status code of the response: does it match
400 // the expected response(s)?
401 if (logger.isDebugEnabled()) {
402 logger.debug(testName + ": status = " + statusCode);
404 Assert.assertTrue(testRequestType.isValidStatusCode(statusCode),
405 invalidStatusCodeMessage(testRequestType, statusCode));
406 Assert.assertEquals(statusCode, testExpectedStatusCode);
408 // Verify that the number of resources matched by the search
409 // is identical to the expected result
410 long NUM_MATCHES_EXPECTED = 0;
411 long numMatched = getNumMatched(res, NUM_MATCHES_EXPECTED, testName);
412 Assert.assertEquals(numMatched, NUM_MATCHES_EXPECTED);
416 // ---------------------------------------------------------------
417 // Cleanup of resources created during testing
418 // ---------------------------------------------------------------
420 * Deletes all resources created by setup and tests, after all tests have
423 * This cleanup method will always be run, even if one or more tests fail.
424 * For this reason, it attempts to remove all resources created at any point
425 * during testing, even if some of those resources may be expected to be
426 * deleted by certain tests.
428 @AfterClass(alwaysRun = true)
429 public void cleanUp() {
430 String noTest = System.getProperty("noTestCleanup");
431 if (Boolean.TRUE.toString().equalsIgnoreCase(noTest)) {
432 if (logger.isDebugEnabled()) {
433 logger.debug("Skipping Cleanup phase ...");
437 if (logger.isDebugEnabled()) {
438 logger.debug("Cleaning up temporary resources created for testing ...");
440 CollectionObjectClient collectionObjectClient = new CollectionObjectClient();
441 for (String resourceId : allResourceIdsCreated) {
442 // Note: Any non-success responses are ignored and not reported.
443 collectionObjectClient.delete(resourceId).releaseConnection();
447 // ---------------------------------------------------------------
448 // Utility methods used by tests above
449 // ---------------------------------------------------------------
450 private void createCollectionObjects(long numToCreate, String keyword) {
451 List keywords = new ArrayList<String>();
452 keywords.add(keyword);
453 boolean keywordsInSameField = true;
454 createCollectionObjects(numToCreate, keywords, keywordsInSameField);
457 private void createCollectionObjects(long numToCreate,
458 List<String> keywords, boolean keywordsInSameField) {
459 testSetup(STATUS_CREATED, ServiceRequestType.CREATE);
460 CollectionObjectClient client = new CollectionObjectClient();
461 for (long i = 0; i < numToCreate; i++) {
462 PoxPayloadOut multipart = createCollectionObjectInstance(i,
463 keywords, keywordsInSameField);
464 ClientResponse<Response> res = client.create(multipart);
466 int statusCode = res.getStatus();
467 Assert.assertEquals(statusCode, testExpectedStatusCode);
468 String id = extractId(res);
469 allResourceIdsCreated.add(id);
470 if (logger.isDebugEnabled()) {
471 logger.debug("Created new resource [" + i + "] with ID "
475 res.releaseConnection();
480 private PoxPayloadOut createCollectionObjectInstance(long i,
481 List<String> keywords, boolean keywordsInSameField) {
482 CollectionobjectsCommon collectionObject = new CollectionobjectsCommon();
483 collectionObject.setObjectNumber(createIdentifier());
484 if (keywordsInSameField) {
485 collectionObject.setDistinguishingFeatures(listToString(keywords,
488 if (keywords.size() == 1) {
489 collectionObject.setDistinguishingFeatures(keywords.get(0));
490 } else if (keywords.size() == 2) {
491 collectionObject.setDistinguishingFeatures(keywords.get(0));
492 collectionObject.setPhysicalDescription(keywords.get(1));
494 Assert.fail("List of keywords must have exactly one or two members.");
497 PoxPayloadOut multipart = new PoxPayloadOut(
498 CollectionObjectClient.SERVICE_PAYLOAD_NAME);
499 PayloadOutputPart commonPart = multipart.addPart(collectionObject,
500 MediaType.APPLICATION_XML_TYPE);
501 commonPart.setLabel(new CollectionObjectClient().getCommonPartName());
505 private static String listToString(List<String> list, String separator) {
506 StringBuffer sb = new StringBuffer();
507 if (list.size() > 0) {
508 sb.append(list.get(0));
509 for (int i = 1; i < list.size(); i++) {
510 sb.append(separator);
511 sb.append(list.get(i));
514 return sb.toString();
517 private ClientResponse<AbstractCommonList> doSearch(List<String> keywords) {
518 String searchParamValue = listToString(keywords, KEYWORD_SEPARATOR);
519 return doSearch(searchParamValue);
522 private ClientResponse<AbstractCommonList> doAdvancedSearch(
523 String propertyName, String propertyValue, String operator) {
524 if (logger.isDebugEnabled()) {
525 logger.debug("Searching on property: " + propertyName + "="
526 + "'" + propertyValue + "'");
528 String whereClause = propertyName + operator +
529 "'" + propertyValue + "'";
530 CollectionObjectClient client = new CollectionObjectClient();
531 ClientResponse<AbstractCommonList> res = client
532 .advancedSearchIncludeDeleted(whereClause, false); // NOT_INCLUDING_DELETED_RESOURCES
536 private ClientResponse<AbstractCommonList> doSearch(String keyword) {
537 String searchParamValue = keyword;
538 if (logger.isDebugEnabled()) {
539 logger.debug("Searching on keyword(s): " + searchParamValue
542 CollectionObjectClient client = new CollectionObjectClient();
543 final boolean NOT_INCLUDING_DELETED_RESOURCES = false;
544 ClientResponse<AbstractCommonList> res = client
545 .keywordSearchIncludeDeleted(searchParamValue,
546 NOT_INCLUDING_DELETED_RESOURCES);
550 private long getNumMatched(ClientResponse<AbstractCommonList> res,
551 long numExpectedMatches, String testName) {
552 AbstractCommonList list = (AbstractCommonList) res
553 .getEntity(AbstractCommonList.class);
554 long numMatched = list.getTotalItems();
555 if (logger.isDebugEnabled()) {
556 logger.debug("Keyword search matched " + numMatched
557 + " resources, expected to match " + numExpectedMatches);
560 // Optionally output additional data about list members for debugging.
561 if (logger.isTraceEnabled()) {
562 AbstractCommonListUtils.ListItemsInAbstractCommonList(list, logger,
569 private void itemizeListItems(AbstractCommonList list) {
570 List<AbstractCommonList.ListItem> items = list.getListItem();
572 for (AbstractCommonList.ListItem item : items) {
573 logger.debug("list-item["
576 + AbstractCommonListUtils.ListItemGetElementValue(item,
578 logger.debug("list-item["
581 + AbstractCommonListUtils.ListItemGetElementValue(item,
587 public static String getSystemTimeIdentifier() {
588 return Long.toString(System.currentTimeMillis());