001/*
002 * Licensed to DuraSpace under one or more contributor license agreements.
003 * See the NOTICE file distributed with this work for additional information
004 * regarding copyright ownership.
005 *
006 * DuraSpace licenses this file to you under the Apache License,
007 * Version 2.0 (the "License"); you may not use this file except in
008 * compliance with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.fcrepo.integration.http.api;
019
020import com.fasterxml.jackson.databind.ObjectMapper;
021import org.apache.http.client.methods.CloseableHttpResponse;
022import org.apache.http.client.methods.HttpDelete;
023import org.apache.http.client.methods.HttpGet;
024import org.apache.http.client.methods.HttpPatch;
025import org.apache.http.entity.StringEntity;
026import org.fcrepo.search.api.Condition;
027import org.fcrepo.search.api.SearchResult;
028import org.junit.Test;
029import org.springframework.test.context.TestExecutionListeners;
030
031import java.io.IOException;
032import java.net.URLEncoder;
033import java.nio.charset.StandardCharsets;
034import java.time.Duration;
035import java.time.Instant;
036import java.util.ArrayList;
037import java.util.Arrays;
038import java.util.Collections;
039import java.util.List;
040import java.util.Map;
041import java.util.concurrent.TimeUnit;
042import java.util.stream.Collectors;
043import java.util.stream.Stream;
044
045import static javax.ws.rs.core.Response.Status.BAD_REQUEST;
046import static javax.ws.rs.core.Response.Status.NO_CONTENT;
047import static javax.ws.rs.core.Response.Status.OK;
048import static org.fcrepo.kernel.api.FedoraTypes.FEDORA_ID_PREFIX;
049import static org.fcrepo.search.api.Condition.Field.CONTENT_SIZE;
050import static org.fcrepo.search.api.Condition.Field.CREATED;
051import static org.fcrepo.search.api.Condition.Field.FEDORA_ID;
052import static org.fcrepo.search.api.Condition.Field.MIME_TYPE;
053import static org.fcrepo.search.api.Condition.Field.MODIFIED;
054import static org.fcrepo.search.api.Condition.Field.RDF_TYPE;
055import static org.junit.Assert.assertEquals;
056import static org.junit.Assert.assertNotEquals;
057import static org.junit.Assert.assertNotNull;
058import static org.junit.Assert.assertTrue;
059
060/**
061 * @author dbernstein
062 * @since 05/06/20
063 */
064@TestExecutionListeners(
065        listeners = { TestIsolationExecutionListener.class },
066        mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS)
067public class FedoraSearchIT extends AbstractResourceIT {
068
069    private static final ObjectMapper objectMapper = new ObjectMapper();
070
071    private String getSearchEndpoint() {
072        return serverAddress + "fcr:search?";
073    }
074
075    private List<String> createResources(final int count) throws IOException {
076        return createResources(getRandomUniqueId(), count);
077    }
078
079    private List<String> createResources(final String prefix, final int count) throws IOException {
080        final var resources = new ArrayList<String>();
081        for (int i = 0; i < count; i++) {
082            try (final CloseableHttpResponse response = createObject(prefix + "-" + String.format("%05d", i))) {
083                resources.add(getLocation(response));
084            }
085        }
086        return resources;
087    }
088
089    @Test
090    public void testSearchAllResources() throws Exception {
091        final var resources = createResources(3);
092        final var condition = FEDORA_ID + "=*";
093        List<String> notFound = resources;
094
095        final int maxResults = 10;
096        int offset = 0;
097
098        // Loop until all resources are found
099        // - Failure case: loop dies with assertion for non-zero results in 'doSearchAllResources()'
100        while (!notFound.isEmpty()) {
101            notFound = doSearchAllResources(notFound, condition, maxResults, offset);
102            offset += maxResults;
103        }
104    }
105
106    private List<String> doSearchAllResources(final List<String> resources,
107                                              final String condition,
108                                              final int maxResults,
109                                              final int offset) throws IOException {
110        final String searchUrl = getSearchEndpoint()
111                + "condition=" + encode(condition) + "&max_results=" + maxResults + "&offset=" + offset;
112
113        final List<String> notFound = new ArrayList<>();
114        try (final CloseableHttpResponse response = execute(new HttpGet(searchUrl))) {
115            assertEquals(OK.getStatusCode(), getStatus(response));
116            final SearchResult result = objectMapper.readValue(response.getEntity().getContent(), SearchResult.class);
117            assertNotNull(result);
118            assertNotNull(result.getPagination());
119            assertTrue("Items not found! " + resources, result.getItems().size() > 0);
120            final var resultingFedoraIds =
121                    result.getItems().stream().map(x -> x.get("fedora_id")).collect(Collectors.toList());
122
123            resources.forEach(r -> {
124                if (!resultingFedoraIds.contains(r)) {
125                    notFound.add(r);
126                }
127            });
128        }
129        return notFound;
130    }
131
132    @Test
133    public void testWildcardMatchingOnPartialFedoraId() throws Exception {
134        final var id = getRandomUniqueId();
135        final int count = 3;
136        createResources(id, count);
137        final var urlPrefix = serverAddress + id;
138        // try all valid prefix formulations
139        final var prefixes = new String[]{id, "/" + id, urlPrefix, FEDORA_ID_PREFIX + "/" + id};
140        for (final String prefix : prefixes) {
141            final var condition = FEDORA_ID + "=" + prefix + "*";
142            final String searchUrl = getSearchEndpoint() + "condition=" + encode(condition);
143            try (final CloseableHttpResponse response = execute(new HttpGet(searchUrl))) {
144                assertEquals(OK.getStatusCode(), getStatus(response));
145                final SearchResult result = objectMapper.readValue(response.getEntity().getContent(),
146                        SearchResult.class);
147                assertEquals("expected " + count + " items where condition = " + condition, count,
148                        result.getItems().size());
149
150                assertTrue(result.getItems().stream().map(x -> x.get(FEDORA_ID.toString()))
151                        .allMatch(x -> x.toString().startsWith(urlPrefix)));
152            }
153        }
154    }
155
156    @Test
157    public void testUpdateSearchIndexOnResourceUpdate() throws Exception {
158        final var resourceId = createResources(1).get(0);
159        final var condition = FEDORA_ID + "=" + resourceId;
160        final String searchUrl = getSearchEndpoint() + "condition=" + encode(condition);
161        String modified = null;
162        try (final CloseableHttpResponse response = execute(new HttpGet(searchUrl))) {
163            assertEquals(OK.getStatusCode(), getStatus(response));
164            final SearchResult result = objectMapper.readValue(response.getEntity().getContent(),
165                    SearchResult.class);
166            assertEquals("expected 1 item where condition = " + condition, 1,
167                    result.getItems().size());
168            modified = result.getItems().get(0).get(MODIFIED.toString()).toString();
169        }
170
171        TimeUnit.SECONDS.sleep(1);
172
173        final var patch = new HttpPatch(resourceId);
174        patch.setHeader("Content-Type", "application/sparql-update");
175        patch.setEntity(new StringEntity("insert data { <> <http://example/blah> \"Blah\". }"));
176        try (final CloseableHttpResponse response = execute(patch)) {
177            assertEquals(NO_CONTENT.getStatusCode(), getStatus(response));
178        }
179
180        try (final CloseableHttpResponse response = execute(new HttpGet(searchUrl))) {
181            assertEquals(OK.getStatusCode(), getStatus(response));
182            final SearchResult result = objectMapper.readValue(response.getEntity().getContent(),
183                    SearchResult.class);
184            assertEquals("expected 1 item where condition = " + condition, 1,
185                    result.getItems().size());
186            final var newModified = result.getItems().get(0).get(MODIFIED.toString()).toString();
187            assertNotEquals("Modified date should have changed  but it did not.", modified, newModified);
188        }
189    }
190
191    @Test
192    public void testUpdateSearchIndexOnResourceDelete() throws Exception {
193        final var resourceId = createResources(1).get(0);
194        final var condition = FEDORA_ID + "=" + resourceId;
195        final String searchUrl = getSearchEndpoint() + "condition=" + encode(condition);
196        try (final CloseableHttpResponse response = execute(new HttpGet(searchUrl))) {
197            assertEquals(OK.getStatusCode(), getStatus(response));
198            final SearchResult result = objectMapper.readValue(response.getEntity().getContent(),
199                    SearchResult.class);
200            assertEquals("expected 1 item where condition = " + condition, 1,
201                    result.getItems().size());
202        }
203
204        final var httpDelete = new HttpDelete(resourceId);
205        try (final CloseableHttpResponse response = execute(httpDelete)) {
206            assertEquals(NO_CONTENT.getStatusCode(), getStatus(response));
207        }
208
209        try (final CloseableHttpResponse response = execute(new HttpGet(searchUrl))) {
210            assertEquals(OK.getStatusCode(), getStatus(response));
211            final SearchResult result = objectMapper.readValue(response.getEntity().getContent(),
212                    SearchResult.class);
213            assertEquals("expected 0 items where condition = " + condition, 0,
214                    result.getItems().size());
215        }
216    }
217
218    public void testModifiedGreaterAndLessThan() throws Exception {
219        final var id = getRandomUniqueId();
220        final int count = 1;
221        final var instant = Instant.now();
222        final var now = instant.toString();
223        final var tomorrow = instant.plus(Duration.ofDays(1)).toString();
224        createResources(id, count);
225        final var fedoraId = FEDORA_ID_PREFIX + "/" + id;
226        final var fedoraIdCondition = FEDORA_ID + "=" + fedoraId;
227        final var lessThanNow = MODIFIED + "<" + now;
228        final var greaterThanNow = MODIFIED + ">" + now;
229        final var lessThanTomorrow = MODIFIED + "<" + tomorrow;
230        final var greaterThanTomorrow = MODIFIED + ">" + tomorrow;
231
232        //no results for resources modified before now
233        String searchUrl =
234                getSearchEndpoint() + "condition=" + encode(fedoraIdCondition) + "&" + lessThanNow;
235        try (final CloseableHttpResponse response = execute(new HttpGet(searchUrl))) {
236            assertEquals(OK.getStatusCode(), getStatus(response));
237            final SearchResult result = objectMapper.readValue(response.getEntity().getContent(),
238                    SearchResult.class);
239            assertEquals("expected no results", 0,
240                    result.getItems().size());
241        }
242        //no results for resources modified after tomorrow
243        searchUrl =
244                getSearchEndpoint() + "condition=" + encode(fedoraIdCondition) + "&" + greaterThanTomorrow;
245        try (final CloseableHttpResponse response = execute(new HttpGet(searchUrl))) {
246            assertEquals(OK.getStatusCode(), getStatus(response));
247            final SearchResult result = objectMapper.readValue(response.getEntity().getContent(),
248                    SearchResult.class);
249            assertEquals("expected no results", 0,
250                    result.getItems().size());
251        }
252
253        // ensure that greater than now returns a result
254        searchUrl = getSearchEndpoint() + "condition=" + encode(fedoraIdCondition) + "&" + greaterThanNow;
255        try (final CloseableHttpResponse response = execute(new HttpGet(searchUrl))) {
256            assertEquals(OK.getStatusCode(), getStatus(response));
257            final SearchResult result = objectMapper.readValue(response.getEntity().getContent(),
258                    SearchResult.class);
259            assertEquals("expected " + count + " results", count,
260                    result.getItems().size());
261
262            assertTrue(result.getItems().stream().map(x -> x.get("fedora_id"))
263                    .allMatch(x -> x.toString().equals(fedoraId)));
264        }
265
266        // ensure that less than tomorrow returns a result
267        searchUrl = getSearchEndpoint() + "condition=" + encode(fedoraIdCondition) + "&" + lessThanTomorrow;
268        try (final CloseableHttpResponse response = execute(new HttpGet(searchUrl))) {
269            assertEquals(OK.getStatusCode(), getStatus(response));
270            final SearchResult result = objectMapper.readValue(response.getEntity().getContent(),
271                    SearchResult.class);
272            assertEquals("expected " + count + " results", count,
273                    result.getItems().size());
274
275            assertTrue(result.getItems().stream().map(x -> x.get("fedora_id"))
276                    .allMatch(x -> x.toString().equals(fedoraId)));
277        }
278
279        // ensure that between now and tomorrow returns a result.
280        searchUrl = getSearchEndpoint() + "condition=" + encode(fedoraIdCondition) + "&" + greaterThanNow +
281                "&" + lessThanTomorrow;
282        try (final CloseableHttpResponse response = execute(new HttpGet(searchUrl))) {
283            assertEquals(OK.getStatusCode(), getStatus(response));
284            final SearchResult result = objectMapper.readValue(response.getEntity().getContent(),
285                    SearchResult.class);
286            assertEquals("expected " + count + " results ", count,
287                    result.getItems().size());
288            assertTrue(result.getItems().stream().map(x -> x.get("fedora_id"))
289                    .allMatch(x -> x.toString().equals(fedoraId)));
290        }
291    }
292
293    @Test
294    public void testFieldsReturnsSpecifiedFields() throws Exception {
295        testFieldsReturnsOnlySpecifiedFields(Arrays.asList(FEDORA_ID, CREATED));
296    }
297
298    @Test
299    public void testFieldsReturnsMimeType() throws Exception {
300        testFieldsReturnsOnlySpecifiedFields(Arrays.asList(MIME_TYPE));
301    }
302
303    @Test
304    public void testFieldsReturnsContentSize() throws Exception {
305        testFieldsReturnsOnlySpecifiedFields(Arrays.asList(CONTENT_SIZE));
306    }
307
308    @Test
309    public void testFieldsReturnsRDFType() throws Exception {
310        testFieldsReturnsOnlySpecifiedFields(Arrays.asList(RDF_TYPE));
311    }
312
313
314    private void testFieldsReturnsOnlySpecifiedFields(final List<Condition.Field> fieldList) throws Exception {
315        final var id = getRandomUniqueId();
316        final var count = 1;
317        createResources(id, count);
318        final var fields = fieldList.stream()
319                .map(Condition.Field::toString).collect(Collectors.toList());
320        final String searchUrl =
321                getSearchEndpoint() + "condition=" + encode("fedora_id=" + id + "*") + "&fields=" + String.join(",",
322                        fields);
323        try (final CloseableHttpResponse response = execute(new HttpGet(searchUrl))) {
324            assertEquals(OK.getStatusCode(), getStatus(response));
325            final SearchResult result = objectMapper.readValue(response.getEntity().getContent(), SearchResult.class);
326            final var items = result.getItems();
327            assertEquals("expected " + count + " items", count, items.size());
328            for (final Map<String, Object> item : items) {
329                assertEquals("expected " + fields.size() + " fields returned", fields.size(), item.size());
330                for (final String field : fields) {
331                    assertTrue("Result does not contain expected field: " + field, item.containsKey(field));
332                }
333            }
334        }
335    }
336
337    @Test
338    public void testMaxResultsAndOffset() throws Exception {
339        final var prefix = getRandomUniqueId();
340        final int count = 3;
341        final var resources = createResources(prefix, count);
342        final var condition = FEDORA_ID + "=" + prefix + "*";
343        final var maxResults = 1;
344        final String searchUrl =
345                getSearchEndpoint() + "condition=" + encode(condition) + "&max_results=" + maxResults + "&offset=2" +
346                        "&order_by=fedora_id";
347        try (final CloseableHttpResponse response = execute(new HttpGet(searchUrl))) {
348            assertEquals(OK.getStatusCode(), getStatus(response));
349            final SearchResult result = objectMapper.readValue(response.getEntity().getContent(), SearchResult.class);
350            assertEquals(maxResults, result.getItems().size());
351            final var returnedIds =
352                    result.getItems().stream().map(x -> x.get("fedora_id")).collect(Collectors.toList());
353            assertEquals(resources.get(2), returnedIds.get(0));
354        }
355    }
356
357    @Test
358    public void testDoNotIncludeTotalCount() throws Exception {
359        final var prefix = getRandomUniqueId();
360        final int count = 3;
361        createResources(prefix, count);
362        final var condition = FEDORA_ID + "=" + prefix + "*";
363        final var maxResults = 1;
364        final String searchUrl =
365                getSearchEndpoint() + "condition=" + encode(condition) + "&max_results=" + maxResults +
366                        "&include_total_result_count=false";
367        try (final CloseableHttpResponse response = execute(new HttpGet(searchUrl))) {
368            assertEquals(OK.getStatusCode(), getStatus(response));
369            final SearchResult result = objectMapper.readValue(response.getEntity().getContent(), SearchResult.class);
370            assertEquals(maxResults, result.getItems().size());
371            assertEquals(-1, result.getPagination().getTotalResults());
372        }
373    }
374
375    @Test
376    public void testIncludeTotalCount() throws Exception {
377        final var prefix = getRandomUniqueId();
378        final int count = 3;
379        createResources(prefix, count);
380        final var condition = FEDORA_ID + "=" + prefix + "*";
381        final var maxResults = 1;
382        final String searchUrl =
383                getSearchEndpoint() + "condition=" + encode(condition) + "&max_results=" + maxResults +
384                        "&include_total_result_count=true";
385        try (final CloseableHttpResponse response = execute(new HttpGet(searchUrl))) {
386            assertEquals(OK.getStatusCode(), getStatus(response));
387            final SearchResult result = objectMapper.readValue(response.getEntity().getContent(), SearchResult.class);
388            assertEquals(maxResults, result.getItems().size());
389            assertEquals(count, result.getPagination().getTotalResults());
390        }
391    }
392
393    @Test
394    public void testSearchByContentSize() throws Exception {
395        final var resourceId = getRandomUniqueId();
396        createObjectAndClose(resourceId);
397        final var ds1 = "test";
398        createDatastream(resourceId, ds1, ds1);
399        final var ds2 = "test-longer-length";
400        createDatastream(resourceId, ds2, ds2);
401        final var condition = FEDORA_ID + "=" + resourceId + "*";
402        final var contentSizeCondition = CONTENT_SIZE + ">=" + ds1.getBytes().length;
403        final var contentSizeConditionLongerLength = CONTENT_SIZE + ">=" + ds2.getBytes().length;
404        final String searchUrl =
405                getSearchEndpoint() + "condition=" + encode(condition) + "&condition=" + encode(contentSizeCondition);
406        try (final CloseableHttpResponse response = execute(new HttpGet(searchUrl))) {
407            assertEquals(OK.getStatusCode(), getStatus(response));
408            final SearchResult result = objectMapper.readValue(response.getEntity().getContent(), SearchResult.class);
409            assertEquals(2, result.getItems().size());
410        }
411
412        final String searchUrl2 =
413                getSearchEndpoint() + "condition=" + encode(condition) +
414                        "&condition=" + encode(contentSizeConditionLongerLength);
415
416        try (final CloseableHttpResponse response = execute(new HttpGet(searchUrl2))) {
417            assertEquals(OK.getStatusCode(), getStatus(response));
418            final SearchResult result = objectMapper.readValue(response.getEntity().getContent(), SearchResult.class);
419            assertEquals(1, result.getItems().size());
420        }
421    }
422
423    @Test
424    public void testSearchByMimeType() throws Exception {
425        final var resourceId = getRandomUniqueId();
426        createObjectAndClose(resourceId);
427        final var ds1 = "test";
428        createDatastream(resourceId, ds1, ds1);
429        final var condition = FEDORA_ID + "=" + resourceId + "*";
430        final var matchingMimetypeCondition = MIME_TYPE + "=text/plain";
431        final var nonMatchingMimetypeCondition = MIME_TYPE + "=image/jpg";
432        final String searchUrl =
433                getSearchEndpoint() + "condition=" + encode(condition) +
434                        "&condition=" + encode(matchingMimetypeCondition);
435        try (final CloseableHttpResponse response = execute(new HttpGet(searchUrl))) {
436            assertEquals(OK.getStatusCode(), getStatus(response));
437            final SearchResult result = objectMapper.readValue(response.getEntity().getContent(), SearchResult.class);
438
439            assertEquals(1, result.getItems().size());
440        }
441
442        final String searchUrl2 =
443                getSearchEndpoint() + "condition=" + encode(condition) +
444                        "&condition=" + encode(nonMatchingMimetypeCondition);
445
446        try (final CloseableHttpResponse response = execute(new HttpGet(searchUrl2))) {
447            assertEquals(OK.getStatusCode(), getStatus(response));
448            final SearchResult result = objectMapper.readValue(response.getEntity().getContent(), SearchResult.class);
449            assertEquals(0, result.getItems().size());
450        }
451    }
452
453    @Test
454    public void testSearchNoMatchingFedoraIds() throws Exception {
455        final var condition = FEDORA_ID + "=" + serverAddress + "this-should-not-match-any-fedora-id";
456        final String searchUrl = getSearchEndpoint() + "condition=" + encode(condition);
457        try (final CloseableHttpResponse response = execute(new HttpGet(searchUrl))) {
458            assertEquals(OK.getStatusCode(), getStatus(response));
459            final SearchResult result = objectMapper.readValue(response.getEntity().getContent(), SearchResult.class);
460            assertNotNull(result);
461            assertNotNull(result.getPagination());
462            assertEquals("No results expected.", 0, result.getItems().size());
463        }
464    }
465
466    @Test
467    public void testMalformedCondition() throws Exception {
468        final var condition = "this_is_not_a_valid_condition";
469        final String searchUrl = getSearchEndpoint() + "condition=" + encode(condition);
470        try (final CloseableHttpResponse response = execute(new HttpGet(searchUrl))) {
471            assertEquals(BAD_REQUEST.getStatusCode(), getStatus(response));
472        }
473    }
474
475    @Test
476    public void testOrderingISAscendingByDefaultIfOrderByIsDefined() throws Exception {
477        final var prefix = getRandomUniqueId();
478        final var resources = createResources(prefix, 3);
479        final var condition = FEDORA_ID + "=" + prefix + "*";
480        final String searchUrl = getSearchEndpoint() + "condition=" + encode(condition) + "&order_by=fedora_id";
481        try (final CloseableHttpResponse response = execute(new HttpGet(searchUrl))) {
482            assertEquals(OK.getStatusCode(), getStatus(response));
483            final SearchResult result = objectMapper.readValue(response.getEntity().getContent(), SearchResult.class);
484            assertEquals(resources, result.getItems().stream()
485                    .map(x -> x.get("fedora_id")).collect(Collectors.toList()));
486        }
487    }
488
489    @Test
490    public void testDescOrdering() throws Exception {
491        final var prefix = getRandomUniqueId();
492        final var resources = createResources(prefix, 3);
493        Collections.reverse(resources);
494        final var condition = FEDORA_ID + "=" + prefix + "*";
495        final String searchUrl = getSearchEndpoint() + "condition=" + encode(condition) +
496                "&order_by=fedora_id&order=desc";
497        try (final CloseableHttpResponse response = execute(new HttpGet(searchUrl))) {
498            assertEquals(OK.getStatusCode(), getStatus(response));
499            final SearchResult result = objectMapper.readValue(response.getEntity().getContent(), SearchResult.class);
500            assertEquals(resources, result.getItems().stream()
501                    .map(x -> x.get("fedora_id")).collect(Collectors.toList()));
502        }
503    }
504
505    @Test
506    public void testOrderByMimetypeDesc() throws Exception {
507        final var resourceId = getRandomUniqueId();
508        createObjectAndClose(resourceId);
509        final var resourceA = resourceId + "/a-video";
510        final var resourceB = resourceId + "/b-image";
511        final var resourceC = resourceId + "/c-text";
512
513        assertEquals(201, getStatus(putObjMethod(resourceA, "video/mp4",
514                "video")));
515        assertEquals(201, getStatus(putObjMethod(resourceB, "image/jpg",
516                "image")));
517        assertEquals(201, getStatus(putObjMethod(resourceC, "text/plain",
518                "text")));
519
520        final var resources =
521                Stream.of(resourceA, resourceC, resourceB).map(x -> serverAddress + x)
522                        .collect(Collectors.toList());
523        final var condition = FEDORA_ID + "=" + resourceId + "/*";
524        final String searchUrl = getSearchEndpoint() + "condition=" + encode(condition) +
525                "&order_by=mime_type&order=desc";
526        try (final CloseableHttpResponse response = execute(new HttpGet(searchUrl))) {
527            assertEquals(OK.getStatusCode(), getStatus(response));
528            final SearchResult result = objectMapper.readValue(response.getEntity().getContent(), SearchResult.class);
529            assertEquals(resources, result.getItems().stream()
530                    .map(x -> x.get("fedora_id")).collect(Collectors.toList()));
531        }
532    }
533
534
535    @Test
536    public void testSearchByRdfSourceWildCards() throws Exception {
537        testSearchByRdfTypeParam("*Basic*");
538    }
539
540    @Test
541    public void testSearchByRdfSourceWildcard() throws Exception {
542        testSearchByRdfTypeParam("*Container");
543    }
544
545    @Test
546    public void testSearchByRdfSourceExactMatch() throws Exception {
547        testSearchByRdfTypeParam("http://www.w3.org/ns/ldp#RDFSource");
548    }
549
550    private void testSearchByRdfTypeParam(final String rdfTypeString) throws Exception {
551        final var resourceId = getRandomUniqueId();
552        createObjectAndClose(resourceId);
553        final var condition = FEDORA_ID + "=" + resourceId;
554        final var rdfTypeCondition = RDF_TYPE + "=" + rdfTypeString;
555        final String searchUrl =
556                getSearchEndpoint() + "condition=" + encode(condition) + "&condition=" + encode(rdfTypeCondition);
557        try (final CloseableHttpResponse response = execute(new HttpGet(searchUrl))) {
558            assertEquals(OK.getStatusCode(), getStatus(response));
559            final SearchResult result = objectMapper.readValue(response.getEntity().getContent(), SearchResult.class);
560            assertEquals(1, result.getItems().size());
561        }
562
563        final String searchUrl2 =
564                getSearchEndpoint() + "condition=" + encode(condition) +
565                        "&condition=" + encode(rdfTypeCondition + "with-non-matching-suffix");
566
567        try (final CloseableHttpResponse response = execute(new HttpGet(searchUrl2))) {
568            assertEquals(OK.getStatusCode(), getStatus(response));
569            final SearchResult result = objectMapper.readValue(response.getEntity().getContent(), SearchResult.class);
570            assertEquals(0, result.getItems().size());
571        }
572    }
573
574    private String encode(final String value) {
575        return URLEncoder.encode(value, StandardCharsets.UTF_8);
576    }
577
578    @Test
579    public void testTotalResults() throws Exception {
580        final String prefix = "test_total_";
581        final var resources = createResources(prefix, 6);
582        final var condition = FEDORA_ID + "=" + prefix + "*";
583        final var maxResults = 2;
584        final String searchUrl =
585                getSearchEndpoint() + "include_total_result_count=true&condition=" + encode(condition) +
586                        "&max_results=" + maxResults;
587        try (final var response = execute(new HttpGet(searchUrl))) {
588            assertEquals(OK.getStatusCode(), getStatus(response));
589            final SearchResult result = objectMapper.readValue(response.getEntity().getContent(), SearchResult.class);
590            assertEquals(2, result.getItems().size());
591            assertEquals(6, result.getPagination().getTotalResults());
592        }
593        // Try the next offset and see the total is the same
594        final String searchUrl2 = searchUrl + "&offset=2";
595        try (final var response = execute(new HttpGet(searchUrl2))) {
596            assertEquals(OK.getStatusCode(), getStatus(response));
597            final SearchResult result = objectMapper.readValue(response.getEntity().getContent(), SearchResult.class);
598            assertEquals(2, result.getItems().size());
599            assertEquals(6, result.getPagination().getTotalResults());
600        }
601    }
602
603    @Test
604    public void testTotalResultsMore() throws Exception {
605        final String prefix = "test_total1_";
606        final String prefix2 = "test_total2_";
607        final var resources = createResources(prefix, 6);
608        final var resources2 = createResources(prefix2, 6);
609        final var condition = FEDORA_ID + "=" + prefix + "*";
610        final var maxResults = 5;
611        final String searchUrl =
612                getSearchEndpoint() + "include_total_result_count=true&condition=" + encode(condition) +
613                        "&max_results=" + maxResults;
614        try (final var response = execute(new HttpGet(searchUrl))) {
615            assertEquals(OK.getStatusCode(), getStatus(response));
616            final SearchResult result = objectMapper.readValue(response.getEntity().getContent(), SearchResult.class);
617            assertEquals(5, result.getItems().size());
618            assertEquals(6, result.getPagination().getTotalResults());
619            assertTrue(result.getItems().stream().allMatch(r -> resources.contains(r.get("fedora_id").toString()) &&
620                    !resources2.contains(r.get("fedora_id").toString())));
621        }
622        // Try the next offset and see the total is the same
623        final String searchUrl2 = searchUrl + "&offset=5";
624        try (final var response = execute(new HttpGet(searchUrl2))) {
625            assertEquals(OK.getStatusCode(), getStatus(response));
626            final SearchResult result = objectMapper.readValue(response.getEntity().getContent(), SearchResult.class);
627            assertEquals(1, result.getItems().size());
628            assertEquals(6, result.getPagination().getTotalResults());
629            assertTrue(result.getItems().stream().allMatch(r -> resources.contains(r.get("fedora_id").toString()) &&
630                    !resources2.contains(r.get("fedora_id").toString())));
631        }
632    }
633
634    @Test
635    public void testUnderScoreEscaping() throws Exception {
636        final String prefix = "test_total_";
637        final String prefix2 = "test_total2_";
638        final String resource1;
639        final String resource2;
640        try (final var response = createObject(prefix + "-1")) {
641            resource1 = getLocation(response);
642        }
643        try (final var response = createObject(prefix2 + "-1")) {
644            resource2 = getLocation(response);
645        }
646        final var condition = FEDORA_ID + "=" + prefix + "*";
647
648        final String searchUrl =
649                getSearchEndpoint() + "include_total_result_count=true&condition=" + encode(condition) +
650                        "&max_results=2";
651        try (final var response = execute(new HttpGet(searchUrl))) {
652            assertEquals(OK.getStatusCode(), getStatus(response));
653            final SearchResult result = objectMapper.readValue(response.getEntity().getContent(), SearchResult.class);
654            assertEquals(1, result.getItems().size());
655            assertEquals(1, result.getPagination().getTotalResults());
656            assertEquals(resource1, result.getItems().get(0).get("fedora_id"));
657        }
658    }
659}