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.CollectionObjectClient;
33 import org.collectionspace.services.client.CollectionSpaceClient;
34 import org.collectionspace.services.client.PayloadOutputPart;
35 import org.collectionspace.services.client.PoxPayloadOut;
36 import org.collectionspace.services.collectionobject.CollectionobjectsCommon;
37 import org.collectionspace.services.common.AbstractCommonListUtils;
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 {
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();
132 * @see org.collectionspace.services.client.test.BaseServiceTest#
133 * getAbstractCommonList(org.jboss.resteasy.client.ClientResponse)
136 protected AbstractCommonList getAbstractCommonList(
137 ClientResponse<AbstractCommonList> response) {
138 return response.getEntity(AbstractCommonList.class);
142 * Creates one or more resources containing a "noise" keyword, which should
143 * NOT be retrieved by keyword searches.
145 * This also helps ensure that searches will not fail, due to a
146 * database-specific constraint or otherwise, if the number of records
147 * containing a particular keyword represent too high a proportion of the
148 * total number of records.
150 @BeforeClass(alwaysRun = true)
151 public void setup() {
152 if (logger.isDebugEnabled()) {
153 logger.debug("Creating " + numNoiseWordResources
154 + " 'noise word' resources ...");
156 createCollectionObjects(numNoiseWordResources, NOISE_WORD);
159 // ---------------------------------------------------------------
161 // ---------------------------------------------------------------
164 @Test(dataProvider = "testName", dataProviderClass = AbstractServiceTestImpl.class, groups = { "advancedSearch" })
165 public void advancedSearch(String testName) throws Exception {
167 if (logger.isDebugEnabled()) {
168 logger.debug(testBanner(testName, CLASS_NAME));
171 // Create one or more keyword retrievable resources, each containing
172 // a specified keyword.
173 String theKeyword = KEYWORD + "COW";
174 long numKeywordRetrievableResources = 1;
175 createCollectionObjects(numKeywordRetrievableResources, theKeyword);
177 // Set the expected status code and group of valid status codes
178 testSetup(STATUS_OK, ServiceRequestType.SEARCH);
180 // Send the search request and receive a response
181 String propertyName = CollectionObjectClient.SERVICE_COMMON_PART_NAME + ":" +
182 CollectionObjectJAXBSchema.DISTINGUISHING_FEATURES;
183 String propertyValue = theKeyword;
184 ClientResponse<AbstractCommonList> res = doAdvancedSearch(propertyName, propertyValue, "=");
185 int statusCode = res.getStatus();
187 // Check the status code of the response: does it match
188 // the expected response(s)?
189 if (logger.isDebugEnabled()) {
190 logger.debug(testName + ": status = " + statusCode);
192 Assert.assertTrue(REQUEST_TYPE.isValidStatusCode(statusCode),
193 invalidStatusCodeMessage(REQUEST_TYPE, statusCode));
194 Assert.assertEquals(statusCode, EXPECTED_STATUS_CODE);
196 // Verify that the number of resources matched by the search
197 // is identical to the expected result
198 long NUM_MATCHES_EXPECTED = numKeywordRetrievableResources;
199 long numMatched = getNumMatched(res, NUM_MATCHES_EXPECTED, testName);
200 Assert.assertEquals(numMatched, NUM_MATCHES_EXPECTED);
203 @Test(dataProvider = "testName", dataProviderClass = AbstractServiceTestImpl.class, groups = { "oneKeyword" })
204 public void searchWithOneKeyword(String testName) throws Exception {
206 if (logger.isDebugEnabled()) {
207 logger.debug(testBanner(testName, CLASS_NAME));
210 // Create one or more keyword retrievable resources, each containing
211 // a specified keyword.
212 long numKeywordRetrievableResources = (long) (numNoiseWordResources * pctNonNoiseWordResources);
213 if (logger.isDebugEnabled()) {
214 logger.debug("Creating " + numKeywordRetrievableResources
215 + " keyword-retrievable resources ...");
217 createCollectionObjects(numKeywordRetrievableResources, KEYWORD);
219 // Set the expected status code and group of valid status codes
220 testSetup(STATUS_OK, ServiceRequestType.SEARCH);
222 // Send the search request and receive a response
223 ClientResponse<AbstractCommonList> res = doSearch(KEYWORD);
224 int statusCode = res.getStatus();
226 // Check the status code of the response: does it match
227 // the expected response(s)?
228 if (logger.isDebugEnabled()) {
229 logger.debug(testName + ": status = " + statusCode);
231 Assert.assertTrue(REQUEST_TYPE.isValidStatusCode(statusCode),
232 invalidStatusCodeMessage(REQUEST_TYPE, statusCode));
233 Assert.assertEquals(statusCode, EXPECTED_STATUS_CODE);
235 // Verify that the number of resources matched by the search
236 // is identical to the expected result
237 long NUM_MATCHES_EXPECTED = numKeywordRetrievableResources;
238 long numMatched = getNumMatched(res, NUM_MATCHES_EXPECTED, testName);
239 Assert.assertEquals(numMatched, NUM_MATCHES_EXPECTED);
242 @Test(dataProvider = "testName", dataProviderClass = AbstractServiceTestImpl.class)
243 public void searchWithTwoKeywordsInSameField(String testName)
246 if (logger.isDebugEnabled()) {
247 logger.debug(testBanner(testName, CLASS_NAME));
250 // Create one or more keyword retrievable resources, each containing
251 // two specified keywords.
252 long numKeywordRetrievableResources = (long) (numNoiseWordResources * pctNonNoiseWordResources);
253 if (logger.isDebugEnabled()) {
254 logger.debug("Creating " + numKeywordRetrievableResources
255 + " keyword-retrievable resources ...");
257 boolean keywordsInSameField = true;
258 createCollectionObjects(numKeywordRetrievableResources, TWO_KEYWORDS,
259 keywordsInSameField);
261 // Set the expected status code and group of valid status codes
262 testSetup(STATUS_OK, ServiceRequestType.SEARCH);
264 // Search using both terms
266 // Send the search request and receive a response
267 ClientResponse<AbstractCommonList> res = doSearch(TWO_KEYWORDS);
268 int statusCode = res.getStatus();
270 // Check the status code of the response: does it match
271 // the expected response(s)?
272 if (logger.isDebugEnabled()) {
273 logger.debug(testName + ": status = " + statusCode);
275 Assert.assertTrue(REQUEST_TYPE.isValidStatusCode(statusCode),
276 invalidStatusCodeMessage(REQUEST_TYPE, statusCode));
277 Assert.assertEquals(statusCode, EXPECTED_STATUS_CODE);
279 // Verify that the number of resources matched by the search
280 // is identical to the expected result
281 long NUM_MATCHES_EXPECTED = numKeywordRetrievableResources;
282 long numMatched = getNumMatched(res, NUM_MATCHES_EXPECTED, testName);
283 Assert.assertEquals(numMatched, NUM_MATCHES_EXPECTED);
285 // Search using a single term
287 // Send the search request and receive a response
288 res = doSearch(TWO_KEYWORDS.get(0));
289 statusCode = res.getStatus();
291 // Check the status code of the response: does it match
292 // the expected response(s)?
293 if (logger.isDebugEnabled()) {
294 logger.debug(testName + ": status = " + statusCode);
296 Assert.assertTrue(REQUEST_TYPE.isValidStatusCode(statusCode),
297 invalidStatusCodeMessage(REQUEST_TYPE, statusCode));
298 Assert.assertEquals(statusCode, EXPECTED_STATUS_CODE);
300 // Verify that the number of resources matched by the search
301 // is identical to the expected result
302 NUM_MATCHES_EXPECTED = numKeywordRetrievableResources;
303 numMatched = getNumMatched(res, NUM_MATCHES_EXPECTED, testName);
304 Assert.assertEquals(numMatched, NUM_MATCHES_EXPECTED);
308 @Test(dataProvider = "testName", dataProviderClass = AbstractServiceTestImpl.class)
309 public void searchWithTwoKeywordsAcrossTwoFields(String testName)
312 if (logger.isDebugEnabled()) {
313 logger.debug(testBanner(testName, CLASS_NAME));
316 // Create one or more keyword retrievable resources, each containing
317 // two specified keywords.
318 long numKeywordRetrievableResources = 5;
319 if (logger.isDebugEnabled()) {
320 logger.debug("Creating " + numKeywordRetrievableResources
321 + " keyword-retrievable resources ...");
323 boolean keywordsInSameField = false;
324 createCollectionObjects(numKeywordRetrievableResources,
325 TWO_MORE_KEYWORDS, keywordsInSameField);
327 // Set the expected status code and group of valid status codes
328 testSetup(STATUS_OK, ServiceRequestType.SEARCH);
330 // Search using both terms
332 // Send the search request and receive a response
333 ClientResponse<AbstractCommonList> res = doSearch(TWO_MORE_KEYWORDS);
334 int statusCode = res.getStatus();
336 // Check the status code of the response: does it match
337 // the expected response(s)?
338 if (logger.isDebugEnabled()) {
339 logger.debug(testName + ": status = " + statusCode);
341 Assert.assertTrue(REQUEST_TYPE.isValidStatusCode(statusCode),
342 invalidStatusCodeMessage(REQUEST_TYPE, statusCode));
343 Assert.assertEquals(statusCode, EXPECTED_STATUS_CODE);
345 // Verify that the number of resources matched by the search
346 // is identical to the expected result
347 long NUM_MATCHES_EXPECTED = numKeywordRetrievableResources;
348 long numMatched = getNumMatched(res, NUM_MATCHES_EXPECTED, testName);
349 Assert.assertEquals(numMatched, NUM_MATCHES_EXPECTED);
351 // Search using a single term
353 // Send the search request and receive a response
354 res = doSearch(TWO_MORE_KEYWORDS.get(0));
355 statusCode = res.getStatus();
357 // Check the status code of the response: does it match
358 // the expected response(s)?
359 if (logger.isDebugEnabled()) {
360 logger.debug(testName + ": status = " + statusCode);
362 Assert.assertTrue(REQUEST_TYPE.isValidStatusCode(statusCode),
363 invalidStatusCodeMessage(REQUEST_TYPE, statusCode));
364 Assert.assertEquals(statusCode, EXPECTED_STATUS_CODE);
366 // Verify that the number of resources matched by the search
367 // is identical to the expected result
368 NUM_MATCHES_EXPECTED = numKeywordRetrievableResources;
369 numMatched = getNumMatched(res, NUM_MATCHES_EXPECTED, testName);
370 Assert.assertEquals(numMatched, NUM_MATCHES_EXPECTED);
374 // @Test(dataProvider="testName",
375 // dataProviderClass=AbstractServiceTestImpl.class)
376 // public void searchWithOneKeywordInRepeatableScalarField(String testName)
377 // throws Exception {
378 // BriefDescriptionList descriptionList = new BriefDescriptionList();
379 // List<String> descriptions = descriptionList.getBriefDescription();
380 // if (TWO_KEYWORDS.size() >= 2) {
381 // descriptions.add(TWO_KEYWORDS.get(0));
382 // descriptions.add(TWO_KEYWORDS.get(1));
385 @Test(dataProvider = "testName", dataProviderClass = AbstractServiceTestImpl.class, groups = { "utf8" })
386 public void searchWithUTF8Keyword(String testName) {
388 if (logger.isDebugEnabled()) {
389 logger.debug(testBanner(testName, CLASS_NAME));
392 // Create one or more keyword retrievable resources, each containing
393 // two specified keywords.
394 long numKeywordRetrievableResources = 2;
395 if (logger.isDebugEnabled()) {
396 logger.debug("Creating " + numKeywordRetrievableResources
397 + " keyword-retrievable resources ...");
399 createCollectionObjects(numKeywordRetrievableResources, UTF8_KEYWORD);
401 // Set the expected status code and group of valid status codes
402 testSetup(STATUS_OK, ServiceRequestType.SEARCH);
404 // Send the search request and receive a response
405 ClientResponse<AbstractCommonList> res = doSearch(UTF8_KEYWORD);
406 int statusCode = res.getStatus();
408 // Check the status code of the response: does it match
409 // the expected response(s)?
410 if (logger.isDebugEnabled()) {
411 logger.debug(testName + ": status = " + statusCode);
413 Assert.assertTrue(REQUEST_TYPE.isValidStatusCode(statusCode),
414 invalidStatusCodeMessage(REQUEST_TYPE, statusCode));
415 Assert.assertEquals(statusCode, EXPECTED_STATUS_CODE);
417 // Verify that the number of resources matched by the search
418 // is identical to the expected result
419 long NUM_MATCHES_EXPECTED = numKeywordRetrievableResources;
420 long numMatched = getNumMatched(res, NUM_MATCHES_EXPECTED, testName);
421 Assert.assertEquals(numMatched, NUM_MATCHES_EXPECTED);
425 // FIXME: Rename to searchWithNonExistentKeyword
426 @Test(dataProvider = "testName", dataProviderClass = AbstractServiceTestImpl.class)
427 public void keywordSearchNonExistentKeyword(String testName)
430 if (logger.isDebugEnabled()) {
431 logger.debug(testBanner(testName, CLASS_NAME));
434 // Set the expected status code and group of valid status codes
435 testSetup(STATUS_OK, ServiceRequestType.SEARCH);
437 // Send the search request and receive a response
438 ClientResponse<AbstractCommonList> res = doSearch(NON_EXISTENT_KEYWORD);
439 int statusCode = res.getStatus();
441 // Check the status code of the response: does it match
442 // the expected response(s)?
443 if (logger.isDebugEnabled()) {
444 logger.debug(testName + ": status = " + statusCode);
446 Assert.assertTrue(REQUEST_TYPE.isValidStatusCode(statusCode),
447 invalidStatusCodeMessage(REQUEST_TYPE, statusCode));
448 Assert.assertEquals(statusCode, EXPECTED_STATUS_CODE);
450 // Verify that the number of resources matched by the search
451 // is identical to the expected result
452 long NUM_MATCHES_EXPECTED = 0;
453 long numMatched = getNumMatched(res, NUM_MATCHES_EXPECTED, testName);
454 Assert.assertEquals(numMatched, NUM_MATCHES_EXPECTED);
458 // ---------------------------------------------------------------
459 // Cleanup of resources created during testing
460 // ---------------------------------------------------------------
462 * Deletes all resources created by setup and tests, after all tests have
465 * This cleanup method will always be run, even if one or more tests fail.
466 * For this reason, it attempts to remove all resources created at any point
467 * during testing, even if some of those resources may be expected to be
468 * deleted by certain tests.
470 @AfterClass(alwaysRun = true)
471 public void cleanUp() {
472 String noTest = System.getProperty("noTestCleanup");
473 if (Boolean.TRUE.toString().equalsIgnoreCase(noTest)) {
474 if (logger.isDebugEnabled()) {
475 logger.debug("Skipping Cleanup phase ...");
479 if (logger.isDebugEnabled()) {
480 logger.debug("Cleaning up temporary resources created for testing ...");
482 CollectionObjectClient collectionObjectClient = new CollectionObjectClient();
483 for (String resourceId : allResourceIdsCreated) {
484 // Note: Any non-success responses are ignored and not reported.
485 collectionObjectClient.delete(resourceId).releaseConnection();
489 // ---------------------------------------------------------------
490 // Utility methods used by tests above
491 // ---------------------------------------------------------------
492 private void createCollectionObjects(long numToCreate, String keyword) {
493 List keywords = new ArrayList<String>();
494 keywords.add(keyword);
495 boolean keywordsInSameField = true;
496 createCollectionObjects(numToCreate, keywords, keywordsInSameField);
499 private void createCollectionObjects(long numToCreate,
500 List<String> keywords, boolean keywordsInSameField) {
501 testSetup(STATUS_CREATED, ServiceRequestType.CREATE);
502 CollectionObjectClient client = new CollectionObjectClient();
503 for (long i = 0; i < numToCreate; i++) {
504 PoxPayloadOut multipart = createCollectionObjectInstance(i,
505 keywords, keywordsInSameField);
506 ClientResponse<Response> res = client.create(multipart);
508 int statusCode = res.getStatus();
509 Assert.assertEquals(statusCode, EXPECTED_STATUS_CODE);
510 String id = extractId(res);
511 allResourceIdsCreated.add(id);
512 if (logger.isDebugEnabled()) {
513 logger.debug("Created new resource [" + i + "] with ID "
517 res.releaseConnection();
522 private PoxPayloadOut createCollectionObjectInstance(long i,
523 List<String> keywords, boolean keywordsInSameField) {
524 CollectionobjectsCommon collectionObject = new CollectionobjectsCommon();
525 collectionObject.setObjectNumber(createIdentifier());
526 if (keywordsInSameField) {
527 collectionObject.setDistinguishingFeatures(listToString(keywords,
530 if (keywords.size() == 1) {
531 collectionObject.setDistinguishingFeatures(keywords.get(0));
532 } else if (keywords.size() == 2) {
533 collectionObject.setDistinguishingFeatures(keywords.get(0));
534 collectionObject.setPhysicalDescription(keywords.get(1));
536 Assert.fail("List of keywords must have exactly one or two members.");
539 PoxPayloadOut multipart = new PoxPayloadOut(
540 CollectionObjectClient.SERVICE_PAYLOAD_NAME);
541 PayloadOutputPart commonPart = multipart.addPart(collectionObject,
542 MediaType.APPLICATION_XML_TYPE);
543 commonPart.setLabel(new CollectionObjectClient().getCommonPartName());
547 private static String listToString(List<String> list, String separator) {
548 StringBuffer sb = new StringBuffer();
549 if (list.size() > 0) {
550 sb.append(list.get(0));
551 for (int i = 1; i < list.size(); i++) {
552 sb.append(separator);
553 sb.append(list.get(i));
556 return sb.toString();
559 private ClientResponse<AbstractCommonList> doSearch(List<String> keywords) {
560 String searchParamValue = listToString(keywords, KEYWORD_SEPARATOR);
561 return doSearch(searchParamValue);
564 private ClientResponse<AbstractCommonList> doAdvancedSearch(
565 String propertyName, String propertyValue, String operator) {
566 if (logger.isDebugEnabled()) {
567 logger.debug("Searching on property: " + propertyName + "="
568 + "'" + propertyValue + "'");
570 String whereClause = propertyName + operator +
571 "'" + propertyValue + "'";
572 CollectionObjectClient client = new CollectionObjectClient();
573 ClientResponse<AbstractCommonList> res = client
574 .advancedSearchIncludeDeleted(whereClause, false); // NOT_INCLUDING_DELETED_RESOURCES
578 private ClientResponse<AbstractCommonList> doSearch(String keyword) {
579 String searchParamValue = keyword;
580 if (logger.isDebugEnabled()) {
581 logger.debug("Searching on keyword(s): " + searchParamValue
584 CollectionObjectClient client = new CollectionObjectClient();
585 final boolean NOT_INCLUDING_DELETED_RESOURCES = false;
586 ClientResponse<AbstractCommonList> res = client
587 .keywordSearchIncludeDeleted(searchParamValue,
588 NOT_INCLUDING_DELETED_RESOURCES);
592 private long getNumMatched(ClientResponse<AbstractCommonList> res,
593 long numExpectedMatches, String testName) {
594 AbstractCommonList list = (AbstractCommonList) res
595 .getEntity(AbstractCommonList.class);
596 long numMatched = list.getTotalItems();
597 if (logger.isDebugEnabled()) {
598 logger.debug("Keyword search matched " + numMatched
599 + " resources, expected to match " + numExpectedMatches);
602 // Optionally output additional data about list members for debugging.
603 if (logger.isTraceEnabled()) {
604 AbstractCommonListUtils.ListItemsInAbstractCommonList(list, logger,
611 private void itemizeListItems(AbstractCommonList list) {
612 List<AbstractCommonList.ListItem> items = list.getListItem();
614 for (AbstractCommonList.ListItem item : items) {
615 logger.debug("list-item["
618 + AbstractCommonListUtils.ListItemGetElementValue(item,
620 logger.debug("list-item["
623 + AbstractCommonListUtils.ListItemGetElementValue(item,
629 public static String getSystemTimeIdentifier() {
630 return Long.toString(System.currentTimeMillis());