Skip to content

Commit b003108

Browse files
Merge pull request #99 from ral-facilities/98_search_files
Add /search/files endpoint using icat.lucene
2 parents a166548 + c4a070f commit b003108

File tree

5 files changed

+162
-8
lines changed

5 files changed

+162
-8
lines changed

src/main/config/run.properties.example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,11 @@ queue.priority.investigationUser.default = 4
156156
queue.priority.authenticated = {"orcid": 6, "anon": 0}
157157
queue.priority.default = 5
158158

159+
# Whether the API endpoint perform Lucene searches is enabled
160+
search.enabled = false
161+
# The maximum number of results to return in a single request to the Lucene component
162+
search.maxResults = 10000
163+
159164
# Configurable limit for the length of the GET URL for requesting Datafiles by a list of file locations
160165
# The exact limit may depend on the server
161166
getUrlLimit=1024

src/main/java/org/icatproject/topcat/httpclient/HttpClient.java

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,46 +34,56 @@ public Integer urlLength(String offset){
3434
}
3535

3636
public Response get(String offset, Map<String, String> headers, int readTimeout) throws Exception {
37-
return send("GET", offset, headers, null, readTimeout);
37+
return send("GET", offset, headers, null, readTimeout, null);
3838
}
3939

4040
public Response get(String offset, Map<String, String> headers) throws Exception {
4141
return get(offset, headers, -1);
4242
}
4343

44+
public Response post(String offset, Map<String, String> headers, String data, int readTimeout, String contentType)
45+
throws Exception {
46+
return send("POST", offset, headers, data, readTimeout, contentType);
47+
}
48+
4449
public Response post(String offset, Map<String, String> headers, String data, int readTimeout) throws Exception {
45-
return send("POST", offset, headers, data, readTimeout);
50+
return send("POST", offset, headers, data, readTimeout, null);
51+
}
52+
53+
public Response post(String offset, Map<String, String> headers, String data, String contentType) throws Exception {
54+
return post(offset, headers, data, -1, contentType);
4655
}
4756

4857
public Response post(String offset, Map<String, String> headers, String data) throws Exception {
49-
return post(offset, headers, data, -1);
58+
return post(offset, headers, data, -1, null);
5059
}
5160

5261
public Response delete(String offset, Map<String, String> headers, int readTimeout) throws Exception {
53-
return send("DELETE", offset, headers, null, readTimeout);
62+
return send("DELETE", offset, headers, null, readTimeout, null);
5463
}
5564

5665
public Response delete(String offset, Map<String, String> headers) throws Exception {
5766
return delete(offset, headers, -1);
5867
}
5968

6069
public Response put(String offset, Map<String, String> headers, String data, int readTimeout) throws Exception {
61-
return send("PUT", offset, headers, data, readTimeout);
70+
return send("PUT", offset, headers, data, readTimeout, null);
6271
}
6372

6473
public Response put(String offset, Map<String, String> headers, String data) throws Exception {
6574
return put(offset, headers, data, -1);
6675
}
6776

6877
public Response head(String offset, Map<String, String> headers, int readTimeout) throws Exception {
69-
return send("HEAD", offset, headers, null, readTimeout);
78+
return send("HEAD", offset, headers, null, readTimeout, null);
7079
}
7180

7281
public Response head(String offset, Map<String, String> headers) throws Exception {
7382
return head(offset, headers, -1);
7483
}
7584

