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.BriefDescriptionList;
35 import org.collectionspace.services.collectionobject.CollectionobjectsCommon;
36 import org.collectionspace.services.collectionobject.CollectionobjectsCommonList;
37 import org.collectionspace.services.jaxb.AbstractCommonList;
38 import org.jboss.resteasy.client.ClientResponse;
39 import org.slf4j.Logger;
40 import org.slf4j.LoggerFactory;
41 import org.testng.Assert;
42 import org.testng.annotations.AfterClass;
43 import org.testng.annotations.BeforeClass;
44 import org.testng.annotations.Test;
47 * CollectionObjectSearchTest, carries out tests of keyword
48 * search functionality against a deployed and running
49 * CollectionObject Service.
51 * $LastChangedRevision: 1327 $
52 * $LastChangedDate: 2010-02-12 10:35:11 -0800 (Fri, 12 Feb 2010) $
54 public class CollectionObjectSearchTest extends BaseServiceTest {
57 protected String getServiceName() {
58 throw new UnsupportedOperationException(); //FIXME: REM - See http://issues.collectionspace.org/browse/CSPACE-3498
62 protected String getServicePathComponent() {
63 // TODO Auto-generated method stub
64 throw new UnsupportedOperationException(); //FIXME: REM - See http://issues.collectionspace.org/browse/CSPACE-3498
68 private final String CLASS_NAME = CollectionObjectSearchTest.class.getName();
69 private final Logger logger = LoggerFactory.getLogger(CLASS_NAME);
71 final static String IDENTIFIER = getSystemTimeIdentifier();
73 final static String KEYWORD = "Tsolyani" + IDENTIFIER;
74 final static List<String> TWO_KEYWORDS =
75 Arrays.asList(new String[]{"Cheggarra" + IDENTIFIER, "Ahoggya" + IDENTIFIER});
76 final static List<String> TWO_MORE_KEYWORDS =
77 Arrays.asList(new String[]{"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)
88 final String UTF8_KEYWORD = "to" + '\u0394' + '\u04C1' + '\u0174' +'\u03A9';
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";
94 final static String KEYWORD_SEPARATOR = " ";
95 final long numNoiseWordResources = 10;
96 final double pctNonNoiseWordResources = 0.5;
98 /* Use this to keep track of resources to delete */
99 private List<String> allResourceIdsCreated = new ArrayList<String>();
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(CollectionobjectsCommonList.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);
144 // ---------------------------------------------------------------
146 // ---------------------------------------------------------------
150 @Test(dataProvider="testName", dataProviderClass=AbstractServiceTestImpl.class,
151 groups = {"oneKeyword"})
152 public void searchWithOneKeyword(String testName) throws Exception {
154 if (logger.isDebugEnabled()) {
155 logger.debug(testBanner(testName, CLASS_NAME));
158 // Create one or more keyword retrievable resources, each containing
159 // a specified keyword.
160 long numKeywordRetrievableResources =
161 (long) (numNoiseWordResources * pctNonNoiseWordResources);
162 if (logger.isDebugEnabled()) {
163 logger.debug("Creating " + numKeywordRetrievableResources +
164 " keyword-retrievable resources ...");
166 createCollectionObjects(numKeywordRetrievableResources, KEYWORD);
168 // Set the expected status code and group of valid status codes
169 testSetup(STATUS_OK, ServiceRequestType.SEARCH);
171 // Send the search request and receive a response
172 ClientResponse<CollectionobjectsCommonList> res = doSearch(KEYWORD);
173 int statusCode = res.getStatus();
175 // Check the status code of the response: does it match
176 // the expected response(s)?
177 if (logger.isDebugEnabled()) {
178 logger.debug(testName + ": status = " + statusCode);
180 Assert.assertTrue(REQUEST_TYPE.isValidStatusCode(statusCode),
181 invalidStatusCodeMessage(REQUEST_TYPE, statusCode));
182 Assert.assertEquals(statusCode, EXPECTED_STATUS_CODE);
184 // Verify that the number of resources matched by the search
185 // is identical to the expected result
186 long NUM_MATCHES_EXPECTED = numKeywordRetrievableResources;
187 long numMatched = getNumMatched(res, NUM_MATCHES_EXPECTED);
188 Assert.assertEquals(numMatched, NUM_MATCHES_EXPECTED);
191 @Test(dataProvider="testName", dataProviderClass=AbstractServiceTestImpl.class)
192 public void searchWithTwoKeywordsInSameField(String testName) throws Exception {
194 if (logger.isDebugEnabled()) {
195 logger.debug(testBanner(testName, CLASS_NAME));
198 // Create one or more keyword retrievable resources, each containing
199 // two specified keywords.
200 long numKeywordRetrievableResources =
201 (long) (numNoiseWordResources * pctNonNoiseWordResources);
202 if (logger.isDebugEnabled()) {
203 logger.debug("Creating " + numKeywordRetrievableResources +
204 " keyword-retrievable resources ...");
206 boolean keywordsInSameField = true;
207 createCollectionObjects(numKeywordRetrievableResources, TWO_KEYWORDS, keywordsInSameField);
209 // Set the expected status code and group of valid status codes
210 testSetup(STATUS_OK, ServiceRequestType.SEARCH);
212 // Search using both terms
214 // Send the search request and receive a response
215 ClientResponse<CollectionobjectsCommonList> res = doSearch(TWO_KEYWORDS);
216 int statusCode = res.getStatus();
218 // Check the status code of the response: does it match
219 // the expected response(s)?
220 if (logger.isDebugEnabled()) {
221 logger.debug(testName + ": status = " + statusCode);
223 Assert.assertTrue(REQUEST_TYPE.isValidStatusCode(statusCode),
224 invalidStatusCodeMessage(REQUEST_TYPE, statusCode));
225 Assert.assertEquals(statusCode, EXPECTED_STATUS_CODE);
227 // Verify that the number of resources matched by the search
228 // is identical to the expected result
229 long NUM_MATCHES_EXPECTED = numKeywordRetrievableResources;
230 long numMatched = getNumMatched(res, NUM_MATCHES_EXPECTED);
231 Assert.assertEquals(numMatched, NUM_MATCHES_EXPECTED);
233 // Search using a single term
235 // Send the search request and receive a response
236 res = doSearch(TWO_KEYWORDS.get(0));
237 statusCode = res.getStatus();
239 // Check the status code of the response: does it match
240 // the expected response(s)?
241 if (logger.isDebugEnabled()) {
242 logger.debug(testName + ": status = " + statusCode);
244 Assert.assertTrue(REQUEST_TYPE.isValidStatusCode(statusCode),
245 invalidStatusCodeMessage(REQUEST_TYPE, statusCode));
246 Assert.assertEquals(statusCode, EXPECTED_STATUS_CODE);
248 // Verify that the number of resources matched by the search
249 // is identical to the expected result
250 NUM_MATCHES_EXPECTED = numKeywordRetrievableResources;
251 numMatched = getNumMatched(res, NUM_MATCHES_EXPECTED);
252 Assert.assertEquals(numMatched, NUM_MATCHES_EXPECTED);
256 @Test(dataProvider="testName", dataProviderClass=AbstractServiceTestImpl.class)
257 public void searchWithTwoKeywordsAcrossTwoFields(String testName) throws Exception {
259 if (logger.isDebugEnabled()) {
260 logger.debug(testBanner(testName, CLASS_NAME));
263 // Create one or more keyword retrievable resources, each containing
264 // two specified keywords.
265 long numKeywordRetrievableResources = 5;
266 if (logger.isDebugEnabled()) {
267 logger.debug("Creating " + numKeywordRetrievableResources +
268 " keyword-retrievable resources ...");
270 boolean keywordsInSameField = false;
271 createCollectionObjects(numKeywordRetrievableResources, TWO_MORE_KEYWORDS, keywordsInSameField);
273 // Set the expected status code and group of valid status codes
274 testSetup(STATUS_OK, ServiceRequestType.SEARCH);
276 // Search using both terms
278 // Send the search request and receive a response
279 ClientResponse<CollectionobjectsCommonList> res = doSearch(TWO_MORE_KEYWORDS);
280 int statusCode = res.getStatus();
282 // Check the status code of the response: does it match
283 // the expected response(s)?
284 if (logger.isDebugEnabled()) {
285 logger.debug(testName + ": status = " + statusCode);
287 Assert.assertTrue(REQUEST_TYPE.isValidStatusCode(statusCode),
288 invalidStatusCodeMessage(REQUEST_TYPE, statusCode));
289 Assert.assertEquals(statusCode, EXPECTED_STATUS_CODE);
291 // Verify that the number of resources matched by the search
292 // is identical to the expected result
293 long NUM_MATCHES_EXPECTED = numKeywordRetrievableResources;
294 long numMatched = getNumMatched(res, NUM_MATCHES_EXPECTED);
295 Assert.assertEquals(numMatched, NUM_MATCHES_EXPECTED);
297 // Search using a single term
299 // Send the search request and receive a response
300 res = doSearch(TWO_MORE_KEYWORDS.get(0));
301 statusCode = res.getStatus();
303 // Check the status code of the response: does it match
304 // the expected response(s)?
305 if (logger.isDebugEnabled()) {
306 logger.debug(testName + ": status = " + statusCode);
308 Assert.assertTrue(REQUEST_TYPE.isValidStatusCode(statusCode),
309 invalidStatusCodeMessage(REQUEST_TYPE, statusCode));
310 Assert.assertEquals(statusCode, EXPECTED_STATUS_CODE);
312 // Verify that the number of resources matched by the search
313 // is identical to the expected result
314 NUM_MATCHES_EXPECTED = numKeywordRetrievableResources;
315 numMatched = getNumMatched(res, NUM_MATCHES_EXPECTED);
316 Assert.assertEquals(numMatched, NUM_MATCHES_EXPECTED);
320 // @Test(dataProvider="testName", dataProviderClass=AbstractServiceTestImpl.class)
321 // public void searchWithOneKeywordInRepeatableScalarField(String testName) throws Exception {
322 // BriefDescriptionList descriptionList = new BriefDescriptionList();
323 // List<String> descriptions = descriptionList.getBriefDescription();
324 // if (TWO_KEYWORDS.size() >= 2) {
325 // descriptions.add(TWO_KEYWORDS.get(0));
326 // descriptions.add(TWO_KEYWORDS.get(1));
331 @Test(dataProvider="testName", dataProviderClass=AbstractServiceTestImpl.class,
333 public void searchWithUTF8Keyword(String testName) {
335 if (logger.isDebugEnabled()) {
336 logger.debug(testBanner(testName, CLASS_NAME));
339 // Create one or more keyword retrievable resources, each containing
340 // two specified keywords.
341 long numKeywordRetrievableResources = 2;
342 if (logger.isDebugEnabled()) {
343 logger.debug("Creating " + numKeywordRetrievableResources +
344 " keyword-retrievable resources ...");
346 createCollectionObjects(numKeywordRetrievableResources, UTF8_KEYWORD);
348 // Set the expected status code and group of valid status codes
349 testSetup(STATUS_OK, ServiceRequestType.SEARCH);
351 // Send the search request and receive a response
352 ClientResponse<CollectionobjectsCommonList> res = doSearch(UTF8_KEYWORD);
353 int statusCode = res.getStatus();
355 // Check the status code of the response: does it match
356 // the expected response(s)?
357 if (logger.isDebugEnabled()) {
358 logger.debug(testName + ": status = " + statusCode);
360 Assert.assertTrue(REQUEST_TYPE.isValidStatusCode(statusCode),
361 invalidStatusCodeMessage(REQUEST_TYPE, statusCode));
362 Assert.assertEquals(statusCode, EXPECTED_STATUS_CODE);
364 // Verify that the number of resources matched by the search
365 // is identical to the expected result
366 long NUM_MATCHES_EXPECTED = numKeywordRetrievableResources;
367 long numMatched = getNumMatched(res, NUM_MATCHES_EXPECTED);
368 Assert.assertEquals(numMatched, NUM_MATCHES_EXPECTED);
373 // FIXME: Rename to searchWithNonExistentKeyword
374 @Test(dataProvider="testName", dataProviderClass=AbstractServiceTestImpl.class)
375 public void keywordSearchNonExistentKeyword(String testName) throws Exception {
377 if (logger.isDebugEnabled()) {
378 logger.debug(testBanner(testName, CLASS_NAME));
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 ClientResponse<CollectionobjectsCommonList> res = doSearch(NON_EXISTENT_KEYWORD);
386 int statusCode = res.getStatus();
388 // Check the status code of the response: does it match
389 // the expected response(s)?
390 if (logger.isDebugEnabled()) {
391 logger.debug(testName + ": status = " + statusCode);
393 Assert.assertTrue(REQUEST_TYPE.isValidStatusCode(statusCode),
394 invalidStatusCodeMessage(REQUEST_TYPE, statusCode));
395 Assert.assertEquals(statusCode, EXPECTED_STATUS_CODE);
397 // Verify that the number of resources matched by the search
398 // is identical to the expected result
399 long NUM_MATCHES_EXPECTED = 0;
400 long numMatched = getNumMatched(res, NUM_MATCHES_EXPECTED);
401 Assert.assertEquals(numMatched, NUM_MATCHES_EXPECTED);
405 // ---------------------------------------------------------------
406 // Cleanup of resources created during testing
407 // ---------------------------------------------------------------
410 * Deletes all resources created by setup and tests, after all tests have been run.
412 * This cleanup method will always be run, even if one or more tests fail.
413 * For this reason, it attempts to remove all resources created
414 * at any point during testing, even if some of those resources
415 * may be expected to be deleted by certain tests.
417 @AfterClass(alwaysRun=true)
418 public void cleanUp() {
419 String noTest = System.getProperty("noTestCleanup");
420 if(Boolean.TRUE.toString().equalsIgnoreCase(noTest)) {
421 if (logger.isDebugEnabled()) {
422 logger.debug("Skipping Cleanup phase ...");
426 if (logger.isDebugEnabled()) {
427 logger.debug("Cleaning up temporary resources created for testing ...");
429 CollectionObjectClient collectionObjectClient = new CollectionObjectClient();
430 for (String resourceId : allResourceIdsCreated) {
431 // Note: Any non-success responses are ignored and not reported.
432 collectionObjectClient.delete(resourceId).releaseConnection();
436 // ---------------------------------------------------------------
437 // Utility methods used by tests above
438 // ---------------------------------------------------------------
440 private void createCollectionObjects(long numToCreate, String keyword) {
441 List keywords = new ArrayList<String>();
442 keywords.add(keyword);
443 boolean keywordsInSameField = true;
444 createCollectionObjects(numToCreate, keywords, keywordsInSameField);
447 private void createCollectionObjects(long numToCreate, List<String> keywords,
448 boolean keywordsInSameField) {
449 testSetup(STATUS_CREATED, ServiceRequestType.CREATE);
450 CollectionObjectClient client = new CollectionObjectClient();
451 for (long i = 0; i < numToCreate; i++) {
452 PoxPayloadOut multipart =
453 createCollectionObjectInstance(i, keywords, keywordsInSameField);
454 ClientResponse<Response> res = client.create(multipart);
456 int statusCode = res.getStatus();
457 Assert.assertEquals(statusCode, EXPECTED_STATUS_CODE);
458 String id = extractId(res);
459 allResourceIdsCreated.add(id);
460 if (logger.isDebugEnabled()) {
461 logger.debug("Created new resource [" + i + "] with ID " + id);
464 res.releaseConnection();
469 private PoxPayloadOut createCollectionObjectInstance(long i, List<String> keywords,
470 boolean keywordsInSameField) {
471 CollectionobjectsCommon collectionObject = new CollectionobjectsCommon();
472 collectionObject.setObjectNumber(createIdentifier());
473 if (keywordsInSameField) {
474 collectionObject.setDistinguishingFeatures(listToString(keywords, KEYWORD_SEPARATOR));
476 if (keywords.size() == 1) {
477 collectionObject.setDistinguishingFeatures(keywords.get(0));
478 } else if (keywords.size() == 2) {
479 collectionObject.setDistinguishingFeatures(keywords.get(0));
480 collectionObject.setPhysicalDescription(keywords.get(1));
482 Assert.fail("List of keywords must have exactly one or two members.");
485 PoxPayloadOut multipart = new PoxPayloadOut(CollectionObjectClient.SERVICE_PAYLOAD_NAME);
486 PayloadOutputPart commonPart = multipart.addPart(collectionObject,
487 MediaType.APPLICATION_XML_TYPE);
488 commonPart.setLabel(new CollectionObjectClient().getCommonPartName());
492 private static String listToString(List<String> list, String separator) {
493 StringBuffer sb = new StringBuffer();
494 if (list.size() > 0) {
495 sb.append(list.get(0));
496 for (int i=1; i < list.size(); i++) {
497 sb.append(separator);
498 sb.append(list.get(i));
501 return sb.toString();
504 private ClientResponse<CollectionobjectsCommonList> doSearch(List<String> keywords) {
505 String searchParamValue = listToString(keywords, KEYWORD_SEPARATOR);
506 return doSearch(searchParamValue);
509 private ClientResponse<CollectionobjectsCommonList> doSearch(String keyword) {
510 String searchParamValue = keyword;
511 if (logger.isDebugEnabled()) {
512 logger.debug("Searching on keyword(s): " + searchParamValue + " ...");
514 CollectionObjectClient client = new CollectionObjectClient();
515 ClientResponse<CollectionobjectsCommonList> res =
516 client.keywordSearch(searchParamValue);
520 private long getNumMatched(ClientResponse<CollectionobjectsCommonList> res,
521 long numExpectedMatches) {
522 CollectionobjectsCommonList list = (CollectionobjectsCommonList)
523 res.getEntity(CollectionobjectsCommonList.class);
524 long numMatched = list.getTotalItems();
526 if (logger.isDebugEnabled()) {
527 logger.debug("Keyword search matched " + numMatched +
528 " resources, expected to match " + numExpectedMatches);
531 // Optionally output additional data about list members for debugging.
532 boolean iterateThroughList = false;
533 if (iterateThroughList && logger.isDebugEnabled()) {
534 itemizeListItems(list);
539 private void itemizeListItems(CollectionobjectsCommonList list) {
540 List<CollectionobjectsCommonList.CollectionObjectListItem> items =
541 list.getCollectionObjectListItem();
543 for (CollectionobjectsCommonList.CollectionObjectListItem item : items) {
544 logger.debug("list-item[" + i + "] title="
546 logger.debug("list-item[" + i + "] URI="
552 public static String getSystemTimeIdentifier() {
553 return Long.toString(System.currentTimeMillis());