package controllers;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.github.fge.jsonschema.core.exceptions.ProcessingException;
import com.github.fge.jsonschema.core.report.ListProcessingReport;
import com.github.fge.jsonschema.core.report.ProcessingReport;
import helpers.JSONForm;
import helpers.JsonLdConstants;
import helpers.SCHEMA;
import models.Commit;
import models.Record;
import models.Resource;
import models.ResourceList;
import models.TripleCommit;
import org.apache.commons.lang3.StringUtils;
import org.apache.jena.rdf.model.ResourceFactory;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.aggregations.AggregationBuilder;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import play.Configuration;
import play.Environment;
import play.Logger;
import play.mvc.Result;
import services.AggregationProvider;
import services.QueryContext;
import services.SearchConfig;
import services.export.CalendarExporter;
import services.export.CsvWithNestedIdsExporter;
import services.export.GeoJsonExporter;
import javax.inject.Inject;
import java.io.IOException;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @author fo
*/
public class ResourceIndex extends OERWorldMap {
@Inject
public ResourceIndex(Configuration aConf, Environment aEnv) {
super(aConf, aEnv);
}
public Result listDefault(String q, int from, int size, String sort, boolean list) throws IOException {
return list(q, from, size, sort, list, null);
}
public Result list(String q, int from, int size, String sort, boolean list, String extension)
throws IOException {
// Extract filters directly from query params
Map<String, List<String>> filters = new HashMap<>();
Pattern filterPattern = Pattern.compile("^filter\\.(.*)$");
for (Map.Entry<String, String[]> entry : ctx().request().queryString().entrySet()) {
Matcher filterMatcher = filterPattern.matcher(entry.getKey());
if (filterMatcher.find()) {
filters.put(filterMatcher.group(1), new ArrayList<>(Arrays.asList(entry.getValue())));
}
}
QueryContext queryContext = getQueryContext();
// Check for bounding box
String[] boundingBoxParam = ctx().request().queryString().get("boundingBox");
if (boundingBoxParam != null && boundingBoxParam.length > 0) {
String boundingBox = boundingBoxParam[0];
if (boundingBox != null) {
try {
queryContext.setBoundingBox(boundingBox);
} catch (NumberFormatException e) {
Logger.trace("Invalid bounding box: ".concat(boundingBox), e);
}
}
}
// Sort by dateCreated if no query string given
if (StringUtils.isEmpty(q) && StringUtils.isEmpty(sort)) {
sort = "dateCreated:DESC";
}
queryContext.setFetchSource(new String[]{
"@id", "@type", "dateCreated", "author", "dateModified", "contributor",
"about.@id", "about.@type", "about.name", "about.alternateName", "about.location", "about.image",
"about.provider.@id", "about.provider.@type", "about.provider.name", "about.provider.location",
"about.participant.@id", "about.participant.@type", "about.participant.name", "about.participant.location",
"about.agent.@id", "about.agent.@type", "about.agent.name", "about.agent.location",
"about.mentions.@id", "about.mentions.@type", "about.mentions.name", "about.mentions.location",
"about.mainEntity.@id", "about.mainEntity.@type", "about.mainEntity.name", "about.mainEntity.location",
"about.startDate", "about.endDate", "about.organizer", "about.description", "about.displayName"
});
queryContext.setElasticsearchFieldBoosts(new SearchConfig().getBoostsForElasticsearch());
ResourceList resourceList = mBaseRepository.query(q, from, size, sort, filters, queryContext);
Map<String, String> alternates = new HashMap<>();
String baseUrl = mConf.getString("proxy.host");
String filterString = "";
for (Map.Entry<String, List<String>> filter : filters.entrySet()) {
String filterKey = "filter.".concat(filter.getKey());
for (String filterValue : filter.getValue()) {
filterString = filterString.concat("&".concat(filterKey).concat("=").concat(filterValue));
}
}
alternates.put("JSON", baseUrl.concat(routes.ResourceIndex.list(q, 0, 9999, sort, list, "json").url().concat(filterString)));
alternates.put("CSV", baseUrl.concat(routes.ResourceIndex.list(q, 0, 9999, sort, list, "csv").url().concat(filterString)));
alternates.put("GeoJSON", baseUrl.concat(routes.ResourceIndex.list(q, 0, 9999, sort, list, "geojson").url().concat(filterString)));
if (resourceList.containsType("Event")) {
alternates.put("iCal", baseUrl.concat(routes.ResourceIndex.list(q, 0, 9999, sort, list, "ics").url().concat(filterString)));
}
Map<String, Object> scope = new HashMap<>();
scope.put("list", list);
scope.put("resources", resourceList.toResource());
scope.put("alternates", alternates);
String format = null;
if (! StringUtils.isEmpty(extension)) {
switch (extension) {
case "html":
format = "text/html";
break;
case "json":
format = "application/json";
break;
case "csv":
format = "text/csv";
break;
case "ics":
format = "text/calendar";
break;
case "geojson":
format = "application/geo+json";
break;
}
} else if (request().accepts("text/html")) {
format = "text/html";
} else if (request().accepts("text/csv")) {
format = "text/csv";
} else if (request().accepts("text/calendar")) {
format = "text/calendar";
} else if (request().accepts("application/geo+json")) {
format = "application/geo+json";
} else {
format = "application/json";
}
if (format == null) {
return notFound("Not found");
} else if (format.equals("text/html")) {
return ok(render("OER World Map", "ResourceIndex/index.mustache", scope));
} //
else if (format.equals("text/csv")) {
return ok(new CsvWithNestedIdsExporter().export(resourceList)).as("text/csv");
} //
else if (format.equals("text/calendar")) {
return ok(new CalendarExporter(Locale.ENGLISH).export(resourceList)).as("text/calendar");
} //
else if (format.equals("application/json")) {
return ok(resourceList.toResource().toString()).as("application/json");
}
else if (format.equals("application/geo+json")) {
return ok(new GeoJsonExporter().export(resourceList)).as("application/geo+json");
}
return notFound("Not found");
}
public Result importResources() throws IOException {
JsonNode json = ctx().request().body().asJson();
List<Resource> resources = new ArrayList<>();
if (json.isArray()) {
for (JsonNode node : json) {
resources.add(Resource.fromJson(node));
}
} else if (json.isObject()) {
resources.add(Resource.fromJson(json));
} else {
return badRequest();
}
mBaseRepository.importResources(resources, getMetadata());
return ok(Integer.toString(resources.size()).concat(" resources imported."));
}
public Result addResource() throws IOException {
JsonNode jsonNode = getJsonFromRequest();
if (jsonNode == null || (!jsonNode.isArray() && !jsonNode.isObject())) {
return badRequest("Bad or empty JSON");
} else if (jsonNode.isArray()) {
return upsertResources();
} else {
return upsertResource(false);
}
}
public Result updateResource(String aId) throws IOException {
// If updating a resource, check if it actually exists
Resource originalResource = mBaseRepository.getResource(aId);
if (originalResource == null) {
return notFound("Not found: ".concat(aId));
}
return upsertResource(true);
}
private Result upsertResource(boolean isUpdate) throws IOException {
// Extract resource
Resource resource = Resource.fromJson(getJsonFromRequest());
resource.put(JsonLdConstants.CONTEXT, mConf.getString("jsonld.context"));
// Person create /update only through UserIndex, which is restricted to admin
if (!isUpdate && "Person".equals(resource.getType())) {
return forbidden("Upsert Person forbidden.");
}
// Validate
Resource staged = mBaseRepository.stage(resource);
ProcessingReport processingReport = staged.validate();
if (!processingReport.isSuccess()) {
ListProcessingReport listProcessingReport = new ListProcessingReport();
try {
listProcessingReport.mergeWith(processingReport);
} catch (ProcessingException e) {
Logger.warn("Failed to create list processing report", e);
}
if (request().accepts("text/html")) {
Map<String, Object> scope = new HashMap<>();
scope.put("report", new ObjectMapper().convertValue(listProcessingReport.asJson(), ArrayList.class));
scope.put("type", resource.getType());
return badRequest(render("Upsert failed", "ProcessingReport/list.mustache", scope));
} else {
return badRequest(listProcessingReport.asJson());
}
}
// Save
mBaseRepository.addResource(resource, getMetadata());
// Respond
if (isUpdate) {
if (request().accepts("text/html")) {
return read(resource.getId(), "HEAD", "html");
} else {
return ok("Updated " + resource.getId());
}
} else {
response().setHeader(LOCATION, routes.ResourceIndex.readDefault(resource.getId(), "HEAD")
.absoluteURL(request()));
if (request().accepts("text/html")) {
return created(render("Created", "created.mustache", resource));
} else {
return created("Created " + resource.getId());
}
}
}
private Result upsertResources() throws IOException {
// Extract resources
List<Resource> resources = new ArrayList<>();
for (JsonNode jsonNode : getJsonFromRequest()) {
Resource resource = Resource.fromJson(jsonNode);
resource.put(JsonLdConstants.CONTEXT, mConf.getString("jsonld.context"));
resources.add(resource);
}
// Validate
ListProcessingReport listProcessingReport = new ListProcessingReport();
for (Resource resource : resources) {
// Person create /update only through UserIndex, which is restricted to admin
if ("Person".equals(resource.getType())) {
return forbidden("Upsert Person forbidden.");
}
// Stage and validate each resource
try {
Resource staged = mBaseRepository.stage(resource);
ProcessingReport processingMessages = staged.validate();
if (!processingMessages.isSuccess()) {
Logger.debug(processingMessages.toString());
Logger.debug(staged.toString());
}
listProcessingReport.mergeWith(processingMessages);
} catch (ProcessingException e) {
Logger.error("Could not process validation report", e);
return badRequest();
}
}
if (!listProcessingReport.isSuccess()) {
return badRequest(listProcessingReport.asJson());
}
mBaseRepository.addResources(resources, getMetadata());
return ok("Added resources");
}
public Result readDefault(String id, String version) throws IOException {
return read(id, version, null);
}
public Result read(String id, String version, String extension) throws IOException {
Resource resource = mBaseRepository.getResource(id, version);
if (null == resource) {
return notFound("Not found");
}
String type = resource.get(JsonLdConstants.TYPE).toString();
if (type.equals("Concept")) {
ResourceList relatedList = mBaseRepository.query("about.about.@id:\"".concat(id)
.concat("\" OR about.audience.@id:\"").concat(id).concat("\""), 0, 999, null, null);
resource.put("related", relatedList.getItems());
}
if (type.equals("ConceptScheme")) {
Resource conceptScheme = null;
String field = null;
if ("https://w3id.org/class/esc/scheme".equals(id)) {
conceptScheme = Resource.fromJson(mEnv.classLoader().getResourceAsStream("public/json/esc.json"));
field = "about.about.@id";
} else if ("https://w3id.org/isced/1997/scheme".equals(id)) {
field = "about.audience.@id";
conceptScheme = Resource.fromJson(mEnv.classLoader().getResourceAsStream("public/json/isced-1997.json"));
}
if (!(null == conceptScheme)) {
AggregationBuilder conceptAggregation = AggregationBuilders.filter("services")
.filter(QueryBuilders.termQuery("about.@type", "Service"));
for (Resource topLevelConcept : conceptScheme.getAsList("hasTopConcept")) {
conceptAggregation.subAggregation(
AggregationProvider.getNestedConceptAggregation(topLevelConcept, field));
}
Resource nestedConceptAggregation = mBaseRepository.aggregate(conceptAggregation);
resource.put("aggregation", nestedConceptAggregation);
return ok(render("", "ResourceIndex/ConceptScheme/read.mustache", resource));
}
}
List<Resource> comments = new ArrayList<>();
for (String commentId : resource.getIdList("comment")) {
comments.add(mBaseRepository.getResource(commentId));
}
String title;
try {
title = ((Resource) ((ArrayList<?>) resource.get("name")).get(0)).get("@value").toString();
} catch (NullPointerException e) {
title = id;
}
boolean mayEdit = (getUser() != null) && ((resource.getType().equals("Person") && getUser().getId().equals(id))
|| (!resource.getType().equals("Person"))
|| mAccountService.getGroups(getHttpBasicAuthUser()).contains("admin"));
boolean mayLog = (getUser() != null) && (mAccountService.getGroups(getHttpBasicAuthUser()).contains("admin")
|| mAccountService.getGroups(getHttpBasicAuthUser()).contains("editor"));
boolean mayAdminister = (getUser() != null) && mAccountService.getGroups(getHttpBasicAuthUser()).contains("admin");
boolean mayComment = (getUser() != null) && (!resource.getType().equals("Person"));
boolean mayDelete = (getUser() != null) && (resource.getType().equals("Person") && getUser().getId().equals(id)
|| mAccountService.getGroups(getHttpBasicAuthUser()).contains("admin"));
Map<String, Object> permissions = new HashMap<>();
permissions.put("edit", mayEdit);
permissions.put("log", mayLog);
permissions.put("administer", mayAdminister);
permissions.put("comment", mayComment);
permissions.put("delete", mayDelete);
Map<String, String> alternates = new HashMap<>();
String baseUrl = mConf.getString("proxy.host");
alternates.put("JSON", baseUrl.concat(routes.ResourceIndex.read(id, version, "json").url()));
alternates.put("CSV", baseUrl.concat(routes.ResourceIndex.read(id, version, "csv").url()));
if (resource.getType().equals("Event")) {
alternates.put("iCal", baseUrl.concat(routes.ResourceIndex.read(id, version, "ics").url()));
}
List<Commit> history = mBaseRepository.log(id);
resource = new Record(resource);
resource.put(Record.CONTRIBUTOR, history.get(0).getHeader().getAuthor());
try {
resource.put(Record.AUTHOR, history.get(history.size() - 1).getHeader().getAuthor());
} catch (NullPointerException e) {
Logger.trace("Could not read author from commit " + history.get(history.size() - 1), e);
}
resource.put(Record.DATE_MODIFIED, history.get(0).getHeader().getTimestamp()
.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME));
try {
resource.put(Record.DATE_CREATED, history.get(history.size() - 1).getHeader().getTimestamp()
.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME));
} catch (NullPointerException e) {
Logger.trace("Could not read timestamp from commit " + history.get(history.size() - 1), e);
}
Map<String, Object> scope = new HashMap<>();
scope.put("resource", resource);
scope.put("comments", comments);
scope.put("permissions", permissions);
scope.put("alternates", alternates);
String format = null;
if (! StringUtils.isEmpty(extension)) {
switch (extension) {
case "html":
format = "text/html";
break;
case "json":
format = "application/json";
break;
case "csv":
format = "text/csv";
break;
case "ics":
format = "text/calendar";
break;
}
} else if (request().accepts("text/html")) {
format = "text/html";
} else if (request().accepts("text/csv")) {
format = "text/csv";
} else if (request().accepts("text/calendar")) {
format = "text/calendar";
} else {
format = "application/json";
}
if (format == null) {
return notFound("Not found");
} else if (format.equals("text/html")) {
return ok(render(title, "ResourceIndex/read.mustache", scope));
} else if (format.equals("application/json")) {
return ok(resource.toString()).as("application/json");
} else if (format.equals("text/csv")) {
return ok(new CsvWithNestedIdsExporter().export(resource)).as("text/csv");
} else if (format.equals("text/calendar")) {
String ical = new CalendarExporter(Locale.ENGLISH).export(resource);
if (ical != null) {
return ok(ical).as("text/calendar");
}
}
return notFound("Not found");
}
public Result delete(String aId) throws IOException {
Resource resource = mBaseRepository.deleteResource(aId, getMetadata());
if (null != resource) {
// If deleting personal profile, also delete corresponding user
if ("Person".equals(resource.getType())) {
String username = mAccountService.getUsername(aId);
if (!mAccountService.removePermissions(aId)) {
Logger.error("Could not remove permissions for " + aId);
}
if (!mAccountService.setProfileId(username, null)) {
Logger.error("Could not unset profile ID for " + username);
}
if (!mAccountService.deleteUser(username)) {
Logger.error("Could not delete user " + username);
}
return ok("deleted user " + aId);
} else {
return ok("deleted resource " + aId);
}
} else {
return badRequest("Failed to delete resource " + aId);
}
}
public Result log(String aId) {
Map<String, Object> scope = new HashMap<>();
scope.put("commits", mBaseRepository.log(aId));
scope.put("resource", aId);
if (StringUtils.isEmpty(aId)) {
return ok(mBaseRepository.log(aId).toString());
}
return ok(render("Log ".concat(aId), "ResourceIndex/log.mustache", scope));
}
public Result index(String aId) {
mBaseRepository.index(aId);
return ok("Indexed ".concat(aId));
}
public Result commentResource(String aId) throws IOException {
ObjectNode jsonNode = (ObjectNode) JSONForm.parseFormData(request().body().asFormUrlEncoded());
jsonNode.put(JsonLdConstants.CONTEXT, mConf.getString("jsonld.context"));
Resource comment = Resource.fromJson(jsonNode);
comment.put("author", getUser());
comment.put("dateCreated", ZonedDateTime.now().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME));
TripleCommit.Diff diff = (TripleCommit.Diff) mBaseRepository.getDiff(comment);
diff.addStatement(ResourceFactory.createStatement(
ResourceFactory.createResource(aId), SCHEMA.comment, ResourceFactory.createResource(comment.getId())
));
Map<String, String> metadata = getMetadata();
TripleCommit.Header header = new TripleCommit.Header(metadata.get(TripleCommit.Header.AUTHOR_HEADER),
ZonedDateTime.parse(metadata.get(TripleCommit.Header.DATE_HEADER)));
TripleCommit commit = new TripleCommit(header, diff);
mBaseRepository.commit(commit);
return seeOther("/resource/" + aId);
}
public Result feed() {
ResourceList resourceList = mBaseRepository.query("", 0, 20, "dateCreated:DESC", null, getQueryContext());
Map<String, Object> scope = new HashMap<>();
scope.put("resources", resourceList.toResource());
return ok(render("OER World Map", "ResourceIndex/feed.mustache", scope));
}
public Result label(String aId) {
return ok(mBaseRepository.label(aId));
}
}