76-
private Response send(String method, String offset, Map<String, String> headers, String body, int readTimeout) throws Exception {
85+
private Response send(String method, String offset, Map<String, String> headers, String body, int readTimeout,
86+
String contentType) throws Exception {
7787
StringBuilder url = new StringBuilder(this.url + "/" + offset);
7888

7989
HttpURLConnection connection = null;
@@ -95,12 +105,14 @@ private Response send(String method, String offset, Map<String, String> headers,
95105
if(body != null && (method.equals("POST") || method.equals("PUT"))){
96106
connection.setDoOutput(true);
97107
connection.setRequestProperty("Content-Length", Integer.toString(body.toString().getBytes().length));
108+
if (contentType != null) {
109+
connection.setRequestProperty("Content-Type", contentType);
110+
}
98111

99112
DataOutputStream request = new DataOutputStream(connection.getOutputStream());
100113
request.writeBytes(body.toString());
101114
request.close();
102115
}
103-
104116
Integer responseCode = connection.getResponseCode();
105117

106118
Map<String, String> responseHeaders = new HashMap();

src/main/java/org/icatproject/topcat/web/rest/UserResource.java

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333

3434
import org.icatproject.topcat.domain.*;
3535
import org.icatproject.topcat.exceptions.*;
36+
import org.icatproject.topcat.httpclient.HttpClient;
3637
import org.icatproject.topcat.repository.*;
3738
import org.slf4j.Logger;
3839
import org.slf4j.LoggerFactory;
@@ -69,6 +70,9 @@ public class UserResource {
6970

7071
private String anonUserName;
7172
private String defaultPlugin;
73+
private boolean queryEnabled;
74+
private int maxResults;
75+
private int maxFileCount;
7276

7377
@PersistenceContext(unitName = "topcat")
7478
EntityManager em;
@@ -77,6 +81,9 @@ public UserResource() {
7781
Properties properties = Properties.getInstance();
7882
this.anonUserName = properties.getProperty("anonUserName", "");
7983
this.defaultPlugin = properties.getProperty("defaultPlugin", "simple");
84+
this.queryEnabled = Boolean.valueOf(properties.getProperty("search.enabled", "false"));
85+
this.maxResults = Integer.valueOf(properties.getProperty("search.maxResults", "10000"));
86+
this.maxFileCount = Integer.valueOf(properties.getProperty("queue.files.maxFileCount", "10000"));
8087
}
8188

8289
/**
@@ -1030,6 +1037,74 @@ public Response queueFiles(@FormParam("facilityName") String facilityName,
10301037
return Response.ok(jsonObjectBuilder.build()).build();
10311038
}
10321039

1040+
/**
1041+
* Perform a search using the icat.lucene component for Datafile.locations, so that
1042+
* these can then be used with the /queue/files endpoint.
1043+
*
1044+
* Must be explicitly enabled by the run.properties.
1045+
*
1046+
* @param facilityName ICAT Facility.name
1047+
* @param sessionId ICAT sessionId
1048+
* @param maxResults The number of results to request in this batch. If there are
1049+
* more matching Datafiles, these should be requested separately
1050+
* using searchAfter.
1051+
* @param query Query using Lucene syntax and supported Datafile fields.
1052+
* @param searchAfter Optionally ignore initial results and return those after the
1053+
* Datafile represented by this object. For use when
1054+
* batching/paginating results.
1055+
* @return The Datafile.location of matching results, plus the searchAfter object
1056+
* for subsequent searches if the final result wasn't reached.
1057+
* @throws Exception If authentication fails, more than the configured maxResults
1058+
* requested, or if the underlying search fails.
1059+
*/
1060+
@POST
1061+
@Path("/search/files")
1062+
public Response searchFiles(@QueryParam("facilityName") String facilityName,
1063+
@QueryParam("sessionId") String sessionId, @QueryParam("maxResults") int maxResults,
1064+
@FormParam("query") String query, @FormParam("searchAfter") String searchAfter) throws Exception {
1065+
1066+
if (!queryEnabled) {
1067+
throw new BadRequestException("Querying not enabled");
1068+
}
1069+
FacilityMap facilityMap = FacilityMap.getInstance();
1070+
facilityName = facilityMap.validateFacilityName(facilityName);
1071+
String icatUrl = facilityMap.getIcatUrl(facilityName);
1072+
IcatClient icatClient = new IcatClient(icatUrl, sessionId);
1073+
String userName = icatClient.getUserName();
1074+
1075+
if (maxResults > this.maxResults) {
1076+
throw new BadRequestException("maxResults cannot exceed " + this.maxResults);
1077+
} else if (maxResults < 1) {
1078+
maxResults = Math.min(this.maxResults, maxFileCount);
1079+
}
1080+
String queryParameters = "?maxResults=" + maxResults;
1081+
if (searchAfter != null && !searchAfter.equals("")) {
1082+
queryParameters += "&search_after=" + searchAfter;
1083+
}
1084+
1085+
JsonArrayBuilder fieldsBuilder = Json.createArrayBuilder();
1086+
fieldsBuilder.add("location");
1087+
JsonObjectBuilder queryBuilder = Json.createObjectBuilder();
1088+
queryBuilder.add("text", query);
1089+
if (!icatClient.isAdmin()) {
1090+
queryBuilder.add("user", userName);
1091+
}
1092+
JsonObjectBuilder bodyBuilder = Json.createObjectBuilder();
1093+
bodyBuilder.add("fields", fieldsBuilder);
1094+
bodyBuilder.add("query", queryBuilder);
1095+
String body = bodyBuilder.build().toString();
1096+
1097+
HttpClient httpClient = new HttpClient(icatUrl + "/icat.lucene");
1098+
String path = "datafile" + queryParameters;
1099+
String contentType = "application/json; charset=utf-8";
1100+
org.icatproject.topcat.httpclient.Response response = httpClient.post(path, Map.of(), body, contentType);
1101+
1102+
if (response.getCode() != 200) {
1103+
throw new TopcatException(response.getCode(), response.toString());
1104+
}
1105+
return Response.ok(response.toString()).build();
1106+
}
1107+
10331108
/**
10341109
* Format the filename for a queued Download, possibly one part of many.
10351110
*

src/test/java/org/icatproject/topcat/UserResourceTest.java

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ public static JavaArchive createDeployment() {
8888
private UserResource userResource;
8989

9090
private static String sessionId;
91+
private static String nonAdminSessionId;
9192

9293
@BeforeAll
9394
public static void beforeAll() {
@@ -101,6 +102,12 @@ public void setup() throws Exception {
101102
"{\"plugin\":\"simple\", \"credentials\":[{\"username\":\"root\"}, {\"password\":\"pw\"}]}", "UTF8");
102103
String response = httpClient.post("session", new HashMap<String, String>(), loginData).toString();
103104
sessionId = Utils.parseJsonObject(response).getString("sessionId");
105+
106+
loginData = "json=" + URLEncoder.encode(
107+
"{\"plugin\":\"simple\", \"credentials\":[{\"username\":\"icatuser\"}, {\"password\":\"icatuserpw\"}]}",
108+
"UTF8");
109+
response = httpClient.post("session", new HashMap<String, String>(), loginData).toString();
110+
nonAdminSessionId = Utils.parseJsonObject(response).getString("sessionId");
104111
}
105112

106113
@Test
@@ -487,6 +494,59 @@ public void testQueueFiles() throws Exception {
487494
}
488495
}
489496

497+
@Test
498+
public void testSearchFiles() throws Exception {
499+
System.out.println("DEBUG testSearchFiles");
500+
Response response = userResource.searchFiles(null, sessionId, 100, "visitId:\"Proposal 0 - 0 0\"", null);
501+
assertEquals(200, response.getStatus());
502+
JsonObject responseObject = Utils.parseJsonObject(response.getEntity().toString());
503+
JsonArray results = responseObject.getJsonArray("results");
504+
String firstSearchAfter = responseObject.getJsonObject("search_after").toString();
505+
assertEquals(100, results.size());
506+
JsonObject firstSearchAfterObject = Utils.parseJsonObject(firstSearchAfter);
507+
JsonArray firstFields = firstSearchAfterObject.getJsonArray("fields");
508+
int firstDoc = firstSearchAfterObject.getJsonNumber("doc").intValueExact();
509+
double firstScore = firstFields.getJsonNumber(0).doubleValue();
510+
long firstIcatId = firstFields.getJsonNumber(1).longValueExact();
511+
assertEquals(0, firstSearchAfterObject.getJsonNumber("shardIndex").intValueExact());
512+
assertEquals(firstScore, firstSearchAfterObject.getJsonNumber("score").doubleValue());
513+
514+
// If maxResults is not provided, it will default to 0 and then should use the queue.maxFileCount value of 3
515+
response = userResource.searchFiles(null, sessionId, 0, "+visitId:\"Proposal 0 - 0 0\"", firstSearchAfter);
516+
assertEquals(200, response.getStatus());
517+
responseObject = Utils.parseJsonObject(response.getEntity().toString());
518+
results = responseObject.getJsonArray("results");
519+
String secondSearchAfter = responseObject.getJsonObject("search_after").toString();
520+
assertEquals(3, results.size());
521+
JsonObject secondSearchAfterObject = Utils.parseJsonObject(secondSearchAfter);
522+
JsonArray secondFields = secondSearchAfterObject.getJsonArray("fields");
523+
assertEquals(0, secondSearchAfterObject.getJsonNumber("shardIndex").intValueExact());
524+
assertTrue(secondSearchAfterObject.getJsonNumber("doc").intValueExact() >= firstDoc + 3);
525+
assertEquals(firstScore, secondSearchAfterObject.getJsonNumber("score").doubleValue());
526+
assertEquals(firstScore, secondFields.getJsonNumber(0).doubleValue());
527+
assertEquals(firstIcatId + 3, secondFields.getJsonNumber(1).longValueExact());
528+
}
529+
530+
@Test
531+
public void testSearchFilesUnauthorized() throws Exception {
532+
System.out.println("DEBUG testSearchFilesUnauthorized");
533+
// This user is not on any of the investigations, so should not see any results
534+
Response response = userResource.searchFiles(null, nonAdminSessionId, 100, "visitId:\"Proposal - 0 0\"", null);
535+
assertEquals(200, response.getStatus());
536+
JsonObject responseObject = Utils.parseJsonObject(response.getEntity().toString());
537+
JsonArray results = responseObject.getJsonArray("results");
538+
JsonObject searchAfter = responseObject.getJsonObject("search_after");
539+
assertEquals(0, results.size());
540+
assertNull(searchAfter);
541+
}
542+
543+
@Test
544+
public void testSearchFilesMaxResultsExceeded() throws Exception {
545+
System.out.println("DEBUG testSearchFilesMaxResultsExceeded");
546+
Executable executable = () -> userResource.searchFiles(null, sessionId, 10001, "visitId:\"Proposal - 0 0\"", null);
547+
assertThrows(BadRequestException.class, executable);
548+
}
549+
490550
@Test
491551
public void testQueueFilesBadRequestEmpty() throws Exception {
492552
System.out.println("DEBUG testQueueFilesBadRequestEmpty");

src/test/resources/run.properties

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ queue.priority.user = {"simple/test": 1}
4242
queue.priority.authenticated = {"anon": 0}
4343
queue.priority.default = 2
4444

45+
search.enabled = true
46+
4547
# Each get request for Datafiles has a minimum size of 132, each of 3 locations is ~25
4648
# A value of 200 allows us to chunk this into one chunk of 2, and a second chunk of 1, hitting both branches of the code
4749
getUrlLimit=200

0 commit comments

Comments
 (0)