AmphibianController.java
/*
* Copyright 2019 Global Crop Diversity Trust
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.genesys.server.api.v1;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import org.apache.commons.lang3.StringUtils;
import org.genesys.amphibian.client.model.DatasetTable;
import org.genesys.amphibian.client.model.HeatMap;
import org.genesys.amphibian.client.model.LongToWide;
import org.genesys.amphibian.client.model.ObservationChart;
import org.genesys.amphibian.client.model.ObservationHistogram;
import org.genesys.amphibian.client.model.Preview;
import org.genesys.amphibian.client.model.PreviewDataFilter;
import org.genesys.amphibian.client.model.ValueMapping;
import org.genesys.filerepository.InvalidRepositoryFileDataException;
import org.genesys.filerepository.InvalidRepositoryPathException;
import org.genesys.filerepository.NoSuchRepositoryFileException;
import org.genesys.filerepository.model.RepositoryFile;
import org.genesys.filerepository.service.RepositoryService;
import org.genesys.server.api.ApiBaseController;
import org.genesys.server.api.Pagination;
import org.genesys.server.component.aspect.DownloadEndpoint;
import org.genesys.server.exception.InvalidApiUsageException;
import org.genesys.server.exception.NotFoundElement;
import org.genesys.server.model.traits.Descriptor;
import org.genesys.server.service.AmphibianService;
import org.genesys.server.service.AmphibianService.AmphibianException;
import org.genesys.server.service.AmphibianService.NoAccessionsForFilterException;
import org.genesys.server.service.AmphibianService.PreviewAccessionRef;
import org.genesys.server.service.AmphibianService.TraitFilters;
import org.genesys.server.service.DatasetService;
import org.genesys.server.service.DescriptorService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
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.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.multipart.MultipartFile;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.google.common.collect.ImmutableList;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
@RestController
@PreAuthorize("isAuthenticated()")
@RequestMapping(AmphibianController.CONTROLLER_URL)
public class AmphibianController extends ApiBaseController implements InitializingBean {
private static final Logger LOG = LoggerFactory.getLogger(AmphibianController.class);
/** The Constant CONTROLLER_URL. */
public static final String CONTROLLER_URL = ApiBaseController.APIv1_BASE + "/amphibian";
@Autowired
private AmphibianService amphibianService;
@Autowired
private RepositoryService repositoryService;
@Autowired
private DescriptorService descriptorService;
@Autowired
private DatasetService datasetService;
private ObjectMapper objectMapper;
@Override
public void afterPropertiesSet() throws Exception {
objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
objectMapper.enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT);
objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
objectMapper.configure(DeserializationFeature.FAIL_ON_MISSING_EXTERNAL_TYPE_ID_PROPERTY, false);
objectMapper.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY);
}
/**
* Gets the preview.
*
* @param uuid the repositoryFile UUID
* @return the preview
* @throws NoSuchRepositoryFileException the no such repository file exception
* @throws IOException Signals that an I/O exception has occurred.
* @throws AmphibianException
*/
@GetMapping(value = "/preview/{uuid:\\w{8}\\-\\w{4}\\-.{22}}")
@ApiOperation(value = "Get preview of parsed data", notes = "Data will be parsed if the preview is not yet available")
public Preview getPreview(@PathVariable(name = "uuid", required = true) UUID uuid) throws NoSuchRepositoryFileException, IOException, AmphibianException {
RepositoryFile repositoryFile = repositoryService.getFile(uuid);
try {
return amphibianService.loadPreview(uuid);
} catch (HttpClientErrorException e) {
if (e.getStatusCode() == HttpStatus.NOT_FOUND) {
return amphibianService.makePreview(uuid, repositoryFile);
} else {
throw e;
}
}
}
/**
* Delete the preview.
*
* @param uuid the repositoryFile UUID
* @throws IOException Signals that an I/O exception has occurred.
* @throws AmphibianException
*/
@DeleteMapping(value = "/preview/{uuid:\\w{8}\\-\\w{4}\\-.{22}}")
@ApiOperation(value = "Delete the preview of parsed data")
public boolean deletePreview(@PathVariable(name = "uuid", required = true) UUID uuid) throws NoSuchRepositoryFileException, IOException, AmphibianException {
try {
var preview = amphibianService.loadPreview(uuid);
if (preview != null) {
repositoryService.getFile(uuid); // Permission check
amphibianService.deletePreview(preview);
}
return true;
} catch (HttpClientErrorException e) {
if (e.getStatusCode() == HttpStatus.NOT_FOUND) {
return false;
} else {
throw e;
}
}
}
@GetMapping(path = { "/preview/{uuid:\\w{8}\\-\\w{4}\\-.{22}}/{sheet:\\d+}/{startRow:\\d+}" })
@ApiOperation(value = "Get the overview of the parsed dataset", notes = "Use the same reference UUID as provided when ingesting a dataset")
public List<Object> getData(@ApiParam(value = "Your reference UUID", required = true) @PathVariable UUID uuid, // uuid
@ApiParam(value = "Sheet index", required = true) @PathVariable int sheet, // sheet index
@ApiParam(value = "Index of the first row", required = true) @PathVariable long startRow, // start row
@ApiParam(value = "Number of rows to return", required = false) @RequestParam(name = "count", required = false, defaultValue = "50") Optional<Integer> count,
@ApiParam(value = "Specify fields to return", required = true) @RequestParam(required = false) List<String> fields // selected columns
) throws NoSuchRepositoryFileException, AmphibianException {
repositoryService.getFile(uuid);
return amphibianService.getPreviewData(uuid, sheet, startRow, count.orElse(50), fields == null ? List.of() : fields, null);
}
@PostMapping(path = { "/preview/{uuid:\\w{8}\\-\\w{4}\\-.{22}}/{sheet:\\d+}/{startRow:\\d+}" })
@ApiOperation(value = "Get the overview data matching preview filters", notes = "Use the same reference UUID as provided when ingesting a dataset")
public List<Object> getDataFiltered(@ApiParam(value = "Your reference UUID", required = true) @PathVariable UUID uuid, // uuid
@ApiParam(value = "Sheet index", required = true) @PathVariable int sheet, // sheet index
@ApiParam(value = "Index of the first row", required = true) @PathVariable long startRow, // start row
@ApiParam(value = "Number of rows to return", required = false) @RequestParam(name = "count", required = false, defaultValue = "50") Optional<Integer> count,
@ApiParam(value = "Specify fields to return", required = true) @RequestParam(required = false) List<String> fields, // selected columns
@ApiParam(value = "Preview data filters", required = false) @RequestBody(required = false) Optional<PreviewDataFilter> previewFilter // filters
) throws NoSuchRepositoryFileException, AmphibianException {
repositoryService.getFile(uuid);
return amphibianService.getPreviewData(uuid, sheet, startRow, count.orElse(50), fields == null ? List.of() : fields, previewFilter.orElse(null));
}
@PutMapping(path = { "/preview/{uuid:\\w{8}\\-\\w{4}\\-.{22}}/{sheet:\\d+}/meta" })
@ApiOperation(value = "Add accessionRefs to Preview", notes = "Use the same reference UUID as provided when ingesting a dataset")
public long addAccessionRefs(@ApiParam(value = "Your reference UUID", required = true) @PathVariable UUID uuid, // uuid
@ApiParam(value = "Sheet index", required = true) @PathVariable int sheet, // sheet index
@ApiParam(value = "Accession references", required = true) @RequestBody(required = true) @Valid List<PreviewAccessionRef> accessionRefs // unlinked accession refs
) throws NoSuchRepositoryFileException, AmphibianException {
repositoryService.getFile(uuid);
return amphibianService.addAccessionRefs(uuid, sheet, accessionRefs);
}
@PostMapping(path = { "/preview/{uuid:\\w{8}\\-\\w{4}\\-.{22}}/pivot" })
@ApiOperation(value = "Add a sheet of wide data to the Preview", notes = "Use the same reference UUID as provided when ingesting a dataset")
public Preview longToWide(@ApiParam(value = "Your reference UUID", required = true) @PathVariable UUID uuid, // uuid
@RequestBody @Valid LongToWide longToWide
) throws NoSuchRepositoryFileException, AmphibianException {
repositoryService.getFile(uuid);
return amphibianService.longToWide(uuid, longToWide);
}
@PostMapping(value = "/upload")
@ApiOperation(value = "Upload any dataset file")
public Preview uploadFile(@RequestPart(name = "file", required = true) final MultipartFile file) throws InvalidRepositoryFileDataException,
AmphibianException, InvalidRepositoryPathException, IOException {
return amphibianService.uploadFile(file);
}
@PutMapping(value = "/update/{uuid:\\w{8}\\-\\w{4}\\-.{22}}")
@ApiOperation(value = "Update the content of the existing dataset file")
public Preview updateFileContent(@PathVariable(required = true) final UUID uuid, @RequestPart(name = "file", required = true) final MultipartFile file)
throws NoSuchRepositoryFileException, AmphibianException, IOException {
return amphibianService.updateFileContent(uuid, file);
}
@DeleteMapping(value = "/delete/{uuid:\\w{8}\\-\\w{4}\\-.{22}}")
@ApiOperation(value = "Delete the existing dataset file")
public void deleteFile(@PathVariable(required = true) final UUID uuid)
throws NoSuchRepositoryFileException, AmphibianException, IOException {
amphibianService.deleteFile(uuid);
}
@GetMapping(value = "/list-files")
@ApiOperation(value = "List of uploaded dataset files")
public List<RepositoryFile> listFiles() throws InvalidRepositoryPathException {
return amphibianService.myFiles();
}
@PostMapping(value = "/stats/{uuid:\\w{8}\\-\\w{4}\\-.{22}}/{sheet}/{startRow}")
@ApiOperation(value = "Get the statistics of the parsed dataset", notes = "Use the same reference UUID as provided when ingesting a dataset")
public List<?> getStatisticsData(@ApiParam(value = "Your reference UUID", required = true) @PathVariable UUID uuid, // uuid
@ApiParam(value = "Sheet index", required = true) @PathVariable int sheet, // sheet index
@ApiParam(value = "Index of the first row", required = true) @PathVariable long startRow, // start row
@ApiParam(value = "Maximum number of distinct values to fetch", required = false) @RequestParam(required = true, defaultValue = "100") int maxDistinct, // start row
@ApiParam(value = "Specify fields to analyze", required = true) @RequestParam(required = true) String[] fields)
throws NoSuchRepositoryFileException, AmphibianException {
repositoryService.getFile(uuid);
LOG.debug("Getting {} stats for {}", maxDistinct, fields);
return amphibianService.getStatisticsData(uuid, sheet, startRow, maxDistinct, ImmutableList.copyOf(fields));
}
@PostMapping(path = { "/preview/{uuid:\\w{8}\\-\\w{4}\\-.{22}}/{sheet:\\d+}/validate/{field:C\\d+}" })
@ApiOperation(value = "Validate Preview data against Descriptor", notes = "Use the same reference UUID as provided when ingesting a dataset")
public Set<?> validatePreviewField(
@ApiParam(value = "Your reference UUID", required = true) @PathVariable UUID uuid, // uuid
@ApiParam(value = "Sheet index", required = true) @PathVariable int sheet, // sheet index
@ApiParam(value = "Index of the first row", required = true) @RequestParam long startRow, // start row
@ApiParam(value = "Preview column to validate", required = true) @PathVariable String field, // Field to validate
@ApiParam(value = "Array separator", required = true) @RequestParam String arraySeparator, // Separator
@ApiParam(value = "Descriptor UUID", required = true) @RequestParam UUID descriptor, // descriptor
@ApiParam(value = "Value mapping") @RequestBody(required = false) List<ValueMapping> mapping // value mapping
) throws NoSuchRepositoryFileException, AmphibianException {
repositoryService.getFile(uuid);
var d = descriptorService.loadDescriptor(descriptor);
return amphibianService.ingestDryRun(uuid, sheet, startRow, field, arraySeparator, mapping, d);
}
@PostMapping(path = { "/{datasetUuid:\\w{8}\\-\\w{4}\\-.{22}}/add-observations" })
@ApiOperation(value = "Add observations from Preview to Datatable", notes = "")
public Set<?> addPreviewObservationsToDatatable(
@ApiParam(value = "Dataset UUID", required = true) @PathVariable UUID datasetUuid, // Dataset UUID
@ApiParam(value = "Descriptor UUID", required = true) @RequestParam UUID descriptor, // descriptor
@ApiParam(value = "Preview UUID", required = true) @RequestParam("preview") UUID previewUuid, // Preview UUID
@ApiParam(value = "Sheet index", required = true) @RequestParam int sheet, // sheet index
@ApiParam(value = "Index of the first row", required = true) @RequestParam long startRow, // start row
@ApiParam(value = "Preview column to validate", required = true) @RequestParam String field, // Field to validate
@ApiParam(value = "Array separator", required = true) @RequestParam String arraySeparator, // Separator
@ApiParam(value = "Value mapping") @RequestBody(required = false) List<ValueMapping> mapping // value mapping
) throws NoSuchRepositoryFileException, AmphibianException {
var dataset = datasetService.getDataset(datasetUuid);
var preview = amphibianService.loadPreview(previewUuid);
var d = descriptorService.loadDescriptor(descriptor);
return amphibianService.ingestFromPreview(dataset, d, preview, sheet, startRow, field, arraySeparator, mapping);
}
@DeleteMapping(path = { "/{datasetUuid:\\w{8}\\-\\w{4}\\-.{22}}" })
@ApiOperation(value = "Remove observations of descriptor from Datatable", notes = "")
public DatasetTable removeObservationsFromDatatable(
@ApiParam(value = "Dataset UUID", required = true) @PathVariable UUID datasetUuid, // Dataset UUID
@ApiParam(value = "Descriptor UUID", required = true) @RequestParam UUID descriptor // descriptor
) throws NoSuchRepositoryFileException, AmphibianException {
var dataset = datasetService.getDataset(datasetUuid);
var d = descriptorService.loadDescriptor(descriptor);
return amphibianService.removeObservations(dataset, d);
}
@GetMapping(path = { "/{datasetUuid:\\w{8}\\-\\w{4}\\-.{22}}" })
@ApiOperation(value = "Get Datatable info", notes = "Fetched from Amphibian for the selected Genesys Dataset")
public DatasetTable getDatatableInfo(
@ApiParam(value = "Dataset UUID", required = true) @PathVariable UUID datasetUuid // Dataset UUID
) {
var dataset = datasetService.getDataset(datasetUuid);
return amphibianService.getAmphibianDataset(dataset);
}
@PostMapping(path = { "/datatable/list" })
@ApiOperation(value = "Get list of Datatable info", notes = "Fetched from Amphibian for the selected Genesys Datasets")
public List<DatasetTable> getDatatableInfoList(
@ApiParam(value = "Dataset UUIDs", required = true) @RequestBody List<UUID> datasetUuids // Dataset UUIDs
) {
return amphibianService.getAmphibianDatasetsList(datasetUuids);
}
@PostMapping(path = { "/{datasetUuid:\\w{8}\\-\\w{4}\\-.{22}}/data" })
@ApiOperation(value = "Read observations from Datatable", notes = "")
public DatasetDataResponse filterDatasetData(
@ApiParam(value = "Dataset UUID", required = true) @PathVariable UUID datasetUuid, // Dataset UUID
final Pagination page,
@ApiParam(value = "Data filters and fields") @RequestBody(required = true) DatsetDataRequest dataRequest
) {
var dataset = datasetService.getDataset(datasetUuid);
var pagination = page.toPageRequest(MAX_PAGE_SIZE, DEFAULT_PAGE_SIZE);
var observations = amphibianService.getObservations(dataset, pagination, dataRequest.fields, dataRequest.filters);
return new DatasetDataResponse(observations, dataRequest.filters);
}
public static final class DatsetDataRequest {
public List<UUID> fields;
public TraitFilters filters;
}
public static final class DatasetDataResponse {
@JsonUnwrapped
@JsonIgnoreProperties({ "pageable" })
public Page<?> content;
public TraitFilters filters;
public DatasetDataResponse() {
}
public DatasetDataResponse(Page<?> content, TraitFilters filters) {
this.content = content;
this.filters = filters;
}
}
@PostMapping(path = { "/{datasetUuid:\\w{8}\\-\\w{4}\\-.{22}}/histogram" })
@ApiOperation(value = "getObservationsHistogram", notes = "Get histogram of numerical observations from dataset")
public List<ObservationHistogram> getObservationHistograms(
@ApiParam(value = "Table key") @PathVariable(name = "datasetUuid") UUID datasetUuid, // tableKey
@ApiParam(value = "Specify fields to return") @RequestParam(name = "fields") List<UUID> fields, // selected fields
@ApiParam(value = "Number of bins") @RequestParam(name = "bins", required = false, defaultValue = "4") Integer binsNumber, // number of bins
@ApiParam(value = "Data filters", required = true) @RequestBody(required = false) TraitFilters filter // data filters
) {
var dataset = datasetService.getDataset(datasetUuid);
if (dataset == null) {
throw new NotFoundElement("No such dataset");
}
return amphibianService.getDatasetsHistograms(List.of(dataset), fields, filter, binsNumber);
}
@PostMapping(path = { "/{datasetUuid:\\w{8}\\-\\w{4}\\-.{22}}/heatmap" })
@ApiOperation(value = "getObservationsHeatMap", notes = "Calculate a heat map of observations for two selected categorical descriptors in Dataset")
public HeatMap getHeatMapData(
@ApiParam(value = "Table key") @PathVariable(name = "datasetUuid") UUID datasetUuid, // tableKey
@ApiParam(value = "X category field uuid") @RequestParam(name = "xCategory") UUID xCategoryField, // x category field uuid
@ApiParam(value = "Y category field uuid") @RequestParam(name = "yCategory") UUID yCategoryField, // y category field uuid
@ApiParam(value = "Data filters", required = true) @RequestBody(required = false) TraitFilters filter // data filters
) throws NoAccessionsForFilterException {
var dataset = datasetService.getDataset(datasetUuid);
if (dataset == null) {
throw new NotFoundElement("No such dataset");
}
var xCategoryDescriptor = descriptorService.getDescriptor(xCategoryField);
var yCategoryDescriptor = descriptorService.getDescriptor(yCategoryField);
if (xCategoryDescriptor == null || yCategoryDescriptor == null) {
throw new NotFoundElement("No such categorical descriptor");
}
if (!xCategoryDescriptor.getDataType().equals(Descriptor.DataType.CODED) || !yCategoryDescriptor.getDataType().equals(Descriptor.DataType.CODED)) {
throw new InvalidApiUsageException("Categorical Descriptor must have CODED data type");
}
return amphibianService.getDatasetsHeatMap(List.of(dataset), xCategoryDescriptor.getUuid(), yCategoryDescriptor.getUuid(), filter);
}
@PostMapping(path = { "/{datasetUuid:\\w{8}\\-\\w{4}\\-.{22}}/chart" })
@ApiOperation(value = "getObservationsCharts", notes = "Get charts of the number of accessions for the distinct values of the selected field(s)t")
public List<ObservationChart> getObservationCharts(
@ApiParam(value = "Table key") @PathVariable(name = "datasetUuid") UUID datasetUuid, // tableKey
@ApiParam(value = "Specify fields to return") @RequestParam(name = "fields") List<UUID> fields, // selected fields
@ApiParam(value = "Data filters", required = true) @RequestBody(required = false) TraitFilters filter // data filters
) {
var dataset = datasetService.getDataset(datasetUuid);
if (dataset == null) {
throw new NotFoundElement("No such dataset");
}
return amphibianService.getDatasetsCharts(List.of(dataset), fields, filter);
}
@DownloadEndpoint
@PostMapping(path = "/{datasetUuid:\\w{8}\\-\\w{4}\\-.{22}}/download")
public void downloadObservations(
@ApiParam(value = "Table key") @PathVariable(name = "datasetUuid") UUID datasetUuid, // tableKey
@ApiParam(value = "Data filters", required = false) @RequestBody(required = false) TraitFilters filter, // data filters
final HttpServletResponse response
) throws IOException {
var dataset = datasetService.loadDataset(datasetUuid);
if (dataset == null) {
throw new NotFoundElement("No such dataset");
}
try {
response.setHeader(HttpHeaders.CACHE_CONTROL, "max-age=86400, s-maxage=86400, public, no-transform");
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.addHeader("Content-Disposition", String.format("attachment; filename=\"%s.%s\"", dataset.getTitle(), "xlsx"));
amphibianService.downloadXlsxDatasetObservations(dataset, filter, response.getOutputStream());
response.flushBuffer();
} catch (NoAccessionsForFilterException e) {
response.reset();
throw new NotFoundElement("No matching accessions found.");
}
}
@DownloadEndpoint
@RequestMapping(value = "/{datasetUuid:\\w{8}\\-\\w{4}\\-.{22}}/download", method = RequestMethod.POST, params = { "form" })
public void downloadObservationsByRequestParams(
@ApiParam(value = "Table key") @PathVariable(name = "datasetUuid") UUID datasetUuid, // tableKey
@ApiParam(value = "Data filters", required = false) @RequestParam(value="filter", required = false) String jsonFilter, // data filters
final HttpServletResponse response
) throws IOException {
var dataset = datasetService.loadDataset(datasetUuid);
if (dataset == null) {
throw new NotFoundElement("No such dataset");
}
TraitFilters filter = null;
if (StringUtils.isNotBlank(jsonFilter)) {
filter = objectMapper.readValue(jsonFilter, TraitFilters.class);
}
try {
response.setHeader(HttpHeaders.CACHE_CONTROL, "max-age=86400, s-maxage=86400, public, no-transform");
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.addHeader("Content-Disposition", String.format("attachment; filename=\"%s.%s\"", dataset.getTitle(), "xlsx"));
amphibianService.downloadXlsxDatasetObservations(dataset, filter, response.getOutputStream());
response.flushBuffer();
} catch (NoAccessionsForFilterException e) {
response.reset();
throw new NotFoundElement("No matching accessions found.");
}
}
}