diff --git a/rre-maven-plugin/rre-maven-report-plugin/src/main/java/io/sease/rre/maven/plugin/report/RREMavenReport.java b/rre-maven-plugin/rre-maven-report-plugin/src/main/java/io/sease/rre/maven/plugin/report/RREMavenReport.java index 23e7d129..9b7fa372 100644 --- a/rre-maven-plugin/rre-maven-report-plugin/src/main/java/io/sease/rre/maven/plugin/report/RREMavenReport.java +++ b/rre-maven-plugin/rre-maven-report-plugin/src/main/java/io/sease/rre/maven/plugin/report/RREMavenReport.java @@ -6,6 +6,7 @@ import io.sease.rre.maven.plugin.report.formats.OutputFormat; import io.sease.rre.maven.plugin.report.formats.impl.RREOutputFormat; import io.sease.rre.maven.plugin.report.formats.impl.SpreadsheetOutputFormat; +import io.sease.rre.maven.plugin.report.formats.impl.UrlRREOutputFormat; import io.sease.rre.persistence.impl.JsonPersistenceHandler; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; @@ -43,6 +44,7 @@ public class RREMavenReport extends AbstractMavenReport { { formatters.put("spreadsheet", new SpreadsheetOutputFormat()); formatters.put("rre-server", new RREOutputFormat()); + formatters.put("url-rre-server", new UrlRREOutputFormat()); } private final ObjectMapper mapper = new ObjectMapper(); @@ -105,6 +107,10 @@ public String getDescription(final Locale locale) { return "N.A."; } + public String getEvaluationFile() { + return evaluationFile; + } + /** * Returns the endpoint of a running RRE server. * Note that this is supposed to be used only in conjunction with the corresponding output format. diff --git a/rre-maven-plugin/rre-maven-report-plugin/src/main/java/io/sease/rre/maven/plugin/report/formats/impl/UrlRREOutputFormat.java b/rre-maven-plugin/rre-maven-report-plugin/src/main/java/io/sease/rre/maven/plugin/report/formats/impl/UrlRREOutputFormat.java new file mode 100644 index 00000000..66b1a221 --- /dev/null +++ b/rre-maven-plugin/rre-maven-report-plugin/src/main/java/io/sease/rre/maven/plugin/report/formats/impl/UrlRREOutputFormat.java @@ -0,0 +1,42 @@ +package io.sease.rre.maven.plugin.report.formats.impl; + +import com.fasterxml.jackson.databind.JsonNode; +import io.sease.rre.maven.plugin.report.RREMavenReport; +import io.sease.rre.maven.plugin.report.domain.EvaluationMetadata; +import io.sease.rre.maven.plugin.report.formats.OutputFormat; +import okhttp3.*; + +import java.io.File; +import java.util.Locale; + +import static java.util.Objects.requireNonNull; + +/** + * RRE server implementation of OutputFormat that sends a file URL to the + * server, rather than bundling the whole evaluation output in one request. + * + * @author Matt Pearce (matt@flax.co.uk) + */ +public class UrlRREOutputFormat implements OutputFormat { + + @Override + public void writeReport(JsonNode data, EvaluationMetadata metadata, Locale locale, RREMavenReport plugin) { + try { + Request request = new Request.Builder() + .url(requireNonNull(HttpUrl.parse(plugin.getEndpoint() + "/evaluation"))) + .post(RequestBody.create(MediaType.parse("application/json"), + "{ \"url\": \"" + new File(plugin.getEvaluationFile()).toURI().toURL() + "\" }")) + .build(); + + try (final Response response = new OkHttpClient().newCall(request).execute()) { + if (response.code() != 200) { + plugin.getLog().error("Exception while communicating with RREServer. Return code was: " + response.code()); + } else { + plugin.getLog().info("Evaluation data has been correctly sent to RRE Server located at " + plugin.getEndpoint()); + } + } + } catch (final Exception exception) { + plugin.getLog().error("RRE: Unable to connect to RRE Server. See below for further details.", exception); + } + } +} diff --git a/rre-server/src/main/java/io/sease/rre/server/RREServer.java b/rre-server/src/main/java/io/sease/rre/server/RREServer.java index 54d3cff0..c2de2e71 100644 --- a/rre-server/src/main/java/io/sease/rre/server/RREServer.java +++ b/rre-server/src/main/java/io/sease/rre/server/RREServer.java @@ -2,6 +2,8 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.StandardEnvironment; /** * RRE Server main entry point. diff --git a/rre-server/src/main/java/io/sease/rre/server/controllers/RREController.java b/rre-server/src/main/java/io/sease/rre/server/controllers/RREController.java index 017c01af..1cb6f53a 100644 --- a/rre-server/src/main/java/io/sease/rre/server/controllers/RREController.java +++ b/rre-server/src/main/java/io/sease/rre/server/controllers/RREController.java @@ -2,96 +2,32 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import io.sease.rre.core.domain.*; +import io.sease.rre.core.domain.Evaluation; import io.sease.rre.server.domain.EvaluationMetadata; -import io.sease.rre.server.domain.StaticMetric; +import io.sease.rre.server.services.EvaluationHandlerService; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiResponse; import io.swagger.annotations.ApiResponses; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; -import java.math.BigDecimal; -import java.math.RoundingMode; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Map; - -import static java.util.stream.StreamSupport.stream; @RestController public class RREController { - private Evaluation evaluation = new Evaluation(); - private EvaluationMetadata metadata = new EvaluationMetadata(Collections.emptyList(), Collections.emptyList()); - private ObjectMapper mapper = new ObjectMapper(); - - @PostMapping("/evaluation") - public void updateEvaluationData(@RequestBody final JsonNode evaluation) { - this.evaluation = make(evaluation); - - metadata = evaluationMetadata(this.evaluation); - } - - /** - * Creates an evaluation object from the input JSON data. - * - * @param data the JSON payload. - * @return a session evaluation instance. - */ - private Evaluation make(final JsonNode data) { - final Evaluation evaluation = new Evaluation(); - evaluation.setName(data.get("name").asText()); - - metrics(data, evaluation); - - data.get("corpora").iterator().forEachRemaining(corpusNode -> { - final String cname = corpusNode.get("name").asText(); - final Corpus corpus = evaluation.findOrCreate(cname, Corpus::new); - - metrics(corpusNode, corpus); - - corpusNode.get("topics").iterator().forEachRemaining(topicNode -> { - final String tname = topicNode.get("name").asText(); - final Topic topic = corpus.findOrCreate(tname, Topic::new); - metrics(topicNode, topic); - - topicNode.get("query-groups").iterator().forEachRemaining(groupNode -> { - final String gname = groupNode.get("name").asText(); - final QueryGroup group = topic.findOrCreate(gname, QueryGroup::new); - metrics(groupNode, group); - - groupNode.get("query-evaluations").iterator().forEachRemaining(queryNode -> { - final String qename = queryNode.get("query").asText(); - final Query q = group.findOrCreate(qename, Query::new); - metrics(queryNode, q); + @Autowired + private EvaluationHandlerService evaluationHandler; - queryNode.get("results").fields().forEachRemaining(resultsEntry -> { - final MutableQueryOrSearchResponse versionedResponse = - q.getResults().computeIfAbsent( - resultsEntry.getKey(), - version -> new MutableQueryOrSearchResponse()); - - JsonNode content = resultsEntry.getValue(); - versionedResponse.setTotalHits(content.get("total-hits").asLong(), null); - - stream(content.get("hits").spliterator(), false) - .map(hit -> mapper.convertValue(hit, Map.class)) - .forEach(hit -> versionedResponse.collect(hit, -1, null)); - }); - }); - }); - }); - }); - - return evaluation; + @PostMapping("/evaluation") + public void updateEvaluationData(@RequestBody final JsonNode requestBody) throws Exception { + evaluationHandler.processEvaluationRequest(requestBody); } public EvaluationMetadata getMetadata() { - return metadata; + return evaluationHandler.getEvaluationMetadata(); } @ApiOperation(value = "Returns the evaluation data.") @@ -101,39 +37,9 @@ public EvaluationMetadata getMetadata() { @ApiResponse(code = 414, message = "Request-URI Too Long"), @ApiResponse(code = 500, message = "System internal failure occurred.") }) - @GetMapping("/evaluation") - public Evaluation getEvaluationData() throws Exception { - return evaluation; - } - - /** - * Creates the evaluation metadata. - * - * @param evaluation the evaluation data. - * @return the evaluation metadata. - */ - private EvaluationMetadata evaluationMetadata(final Evaluation evaluation) { - final List metrics = new ArrayList<>( - evaluation.getChildren() - .iterator().next() - .getMetrics().keySet()); - - final List versions = new ArrayList<>( - evaluation.getChildren() - .iterator().next() - .getMetrics().values().iterator().next().getVersions().keySet()); - - return new EvaluationMetadata(versions, metrics); - } - - private void metrics(final JsonNode data, final DomainMember parent) { - data.get("metrics").fields().forEachRemaining(entry -> { - final StaticMetric metric = new StaticMetric(entry.getKey()); - - entry.getValue().get("versions").fields().forEachRemaining(vEntry -> { - metric.collect(vEntry.getKey(), new BigDecimal(vEntry.getValue().get("value").asDouble()).setScale(4, RoundingMode.CEILING)); - }); - parent.getMetrics().put(metric.getName(), metric); - }); + @GetMapping(value = "/evaluation", produces = { "application/json" }) + @ResponseBody + public Evaluation getEvaluationData() { + return evaluationHandler.getEvaluation(); } } diff --git a/rre-server/src/main/java/io/sease/rre/server/services/EvaluationHandlerException.java b/rre-server/src/main/java/io/sease/rre/server/services/EvaluationHandlerException.java new file mode 100644 index 00000000..cbcd6ba4 --- /dev/null +++ b/rre-server/src/main/java/io/sease/rre/server/services/EvaluationHandlerException.java @@ -0,0 +1,24 @@ +package io.sease.rre.server.services; + +/** + * Exception thrown during evaluation handling. + * + * @author Matt Pearce (matt@flax.co.uk) + */ +public class EvaluationHandlerException extends Exception { + + public EvaluationHandlerException() { + } + + public EvaluationHandlerException(String message) { + super(message); + } + + public EvaluationHandlerException(String message, Throwable cause) { + super(message, cause); + } + + public EvaluationHandlerException(Throwable cause) { + super(cause); + } +} diff --git a/rre-server/src/main/java/io/sease/rre/server/services/EvaluationHandlerService.java b/rre-server/src/main/java/io/sease/rre/server/services/EvaluationHandlerService.java new file mode 100644 index 00000000..fb6faedc --- /dev/null +++ b/rre-server/src/main/java/io/sease/rre/server/services/EvaluationHandlerService.java @@ -0,0 +1,46 @@ +package io.sease.rre.server.services; + +import com.fasterxml.jackson.databind.JsonNode; +import io.sease.rre.core.domain.Evaluation; +import io.sease.rre.server.domain.EvaluationMetadata; +import org.springframework.stereotype.Service; + +/** + * An EvaluationHandlerService can be used to process an incoming evaluation + * update request. It should extract the relevant details from the request, + * and use them to build an Evaluation object that can be used to populate + * the dashboard. + * + * The {@link #processEvaluationRequest(JsonNode)} method should ideally + * return as quickly as possible, to avoid blocking the sender of the incoming + * request. The evaluation data can then be retrieved using {@link #getEvaluation()} + * where the evaluation contains the most recently processed data. + * + * @author Matt Pearce (matt@flax.co.uk) + */ +@Service +public interface EvaluationHandlerService { + + /** + * Update the currently held evaluation data. This may be done + * asynchronously - the method should return as quickly as possible. + * + * @param requestData incoming data giving details of evaluation. + * @throws EvaluationHandlerException if the data cannot be processed. + */ + void processEvaluationRequest(final JsonNode requestData) throws EvaluationHandlerException; + + /** + * Get the current evaluation data. + * + * @return the Evaluation. + */ + Evaluation getEvaluation(); + + /** + * Get the current evaluation metadata. + * + * @return the evaluation metadata. + */ + EvaluationMetadata getEvaluationMetadata(); +} diff --git a/rre-server/src/main/java/io/sease/rre/server/services/HttpEvaluationHandlerService.java b/rre-server/src/main/java/io/sease/rre/server/services/HttpEvaluationHandlerService.java new file mode 100644 index 00000000..75f9e019 --- /dev/null +++ b/rre-server/src/main/java/io/sease/rre/server/services/HttpEvaluationHandlerService.java @@ -0,0 +1,144 @@ +package io.sease.rre.server.services; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.sease.rre.core.domain.*; +import io.sease.rre.server.domain.EvaluationMetadata; +import io.sease.rre.server.domain.StaticMetric; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static java.util.stream.StreamSupport.stream; + +/** + * Implementation of the evaluation manager service which will extract a + * complete Evaluation object from the request data. + * + * @author Matt Pearce (matt@flax.co.uk) + */ +@Service +@Profile({"http", "default"}) +public class HttpEvaluationHandlerService implements EvaluationHandlerService { + + private final ObjectMapper mapper = new ObjectMapper(); + + private Evaluation evaluation = new Evaluation(); + private EvaluationMetadata metadata = new EvaluationMetadata(Collections.emptyList(), Collections.emptyList()); + + @Override + public void processEvaluationRequest(final JsonNode requestData) throws EvaluationHandlerException { + evaluation = make(requestData); + } + + @Override + public Evaluation getEvaluation() { + return evaluation; + } + + @Override + public EvaluationMetadata getEvaluationMetadata() { + return metadata; + } + + void setEvaluation(Evaluation eval) { + this.evaluation = eval; + this.metadata = extractEvaluationMetadata(eval); + } + + ObjectMapper getMapper() { + return mapper; + } + + /** + * Creates an evaluation object from the input JSON data. + * + * @param data the JSON payload. + * @return a session evaluation instance. + */ + protected Evaluation make(final JsonNode data) { + final Evaluation evaluation = new Evaluation(); + evaluation.setName(data.get("name").asText()); + + metrics(data, evaluation); + + data.get("corpora").iterator().forEachRemaining(corpusNode -> { + final String cname = corpusNode.get("name").asText(); + final Corpus corpus = evaluation.findOrCreate(cname, Corpus::new); + + metrics(corpusNode, corpus); + + corpusNode.get("topics").iterator().forEachRemaining(topicNode -> { + final String tname = topicNode.get("name").asText(); + final Topic topic = corpus.findOrCreate(tname, Topic::new); + metrics(topicNode, topic); + + topicNode.get("query-groups").iterator().forEachRemaining(groupNode -> { + final String gname = groupNode.get("name").asText(); + final QueryGroup group = topic.findOrCreate(gname, QueryGroup::new); + metrics(groupNode, group); + + groupNode.get("query-evaluations").iterator().forEachRemaining(queryNode -> { + final String qename = queryNode.get("query").asText(); + final Query q = group.findOrCreate(qename, Query::new); + metrics(queryNode, q); + + + queryNode.get("results").fields().forEachRemaining(resultsEntry -> { + final MutableQueryOrSearchResponse versionedResponse = + q.getResults().computeIfAbsent( + resultsEntry.getKey(), + version -> new MutableQueryOrSearchResponse()); + + JsonNode content = resultsEntry.getValue(); + versionedResponse.setTotalHits(content.get("total-hits").asLong(), null); + + stream(content.get("hits").spliterator(), false) + .map(hit -> mapper.convertValue(hit, Map.class)) + .forEach(hit -> versionedResponse.collect(hit, -1, null)); + }); + }); + }); + }); + }); + + return evaluation; + } + + private void metrics(final JsonNode data, final DomainMember parent) { + data.get("metrics").fields().forEachRemaining(entry -> { + final StaticMetric metric = new StaticMetric(entry.getKey()); + + entry.getValue().get("versions").fields().forEachRemaining(vEntry -> { + metric.collect(vEntry.getKey(), new BigDecimal(vEntry.getValue().get("value").asDouble()).setScale(4, RoundingMode.CEILING)); + }); + parent.getMetrics().put(metric.getName(), metric); + }); + } + + /** + * Extract the evaluation metadata from an evaluation. + * + * @param evaluation the evaluation data. + * @return the evaluation metadata. + */ + public static EvaluationMetadata extractEvaluationMetadata(final Evaluation evaluation) { + final List metrics = new ArrayList<>( + evaluation.getChildren() + .iterator().next() + .getMetrics().keySet()); + + final List versions = new ArrayList<>( + evaluation.getChildren() + .iterator().next() + .getMetrics().values().iterator().next().getVersions().keySet()); + + return new EvaluationMetadata(versions, metrics); + } +} diff --git a/rre-server/src/main/java/io/sease/rre/server/services/URLEvaluationHandlerService.java b/rre-server/src/main/java/io/sease/rre/server/services/URLEvaluationHandlerService.java new file mode 100644 index 00000000..d9fe38ea --- /dev/null +++ b/rre-server/src/main/java/io/sease/rre/server/services/URLEvaluationHandlerService.java @@ -0,0 +1,87 @@ +package io.sease.rre.server.services; + +import com.fasterxml.jackson.databind.JsonNode; +import io.sease.rre.core.domain.Evaluation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ThreadPoolExecutor; + +/** + * Implementation of the evaluation handler that extracts a URL from the + * evaluation update request, and uses that as the endpoint from which the + * evaluation data should be read. + * + * @author Matt Pearce (matt@flax.co.uk) + */ +@Service +@Profile("url") +public class URLEvaluationHandlerService extends HttpEvaluationHandlerService implements EvaluationHandlerService { + + private static final Logger LOGGER = LoggerFactory.getLogger(URLEvaluationHandlerService.class); + + private URLEvaluationUpdater updater = null; + + @Override + public void processEvaluationRequest(JsonNode requestData) throws EvaluationHandlerException { + try { + if (updater != null && updater.isAlive()) { + throw new EvaluationHandlerException("Update is already running - request rejected!"); + } + + final String urlParam = requestData.get("url").asText(); + LOGGER.debug("Extracted URL {} from incoming request", urlParam); + + // Build the evaluation in a separate thread - avoid causing timeouts in the report plugin + updater = createUpdaterThread(new URL(urlParam)); + updater.start(); + } catch (IOException e) { + LOGGER.error("Caught IOException processing request: {}", e.getMessage()); + throw new EvaluationHandlerException(e); + } + } + + private URLEvaluationUpdater createUpdaterThread(URL evaluationUrl) { + URLEvaluationUpdater thread = new URLEvaluationUpdater(evaluationUrl); + // Run the thread in the background + thread.setDaemon(true); + return thread; + } + + + class URLEvaluationUpdater extends Thread { + + private final URL evaluationUrl; + + URLEvaluationUpdater(URL evaluationUrl) { + this.evaluationUrl = evaluationUrl; + } + + @Override + public void run() { + try { + LOGGER.info("Building evaluation from URL {}", evaluationUrl); + final JsonNode evaluationNode = readNodeFromUrl(evaluationUrl); + setEvaluation(make(evaluationNode)); + LOGGER.debug("Evaluation build complete"); + } catch (IOException e) { + LOGGER.error("Caught IOException building evaluation: {}", e.getMessage()); + } + } + + private JsonNode readNodeFromUrl(URL evaluationUrl) throws IOException { + try { + return getMapper().readTree(evaluationUrl); + } catch (IOException e) { + LOGGER.error("Caught IOException reading JSON from {}: {}", evaluationUrl, e.getMessage()); + throw e; + } + } + } +} diff --git a/rre-server/src/main/resources/static/modules/main/config-service.js b/rre-server/src/main/resources/static/modules/main/config-service.js index 4592cd6a..c0fd523b 100644 --- a/rre-server/src/main/resources/static/modules/main/config-service.js +++ b/rre-server/src/main/resources/static/modules/main/config-service.js @@ -9,13 +9,13 @@ * Request interval in milliseconds * @type {number} */ - var requestInterval = 5000; + var requestInterval = 60000; /** * The data request URL * @type {string} */ - var requestUrl = "http://127.0.0.1:8080/evaluation"; + var requestUrl = "/evaluation"; init();