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, "=");
169 int statusCode = res.getStatus();
171 // Check the status code of the response: does it match
172 // the expected response(s)?
173 if (logger.isDebugEnabled()) {
174 logger.debug(testName + ": status = " + statusCode);
176 Assert.assertTrue(testRequestType.isValidStatusCode(statusCode),
177 invalidStatusCodeMessage(testRequestType, statusCode));
178 Assert.assertEquals(statusCode, testExpectedStatusCode);
180 // Verify that the number of resources matched by the search
181 // is identical to the expected result
182 long NUM_MATCHES_EXPECTED = numKeywordRetrievableResources;
183 long numMatched = getNumMatched(res, NUM_MATCHES_EXPECTED, testName);
184 Assert.assertEquals(numMatched, NUM_MATCHES_EXPECTED);
190 @Test(dataProvider = "testName", dataProviderClass = AbstractServiceTestImpl.class, groups = { "oneKeyword" })
191 public void searchWithOneKeyword(String testName) throws Exception {
192 // Create one or more keyword retrievable resources, each containing
193 // a specified keyword.
194 long numKeywordRetrievableResources = (long) (numNoiseWordResources * pctNonNoiseWordResources);
195 if (logger.isDebugEnabled()) {
196 logger.debug("Creating " + numKeywordRetrievableResources
197 + " keyword-retrievable resources ...");
199 createCollectionObjects(numKeywordRetrievableResources, KEYWORD);
201 // Set the expected status code and group of valid status codes
202 testSetup(STATUS_OK, ServiceRequestType.SEARCH);
204 // Send the search request and receive a response
205 ClientResponse<AbstractCommonList> res = doSearch(KEYWORD);
207 int statusCode = res.getStatus();
209 // Check the status code of the response: does it match
210 // the expected response(s)?
211 if (logger.isDebugEnabled()) {
212 logger.debug(testName + ": status = " + statusCode);
214 Assert.assertTrue(testRequestType.isValidStatusCode(statusCode),
215 invalidStatusCodeMessage(testRequestType, statusCode));
216 Assert.assertEquals(statusCode, testExpectedStatusCode);
218 // Verify that the number of resources matched by the search
219 // is identical to the expected result
220 long NUM_MATCHES_EXPECTED = numKeywordRetrievableResources;
221 long numMatched = getNumMatched(res, NUM_MATCHES_EXPECTED, testName);
222 Assert.assertEquals(numMatched, NUM_MATCHES_EXPECTED);
228 @Test(dataProvider = "testName", dataProviderClass = AbstractServiceTestImpl.class)
229 public void searchWithTwoKeywordsInSameField(String testName)
231 // Create one or more keyword retrievable resources, each containing
232 // two specified keywords.
233 long numKeywordRetrievableResources = (long) (numNoiseWordResources * pctNonNoiseWordResources);
234 if (logger.isDebugEnabled()) {
235 logger.debug("Creating " + numKeywordRetrievableResources
236 + " keyword-retrievable resources ...");
238 boolean keywordsInSameField = true;
239 createCollectionObjects(numKeywordRetrievableResources, TWO_KEYWORDS,
240 keywordsInSameField);
242 // Set the expected status code and group of valid status codes
243 testSetup(STATUS_OK, ServiceRequestType.SEARCH);
245 // Search using both terms
247 // Send the search request and receive a response
248 ClientResponse<AbstractCommonList> res = doSearch(TWO_KEYWORDS);
250 int statusCode = res.getStatus();
252 // Check the status code of the response: does it match
253 // the expected response(s)?
254 if (logger.isDebugEnabled()) {
255 logger.debug(testName + ": status = " + statusCode);
257 Assert.assertTrue(testRequestType.isValidStatusCode(statusCode),
258 invalidStatusCodeMessage(testRequestType, statusCode));
259 Assert.assertEquals(statusCode, testExpectedStatusCode);
261 // Verify that the number of resources matched by the search
262 // is identical to the expected result
263 long NUM_MATCHES_EXPECTED = numKeywordRetrievableResources;
264 long numMatched = getNumMatched(res, NUM_MATCHES_EXPECTED, testName);
265 Assert.assertEquals(numMatched, NUM_MATCHES_EXPECTED);
270 // Search using a single term
272 // Send the search request and receive a response
273 res = doSearch(TWO_KEYWORDS.get(0));
275 int statusCode = res.getStatus();
277 // Check the status code of the response: does it match
278 // the expected response(s)?
279 if (logger.isDebugEnabled()) {
280 logger.debug(testName + ": status = " + statusCode);
282 Assert.assertTrue(testRequestType.isValidStatusCode(statusCode),
283 invalidStatusCodeMessage(testRequestType, statusCode));
284 Assert.assertEquals(statusCode, testExpectedStatusCode);
286 // Verify that the number of resources matched by the search
287 // is identical to the expected result
288 long NUM_MATCHES_EXPECTED = numKeywordRetrievableResources;
289 long numMatched = getNumMatched(res, NUM_MATCHES_EXPECTED, testName);
290 Assert.assertEquals(numMatched, NUM_MATCHES_EXPECTED);
296 @Test(dataProvider = "testName", dataProviderClass = AbstractServiceTestImpl.class)
297 public void searchWithTwoKeywordsAcrossTwoFields(String testName)
299 // Create one or more keyword retrievable resources, each containing
300 // two specified keywords.
301 long numKeywordRetrievableResources = 5;
302 if (logger.isDebugEnabled()) {
303 logger.debug("Creating " + numKeywordRetrievableResources
304 + " keyword-retrievable resources ...");
306 boolean keywordsInSameField = false;
307 createCollectionObjects(numKeywordRetrievableResources,
308 TWO_MORE_KEYWORDS, keywordsInSameField);
310 // Set the expected status code and group of valid status codes
311 testSetup(STATUS_OK, ServiceRequestType.SEARCH);
313 // Search using both terms
315 // Send the search request and receive a response
316 ClientResponse<AbstractCommonList> res = doSearch(TWO_MORE_KEYWORDS);
317 int statusCode = res.getStatus();
319 // Check the status code of the response: does it match
320 // the expected response(s)?
321 if (logger.isDebugEnabled()) {
322 logger.debug(testName + ": status = " + statusCode);
324 Assert.assertTrue(testRequestType.isValidStatusCode(statusCode),
325 invalidStatusCodeMessage(testRequestType, statusCode));
326 Assert.assertEquals(statusCode, testExpectedStatusCode);
328 // Verify that the number of resources matched by the search
329 // is identical to the expected result
330 long NUM_MATCHES_EXPECTED = numKeywordRetrievableResources;
331 long numMatched = getNumMatched(res, NUM_MATCHES_EXPECTED, testName);
332 Assert.assertEquals(numMatched, NUM_MATCHES_EXPECTED);
334 // Search using a single term
336 // Send the search request and receive a response
337 res = doSearch(TWO_MORE_KEYWORDS.get(0));
338 statusCode = res.getStatus();
340 // Check the status code of the response: does it match
341 // the expected response(s)?
342 if (logger.isDebugEnabled()) {
343 logger.debug(testName + ": status = " + statusCode);
345 Assert.assertTrue(testRequestType.isValidStatusCode(statusCode),
346 invalidStatusCodeMessage(testRequestType, statusCode));
347 Assert.assertEquals(statusCode, testExpectedStatusCode);
349 // Verify that the number of resources matched by the search
350 // is identical to the expected result
351 NUM_MATCHES_EXPECTED = numKeywordRetrievableResources;
352 numMatched = getNumMatched(res, NUM_MATCHES_EXPECTED, testName);
353 Assert.assertEquals(numMatched, NUM_MATCHES_EXPECTED);
357 // @Test(dataProvider="testName",
358 // dataProviderClass=AbstractServiceTestImpl.class)
359 // public void searchWithOneKeywordInRepeatableScalarField(String testName)
360 // throws Exception {
361 // BriefDescriptionList descriptionList = new BriefDescriptionList();
362 // List<String> descriptions = descriptionList.getBriefDescription();
363 // if (TWO_KEYWORDS.size() >= 2) {
364 // descriptions.add(TWO_KEYWORDS.get(0));
365 // descriptions.add(TWO_KEYWORDS.get(1));
368 @Test(dataProvider = "testName", dataProviderClass = AbstractServiceTestImpl.class, groups = { "utf8" })
369 public void searchWithUTF8Keyword(String testName) {
370 // Create one or more keyword retrievable resources, each containing
371 // two specified keywords.
372 long numKeywordRetrievableResources = 2;
373 if (logger.isDebugEnabled()) {
374 logger.debug("Creating " + numKeywordRetrievableResources
375 + " keyword-retrievable resources ...");
377 createCollectionObjects(numKeywordRetrievableResources, UTF8_KEYWORD);
379 // Set the expected status code and group of valid status codes
380 testSetup(STATUS_OK, ServiceRequestType.SEARCH);
382 // Send the search request and receive a response
383 ClientResponse<AbstractCommonList> res = doSearch(UTF8_KEYWORD);
384 int statusCode = res.getStatus();
386 // Check the status code of the response: does it match
387 // the expected response(s)?
388 if (logger.isDebugEnabled()) {
389 logger.debug(testName + ": status = " + statusCode);
391 Assert.assertTrue(testRequestType.isValidStatusCode(statusCode),
392 invalidStatusCodeMessage(testRequestType, statusCode));
393 Assert.assertEquals(statusCode, testExpectedStatusCode);
395 // Verify that the number of resources matched by the search
396 // is identical to the expected result
397 long NUM_MATCHES_EXPECTED = numKeywordRetrievableResources;
398 long numMatched = getNumMatched(res, NUM_MATCHES_EXPECTED, testName);
399 Assert.assertEquals(numMatched, NUM_MATCHES_EXPECTED);
403 // FIXME: Rename to searchWithNonExistentKeyword
404 @Test(dataProvider = "testName", dataProviderClass = AbstractServiceTestImpl.class)
405 public void keywordSearchNonExistentKeyword(String testName)
407 // Set the expected status code and group of valid status codes
408 testSetup(STATUS_OK, ServiceRequestType.SEARCH);
410 // Send the search request and receive a response
411 ClientResponse<AbstractCommonList> res = doSearch(NON_EXISTENT_KEYWORD);
414 statusCode = res.getStatus();
416 // Check the status code of the response: does it match
417 // the expected response(s)?
418 if (logger.isDebugEnabled()) {
419 logger.debug(testName + ": status = " + statusCode);
421 Assert.assertTrue(testRequestType.isValidStatusCode(statusCode),
422 invalidStatusCodeMessage(testRequestType, statusCode));
423 Assert.assertEquals(statusCode, testExpectedStatusCode);
425 // Verify that the number of resources matched by the search
426 // is identical to the expected result
427 long NUM_MATCHES_EXPECTED = 0;
428 long numMatched = getNumMatched(res, NUM_MATCHES_EXPECTED, testName);
429 Assert.assertEquals(numMatched, NUM_MATCHES_EXPECTED);
435 // ---------------------------------------------------------------
436 // Cleanup of resources created during testing
437 // ---------------------------------------------------------------
439 * Deletes all resources created by setup and tests, after all tests have
442 * This cleanup method will always be run, even if one or more tests fail.
443 * For this reason, it attempts to remove all resources created at any point
444 * during testing, even if some of those resources may be expected to be
445 * deleted by certain tests.
447 @AfterClass(alwaysRun = true)
448 public void cleanUp() {
449 String noTest = System.getProperty("noTestCleanup");
450 if (Boolean.TRUE.toString().equalsIgnoreCase(noTest)) {
451 if (logger.isDebugEnabled()) {
452 logger.debug("Skipping Cleanup phase ...");
456 if (logger.isDebugEnabled()) {
457 logger.debug("Cleaning up temporary resources created for testing ...");
459 CollectionObjectClient collectionObjectClient = new CollectionObjectClient();
460 for (String resourceId : allResourceIdsCreated) {
461 // Note: Any non-success responses are ignored and not reported.
462 collectionObjectClient.delete(resourceId).close();
466 // ---------------------------------------------------------------
467 // Utility methods used by tests above
468 // ---------------------------------------------------------------
469 private void createCollectionObjects(long numToCreate, String keyword) {
470 List keywords = new ArrayList<String>();
471 keywords.add(keyword);
472 boolean keywordsInSameField = true;
473 createCollectionObjects(numToCreate, keywords, keywordsInSameField);
476 private void createCollectionObjects(long numToCreate,
477 List<String> keywords, boolean keywordsInSameField) {
478 testSetup(STATUS_CREATED, ServiceRequestType.CREATE);
479 CollectionObjectClient client = new CollectionObjectClient();
480 for (long i = 0; i < numToCreate; i++) {
481 PoxPayloadOut multipart = createCollectionObjectInstance(i,
482 keywords, keywordsInSameField);
483 Response res = client.create(multipart);
485 int statusCode = res.getStatus();
486 Assert.assertEquals(statusCode, testExpectedStatusCode);
487 String id = extractId(res);
488 allResourceIdsCreated.add(id);
489 if (logger.isDebugEnabled()) {
490 logger.debug("Created new resource [" + i + "] with ID "
499 private PoxPayloadOut createCollectionObjectInstance(long i,
500 List<String> keywords, boolean keywordsInSameField) {
501 CollectionobjectsCommon collectionObject = new CollectionobjectsCommon();
502 collectionObject.setObjectNumber(createIdentifier());
503 if (keywordsInSameField) {
504 collectionObject.setDistinguishingFeatures(listToString(keywords,
507 if (keywords.size() == 1) {
508 collectionObject.setDistinguishingFeatures(keywords.get(0));
509 } else if (keywords.size() == 2) {
510 collectionObject.setDistinguishingFeatures(keywords.get(0));
511 collectionObject.setPhysicalDescription(keywords.get(1));
513 Assert.fail("List of keywords must have exactly one or two members.");
516 PoxPayloadOut multipart = new PoxPayloadOut(
517 CollectionObjectClient.SERVICE_PAYLOAD_NAME);
518 PayloadOutputPart commonPart = multipart.addPart(collectionObject, MediaType.APPLICATION_XML_TYPE);
519 commonPart.setLabel(new CollectionObjectClient().getCommonPartName());
524 private static String listToString(List<String> list, String separator) {
525 StringBuffer sb = new StringBuffer();
526 if (list.size() > 0) {
527 sb.append(list.get(0));
528 for (int i = 1; i < list.size(); i++) {
529 sb.append(separator);
530 sb.append(list.get(i));
533 return sb.toString();
536 private ClientResponse<AbstractCommonList> doSearch(List<String> keywords) {
537 String searchParamValue = listToString(keywords, KEYWORD_SEPARATOR);
538 return doSearch(searchParamValue);
541 private ClientResponse<AbstractCommonList> doAdvancedSearch(
542 String propertyName, String propertyValue, String operator) {
543 if (logger.isDebugEnabled()) {
544 logger.debug("Searching on property: " + propertyName + "="
545 + "'" + propertyValue + "'");
547 String whereClause = propertyName + operator +
548 "'" + propertyValue + "'";
549 CollectionObjectClient client = new CollectionObjectClient();
550 ClientResponse<AbstractCommonList> res = client.advancedSearchIncludeDeleted(whereClause, false); // NOT_INCLUDING_DELETED_RESOURCES
555 private ClientResponse<AbstractCommonList> doSearch(String keyword) {
556 String searchParamValue = keyword;
557 if (logger.isDebugEnabled()) {
558 logger.debug("Searching on keyword(s): " + searchParamValue
561 CollectionObjectClient client = new CollectionObjectClient();
562 final boolean NOT_INCLUDING_DELETED_RESOURCES = false;
563 ClientResponse<AbstractCommonList> res = client
564 .keywordSearchIncludeDeleted(searchParamValue,
565 NOT_INCLUDING_DELETED_RESOURCES);
569 private long getNumMatched(ClientResponse<AbstractCommonList> res,
570 long numExpectedMatches, String testName) {
571 AbstractCommonList list = (AbstractCommonList) res
572 .getEntity(AbstractCommonList.class);
573 long numMatched = list.getTotalItems();
574 if (logger.isDebugEnabled()) {
575 logger.debug("Keyword search matched " + numMatched
576 + " resources, expected to match " + numExpectedMatches);
579 // Optionally output additional data about list members for debugging.
580 if (logger.isTraceEnabled()) {
581 AbstractCommonListUtils.ListItemsInAbstractCommonList(list, logger,
588 private void itemizeListItems(AbstractCommonList list) {
589 List<AbstractCommonList.ListItem> items = list.getListItem();
591 for (AbstractCommonList.ListItem item : items) {
592 logger.debug("list-item["
595 + AbstractCommonListUtils.ListItemGetElementValue(item,
597 logger.debug("list-item["
600 + AbstractCommonListUtils.ListItemGetElementValue(item,
606 public static String getSystemTimeIdentifier() {
607 return Long.toString(System.currentTimeMillis());