Skip to content

Commit

Permalink
Merge branch 'rppd_rpb-169-customizeSuggestions' of https://github.co…
Browse files Browse the repository at this point in the history
…m/hbz/lobid-gnd into rppd

See #398
  • Loading branch information
fsteeg committed Jun 11, 2024
2 parents 52b0d3a + 85b5052 commit 37b922a
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 19 deletions.
13 changes: 6 additions & 7 deletions app/controllers/HomeController.java
Original file line number Diff line number Diff line change
Expand Up @@ -509,7 +509,7 @@ public Result search(String q, String name, String place, String subject, String
}
} catch (Throwable t) {
String message = t.getMessage() + (t.getCause() != null ? ", cause: " + t.getCause().getMessage() : "");
Logger.error("Error: {}", message);
Logger.error("Error: " + message, t);
return internalServerError(views.html.error.render(q, "Error: " + message, allHits()));
}
}
Expand Down Expand Up @@ -592,19 +592,18 @@ static String toSuggestions(JsonNode json, String labelFields) {
"placeOfBusiness", "firstAuthor", "firstComposer", "dateOfProduction");
String fields = labelFields.equals("suggest") ? defaultFields.collect(Collectors.joining(",")) : labelFields;
Stream<JsonNode> documents = Lists.newArrayList(json.elements()).stream();
Stream<JsonNode> suggestions = documents.map((JsonNode document) -> {
Stream<Map<String, Object>> suggestions = documents.map((JsonNode document) -> {
Optional<JsonNode> id = getOptional(document, "id");
Optional<JsonNode> type = getOptional(document, "type");
Stream<String> labels = Arrays.asList(fields.split(",")).stream().map(String::trim)
.map(field -> AuthorityResource.fieldValues(field, document).map((JsonNode node) -> //
(node.isTextual() ? Optional.ofNullable(node) : Optional.ofNullable(node.findValue("label")))
.orElseGet(() -> Json.toJson("")).asText()).collect(Collectors.joining(", ")));
.map(field -> AuthorityResource.fieldValues(field, document)
.collect(Collectors.joining(AuthorityResource.VALUE_DELIMITER)));
List<String> categories = filtered(Lists.newArrayList(type.orElseGet(() -> Json.toJson("[]")).elements())
.stream().map(JsonNode::asText).filter(t -> !t.equals("AuthorityResource"))
.collect(Collectors.toList()));
return Json.toJson(toSuggestionsMap(document, id, labels, categories));
return toSuggestionsMap(document, id, labels, categories);
});
return Json.toJson(suggestions.distinct().collect(Collectors.toList())).toString();
return Json.prettyPrint(Json.toJson(suggestions.distinct().collect(Collectors.toList())));
}

@SuppressWarnings("serial")
Expand Down
66 changes: 54 additions & 12 deletions app/models/AuthorityResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
import java.util.TreeSet;
import java.util.concurrent.ExecutionException;
import java.util.function.IntFunction;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
Expand All @@ -44,6 +46,7 @@ public class AuthorityResource {
public static final String RPPD_PREFIX = "https://rppd.lobid.org/";
public static final String GND_PREFIX = DNB_PREFIX + "gnd/";
public static final String ELEMENTSET = DNB_PREFIX + "standards/elementset/";
public static final String VALUE_DELIMITER = "; ";
private static final List<String> SKIP = Arrays.asList(//
// handled explicitly:
"@context", "id", "type", "depiction", "sameAs", "preferredName", "hasGeometry", "definition",
Expand Down Expand Up @@ -114,7 +117,7 @@ public String title() {
}

public String lifeDates() {
return fieldValues("dateOfBirth-dateOfDeath", json).map(JsonNode::asText)
return fieldValues("dateOfBirth-dateOfDeath", json)
.collect(Collectors.joining());
}

Expand Down Expand Up @@ -544,24 +547,63 @@ private String withDefaultHidden(String field, int size, int i, String result) {
return result;
}

public static Stream<JsonNode> fieldValues(String field, JsonNode document) {
if (field.contains("-")) {
String[] fields = field.split("-");
String v1 = year(document.findValue(fields[0]));
String v2 = year(document.findValue(fields[1]));
return v1.isEmpty() && v2.isEmpty() ? Stream.empty()
: Stream.of(Json.toJson(String.format("%s-%s", v1, v2)));
public static Stream<String> fieldValues(String field, JsonNode document) {
// standard case: `field` is a plain field name, use that:
List<String> result = flatStrings(document.findValues(field));
if (result.isEmpty()) {
// `label_fieldName` template, e.g. `*_dateOfBirth`
if (field.contains("_")) {
Matcher matcher = Pattern.compile("([^_]+)_([A-Za-z]+)").matcher(field);
while (matcher.find()) {
String label = matcher.group(1);
String fieldName = matcher.group(2);
List<JsonNode> findValues = document.findValues(fieldName);
if (!findValues.isEmpty()) {
String values = flatStrings(findValues).stream()
.collect(Collectors.joining(VALUE_DELIMITER));
field = field.replace(matcher.group(), label + " " + values);
} else {
field = field.replace(matcher.group(), "");
}
}
result = field.trim().isEmpty() ? Arrays.asList() : Arrays.asList(field);
}
// date ranges, e.g. `dateOfBirth-dateOfDeath`
else if (field.contains("-")) {
String[] fields = field.split("-");
String v1 = year(document.findValue(fields[0]));
String v2 = year(document.findValue(fields[1]));
result = v1.isEmpty() && v2.isEmpty() ? Lists.newArrayList()
: Arrays.asList(String.format("%s-%s", v1, v2));
}
}
return document.findValues(field).stream().flatMap((node) -> {
return node.isArray() ? Lists.newArrayList(node.elements()).stream() : Arrays.asList(node).stream();
});
return result.stream();
}

private static List<String> flatStrings(List<JsonNode> values) {
return values.stream().flatMap(node -> toArray(node)).map(node -> toString(node))
.collect(Collectors.toList());
}

private static Stream<JsonNode> toArray(JsonNode node) {
return node.isArray() ? Lists.newArrayList(node.elements()).stream()
: Arrays.asList(node).stream();
}

private static String toString(JsonNode node) {
return year((node.isTextual() ? Optional.ofNullable(node)
: Optional.ofNullable(node.findValue("label"))).orElseGet(() -> Json.toJson(""))
.asText());
}

private static String year(JsonNode node) {
if (node == null || !node.isArray() || node.size() == 0) {
return "";
}
String text = node.elements().next().asText();
return year(node.elements().next().asText());
}

private static String year(String text) {
return text.matches(".*\\d{4}.*") ? text.replaceAll(".*(\\d{4}).*", "$1")
: text.replaceAll(".*(\\d{3}).*", "$1");
}
Expand Down
1 change: 1 addition & 0 deletions app/views/api.scala.html
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ <h2 id="auto-complete">Autovervollständigung <small><a href='#auto-complete'><s
@desc("Standardformat für Vorschläge verwenden: \"format=json:suggest\"", routes.HomeController.search("Twain", format="json:suggest"))
@desc("Bestimmtes Feld für Vorschläge verwenden: \"format=json:preferredName\"", routes.HomeController.search("Twain", format="json:preferredName"))
@desc("Vorschläge aus mehreren Feldern zusammenbauen: \"format=json:preferredName,professionOrOccupation\"", routes.HomeController.search("Twain", format="json:preferredName,professionOrOccupation"))
@desc("Feld-Templates zur Anpassung und Gruppierung: \"format=json:preferredName,*_dateOfBirth in_placeOfBirth,†_dateOfDeath in_placeOfDeath\"", routes.HomeController.search("Twain", format="json:preferredName,*_dateOfBirth in_placeOfBirth,†_dateOfDeath in_placeOfDeath"))
<p>Damit kann z.B. eine Autovervollständigung umgesetzt werden, bei der zur Suche an Stelle des gewählten Labels die entsprechende ID verwendet werden kann:</p>
<p><form method="GET" class="form-inline" action="@routes.HomeController.search()">
<input type="text" class="search-gnd" id="label" style="width:350px" placeholder="Suchbegriff für Vorschläge eingeben"/>
Expand Down
47 changes: 47 additions & 0 deletions test/controllers/SuggestionsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,51 @@ public void suggestionsCorsHeader() {

}

@Test
public void suggestionsTemplate() {
Application application = fakeApplication();
running(application, () -> {
String format = "json:preferredName,*_dateOfBirth+in_placeOfBirth";
Result result = route(application,
fakeRequest(GET, "/search?q=*&filter=type:Person&format=" + format));
assertNotNull("We have a result", result);
assertThat(result.contentType().get(), is(equalTo("application/json")));
String content = contentAsString(result);
assertNotNull("We can parse the result as JSON", Json.parse(content));
assertTrue("We replaced the field names in the template with their values",
Json.parse(content).findValues("label").stream()
.anyMatch(label -> label.asText().contains("* 1923 in ")));
});
}

@Test
public void suggestionsTemplateMultiValues() {
Application application = fakeApplication();
running(application, () -> {
String format = "json:preferredName,gndSubjectCategory,aka_variantName";
Result result = route(application,
fakeRequest(GET, "/search?q=Weizenbaum&filter=type:Person&format=" + format));
assertNotNull("We have a result", result);
assertThat(result.contentType().get(), is(equalTo("application/json")));
String content = contentAsString(result);
assertNotNull("We can parse the result as JSON", Json.parse(content));
assertThat("Multi-values use consistent delimiter", content,
allOf(
containsString("Informatik, Datenverarbeitung; Mathematik"),
containsString("aka Weizenbaum, Josef; Weizenbaum, J.")));
});
}

@Test
public void suggestionsArePrettyPrinted() {
Application application = fakeApplication();
running(application, () -> {
Result result = route(application,
fakeRequest(GET, "/search?q=*&format=json:suggest"));
assertNotNull(result);
assertThat(result.contentType().get(), is(equalTo("application/json")));
assertThat(contentAsString(result), containsString("}, {\n"));
});
}

}

0 comments on commit 37b922a

Please sign in to comment.