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;
30 import org.collectionspace.services.client.CollectionObjectClient;
31 import org.collectionspace.services.client.CollectionSpaceClient;
32 import org.collectionspace.services.client.PayloadOutputPart;
33 import org.collectionspace.services.client.PoxPayloadOut;
34 import org.collectionspace.services.collectionobject.CollectionobjectsCommon;
35 import org.collectionspace.services.common.AbstractCommonListUtils;
36 import org.collectionspace.services.jaxb.AbstractCommonList;
37 import org.jboss.resteasy.client.ClientResponse;
38 import org.slf4j.Logger;
39 import org.slf4j.LoggerFactory;
40 import org.testng.Assert;
41 import org.testng.annotations.AfterClass;
42 import org.testng.annotations.BeforeClass;
43 import org.testng.annotations.Test;
46 * CollectionObjectSearchTest, carries out tests of keyword
47 * search functionality against a deployed and running
48 * CollectionObject Service.
50 * $LastChangedRevision: 1327 $
51 * $LastChangedDate: 2010-02-12 10:35:11 -0800 (Fri, 12 Feb 2010) $
53 public class CollectionObjectSearchTest extends BaseServiceTest {
56 private final String CLASS_NAME = CollectionObjectSearchTest.class.getName();
57 private final Logger logger = LoggerFactory.getLogger(CLASS_NAME);
58 final static String IDENTIFIER = getSystemTimeIdentifier();
59 final static String KEYWORD_SEPARATOR = " ";
60 final long numNoiseWordResources = 10;
61 final double pctNonNoiseWordResources = 0.5;
62 // Use this to keep track of resources to delete
63 private List<String> allResourceIdsCreated = new ArrayList<String>();
65 // Constants for data used in search testing
67 // Test keywords unlikely to be encountered in actual collections data,
68 // consisting of the names of mythical creatures in a 1970s role-playing
69 // game, which result in very few 'hits' in Google searches.
70 final static String KEYWORD = "Tsolyani" + IDENTIFIER;
71 final static List<String> TWO_KEYWORDS =
72 Arrays.asList(new String[]{"Cheggarra" + IDENTIFIER, "Ahoggya" + IDENTIFIER});
73 final static List<String> TWO_MORE_KEYWORDS =
74 Arrays.asList(new String[]{"Karihaya" + IDENTIFIER, "Hlikku" + IDENTIFIER});
75 final static String NOISE_WORD = "Mihalli + IDENTIFIER";
76 // Test Unicode UTF-8 term for keyword searching: a random sequence,
77 // unlikely to be encountered in actual collections data, of two USASCII
78 // characters followed by four non-USASCII range Unicode UTF-8 characters:
80 // Δ : Greek capital letter Delta (U+0394)
81 // Ж : Cyrillic capital letter Zhe with breve (U+04C1)
82 // Ŵ : Latin capital letter W with circumflex (U+0174)
83 // Ω : Greek capital letter Omega (U+03A9)
84 final String UTF8_KEYWORD = "to" + '\u0394' + '\u04C1' + '\u0174' + '\u03A9';
85 // Non-existent term unlikely to be encountered in actual collections
86 // data, consisting of two back-to-back sets of the first letters of
87 // each of the words in a short pangram for the English alphabet.
88 final static String NON_EXISTENT_KEYWORD = "jlmbsoqjlmbsoq";
91 protected String getServiceName() {
92 throw new UnsupportedOperationException(); //FIXME: REM - See http://issues.collectionspace.org/browse/CSPACE-3498
96 protected String getServicePathComponent() {
97 // TODO Auto-generated method stub
98 throw new UnsupportedOperationException(); //FIXME: REM - See http://issues.collectionspace.org/browse/CSPACE-3498
102 // * @see org.collectionspace.services.client.test.BaseServiceTest#getServicePathComponent()
105 // protected String getServicePathComponent() {
106 // return new CollectionObjectClient().getServicePathComponent(); //FIXME: REM = Remove all refs to this method.
110 * @see org.collectionspace.services.client.test.BaseServiceTest#getClientInstance()
113 protected CollectionSpaceClient getClientInstance() {
114 return new CollectionObjectClient();
118 * @see org.collectionspace.services.client.test.BaseServiceTest#getAbstractCommonList(org.jboss.resteasy.client.ClientResponse)
121 protected AbstractCommonList getAbstractCommonList(ClientResponse<AbstractCommonList> response) {
122 return response.getEntity(AbstractCommonList.class);
126 * Creates one or more resources containing a "noise" keyword,
127 * which should NOT be retrieved by keyword searches.
129 * This also helps ensure that searches will not fail, due
130 * to a database-specific constraint or otherwise, if the
131 * number of records containing a particular keyword represent
132 * too high a proportion of the total number of records.
134 @BeforeClass(alwaysRun = true)
135 public void setup() {
136 if (logger.isDebugEnabled()) {
137 logger.debug("Creating " + numNoiseWordResources
138 + " 'noise word' resources ...");
140 createCollectionObjects(numNoiseWordResources, NOISE_WORD);
143 // ---------------------------------------------------------------
145 // ---------------------------------------------------------------
147 @Test(dataProvider = "testName", dataProviderClass = AbstractServiceTestImpl.class,
148 groups = {"oneKeyword"})
149 public void searchWithOneKeyword(String testName) throws Exception {
151 if (logger.isDebugEnabled()) {
152 logger.debug(testBanner(testName, CLASS_NAME));
155 // Create one or more keyword retrievable resources, each containing
156 // a specified keyword.
157 long numKeywordRetrievableResources =
158 (long) (numNoiseWordResources * pctNonNoiseWordResources);
159 if (logger.isDebugEnabled()) {
160 logger.debug("Creating " + numKeywordRetrievableResources
161 + " keyword-retrievable resources ...");
163 createCollectionObjects(numKeywordRetrievableResources, KEYWORD);
165 // Set the expected status code and group of valid status codes
166 testSetup(STATUS_OK, ServiceRequestType.SEARCH);
168 // Send the search request and receive a response
169 ClientResponse<AbstractCommonList> res = doSearch(KEYWORD);
170 int statusCode = res.getStatus();
172 // Check the status code of the response: does it match
173 // the expected response(s)?
174 if (logger.isDebugEnabled()) {
175 logger.debug(testName + ": status = " + statusCode);
177 Assert.assertTrue(REQUEST_TYPE.isValidStatusCode(statusCode),
178 invalidStatusCodeMessage(REQUEST_TYPE, statusCode));
179 Assert.assertEquals(statusCode, EXPECTED_STATUS_CODE);
181 // Verify that the number of resources matched by the search
182 // is identical to the expected result
183 long NUM_MATCHES_EXPECTED = numKeywordRetrievableResources;
184 long numMatched = getNumMatched(res, NUM_MATCHES_EXPECTED, testName);
185 Assert.assertEquals(numMatched, NUM_MATCHES_EXPECTED);
188 @Test(dataProvider = "testName", dataProviderClass = AbstractServiceTestImpl.class)
189 public void searchWithTwoKeywordsInSameField(String testName) throws Exception {
191 if (logger.isDebugEnabled()) {
192 logger.debug(testBanner(testName, CLASS_NAME));
195 // Create one or more keyword retrievable resources, each containing
196 // two specified keywords.
197 long numKeywordRetrievableResources =
198 (long) (numNoiseWordResources * pctNonNoiseWordResources);
199 if (logger.isDebugEnabled()) {
200 logger.debug("Creating " + numKeywordRetrievableResources
201 + " keyword-retrievable resources ...");
203 boolean keywordsInSameField = true;
204 createCollectionObjects(numKeywordRetrievableResources, TWO_KEYWORDS, keywordsInSameField);
206 // Set the expected status code and group of valid status codes
207 testSetup(STATUS_OK, ServiceRequestType.SEARCH);
209 // Search using both terms
211 // Send the search request and receive a response
212 ClientResponse<AbstractCommonList> res = doSearch(TWO_KEYWORDS);
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(REQUEST_TYPE.isValidStatusCode(statusCode),
221 invalidStatusCodeMessage(REQUEST_TYPE, statusCode));
222 Assert.assertEquals(statusCode, EXPECTED_STATUS_CODE);
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);
230 // Search using a single term
232 // Send the search request and receive a response
233 res = doSearch(TWO_KEYWORDS.get(0));
234 statusCode = res.getStatus();
236 // Check the status code of the response: does it match
237 // the expected response(s)?
238 if (logger.isDebugEnabled()) {
239 logger.debug(testName + ": status = " + statusCode);
241 Assert.assertTrue(REQUEST_TYPE.isValidStatusCode(statusCode),
242 invalidStatusCodeMessage(REQUEST_TYPE, statusCode));
243 Assert.assertEquals(statusCode, EXPECTED_STATUS_CODE);
245 // Verify that the number of resources matched by the search
246 // is identical to the expected result
247 NUM_MATCHES_EXPECTED = numKeywordRetrievableResources;
248 numMatched = getNumMatched(res, NUM_MATCHES_EXPECTED, testName);
249 Assert.assertEquals(numMatched, NUM_MATCHES_EXPECTED);
253 @Test(dataProvider = "testName", dataProviderClass = AbstractServiceTestImpl.class)
254 public void searchWithTwoKeywordsAcrossTwoFields(String testName) throws Exception {
256 if (logger.isDebugEnabled()) {
257 logger.debug(testBanner(testName, CLASS_NAME));
260 // Create one or more keyword retrievable resources, each containing
261 // two specified keywords.
262 long numKeywordRetrievableResources = 5;
263 if (logger.isDebugEnabled()) {
264 logger.debug("Creating " + numKeywordRetrievableResources
265 + " keyword-retrievable resources ...");
267 boolean keywordsInSameField = false;
268 createCollectionObjects(numKeywordRetrievableResources, TWO_MORE_KEYWORDS, keywordsInSameField);
270 // Set the expected status code and group of valid status codes
271 testSetup(STATUS_OK, ServiceRequestType.SEARCH);
273 // Search using both terms
275 // Send the search request and receive a response
276 ClientResponse<AbstractCommonList> res = doSearch(TWO_MORE_KEYWORDS);
277 int statusCode = res.getStatus();
279 // Check the status code of the response: does it match
280 // the expected response(s)?
281 if (logger.isDebugEnabled()) {
282 logger.debug(testName + ": status = " + statusCode);
284 Assert.assertTrue(REQUEST_TYPE.isValidStatusCode(statusCode),
285 invalidStatusCodeMessage(REQUEST_TYPE, statusCode));
286 Assert.assertEquals(statusCode, EXPECTED_STATUS_CODE);
288 // Verify that the number of resources matched by the search
289 // is identical to the expected result
290 long NUM_MATCHES_EXPECTED = numKeywordRetrievableResources;
291 long numMatched = getNumMatched(res, NUM_MATCHES_EXPECTED, testName);
292 Assert.assertEquals(numMatched, NUM_MATCHES_EXPECTED);
294 // Search using a single term
296 // Send the search request and receive a response
297 res = doSearch(TWO_MORE_KEYWORDS.get(0));
298 statusCode = res.getStatus();
300 // Check the status code of the response: does it match
301 // the expected response(s)?
302 if (logger.isDebugEnabled()) {
303 logger.debug(testName + ": status = " + statusCode);
305 Assert.assertTrue(REQUEST_TYPE.isValidStatusCode(statusCode),
306 invalidStatusCodeMessage(REQUEST_TYPE, statusCode));
307 Assert.assertEquals(statusCode, EXPECTED_STATUS_CODE);
309 // Verify that the number of resources matched by the search
310 // is identical to the expected result
311 NUM_MATCHES_EXPECTED = numKeywordRetrievableResources;
312 numMatched = getNumMatched(res, NUM_MATCHES_EXPECTED, testName);
313 Assert.assertEquals(numMatched, NUM_MATCHES_EXPECTED);
317 // @Test(dataProvider="testName", dataProviderClass=AbstractServiceTestImpl.class)
318 // public void searchWithOneKeywordInRepeatableScalarField(String testName) throws Exception {
319 // BriefDescriptionList descriptionList = new BriefDescriptionList();
320 // List<String> descriptions = descriptionList.getBriefDescription();
321 // if (TWO_KEYWORDS.size() >= 2) {
322 // descriptions.add(TWO_KEYWORDS.get(0));
323 // descriptions.add(TWO_KEYWORDS.get(1));
326 @Test(dataProvider = "testName", dataProviderClass = AbstractServiceTestImpl.class,
328 public void searchWithUTF8Keyword(String testName) {
330 if (logger.isDebugEnabled()) {
331 logger.debug(testBanner(testName, CLASS_NAME));
334 // Create one or more keyword retrievable resources, each containing
335 // two specified keywords.
336 long numKeywordRetrievableResources = 2;
337 if (logger.isDebugEnabled()) {
338 logger.debug("Creating " + numKeywordRetrievableResources
339 + " keyword-retrievable resources ...");
341 createCollectionObjects(numKeywordRetrievableResources, UTF8_KEYWORD);
343 // Set the expected status code and group of valid status codes
344 testSetup(STATUS_OK, ServiceRequestType.SEARCH);
346 // Send the search request and receive a response
347 ClientResponse<AbstractCommonList> res = doSearch(UTF8_KEYWORD);
348 int statusCode = res.getStatus();
350 // Check the status code of the response: does it match
351 // the expected response(s)?
352 if (logger.isDebugEnabled()) {
353 logger.debug(testName + ": status = " + statusCode);
355 Assert.assertTrue(REQUEST_TYPE.isValidStatusCode(statusCode),
356 invalidStatusCodeMessage(REQUEST_TYPE, statusCode));
357 Assert.assertEquals(statusCode, EXPECTED_STATUS_CODE);
359 // Verify that the number of resources matched by the search
360 // is identical to the expected result
361 long NUM_MATCHES_EXPECTED = numKeywordRetrievableResources;
362 long numMatched = getNumMatched(res, NUM_MATCHES_EXPECTED, testName);
363 Assert.assertEquals(numMatched, NUM_MATCHES_EXPECTED);
367 // FIXME: Rename to searchWithNonExistentKeyword
368 @Test(dataProvider = "testName", dataProviderClass = AbstractServiceTestImpl.class)
369 public void keywordSearchNonExistentKeyword(String testName) throws Exception {
371 if (logger.isDebugEnabled()) {
372 logger.debug(testBanner(testName, CLASS_NAME));
375 // Set the expected status code and group of valid status codes
376 testSetup(STATUS_OK, ServiceRequestType.SEARCH);
378 // Send the search request and receive a response
379 ClientResponse<AbstractCommonList> res = doSearch(NON_EXISTENT_KEYWORD);
380 int statusCode = res.getStatus();
382 // Check the status code of the response: does it match
383 // the expected response(s)?
384 if (logger.isDebugEnabled()) {
385 logger.debug(testName + ": status = " + statusCode);
387 Assert.assertTrue(REQUEST_TYPE.isValidStatusCode(statusCode),
388 invalidStatusCodeMessage(REQUEST_TYPE, statusCode));
389 Assert.assertEquals(statusCode, EXPECTED_STATUS_CODE);
391 // Verify that the number of resources matched by the search
392 // is identical to the expected result
393 long NUM_MATCHES_EXPECTED = 0;
394 long numMatched = getNumMatched(res, NUM_MATCHES_EXPECTED, testName);
395 Assert.assertEquals(numMatched, NUM_MATCHES_EXPECTED);
399 // ---------------------------------------------------------------
400 // Cleanup of resources created during testing
401 // ---------------------------------------------------------------
403 * Deletes all resources created by setup and tests, after all tests have been run.
405 * This cleanup method will always be run, even if one or more tests fail.
406 * For this reason, it attempts to remove all resources created
407 * at any point during testing, even if some of those resources
408 * may be expected to be deleted by certain tests.
410 @AfterClass(alwaysRun = true)
411 public void cleanUp() {
412 String noTest = System.getProperty("noTestCleanup");
413 if (Boolean.TRUE.toString().equalsIgnoreCase(noTest)) {
414 if (logger.isDebugEnabled()) {
415 logger.debug("Skipping Cleanup phase ...");
419 if (logger.isDebugEnabled()) {
420 logger.debug("Cleaning up temporary resources created for testing ...");
422 CollectionObjectClient collectionObjectClient = new CollectionObjectClient();
423 for (String resourceId : allResourceIdsCreated) {
424 // Note: Any non-success responses are ignored and not reported.
425 collectionObjectClient.delete(resourceId).releaseConnection();
429 // ---------------------------------------------------------------
430 // Utility methods used by tests above
431 // ---------------------------------------------------------------
432 private void createCollectionObjects(long numToCreate, String keyword) {
433 List keywords = new ArrayList<String>();
434 keywords.add(keyword);
435 boolean keywordsInSameField = true;
436 createCollectionObjects(numToCreate, keywords, keywordsInSameField);
439 private void createCollectionObjects(long numToCreate, List<String> keywords,
440 boolean keywordsInSameField) {
441 testSetup(STATUS_CREATED, ServiceRequestType.CREATE);
442 CollectionObjectClient client = new CollectionObjectClient();
443 for (long i = 0; i < numToCreate; i++) {
444 PoxPayloadOut multipart =
445 createCollectionObjectInstance(i, keywords, keywordsInSameField);
446 ClientResponse<Response> res = client.create(multipart);
448 int statusCode = res.getStatus();
449 Assert.assertEquals(statusCode, EXPECTED_STATUS_CODE);
450 String id = extractId(res);
451 allResourceIdsCreated.add(id);
452 if (logger.isDebugEnabled()) {
453 logger.debug("Created new resource [" + i + "] with ID " + id);
456 res.releaseConnection();
461 private PoxPayloadOut createCollectionObjectInstance(long i, List<String> keywords,
462 boolean keywordsInSameField) {
463 CollectionobjectsCommon collectionObject = new CollectionobjectsCommon();
464 collectionObject.setObjectNumber(createIdentifier());
465 if (keywordsInSameField) {
466 collectionObject.setDistinguishingFeatures(listToString(keywords, KEYWORD_SEPARATOR));
468 if (keywords.size() == 1) {
469 collectionObject.setDistinguishingFeatures(keywords.get(0));
470 } else if (keywords.size() == 2) {
471 collectionObject.setDistinguishingFeatures(keywords.get(0));
472 collectionObject.setPhysicalDescription(keywords.get(1));
474 Assert.fail("List of keywords must have exactly one or two members.");
477 PoxPayloadOut multipart = new PoxPayloadOut(CollectionObjectClient.SERVICE_PAYLOAD_NAME);
478 PayloadOutputPart commonPart = multipart.addPart(collectionObject,
479 MediaType.APPLICATION_XML_TYPE);
480 commonPart.setLabel(new CollectionObjectClient().getCommonPartName());
484 private static String listToString(List<String> list, String separator) {
485 StringBuffer sb = new StringBuffer();
486 if (list.size() > 0) {
487 sb.append(list.get(0));
488 for (int i = 1; i < list.size(); i++) {
489 sb.append(separator);
490 sb.append(list.get(i));
493 return sb.toString();
496 private ClientResponse<AbstractCommonList> doSearch(List<String> keywords) {
497 String searchParamValue = listToString(keywords, KEYWORD_SEPARATOR);
498 return doSearch(searchParamValue);
501 private ClientResponse<AbstractCommonList> doSearch(String keyword) {
502 String searchParamValue = keyword;
503 if (logger.isDebugEnabled()) {
504 logger.debug("Searching on keyword(s): " + searchParamValue + " ...");
506 CollectionObjectClient client = new CollectionObjectClient();
507 final boolean NOT_INCLUDING_DELETED_RESOURCES = false;
508 ClientResponse<AbstractCommonList> res =
509 client.keywordSearchIncludeDeleted(searchParamValue, NOT_INCLUDING_DELETED_RESOURCES);
513 private long getNumMatched(ClientResponse<AbstractCommonList> res,
514 long numExpectedMatches, String testName) {
515 AbstractCommonList list = (AbstractCommonList) res.getEntity(AbstractCommonList.class);
516 long numMatched = list.getTotalItems();
517 if (logger.isDebugEnabled()) {
518 logger.debug("Keyword search matched " + numMatched
519 + " resources, expected to match " + numExpectedMatches);
522 // Optionally output additional data about list members for debugging.
523 if(logger.isTraceEnabled()){
524 AbstractCommonListUtils.ListItemsInAbstractCommonList(list, logger, testName);
530 private void itemizeListItems(AbstractCommonList list) {
531 List<AbstractCommonList.ListItem> items =
534 for (AbstractCommonList.ListItem item : items) {
535 logger.debug("list-item[" + i + "] title="
536 + AbstractCommonListUtils.ListItemGetElementValue(item, "title"));
537 logger.debug("list-item[" + i + "] URI="
538 + AbstractCommonListUtils.ListItemGetElementValue(item, "uri"));
543 public static String getSystemTimeIdentifier() {
544 return Long.toString(System.currentTimeMillis());