package org.lucares.pdbui; import java.io.FileInputStream; import java.io.IOException; import java.io.OutputStream; import java.text.Collator; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.SortedSet; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; import org.apache.commons.lang3.StringUtils; import org.lucares.pdb.datastore.Proposal; import org.lucares.pdb.plot.api.AxisScale; import org.lucares.pdb.plot.api.Limit; import org.lucares.pdb.plot.api.PlotSettings; import org.lucares.pdb.plot.api.PlotType; import org.lucares.pdbui.domain.Aggregate; import org.lucares.pdbui.domain.AutocompleteProposal; import org.lucares.pdbui.domain.AutocompleteProposalByValue; import org.lucares.pdbui.domain.AutocompleteResponse; import org.lucares.pdbui.domain.PlotRequest; import org.lucares.pdbui.domain.PlotResponse; import org.lucares.pdbui.domain.PlotResponseStats; import org.lucares.performance.db.PerformanceDb; import org.lucares.recommind.logs.InternalPlottingException; import org.lucares.recommind.logs.NoDataPointsException; import org.lucares.recommind.logs.PlotResult; import org.lucares.recommind.logs.Plotter; import org.lucares.utils.CollectionUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.http.CacheControl; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.util.StreamUtils; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; @Controller @EnableAutoConfiguration public class PdbController implements HardcodedValues, PropertyKeys { private static final Logger LOGGER = LoggerFactory.getLogger(PdbController.class); private final Plotter plotter; private final PerformanceDb db; private final ReentrantLock plotterLock = new ReentrantLock(); @Value("${" + PRODUCTION_MODE + ":true}") private boolean modeProduction; @Value("${" + CACHE_IMAGES_DURATION_SECONDS + ":" + CACHE_IMAGES_DURATION_SECONDS_DEFAULT + "}") private int cacheDurationInSeconds; public PdbController(final PerformanceDb db, final Plotter plotter) { this.db = db; this.plotter = plotter; } @GetMapping("/") public ModelAndView index() { final String view = "main"; final Map model = new HashMap<>(); // model.put("oldestValue", // LocalDateTime.now().minusDays(7).format(DATE_FORMAT_BEGIN)); // model.put("latestValue", LocalDateTime.now().format(DATE_FORMAT_END)); model.put("isProduction", modeProduction); return new ModelAndView(view, model); } @RequestMapping(path = "/plots", // method = RequestMethod.GET, // consumes = MediaType.APPLICATION_JSON_UTF8_VALUE, // produces = MediaType.APPLICATION_JSON_UTF8_VALUE // ) @ResponseBody ResponseEntity createPlotGet(@RequestParam(name = "request") final String request) throws InternalPlottingException, InterruptedException, JsonParseException, JsonMappingException, IOException { final ObjectMapper objectMapper = new ObjectMapper(); final PlotRequest plotRequest = objectMapper.readValue(request, PlotRequest.class); return createPlot(plotRequest); } @RequestMapping(path = "/plots", // method = RequestMethod.POST, // consumes = MediaType.APPLICATION_JSON_UTF8_VALUE, // produces = MediaType.APPLICATION_JSON_UTF8_VALUE // ) @ResponseBody ResponseEntity createPlot(@RequestBody final PlotRequest request) throws InternalPlottingException, InterruptedException { final PlotSettings plotSettings = PlotSettingsTransformer.toSettings(request); if (StringUtils.isBlank(plotSettings.getQuery())) { throw new BadRequest("The query must not be empty!"); } // TODO the UI should cancel requests that are in flight before sending a plot // request if (plotterLock.tryLock(5, TimeUnit.SECONDS)) { try { final PlotResult result = plotter.plot(plotSettings); final String imageUrl = WEB_IMAGE_OUTPUT_PATH + "/" + result.getImageName(); LOGGER.trace("image url: {}", imageUrl); final String thumbnailUrl = result.getThumbnailPath() != null ? WEB_IMAGE_OUTPUT_PATH + "/" + result.getThumbnailName() : "img/no-thumbnail.png"; final PlotResponseStats stats = PlotResponseStats.fromDataSeries(result.getDataSeries()); final PlotResponse plotResponse = new PlotResponse(stats, imageUrl, thumbnailUrl); final CacheControl cacheControl = CacheControl.maxAge(cacheDurationInSeconds, TimeUnit.SECONDS); return ResponseEntity.ok().cacheControl(cacheControl).body(plotResponse); } catch (final NoDataPointsException e) { throw new NotFoundException(e); } finally { plotterLock.unlock(); } } else { throw new ServiceUnavailableException("Too many parallel requests!"); } } @RequestMapping(path = "/plots", // method = RequestMethod.GET, // produces = MediaType.APPLICATION_OCTET_STREAM_VALUE // ) StreamingResponseBody createPlotImage(@RequestParam(name = "query", defaultValue = "") final String query, @RequestParam(name = "groupBy[]", defaultValue = "") final List aGroupBy, @RequestParam(name = "limitBy.number", defaultValue = "10") final int limit, @RequestParam(name = "limitBy.selected", defaultValue = "NO_LIMIT") final Limit limitBy, @RequestParam(name = "dateFrom", defaultValue = "") final String dateFrom, @RequestParam(name = "dateRange", defaultValue = "1 week") final String dateRange, @RequestParam(name = "axisScale", defaultValue = "LINEAR") final AxisScale axisScale, @RequestParam(name = "plotType", defaultValue = "SCATTER") final PlotType plotType, @RequestParam(name = "aggregate", defaultValue = "NONE") final Aggregate aggregate, @RequestParam(name = "keyOutside", defaultValue = "false") final boolean keyOutside, @RequestParam(name = "width", defaultValue = "1920") final int hidth, @RequestParam(name = "height", defaultValue = "1080") final int height) { return (final OutputStream outputStream) -> { if (StringUtils.isBlank(query)) { throw new BadRequest("The query must not be empty!"); } final PlotSettings plotSettings = new PlotSettings(); plotSettings.setQuery(query); plotSettings.setGroupBy(aGroupBy); plotSettings.setHeight(height); plotSettings.setWidth(hidth); plotSettings.setLimit(limit); plotSettings.setLimitBy(limitBy); plotSettings.setDateFrom(dateFrom); plotSettings.setDateRange(dateRange); plotSettings.setYAxisScale(axisScale); plotSettings.setPlotType(plotType); plotSettings.setAggregate(PlotSettingsTransformer.toAggregateInternal(aggregate)); plotSettings.setKeyOutside(keyOutside); plotSettings.setGenerateThumbnail(false); if (plotterLock.tryLock()) { try { final PlotResult result = plotter.plot(plotSettings); try (FileInputStream in = new FileInputStream(result.getImagePath().toFile())) { StreamUtils.copy(in, outputStream); } } catch (final NoDataPointsException e) { throw new NotFoundException(e); } catch (final InternalPlottingException e) { throw new InternalServerError(e); } finally { plotterLock.unlock(); } } else { throw new ServiceUnavailableException("Too many parallel requests!"); } }; } @RequestMapping(path = "/autocomplete", // method = RequestMethod.GET, // produces = MediaType.APPLICATION_JSON_UTF8_VALUE // ) @ResponseBody AutocompleteResponse autocomplete(@RequestParam(name = "query") final String query, @RequestParam(name = "caretIndex") final int caretIndex) { final AutocompleteResponse result = new AutocompleteResponse(); final int zeroBasedCaretIndex = caretIndex - 1; final List proposals = db.autocomplete(query, zeroBasedCaretIndex); final List nonEmptyProposals = CollectionUtils.filter(proposals, p -> p.hasResults()); final List autocompleteProposals = toAutocompleteProposals(nonEmptyProposals); Collections.sort(autocompleteProposals, new AutocompleteProposalByValue()); result.setProposals(autocompleteProposals); return result; } @RequestMapping(path = "/fields", // method = RequestMethod.GET, // consumes = MediaType.APPLICATION_JSON_UTF8_VALUE, // produces = MediaType.APPLICATION_JSON_UTF8_VALUE // ) @ResponseBody List fields() { final List fields = db.getFields(); fields.sort(Collator.getInstance(Locale.ENGLISH)); return fields; } @RequestMapping(path = "/fields/{fieldName}/values", // method = RequestMethod.GET, // consumes = MediaType.APPLICATION_JSON_UTF8_VALUE, // produces = MediaType.APPLICATION_JSON_UTF8_VALUE // ) @ResponseBody SortedSet fields(@PathVariable(name = "fieldName") final String fieldName, @RequestParam(name = "query") final String query) { final SortedSet fields = db.getFieldsValues(query, fieldName); return fields; } private List toAutocompleteProposals(final List proposals) { final List result = new ArrayList<>(); for (final Proposal proposal : proposals) { final AutocompleteProposal e = new AutocompleteProposal(); e.setValue(proposal.getProposedTag()); e.setNewQuery(proposal.getNewQuery()); e.setNewCaretPosition(proposal.getNewCaretPosition()); result.add(e); } return result; } }