Skip to content

Commit

Permalink
Merge pull request #28 from ts4nfdi/feature/add-response-config-option
Browse files Browse the repository at this point in the history
Feature: add  showResponseConfiguration parameter to search endpoint to show federated response times and called urls
  • Loading branch information
syphax-bouazzouni authored Aug 22, 2024
2 parents dcb9908 + 8eac5e9 commit 68b7212
Show file tree
Hide file tree
Showing 12 changed files with 392 additions and 130 deletions.
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package org.semantics.apigateway.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import org.apache.http.HttpStatus;
import org.apache.jena.rdf.model.ResourceFactory;
import org.apache.lucene.queryparser.classic.ParseException;
Expand Down Expand Up @@ -53,7 +51,7 @@ public void home(HttpServletRequest request, HttpServletResponse response) throw
query = "*";
}

return searchService.performSearch(query + "*", allParams.get("database"), allParams.get("format"), "ols")
return searchService.performSearch(query + "*", allParams.get("database"), allParams.get("format"), "ols", false)
.<ResponseEntity<?>>thenApply(ResponseEntity::ok)
.exceptionally(e -> {
if (e.getCause() instanceof IllegalArgumentException) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,13 @@ public CompletableFuture<ResponseEntity<?>> performDynFederatedSearch(
@RequestParam(required = false) Database database,
@RequestParam(required = false) ResponseFormat format,
@Parameter(description = "Transform the response result to a specific schema")
@RequestParam(required = false) TargetDbSchema targetDbSchema) {
@RequestParam(required = false) TargetDbSchema targetDbSchema,
@Parameter(description = "Display more details about the request results")
@RequestParam(required = false, defaultValue = "false") boolean showResponseConfiguration
) {


return searchService.performSearch(query, database, format, targetDbSchema)
return searchService.performSearch(query, database, format, targetDbSchema, showResponseConfiguration)
.<ResponseEntity<?>>thenApply(ResponseEntity::ok)
.exceptionally(e -> {
if (e.getCause() instanceof IllegalArgumentException) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package org.semantics.apigateway.model.responses;

import com.fasterxml.jackson.annotation.JsonGetter;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Getter
@Setter
@NoArgsConstructor
@JsonSerialize(using = AggregatedApiResponse.CustomSerializer.class)
public class AggregatedApiResponse {
private List<Map<String, Object>> collection = new ArrayList<>();
@JsonIgnore
private List<ApiResponse> originalResponses;
private boolean showConfig = false;

@JsonGetter
public Map<String, Object> responseConfig() {
Map<String, Object> config = new HashMap<>();
config.put("totalResponseTime", totalResponseTime());

config.put("databases", originalResponses.stream().map(x -> {
Map<String, Object> response = new HashMap<>();
response.put("url", x.getUrl());
response.put("status", x.getStatusCode());
response.put("responseTime", x.getResponseTime());
return response;
}));
return config;
}


public static class CustomSerializer extends JsonSerializer<AggregatedApiResponse> {
@Override
public void serialize(AggregatedApiResponse response, JsonGenerator gen, SerializerProvider serializers) throws IOException {
if (!response.isShowConfig()) {
gen.writeStartArray();
for (Map<String, Object> item : response.getCollection()) {
gen.writeObject(item);
}
gen.writeEndArray();
} else {
gen.writeStartObject();
gen.writeFieldName("collection");
gen.writeObject(response.getCollection());
if (response.responseConfig() != null) {
gen.writeFieldName("responseConfig");
gen.writeObject(response.responseConfig());
}
gen.writeEndObject();
}
}
}

public long totalResponseTime() {
return originalResponses.stream().map(ApiResponse::getResponseTime).reduce(0L, Long::sum);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package org.semantics.apigateway.model.responses;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.jena.rdf.model.ResourceFactory;
import org.semantics.apigateway.config.OntologyConfig;
import org.semantics.apigateway.config.ResponseMapping;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Getter
@Setter
@NoArgsConstructor
public class AggregatedResourceBody {
private String iri;
private String label;
private List<String> synonym;
private List<String> description;
@JsonProperty("short_form")
private String shortForm;
private String ontology;
private String type;
private String source;
@JsonProperty("backend_type")
private String backendType;

@JsonProperty("@context")
private String context;
@JsonProperty("@type")
private String typeURI;

public static AggregatedResourceBody fromMap(Map<String, Object> item, OntologyConfig config) throws RuntimeException {
AggregatedResourceBody newItem = new AggregatedResourceBody();
ResponseMapping responseMapping = config.getResponseMapping();

// Mapping fields based on the JSON configuration
try {
if (responseMapping.getIri() != null && item.containsKey(responseMapping.getIri())) {
newItem.setIri((String) item.get(responseMapping.getIri()));
}
if (responseMapping.getLabel() != null && item.containsKey(responseMapping.getLabel())) {
newItem.setLabel((String) item.get(responseMapping.getLabel()));
}
if (responseMapping.getSynonym() != null && item.containsKey(responseMapping.getSynonym())) {
Object label = item.get(responseMapping.getSynonym());
if (label instanceof List) {
newItem.setSynonym((List<String>) label);
} else {
newItem.setSynonym(List.of(label.toString()));
}
}

if (responseMapping.getShortForm() != null && item.containsKey(responseMapping.getShortForm())) {
newItem.setShortForm((String) item.get(responseMapping.getShortForm()));
} else if (newItem.getIri() != null) {
newItem.setShortForm(
ResourceFactory.createResource(newItem.getIri()).getLocalName().toLowerCase());
}

if (responseMapping.getDescription() != null && item.containsKey(responseMapping.getDescription())) {
Object label = item.get(responseMapping.getDescription());
if (label instanceof List) {
newItem.setDescription((List<String>) label);
} else {
newItem.setDescription(List.of(label.toString()));
}
}
if (responseMapping.getOntology() != null && item.containsKey(responseMapping.getOntology())) {
if (responseMapping.getOntology().equals("links")) {
Object keysObject = ((Map<?, ?>) item).get(responseMapping.getOntology());
String ontologyItem = ((Map<?, String>) keysObject).get("ontology");
newItem.setOntology(ResourceFactory.createResource(ontologyItem).getLocalName().toLowerCase());
} else {
newItem.setOntology((String) item.get(responseMapping.getOntology()));
}
}
if (responseMapping.getType() != null && item.containsKey(responseMapping.getType())) {
if (config.getDatabase().equals("ontoportal")) {
newItem.setType("class");
// ontoportal do the search only on classes for now
} else if (config.getDatabase().equals("skosmos")) {
newItem.setType("individual");
// workaround ols type implementation that do not support skos types
} else {
newItem.setType((String) item.get(responseMapping.getType()));
}
}

// Adding the source database as part of the new item
if (String.valueOf(config.getUrl()).contains("/search?")) {
newItem.setSource(String.valueOf(config.getUrl()).substring(0, String.valueOf(config.getUrl()).indexOf("/search?")));
} else if (String.valueOf(config.getUrl()).contains("/select?")) {
newItem.setSource(String.valueOf(config.getUrl()).substring(0, String.valueOf(config.getUrl()).indexOf("/select?")));
} else {
newItem.setSource(config.getUrl());
}

if (item.containsKey("@context")) {
newItem.setContext(item.get("@context").toString());
}

if (item.containsKey("@type")) {
newItem.setTypeURI(item.get("@type").toString());
}

// Adding the backend database type as part of the new item
newItem.setBackendType(config.getDatabase());

} catch (RuntimeException e) {
throw e;
}
// logger.info("Transformed item: {}", newItem);
return newItem;
}

public Map<String, Object> toMap() {
Map<String, Object> map = new HashMap<>();

putIfNotEmpty(map, "iri", this.iri);
putIfNotEmpty(map, "label", this.label);
putIfNotEmpty(map, "synonym", this.synonym);
putIfNotEmpty(map, "description", this.description);
putIfNotEmpty(map, "short_form", this.shortForm);
putIfNotEmpty(map, "type", this.type);
putIfNotEmpty(map, "source", this.source);
putIfNotEmpty(map, "backend_type", this.backendType);
putIfNotEmpty(map, "ontology", this.ontology);

return map;
}


private void putIfNotEmpty(Map<String, Object> map, String key, Object value) {
if (value != null && !(value instanceof String && value.toString().isEmpty())) {
map.put(key, value);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.semantics.apigateway.model.responses;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.util.HashMap;
import java.util.Map;

@Getter
@Setter
@NoArgsConstructor
public class ApiResponse {
private Map<String, Object> responseBody = new HashMap<>();
private int statusCode;
private long responseTime;
private String url;
private String name;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.semantics.apigateway.model.responses;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Getter
@Setter
@NoArgsConstructor
public class TransformedApiResponse {
private List<AggregatedResourceBody> collection = new ArrayList<>();
private ApiResponse originalResponse;

public List<Map<String, Object>> getCollection() {
return collection.stream().map(AggregatedResourceBody::toMap).collect(Collectors.toList());
}
}
32 changes: 23 additions & 9 deletions src/main/java/org/semantics/apigateway/service/ApiAccessor.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@

import lombok.AllArgsConstructor;
import lombok.Setter;
import org.semantics.apigateway.model.responses.ApiResponse;
import org.slf4j.Logger;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ForkJoinPool;
Expand All @@ -32,10 +32,11 @@ public ApiAccessor() {
this.urls = new HashMap<>();
}

public CompletableFuture<Map<String, Map<String, Object>>> get(String query) {
@Async
public CompletableFuture<Map<String, ApiResponse>> get(String query) {
ForkJoinPool customThreadPool = new ForkJoinPool(10);

List<CompletableFuture<Map.Entry<String, Map<String, Object>>>> futures = this.urls.entrySet().stream()
List<CompletableFuture<Map.Entry<String, ApiResponse>>> futures = this.urls.entrySet().stream()
.map(config -> CompletableFuture.supplyAsync(() -> call(query, config.getKey(), config.getValue()), customThreadPool)
.thenApply(response -> Map.entry(config.getKey(), response))
)
Expand All @@ -50,28 +51,41 @@ public CompletableFuture<Map<String, Map<String, Object>>> get(String query) {
)
.exceptionally(e -> {
logger.error("Error processing results: {}", e.getMessage(), e);
return Collections.emptyMap(); // Return an empty map on error
return Collections.emptyMap();
});
}

@Async
protected Map<String, Object> call(String query, String url, String apikey) {

protected ApiResponse call(String query, String url, String apikey) {
ApiResponse result = new ApiResponse();
result.setUrl(url);

try {
String fullUrl = constructUrl(query, url, apikey);
logger.info("Accessing URL: {}", fullUrl);

long startTime = System.currentTimeMillis();

ResponseEntity<Map> response = restTemplate.getForEntity(fullUrl, Map.class);

long endTime = System.currentTimeMillis();
long responseTime = endTime - startTime;

result.setResponseTime(responseTime);

result.setStatusCode(response.getStatusCodeValue());

if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
logger.debug("Raw API Response: {}", response.getBody());
return response.getBody();
result.setResponseBody(response.getBody());
return result;
} else {
logger.error("API Response Error: Status Code - {}", response.getStatusCode());
return Collections.emptyMap();
return result;
}
} catch (Exception e) {
logger.error("An error occurred while processing the request: {}", e.getMessage(), e);
return Collections.emptyMap();
return result;
}
}

Expand Down
Loading

0 comments on commit 68b7212

Please sign in to comment.