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
98 // http://issues.collectionspace.org/browse/CSPACE-3498
102 protected String getServicePathComponent() {
103 // TODO Auto-generated method stub
104 throw new UnsupportedOperationException(); // FIXME: REM - See
105 // http://issues.collectionspace.org/browse/CSPACE-3498
110 // org.collectionspace.services.client.test.BaseServiceTest#getServicePathComponent()
113 // protected String getServicePathComponent() {
114 // return new CollectionObjectClient().getServicePathComponent(); //FIXME:
115 // REM = Remove all refs to this method.
122 * org.collectionspace.services.client.test.BaseServiceTest#getClientInstance
126 protected CollectionSpaceClient getClientInstance() {
127 return new CollectionObjectClient();
131 protected CollectionSpaceClient getClientInstance(String clientPropertiesFilename) {
132 return new CollectionObjectClient(clientPropertiesFilename);
136 * Creates one or more resources containing a "noise" keyword, which should
137 * NOT be retrieved by keyword searches.
139 * This also helps ensure that searches will not fail, due to a
140 * database-specific constraint or otherwise, if the number of records
141 * containing a particular keyword represent too high a proportion of the
142 * total number of records.
144 @BeforeClass(alwaysRun = true)
145 public void setup() {
146 if (logger.isDebugEnabled()) {
147 logger.debug("Creating " + numNoiseWordResources
148 + " 'noise word' resources ...");
150 createCollectionObjects(numNoiseWordResources, NOISE_WORD);
153 // ---------------------------------------------------------------
155 // ---------------------------------------------------------------
158 @Test(dataProvider = "testName", dataProviderClass = AbstractServiceTestImpl.class, groups = { "advancedSearch" })
159 public void advancedSearch(String testName) throws Exception {
160 // Create one or more keyword retrievable resources, each containing
161 // a specified keyword.
162 String theKeyword = KEYWORD + "COW";
163 long numKeywordRetrievableResources = 1;
164 createCollectionObjects(numKeywordRetrievableResources, theKeyword);
166 // Set the expected status code and group of valid status codes
167 testSetup(STATUS_OK, ServiceRequestType.SEARCH);
169 // Send the search request and receive a response
170 String propertyName = CollectionObjectClient.SERVICE_COMMON_PART_NAME + ":" +
171 CollectionObjectJAXBSchema.DISTINGUISHING_FEATURES;
172 String propertyValue = theKeyword;
173 Response res = doAdvancedSearch(propertyName, propertyValue, "=");
175 int statusCode = res.getStatus();
177 // Check the status code of the response: does it match
178 // the expected response(s)?
179 if (logger.isDebugEnabled()) {
180 logger.debug(testName + ": status = " + statusCode);
182 Assert.assertTrue(testRequestType.isValidStatusCode(statusCode),
183 invalidStatusCodeMessage(testRequestType, statusCode));
184 Assert.assertEquals(statusCode, testExpectedStatusCode);
186 // Verify that the number of resources matched by the search
187 // is identical to the expected result
188 long NUM_MATCHES_EXPECTED = numKeywordRetrievableResources;
189 long numMatched = getNumMatched(res, NUM_MATCHES_EXPECTED, testName);
190 Assert.assertEquals(numMatched, NUM_MATCHES_EXPECTED);
196 @Test(dataProvider = "testName", dataProviderClass = AbstractServiceTestImpl.class, groups = { "oneKeyword" })
197 public void searchWithOneKeyword(String testName) throws Exception {
198 // Create one or more keyword retrievable resources, each containing
199 // a specified keyword.
200 long numKeywordRetrievableResources = (long) (numNoiseWordResources * pctNonNoiseWordResources);
201 if (logger.isDebugEnabled()) {
202 logger.debug("Creating " + numKeywordRetrievableResources
203 + " keyword-retrievable resources ...");
205 createCollectionObjects(numKeywordRetrievableResources, KEYWORD);
207 // Set the expected status code and group of valid status codes
208 testSetup(STATUS_OK, ServiceRequestType.SEARCH);
210 // Send the search request and receive a response
211 Response res = doSearch(KEYWORD);
213 int statusCode = res.getStatus();
215 // Check the status code of the response: does it match
216 // the expected response(s)?
217 if (logger.isDebugEnabled()) {
218 logger.debug(testName + ": status = " + statusCode);
220 Assert.assertTrue(testRequestType.isValidStatusCode(statusCode),
221 invalidStatusCodeMessage(testRequestType, statusCode));
222 Assert.assertEquals(statusCode, testExpectedStatusCode);
224 // Verify that the number of resources matched by the search
225 // is identical to the expected result
226 long NUM_MATCHES_EXPECTED = numKeywordRetrievableResources;
227 long numMatched = getNumMatched(res, NUM_MATCHES_EXPECTED, testName);
228 Assert.assertEquals(numMatched, NUM_MATCHES_EXPECTED);
234 @Test(dataProvider = "testName", dataProviderClass = AbstractServiceTestImpl.class)
235 public void searchWithTwoKeywordsInSameField(String testName)
237 // Create one or more keyword retrievable resources, each containing
238 // two specified keywords.
239 long numKeywordRetrievableResources = (long) (numNoiseWordResources * pctNonNoiseWordResources);
240 if (logger.isDebugEnabled()) {
241 logger.debug("Creating " + numKeywordRetrievableResources
242 + " keyword-retrievable resources ...");
244 boolean keywordsInSameField = true;
245 createCollectionObjects(numKeywordRetrievableResources, TWO_KEYWORDS,
246 keywordsInSameField);
248 // Set the expected status code and group of valid status codes
249 testSetup(STATUS_OK, ServiceRequestType.SEARCH);
251 // Search using both terms
253 // Send the search request and receive a response
254 Response res = doSearch(TWO_KEYWORDS);
256 int statusCode = res.getStatus();
258 // Check the status code of the response: does it match
259 // the expected response(s)?
260 if (logger.isDebugEnabled()) {
261 logger.debug(testName + ": status = " + statusCode);
263 Assert.assertTrue(testRequestType.isValidStatusCode(statusCode),
264 invalidStatusCodeMessage(testRequestType, statusCode));
265 Assert.assertEquals(statusCode, testExpectedStatusCode);
267 // Verify that the number of resources matched by the search
268 // is identical to the expected result
269 long NUM_MATCHES_EXPECTED = numKeywordRetrievableResources;
270 long numMatched = getNumMatched(res, NUM_MATCHES_EXPECTED, testName);
271 Assert.assertEquals(numMatched, NUM_MATCHES_EXPECTED);
276 // Search using a single term
278 // Send the search request and receive a response
279 res = doSearch(TWO_KEYWORDS.get(0));
281 int statusCode = res.getStatus();
283 // Check the status code of the response: does it match
284 // the expected response(s)?
285 if (logger.isDebugEnabled()) {
286 logger.debug(testName + ": status = " + statusCode);
288 Assert.assertTrue(testRequestType.isValidStatusCode(statusCode),
289 invalidStatusCodeMessage(testRequestType, statusCode));
290 Assert.assertEquals(statusCode, testExpectedStatusCode);
292 // Verify that the number of resources matched by the search
293 // is identical to the expected result
294 long NUM_MATCHES_EXPECTED = numKeywordRetrievableResources;
295 long numMatched = getNumMatched(res, NUM_MATCHES_EXPECTED, testName);
296 Assert.assertEquals(numMatched, NUM_MATCHES_EXPECTED);
302 @Test(dataProvider = "testName", dataProviderClass = AbstractServiceTestImpl.class)
303 public void searchWithTwoKeywordsAcrossTwoFields(String testName)
305 // Create one or more keyword retrievable resources, each containing
306 // two specified keywords.
307 long numKeywordRetrievableResources = 5;
308 if (logger.isDebugEnabled()) {
309 logger.debug("Creating " + numKeywordRetrievableResources
310 + " keyword-retrievable resources ...");
312 boolean keywordsInSameField = false;
313 createCollectionObjects(numKeywordRetrievableResources,
314 TWO_MORE_KEYWORDS, keywordsInSameField);
316 // Set the expected status code and group of valid status codes
317 testSetup(STATUS_OK, ServiceRequestType.SEARCH);
319 // Search using both terms
321 // Send the search request and receive a response
322 Response res = doSearch(TWO_MORE_KEYWORDS);
324 int statusCode = res.getStatus();
326 // Check the status code of the response: does it match
327 // the expected response(s)?
328 if (logger.isDebugEnabled()) {
329 logger.debug(testName + ": status = " + statusCode);
331 Assert.assertTrue(testRequestType.isValidStatusCode(statusCode),
332 invalidStatusCodeMessage(testRequestType, statusCode));
333 Assert.assertEquals(statusCode, testExpectedStatusCode);
335 // Verify that the number of resources matched by the search
336 // is identical to the expected result
337 long NUM_MATCHES_EXPECTED = numKeywordRetrievableResources;
338 long numMatched = getNumMatched(res, NUM_MATCHES_EXPECTED, testName);
339 Assert.assertEquals(numMatched, NUM_MATCHES_EXPECTED);
344 // Search using a single term
346 // Send the search request and receive a response
347 res = doSearch(TWO_MORE_KEYWORDS.get(0));
349 int statusCode = res.getStatus();
351 // Check the status code of the response: does it match
352 // the expected response(s)?
353 if (logger.isDebugEnabled()) {
354 logger.debug(testName + ": status = " + statusCode);
356 Assert.assertTrue(testRequestType.isValidStatusCode(statusCode),
357 invalidStatusCodeMessage(testRequestType, statusCode));
358 Assert.assertEquals(statusCode, testExpectedStatusCode);
360 // Verify that the number of resources matched by the search
361 // is identical to the expected result
362 long NUM_MATCHES_EXPECTED = numKeywordRetrievableResources;
363 long numMatched = getNumMatched(res, NUM_MATCHES_EXPECTED, testName);
364 Assert.assertEquals(numMatched, NUM_MATCHES_EXPECTED);
371 // @Test(dataProvider="testName",
372 // dataProviderClass=AbstractServiceTestImpl.class)
373 // public void searchWithOneKeywordInRepeatableScalarField(String testName)
374 // throws Exception {
375 // BriefDescriptionList descriptionList = new BriefDescriptionList();
376 // List<String> descriptions = descriptionList.getBriefDescription();
377 // if (TWO_KEYWORDS.size() >= 2) {
378 // descriptions.add(TWO_KEYWORDS.get(0));
379 // descriptions.add(TWO_KEYWORDS.get(1));
382 @Test(dataProvider = "testName", dataProviderClass = AbstractServiceTestImpl.class, groups = { "utf8" })
383 public void searchWithUTF8Keyword(String testName) {
384 // Create one or more keyword retrievable resources, each containing
385 // two specified keywords.
386 long numKeywordRetrievableResources = 2;
387 if (logger.isDebugEnabled()) {
388 logger.debug("Creating " + numKeywordRetrievableResources
389 + " keyword-retrievable resources ...");
391 createCollectionObjects(numKeywordRetrievableResources, UTF8_KEYWORD);
393 // Set the expected status code and group of valid status codes
394 testSetup(STATUS_OK, ServiceRequestType.SEARCH);
396 // Send the search request and receive a response
397 Response res = doSearch(UTF8_KEYWORD);
399 int statusCode = res.getStatus();
401 // Check the status code of the response: does it match
402 // the expected response(s)?
403 if (logger.isDebugEnabled()) {
404 logger.debug(testName + ": status = " + statusCode);
406 Assert.assertTrue(testRequestType.isValidStatusCode(statusCode),
407 invalidStatusCodeMessage(testRequestType, statusCode));
408 Assert.assertEquals(statusCode, testExpectedStatusCode);
410 // Verify that the number of resources matched by the search
411 // is identical to the expected result
412 long NUM_MATCHES_EXPECTED = numKeywordRetrievableResources;
413 long numMatched = getNumMatched(res, NUM_MATCHES_EXPECTED, testName);
414 Assert.assertEquals(numMatched, NUM_MATCHES_EXPECTED);
421 // FIXME: Rename to searchWithNonExistentKeyword
422 @Test(dataProvider = "testName", dataProviderClass = AbstractServiceTestImpl.class)
423 public void keywordSearchNonExistentKeyword(String testName)
425 // Set the expected status code and group of valid status codes
426 testSetup(STATUS_OK, ServiceRequestType.SEARCH);
428 // Send the search request and receive a response
429 Response res = doSearch(NON_EXISTENT_KEYWORD);
431 int statusCode = res.getStatus();
433 // Check the status code of the response: does it match
434 // the expected response(s)?
435 if (logger.isDebugEnabled()) {
436 logger.debug(testName + ": status = " + statusCode);
438 Assert.assertTrue(testRequestType.isValidStatusCode(statusCode),
439 invalidStatusCodeMessage(testRequestType, statusCode));
440 Assert.assertEquals(statusCode, testExpectedStatusCode);
442 // Verify that the number of resources matched by the search
443 // is identical to the expected result
444 long NUM_MATCHES_EXPECTED = 0;
445 long numMatched = getNumMatched(res, NUM_MATCHES_EXPECTED, testName);
446 Assert.assertEquals(numMatched, NUM_MATCHES_EXPECTED);
452 // ---------------------------------------------------------------
453 // Cleanup of resources created during testing
454 // ---------------------------------------------------------------
456 * Deletes all resources created by setup and tests, after all tests have
459 * This cleanup method will always be run, even if one or more tests fail.
460 * For this reason, it attempts to remove all resources created at any point
461 * during testing, even if some of those resources may be expected to be
462 * deleted by certain tests.
464 @AfterClass(alwaysRun = true)
465 public void cleanUp() {
466 String noTest = System.getProperty("noTestCleanup");
467 if (Boolean.TRUE.toString().equalsIgnoreCase(noTest)) {
468 if (logger.isDebugEnabled()) {
469 logger.debug("Skipping Cleanup phase ...");
473 if (logger.isDebugEnabled()) {
474 logger.debug("Cleaning up temporary resources created for testing ...");
476 CollectionObjectClient collectionObjectClient = new CollectionObjectClient();
477 for (String resourceId : allResourceIdsCreated) {
478 // Note: Any non-success responses are ignored and not reported.
479 collectionObjectClient.delete(resourceId).close();
483 // ---------------------------------------------------------------
484 // Utility methods used by tests above
485 // ---------------------------------------------------------------
486 private void createCollectionObjects(long numToCreate, String keyword) {
487 List keywords = new ArrayList<String>();
488 keywords.add(keyword);
489 boolean keywordsInSameField = true;
490 createCollectionObjects(numToCreate, keywords, keywordsInSameField);
493 private void createCollectionObjects(long numToCreate,
494 List<String> keywords, boolean keywordsInSameField) {
495 testSetup(STATUS_CREATED, ServiceRequestType.CREATE);
496 CollectionObjectClient client = new CollectionObjectClient();
497 for (long i = 0; i < numToCreate; i++) {
498 PoxPayloadOut multipart = createCollectionObjectInstance(i,
499 keywords, keywordsInSameField);
500 Response res = client.create(multipart);
502 int statusCode = res.getStatus();
503 Assert.assertEquals(statusCode, testExpectedStatusCode);
504 String id = extractId(res);
505 allResourceIdsCreated.add(id);
506 if (logger.isDebugEnabled()) {
507 logger.debug("Created new resource [" + i + "] with ID "
516 private PoxPayloadOut createCollectionObjectInstance(long i,
517 List<String> keywords, boolean keywordsInSameField) {
518 CollectionobjectsCommon collectionObject = new CollectionobjectsCommon();
519 collectionObject.setObjectNumber(createIdentifier());
520 if (keywordsInSameField) {
521 collectionObject.setDistinguishingFeatures(listToString(keywords,
524 if (keywords.size() == 1) {
525 collectionObject.setDistinguishingFeatures(keywords.get(0));
526 } else if (keywords.size() == 2) {
527 collectionObject.setDistinguishingFeatures(keywords.get(0));
528 collectionObject.setPhysicalDescription(keywords.get(1));
530 Assert.fail("List of keywords must have exactly one or two members.");
533 PoxPayloadOut multipart = new PoxPayloadOut(
534 CollectionObjectClient.SERVICE_PAYLOAD_NAME);
535 PayloadOutputPart commonPart = multipart.addPart(collectionObject, MediaType.APPLICATION_XML_TYPE);
536 commonPart.setLabel(new CollectionObjectClient().getCommonPartName());
541 private static String listToString(List<String> list, String separator) {
542 StringBuffer sb = new StringBuffer();
543 if (list.size() > 0) {
544 sb.append(list.get(0));
545 for (int i = 1; i < list.size(); i++) {
546 sb.append(separator);
547 sb.append(list.get(i));
550 return sb.toString();
553 private Response doSearch(List<String> keywords) {
554 String searchParamValue = listToString(keywords, KEYWORD_SEPARATOR);
555 return doSearch(searchParamValue);
558 private Response doAdvancedSearch(
559 String propertyName, String propertyValue, String operator) {
560 if (logger.isDebugEnabled()) {
561 logger.debug("Searching on property: " + propertyName + "="
562 + "'" + propertyValue + "'");
564 String whereClause = propertyName + operator +
565 "'" + propertyValue + "'";
566 CollectionObjectClient client = new CollectionObjectClient();
567 Response res = client.advancedSearchIncludeDeleted(whereClause, false); // NOT_INCLUDING_DELETED_RESOURCES
572 private Response doSearch(String keyword) {
573 String searchParamValue = keyword;
574 if (logger.isDebugEnabled()) {
575 logger.debug("Searching on keyword(s): " + searchParamValue
578 CollectionObjectClient client = new CollectionObjectClient();
579 final boolean NOT_INCLUDING_DELETED_RESOURCES = false;
580 Response res = client.keywordSearchIncludeDeleted(searchParamValue,
581 NOT_INCLUDING_DELETED_RESOURCES);
585 private long getNumMatched(Response res, long numExpectedMatches, String testName) {
586 AbstractCommonList list = (AbstractCommonList) res.readEntity(AbstractCommonList.class);
587 long numMatched = list.getTotalItems();
588 if (logger.isDebugEnabled()) {
589 logger.debug("Keyword search matched " + numMatched
590 + " resources, expected to match " + numExpectedMatches);
593 // Optionally output additional data about list members for debugging.
594 if (logger.isTraceEnabled()) {
595 AbstractCommonListUtils.ListItemsInAbstractCommonList(list, logger,
602 private void itemizeListItems(AbstractCommonList list) {
603 List<AbstractCommonList.ListItem> items = list.getListItem();
605 for (AbstractCommonList.ListItem item : items) {
606 logger.debug("list-item["
609 + AbstractCommonListUtils.ListItemGetElementValue(item,
611 logger.debug("list-item["
614 + AbstractCommonListUtils.ListItemGetElementValue(item,
620 public static String getSystemTimeIdentifier() {
621 return Long.toString(System.currentTimeMillis());