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;
29 import javax.ws.rs.core.MediaType;
30 import javax.ws.rs.core.Response;
32 import org.collectionspace.services.CollectionObjectJAXBSchema;
33 import org.collectionspace.services.client.AbstractCommonListUtils;
34 import org.collectionspace.services.client.CollectionObjectClient;
35 import org.collectionspace.services.client.CollectionSpaceClient;
36 import org.collectionspace.services.client.PayloadOutputPart;
37 import org.collectionspace.services.client.PoxPayloadOut;
38 import org.collectionspace.services.collectionobject.CollectionobjectsCommon;
39 import org.collectionspace.services.jaxb.AbstractCommonList;
41 //import org.jboss.resteasy.client.ClientResponse;
42 import org.slf4j.Logger;
43 import org.slf4j.LoggerFactory;
44 import org.testng.Assert;
45 import org.testng.annotations.AfterClass;
46 import org.testng.annotations.BeforeClass;
47 import org.testng.annotations.Test;
50 * CollectionObjectSearchTest, carries out tests of keyword search functionality
51 * against a deployed and running CollectionObject Service.
53 * $LastChangedRevision: 1327 $ $LastChangedDate: 2010-02-12 10:35:11 -0800
54 * (Fri, 12 Feb 2010) $
56 public class CollectionObjectSearchTest extends BaseServiceTest<AbstractCommonList> {
59 private final String CLASS_NAME = CollectionObjectSearchTest.class
61 private final Logger logger = LoggerFactory.getLogger(CLASS_NAME);
62 final static String IDENTIFIER = getSystemTimeIdentifier();
63 final static String KEYWORD_SEPARATOR = " ";
64 final long numNoiseWordResources = 10;
65 final double pctNonNoiseWordResources = 0.5;
66 // Use this to keep track of resources to delete
67 private List<String> allResourceIdsCreated = new ArrayList<String>();
69 // Constants for data used in search testing
71 // Test keywords unlikely to be encountered in actual collections data,
72 // consisting of the names of mythical creatures in a 1970s role-playing
73 // game, which result in very few 'hits' in Google searches.
74 final static String KEYWORD = "Tsolyani" + IDENTIFIER;
75 final static List<String> TWO_KEYWORDS = Arrays.asList(new String[] {
76 "Cheggarra" + IDENTIFIER, "Ahoggya" + IDENTIFIER });
77 final static List<String> TWO_MORE_KEYWORDS = Arrays.asList(new String[] {
78 "Karihaya" + IDENTIFIER, "Hlikku" + IDENTIFIER });
79 final static String NOISE_WORD = "Mihalli + IDENTIFIER";
80 // Test Unicode UTF-8 term for keyword searching: a random sequence,
81 // unlikely to be encountered in actual collections data, of two USASCII
82 // characters followed by four non-USASCII range Unicode UTF-8 characters:
84 // Δ : Greek capital letter Delta (U+0394)
85 // Ж : Cyrillic capital letter Zhe with breve (U+04C1)
86 // Ŵ : Latin capital letter W with circumflex (U+0174)
87 // Ω : Greek capital letter Omega (U+03A9)
88 final String UTF8_KEYWORD = "to" + '\u0394' + '\u04C1' + '\u0174'
90 // Non-existent term unlikely to be encountered in actual collections
91 // data, consisting of two back-to-back sets of the first letters of
92 // each of the words in a short pangram for the English alphabet.
93 final static String NON_EXISTENT_KEYWORD = "jlmbsoqjlmbsoq";
96 protected String getServiceName() {
97 throw new UnsupportedOperationException(); // FIXME: REM - See // http://issues.collectionspace.org/browse/CSPACE-3498
101 protected String getServicePathComponent() {
102 throw new UnsupportedOperationException(); // FIXME: REM - See // http://issues.collectionspace.org/browse/CSPACE-3498
109 * org.collectionspace.services.client.test.BaseServiceTest#getClientInstance
113 protected CollectionSpaceClient getClientInstance() throws Exception {
114 return new CollectionObjectClient();
118 protected CollectionSpaceClient getClientInstance(String clientPropertiesFilename) throws Exception {
119 return new CollectionObjectClient(clientPropertiesFilename);
123 * Creates one or more resources containing a "noise" keyword, which should
124 * NOT be retrieved by keyword searches.
126 * This also helps ensure that searches will not fail, due to a
127 * database-specific constraint or otherwise, if the number of records
128 * containing a particular keyword represent too high a proportion of the
129 * total number of records.
132 @BeforeClass(alwaysRun = true)
133 public void setup() throws Exception {
134 if (logger.isDebugEnabled()) {
135 logger.debug("Creating " + numNoiseWordResources
136 + " 'noise word' resources ...");
138 createCollectionObjects(numNoiseWordResources, NOISE_WORD);
141 // ---------------------------------------------------------------
143 // ---------------------------------------------------------------
146 @Test(dataProvider = "testName", dataProviderClass = AbstractServiceTestImpl.class, groups = { "advancedSearch" })
147 public void advancedSearch(String testName) throws Exception {
148 // Create one or more keyword retrievable resources, each containing
149 // a specified keyword.
150 String theKeyword = KEYWORD + "COW";
151 long numKeywordRetrievableResources = 1;
152 createCollectionObjects(numKeywordRetrievableResources, theKeyword);
154 // Set the expected status code and group of valid status codes
155 testSetup(STATUS_OK, ServiceRequestType.SEARCH);
157 // Send the search request and receive a response
158 String propertyName = CollectionObjectClient.SERVICE_COMMON_PART_NAME + ":" +
159 CollectionObjectJAXBSchema.DISTINGUISHING_FEATURES;
160 String propertyValue = theKeyword;
161 Response res = doAdvancedSearch(propertyName, propertyValue, "=");
163 int statusCode = res.getStatus();
165 // Check the status code of the response: does it match
166 // the expected response(s)?
167 if (logger.isDebugEnabled()) {
168 logger.debug(testName + ": status = " + statusCode);
170 Assert.assertTrue(testRequestType.isValidStatusCode(statusCode),
171 invalidStatusCodeMessage(testRequestType, statusCode));
172 Assert.assertEquals(statusCode, testExpectedStatusCode);
174 // Verify that the number of resources matched by the search
175 // is identical to the expected result
176 long NUM_MATCHES_EXPECTED = numKeywordRetrievableResources;
177 long numMatched = getNumMatched(res, NUM_MATCHES_EXPECTED, testName);
178 Assert.assertEquals(numMatched, NUM_MATCHES_EXPECTED);
184 @Test(dataProvider = "testName", dataProviderClass = AbstractServiceTestImpl.class, groups = { "oneKeyword" })
185 public void searchWithOneKeyword(String testName) throws Exception {
186 // Create one or more keyword retrievable resources, each containing
187 // a specified keyword.
188 long numKeywordRetrievableResources = (long) (numNoiseWordResources * pctNonNoiseWordResources);
189 if (logger.isDebugEnabled()) {
190 logger.debug("Creating " + numKeywordRetrievableResources
191 + " keyword-retrievable resources ...");
193 createCollectionObjects(numKeywordRetrievableResources, KEYWORD);
195 // Set the expected status code and group of valid status codes
196 testSetup(STATUS_OK, ServiceRequestType.SEARCH);
198 // Send the search request and receive a response
199 Response res = doSearch(KEYWORD);
201 int statusCode = res.getStatus();
203 // Check the status code of the response: does it match
204 // the expected response(s)?
205 if (logger.isDebugEnabled()) {
206 logger.debug(testName + ": status = " + statusCode);
208 Assert.assertTrue(testRequestType.isValidStatusCode(statusCode),
209 invalidStatusCodeMessage(testRequestType, statusCode));
210 Assert.assertEquals(statusCode, testExpectedStatusCode);
212 // Verify that the number of resources matched by the search
213 // is identical to the expected result
214 long NUM_MATCHES_EXPECTED = numKeywordRetrievableResources;
215 long numMatched = getNumMatched(res, NUM_MATCHES_EXPECTED, testName);
216 Assert.assertEquals(numMatched, NUM_MATCHES_EXPECTED);
222 @Test(dataProvider = "testName", dataProviderClass = AbstractServiceTestImpl.class)
223 public void searchWithTwoKeywordsInSameField(String testName)
225 // Create one or more keyword retrievable resources, each containing
226 // two specified keywords.
227 long numKeywordRetrievableResources = (long) (numNoiseWordResources * pctNonNoiseWordResources);
228 if (logger.isDebugEnabled()) {
229 logger.debug("Creating " + numKeywordRetrievableResources
230 + " keyword-retrievable resources ...");
232 boolean keywordsInSameField = true;
233 createCollectionObjects(numKeywordRetrievableResources, TWO_KEYWORDS,
234 keywordsInSameField);
236 // Set the expected status code and group of valid status codes
237 testSetup(STATUS_OK, ServiceRequestType.SEARCH);
239 // Search using both terms
241 // Send the search request and receive a response
242 Response res = doSearch(TWO_KEYWORDS);
244 int statusCode = res.getStatus();
246 // Check the status code of the response: does it match
247 // the expected response(s)?
248 if (logger.isDebugEnabled()) {
249 logger.debug(testName + ": status = " + statusCode);
251 Assert.assertTrue(testRequestType.isValidStatusCode(statusCode),
252 invalidStatusCodeMessage(testRequestType, statusCode));
253 Assert.assertEquals(statusCode, testExpectedStatusCode);
255 // Verify that the number of resources matched by the search
256 // is identical to the expected result
257 long NUM_MATCHES_EXPECTED = numKeywordRetrievableResources;
258 long numMatched = getNumMatched(res, NUM_MATCHES_EXPECTED, testName);
259 Assert.assertEquals(numMatched, NUM_MATCHES_EXPECTED);
264 // Search using a single term
266 // Send the search request and receive a response
267 res = doSearch(TWO_KEYWORDS.get(0));
269 int statusCode = res.getStatus();
271 // Check the status code of the response: does it match
272 // the expected response(s)?
273 if (logger.isDebugEnabled()) {
274 logger.debug(testName + ": status = " + statusCode);
276 Assert.assertTrue(testRequestType.isValidStatusCode(statusCode),
277 invalidStatusCodeMessage(testRequestType, statusCode));
278 Assert.assertEquals(statusCode, testExpectedStatusCode);
280 // Verify that the number of resources matched by the search
281 // is identical to the expected result
282 long NUM_MATCHES_EXPECTED = numKeywordRetrievableResources;
283 long numMatched = getNumMatched(res, NUM_MATCHES_EXPECTED, testName);
284 Assert.assertEquals(numMatched, NUM_MATCHES_EXPECTED);
290 @Test(dataProvider = "testName", dataProviderClass = AbstractServiceTestImpl.class)
291 public void searchWithTwoKeywordsAcrossTwoFields(String testName)
293 // Create one or more keyword retrievable resources, each containing
294 // two specified keywords.
295 long numKeywordRetrievableResources = 5;
296 if (logger.isDebugEnabled()) {
297 logger.debug("Creating " + numKeywordRetrievableResources
298 + " keyword-retrievable resources ...");
300 boolean keywordsInSameField = false;
301 createCollectionObjects(numKeywordRetrievableResources,
302 TWO_MORE_KEYWORDS, keywordsInSameField);
304 // Set the expected status code and group of valid status codes
305 testSetup(STATUS_OK, ServiceRequestType.SEARCH);
307 // Search using both terms
309 // Send the search request and receive a response
310 Response res = doSearch(TWO_MORE_KEYWORDS);
312 int statusCode = res.getStatus();
314 // Check the status code of the response: does it match
315 // the expected response(s)?
316 if (logger.isDebugEnabled()) {
317 logger.debug(testName + ": status = " + statusCode);
319 Assert.assertTrue(testRequestType.isValidStatusCode(statusCode),
320 invalidStatusCodeMessage(testRequestType, statusCode));
321 Assert.assertEquals(statusCode, testExpectedStatusCode);
323 // Verify that the number of resources matched by the search
324 // is identical to the expected result
325 long NUM_MATCHES_EXPECTED = numKeywordRetrievableResources;
326 long numMatched = getNumMatched(res, NUM_MATCHES_EXPECTED, testName);
327 Assert.assertEquals(numMatched, NUM_MATCHES_EXPECTED);
332 // Search using a single term
334 // Send the search request and receive a response
335 res = doSearch(TWO_MORE_KEYWORDS.get(0));
337 int statusCode = res.getStatus();
339 // Check the status code of the response: does it match
340 // the expected response(s)?
341 if (logger.isDebugEnabled()) {
342 logger.debug(testName + ": status = " + statusCode);
344 Assert.assertTrue(testRequestType.isValidStatusCode(statusCode),
345 invalidStatusCodeMessage(testRequestType, statusCode));
346 Assert.assertEquals(statusCode, testExpectedStatusCode);
348 // Verify that the number of resources matched by the search
349 // is identical to the expected result
350 long NUM_MATCHES_EXPECTED = numKeywordRetrievableResources;
351 long numMatched = getNumMatched(res, NUM_MATCHES_EXPECTED, testName);
352 Assert.assertEquals(numMatched, NUM_MATCHES_EXPECTED);
359 // @Test(dataProvider="testName",
360 // dataProviderClass=AbstractServiceTestImpl.class)
361 // public void searchWithOneKeywordInRepeatableScalarField(String testName)
362 // throws Exception {
363 // BriefDescriptionList descriptionList = new BriefDescriptionList();
364 // List<String> descriptions = descriptionList.getBriefDescription();
365 // if (TWO_KEYWORDS.size() >= 2) {
366 // descriptions.add(TWO_KEYWORDS.get(0));
367 // descriptions.add(TWO_KEYWORDS.get(1));
370 @Test(dataProvider = "testName", dataProviderClass = AbstractServiceTestImpl.class, groups = { "utf8" })
371 public void searchWithUTF8Keyword(String testName) throws Exception {
372 // Create one or more keyword retrievable resources, each containing
373 // two specified keywords.
374 long numKeywordRetrievableResources = 2;
375 if (logger.isDebugEnabled()) {
376 logger.debug("Creating " + numKeywordRetrievableResources
377 + " keyword-retrievable resources ...");
379 createCollectionObjects(numKeywordRetrievableResources, UTF8_KEYWORD);
381 // Set the expected status code and group of valid status codes
382 testSetup(STATUS_OK, ServiceRequestType.SEARCH);
384 // Send the search request and receive a response
385 Response res = doSearch(UTF8_KEYWORD);
387 int statusCode = res.getStatus();
389 // Check the status code of the response: does it match
390 // the expected response(s)?
391 if (logger.isDebugEnabled()) {
392 logger.debug(testName + ": status = " + statusCode);
394 Assert.assertTrue(testRequestType.isValidStatusCode(statusCode),
395 invalidStatusCodeMessage(testRequestType, statusCode));
396 Assert.assertEquals(statusCode, testExpectedStatusCode);
398 // Verify that the number of resources matched by the search
399 // is identical to the expected result
400 long NUM_MATCHES_EXPECTED = numKeywordRetrievableResources;
401 long numMatched = getNumMatched(res, NUM_MATCHES_EXPECTED, testName);
402 Assert.assertEquals(numMatched, NUM_MATCHES_EXPECTED);
409 // FIXME: Rename to searchWithNonExistentKeyword
410 @Test(dataProvider = "testName", dataProviderClass = AbstractServiceTestImpl.class)
411 public void keywordSearchNonExistentKeyword(String testName)
413 // Set the expected status code and group of valid status codes
414 testSetup(STATUS_OK, ServiceRequestType.SEARCH);
416 // Send the search request and receive a response
417 Response res = doSearch(NON_EXISTENT_KEYWORD);
419 int statusCode = res.getStatus();
421 // Check the status code of the response: does it match
422 // the expected response(s)?
423 if (logger.isDebugEnabled()) {
424 logger.debug(testName + ": status = " + statusCode);
426 Assert.assertTrue(testRequestType.isValidStatusCode(statusCode),
427 invalidStatusCodeMessage(testRequestType, statusCode));
428 Assert.assertEquals(statusCode, testExpectedStatusCode);
430 // Verify that the number of resources matched by the search
431 // is identical to the expected result
432 long NUM_MATCHES_EXPECTED = 0;
433 long numMatched = getNumMatched(res, NUM_MATCHES_EXPECTED, testName);
434 Assert.assertEquals(numMatched, NUM_MATCHES_EXPECTED);
440 // ---------------------------------------------------------------
441 // Cleanup of resources created during testing
442 // ---------------------------------------------------------------
444 * Deletes all resources created by setup and tests, after all tests have
447 * This cleanup method will always be run, even if one or more tests fail.
448 * For this reason, it attempts to remove all resources created at any point
449 * during testing, even if some of those resources may be expected to be
450 * deleted by certain tests.
453 @AfterClass(alwaysRun = true)
454 public void cleanUp() throws Exception {
455 String noTest = System.getProperty("noTestCleanup");
456 if (Boolean.TRUE.toString().equalsIgnoreCase(noTest)) {
457 if (logger.isDebugEnabled()) {
458 logger.debug("Skipping Cleanup phase ...");
462 if (logger.isDebugEnabled()) {
463 logger.debug("Cleaning up temporary resources created for testing ...");
465 CollectionObjectClient collectionObjectClient = new CollectionObjectClient();
466 for (String resourceId : allResourceIdsCreated) {
467 // Note: Any non-success responses are ignored and not reported.
468 collectionObjectClient.delete(resourceId).close();
472 // ---------------------------------------------------------------
473 // Utility methods used by tests above
474 // ---------------------------------------------------------------
475 private void createCollectionObjects(long numToCreate, String keyword) throws Exception {
476 List keywords = new ArrayList<String>();
477 keywords.add(keyword);
478 boolean keywordsInSameField = true;
479 createCollectionObjects(numToCreate, keywords, keywordsInSameField);
482 private void createCollectionObjects(long numToCreate,
483 List<String> keywords, boolean keywordsInSameField) throws Exception {
484 testSetup(STATUS_CREATED, ServiceRequestType.CREATE);
485 CollectionObjectClient client = new CollectionObjectClient();
486 for (long i = 0; i < numToCreate; i++) {
487 PoxPayloadOut multipart = createCollectionObjectInstance(i,
488 keywords, keywordsInSameField);
489 Response res = client.create(multipart);
491 int statusCode = res.getStatus();
492 Assert.assertEquals(statusCode, testExpectedStatusCode);
493 String id = extractId(res);
494 allResourceIdsCreated.add(id);
495 if (logger.isDebugEnabled()) {
496 logger.debug("Created new resource [" + i + "] with ID "
505 private PoxPayloadOut createCollectionObjectInstance(long i,
506 List<String> keywords, boolean keywordsInSameField) throws Exception {
507 CollectionobjectsCommon collectionObject = new CollectionobjectsCommon();
508 collectionObject.setObjectNumber(createIdentifier());
509 if (keywordsInSameField) {
510 collectionObject.setDistinguishingFeatures(listToString(keywords,
513 if (keywords.size() == 1) {
514 collectionObject.setDistinguishingFeatures(keywords.get(0));
515 } else if (keywords.size() == 2) {
516 collectionObject.setDistinguishingFeatures(keywords.get(0));
517 collectionObject.setPhysicalDescription(keywords.get(1));
519 Assert.fail("List of keywords must have exactly one or two members.");
522 PoxPayloadOut multipart = new PoxPayloadOut(
523 CollectionObjectClient.SERVICE_PAYLOAD_NAME);
524 PayloadOutputPart commonPart = multipart.addPart(collectionObject, MediaType.APPLICATION_XML_TYPE);
525 commonPart.setLabel(new CollectionObjectClient().getCommonPartName());
530 private static String listToString(List<String> list, String separator) {
531 StringBuffer sb = new StringBuffer();
532 if (list.size() > 0) {
533 sb.append(list.get(0));
534 for (int i = 1; i < list.size(); i++) {
535 sb.append(separator);
536 sb.append(list.get(i));
539 return sb.toString();
542 private Response doSearch(List<String> keywords) throws Exception {
543 String searchParamValue = listToString(keywords, KEYWORD_SEPARATOR);
544 return doSearch(searchParamValue);
547 private Response doAdvancedSearch(
548 String propertyName, String propertyValue, String operator) throws Exception {
549 if (logger.isDebugEnabled()) {
550 logger.debug("Searching on property: " + propertyName + "="
551 + "'" + propertyValue + "'");
553 String whereClause = propertyName + operator +
554 "'" + propertyValue + "'";
555 CollectionObjectClient client = new CollectionObjectClient();
556 Response res = client.advancedSearchIncludeDeleted(whereClause, false); // NOT_INCLUDING_DELETED_RESOURCES
561 private Response doSearch(String keyword) throws Exception {
562 String searchParamValue = keyword;
563 if (logger.isDebugEnabled()) {
564 logger.debug("Searching on keyword(s): " + searchParamValue
567 CollectionObjectClient client = new CollectionObjectClient();
568 final boolean NOT_INCLUDING_DELETED_RESOURCES = false;
569 Response res = client.keywordSearchIncludeDeleted(searchParamValue,
570 NOT_INCLUDING_DELETED_RESOURCES);
574 private long getNumMatched(Response res, long numExpectedMatches, String testName) {
575 AbstractCommonList list = (AbstractCommonList) res.readEntity(AbstractCommonList.class);
576 long numMatched = list.getTotalItems();
577 if (logger.isDebugEnabled()) {
578 logger.debug("Keyword search matched " + numMatched
579 + " resources, expected to match " + numExpectedMatches);
582 // Optionally output additional data about list members for debugging.
583 if (logger.isTraceEnabled()) {
584 AbstractCommonListUtils.ListItemsInAbstractCommonList(list, logger,
591 private void itemizeListItems(AbstractCommonList list) {
592 List<AbstractCommonList.ListItem> items = list.getListItem();
594 for (AbstractCommonList.ListItem item : items) {
595 logger.debug("list-item["
598 + AbstractCommonListUtils.ListItemGetElementValue(item,
600 logger.debug("list-item["
603 + AbstractCommonListUtils.ListItemGetElementValue(item,
609 public static String getSystemTimeIdentifier() {
610 return Long.toString(System.currentTimeMillis());