AccessionController.java
/*
* Copyright 2024 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.v2.impl;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.collect.Sets;
import com.opencsv.CSVWriter;
import io.swagger.annotations.Api;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.StopWatch;
import org.genesys.blocks.model.JsonViews;
import org.genesys.blocks.security.serialization.CurrentPermissionsWriter;
import org.genesys.server.api.ApiBaseController;
import org.genesys.server.api.FilteredPage;
import org.genesys.server.api.FilteredSlice;
import org.genesys.server.api.Pagination;
import org.genesys.server.api.ScrollPagination;
import org.genesys.server.api.model.AccessionHeaderJson;
import org.genesys.server.api.v2.facade.AccessionApiService;
import org.genesys.server.api.v2.model.impl.AccessionDTO;
import org.genesys.server.api.v2.model.impl.Taxonomy2Info;
import org.genesys.server.component.aspect.DownloadEndpoint;
import org.genesys.server.exception.ClientDisconnectedException;
import org.genesys.server.exception.InvalidApiUsageException;
import org.genesys.server.exception.SearchException;
import org.genesys.server.model.genesys.Accession;
import org.genesys.server.model.genesys.AccessionId;
import org.genesys.server.model.genesys.QAccession;
import org.genesys.server.model.impl.AccessionIdentifier3;
import org.genesys.server.service.AccessionService;
import org.genesys.server.service.AmphibianService;
import org.genesys.server.service.DownloadService;
import org.genesys.server.service.ElasticsearchService;
import org.genesys.server.service.GenesysService;
import org.genesys.server.service.ShortFilterService;
import org.genesys.server.service.filter.AccessionFilter;
import org.genesys.server.service.filter.AccessionGeoFilter;
import org.genesys.server.service.impl.AmphibianServiceImpl;
import org.genesys.server.service.worker.AccessionProcessor;
import org.genesys.server.service.worker.ShortFilterProcessor;
import org.genesys.server.service.worker.dupe.AccessionDuplicateFinder;
import org.genesys.server.service.worker.dupe.DuplicateFinder;
import org.genesys.spring.CSVMessageConverter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springdoc.api.annotations.ParameterObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Sort;
import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
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.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.RestController;
import org.springframework.web.servlet.HandlerMapping;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedWriter;
import java.io.EOFException;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
/**
* Accession API v2
*/
@RestController("accessionApi2")
@PreAuthorize("isAuthenticated()")
@RequestMapping(AccessionController.CONTROLLER_URL)
@Api(tags = { "accession" })
public class AccessionController extends ApiBaseController {
private static final Logger LOG = LoggerFactory.getLogger(AccessionController.class);
/** The Constant CONTROLLER_URL. */
public static final String CONTROLLER_URL = ApiBaseController.APIv2_BASE + "/acn";
private static final int DOWNLOAD_LIMIT = 300000;
@Autowired
private AccessionApiService accessionApiService;
@Autowired
protected ShortFilterProcessor shortFilterProcessor;
@Autowired(required = false)
private ElasticsearchService elasticsearchService;
@Autowired
private DownloadService downloadService;
@Autowired
private GenesysService genesysService;
@Autowired
private AmphibianService amphibianService;
@Autowired
private AccessionProcessor accessionProcessor;
@Autowired
private ObjectMapper objectMapper;
private final Cache<String, AccessionService.AccessionOverview> accessionOverviewCache = CacheBuilder.newBuilder().maximumSize(50).expireAfterWrite(5, TimeUnit.MINUTES).build();
private final Cache<String, AccessionService.AccessionMapInfo> accessionMapinfoCache = CacheBuilder.newBuilder().maximumSize(50).expireAfterWrite(5, TimeUnit.MINUTES).build();
private final ObjectMapper mapper = new ObjectMapper();
@Value("${frontend.url}")
private String frontendUrl;
@Value("${cdn.servers}")
private String[] cdnServers;
public static final Set<String> terms = Sets.newHashSet("institute.code", "institute.country.code3", "cropName", "crop.shortName", "taxonomy.genus", "taxonomy.species",
"taxonomy.genusSpecies", "taxonomy.grinTaxonomySpecies.name", "taxonomy.currentTaxonomySpecies.name", "countryOfOrigin.code3", "sampStat", "available", "mlsStatus",
"donorCode", "sgsv", "storage", "duplSite", "breederCode", "aegis", "curationType");
@Autowired
private AccessionDuplicateFinder duplicateFinder;
@GetMapping(value = "/id/{id}", produces = { MediaType.APPLICATION_JSON_VALUE })
public UUID uuidFromId(@PathVariable("id") final long id) {
return accessionApiService.uuidFromId(id);
}
@PostMapping(value = "/id", produces = { MediaType.APPLICATION_JSON_VALUE })
public List<UUID> uuidFromIds(@RequestBody List<Long> ids) {
return accessionApiService.uuidsFromIds(ids);
}
@GetMapping(value = "/acce-number/{acceNumber}", produces = { MediaType.APPLICATION_JSON_VALUE })
public UUID uuidFromAcceNumber(@RequestParam(value = "instCode", required = false) String instCode , @PathVariable("acceNumber") String acceNumber) {
return accessionApiService.uuidFromAcceNumber(instCode, acceNumber);
}
@PostMapping(value = "/acce-number", produces = { MediaType.APPLICATION_JSON_VALUE })
public List<UUID> uuidsFromAcceNumbers(@RequestParam(value = "instCode", required = false) String instCode, @RequestBody List<String> acceNumbers) {
return accessionApiService.uuidsFromAcceNumbers(instCode, acceNumbers);
}
/**
* Gets the accession
*
* @param uuid the uuid
* @return the subset
*/
@GetMapping(value = "/{uuid:\\w{8}\\-\\w{4}.+}", produces = { MediaType.APPLICATION_JSON_VALUE })
public AccessionDTO getByUuid(@PathVariable("uuid") final UUID uuid) {
return accessionApiService.getByUuid(uuid);
}
/**
* Gets the accession
*
* @return the subset
*/
@GetMapping(value = "/10.{doi1:[0-9]+}/**", produces = { MediaType.APPLICATION_JSON_VALUE })
public AccessionDTO getByDoi(final HttpServletRequest request) {
final String doi = ((String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE)).substring((CONTROLLER_URL + "/").length());
return accessionApiService.getByDoi(doi);
}
/**
* List accessions by filterCode or filter
*
* @param page the page
* @param filterCode short filter code
* @param filter the filter
* @return the page
* @throws IOException
*/
@PostMapping(value = "/list", produces = { MediaType.APPLICATION_JSON_VALUE, CSVMessageConverter.TEXT_CSV_VALUE })
public FilteredPage<AccessionDTO, AccessionFilter> list(@RequestParam(name = "f", required = false) String filterCode, @ParameterObject final Pagination page,
@RequestBody(required = false) AccessionFilter filter) throws IOException, SearchException {
ShortFilterService.FilterInfo<AccessionFilter> filterInfo = shortFilterProcessor.processFilter(filterCode, filter, AccessionFilter.class);
return new FilteredPage<>(filterInfo.filterCode, filterInfo.filter, accessionApiService.list(filterInfo.filter, page.toPageRequest(MAX_PAGE_SIZE, DEFAULT_PAGE_SIZE, Sort.Direction.ASC, "seqNo")));
}
/**
* Query accession fields
*
* @param page the page
* @param filterCode short filter code
* @param filter the filter
* @param select list of {@code Accession} fields to include in the response
* @param mcpd Concatenate arrays with ";". Default is <b>{@code false}</b>
* @return the page
* @throws Exception
*/
@PostMapping(value = "/query", produces = { MediaType.APPLICATION_JSON_VALUE })
public FilteredPage<?, AccessionFilter> query(
final Pagination page,
@RequestParam(name = "f", required = false) String filterCode,
@RequestBody(required = false) AccessionFilter filter,
@RequestParam List<String> select,
@RequestParam(required = false, defaultValue = "false") boolean mcpd,
HttpServletRequest request
) throws Exception {
LOG.warn("Using MCPD style? {}", mcpd);
ShortFilterService.FilterInfo<AccessionFilter> filterInfo = shortFilterProcessor.processFilter(filterCode, filter, AccessionFilter.class);
// return new FilteredPage<>(filterInfo.filterCode, filterInfo.filter, accessionService.query(filterInfo.filter, select, page.toPageRequest(MAX_PAGE_SIZE, DEFAULT_PAGE_SIZE), mcpd));
return new FilteredPage<>(filterInfo.filterCode, filterInfo.filter, accessionApiService.query(filterInfo.filter, select, page.toPageRequest(10000, DEFAULT_PAGE_SIZE), mcpd));
}
/**
* Query accession fields (for HTTP Form POST)
*
* @param page the page
* @param filterCode short filter code
* @param filter the filter
* @param select list of {@code Accession} fields to include in the response
* @param mcpd Concatenate arrays with ";". Default is <b>{@code true}</b>
* @throws Exception
*/
@PostMapping(value = "/query", produces = { CSVMessageConverter.TEXT_CSV_VALUE, CSVMessageConverter.TEXT_TSV_VALUE }, consumes = { MediaType.APPLICATION_FORM_URLENCODED_VALUE, MediaType.MULTIPART_FORM_DATA_VALUE })
public void queryCsvForm(
final Pagination page,
@RequestParam(name = "f", required = false) String filterCode,
@RequestParam String filter,
@RequestParam List<String> select,
@RequestParam(required = false, defaultValue = "true") boolean mcpd,
HttpServletRequest request,
HttpServletResponse response
) throws Exception {
queryCsv(page, filterCode, objectMapper.readValue(filter, AccessionFilter.class), select, mcpd, request, response);
}
/**
* Query accession fields.
*
* @param page the page
* @param filterCode short filter code
* @param filter the filter
* @param select list of {@code Accession} fields to include in the response
* @param mcpd Concatenate arrays with ";". Default is <b>{@code true}</b>
* @throws Exception
*/
@PostMapping(value = "/query", produces = { CSVMessageConverter.TEXT_CSV_VALUE, CSVMessageConverter.TEXT_TSV_VALUE })
public void queryCsv(
final Pagination page,
@RequestParam(name = "f", required = false) String filterCode,
@RequestBody(required = false) AccessionFilter filter,
@RequestParam List<String> select,
@RequestParam(required = false, defaultValue = "true") boolean mcpd,
HttpServletRequest request,
HttpServletResponse response
) throws Exception {
LOG.debug("Using MCPD style? {}", mcpd);
ShortFilterService.FilterInfo<AccessionFilter> filterInfo = shortFilterProcessor.processFilter(filterCode, filter, AccessionFilter.class);
response.addHeader("Content-Type", CSVMessageConverter.TEXT_TSV_VALUE);
String fileName = "query" + (StringUtils.isNotBlank(filterInfo.filterCode) ? "-".concat(filterInfo.filterCode) : "");
response.addHeader("Content-Disposition", "attachment; filename=\"" + fileName + ".csv\"");
// Does not work: response.getOutputStream().write('\uFEFF'); // UTF-8 BOM for Excel
try (CSVWriter csvWriter = new CSVWriter(new OutputStreamWriter(response.getOutputStream(), "UTF8"), '\t', '"', '\\', "\n")) {
var keysArr = new ArrayList<>();
var row = new AtomicReference<String[]>();
var counter = new AtomicInteger(0);
accessionApiService.query(filterInfo.filter, select, page.toPageRequest(1000000, 10000, Sort.Direction.ASC, "seqNo"), mcpd, (one) -> {
if (counter.getAndIncrement() == 0) {
var row1 = (Map<?, ?>) one;
var keys = row1.keySet();
keysArr.addAll(keys);
csvWriter.writeNext(row1.keySet().toArray(new String[keys.size()]), false);
row.set(new String[keys.size()]);
}
var r = row.get();
for (var i = 0; i < r.length; i++) {
var val = one.get(keysArr.get(i));
r[i] = val == null ? null : Objects.toString(val);
}
csvWriter.writeNext(r, false);
try {
if (counter.get() % 50 == 0) csvWriter.flush(); // Flush every 50 rows to check if the client is still connected
} catch (IOException e) {
throw new ClientDisconnectedException(e);
}
});
csvWriter.flush();
};
}
/**
* List distinct taxonomic data for filtered accessions
*
* @param filter the accession filter
* @throws IOException
*/
@PostMapping(value = "/species", produces = { MediaType.APPLICATION_JSON_VALUE, CSVMessageConverter.TEXT_CSV_VALUE })
public List<Taxonomy2Info> listSpecies(@RequestBody(required = false) AccessionFilter filter) throws IOException, SearchException {
return accessionApiService.listSpecies(filter);
}
static interface RootNoPermissions extends JsonViews.Root, CurrentPermissionsWriter.NoPermissions { }
/**
* List accessions by filterCode or filter
*
* @param page the page
* @param filterCode short filter code
* @param filter the filter
* @return the page
* @throws IOException
* @throws SearchException
*/
@PostMapping(value = "/images", params = { "p" }, produces = { MediaType.APPLICATION_JSON_VALUE })
public AccessionService.AccessionSuggestionPage<AccessionApiService.AccessionDetails, AccessionFilter> images(@RequestParam(name = "f", required = false) final String filterCode, @ParameterObject final Pagination page,
@RequestBody(required = false) final AccessionFilter filter) throws IOException, SearchException {
ShortFilterService.FilterInfo<AccessionFilter> filterInfo = shortFilterProcessor.processFilter(filterCode, filter, AccessionFilter.class);
// FilteredPage<AccessionService.AccessionDetails> pageRes = new FilteredPage<>(filterInfo.filterCode, filterInfo.filter, accessionService.withImages(filterInfo.filter, page.toPageRequest(MAX_PAGE_SIZE, DEFAULT_PAGE_SIZE, Sort.Direction.ASC, "seqNo")));
FilteredPage<AccessionApiService.AccessionDetails, AccessionFilter> pageRes = new FilteredPage<>(filterInfo.filterCode, filterInfo.filter, accessionApiService.withImages(filterInfo.filter, page.toPageRequest(20, 20, Sort.Direction.ASC, "seqNo")));
filterInfo.filter.images(true);
Map<String, ElasticsearchService.TermResult> suggestionRes = accessionApiService.getSuggestions(filterInfo.filter);
return new AccessionService.AccessionSuggestionPage<>(pageRes, suggestionRes);
}
@PostMapping(value = "/images", params = { "o" }, produces = { MediaType.APPLICATION_JSON_VALUE })
public AccessionService.AccessionSuggestionSlice<AccessionApiService.AccessionDetails> images(@RequestParam(name = "f", required = false) final String filterCode,
final ScrollPagination page, @RequestBody(required = false) final AccessionFilter filter) throws IOException, SearchException {
ShortFilterService.FilterInfo<AccessionFilter> filterInfo = shortFilterProcessor.processFilter(filterCode, filter, AccessionFilter.class);
FilteredSlice<AccessionApiService.AccessionDetails> pageRes = new FilteredSlice<>(filterInfo.filterCode, filterInfo.filter, accessionApiService.withImagesSlice(filterInfo.filter, page.toPageRequest(20, Sort.Direction.ASC, "seqNo")));
filterInfo.filter.images(true);
Map<String, ElasticsearchService.TermResult> suggestionRes = accessionApiService.getSuggestions(filterInfo.filter);
return new AccessionService.AccessionSuggestionSlice<>(pageRes, suggestionRes);
}
@PostMapping(value = "/images/count", produces = { MediaType.APPLICATION_JSON_VALUE })
public int countImages(@RequestParam(name = "f", required = false) final String filterCode,
@RequestBody(required = false) final AccessionFilter filter) throws SearchException, Exception {
ShortFilterService.FilterInfo<AccessionFilter> filterInfo = shortFilterProcessor.processFilter(filterCode, filter, AccessionFilter.class);
return accessionApiService.countAccessionsImages(filterInfo.filter);
}
@PreAuthorize("hasRole('USER')")
@DownloadEndpoint
@RequestMapping(value = "/downloadImages", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE, method = RequestMethod.POST)
public void downloadImages(@RequestParam(value = "f", required = false, defaultValue = "") String filterCode, HttpServletResponse response) throws Exception {
// get AccessionFilter from filterCode
AccessionFilter filter = shortFilterProcessor.filterByCode(filterCode, AccessionFilter.class);
filter.images(true);
final long countFiltered = accessionApiService.countAccessions(filter);
// LOG.info("Attempting to download images for {} accessions", countFiltered);
if (countFiltered > 100) {
throw new InvalidApiUsageException("Refusing to export more than " + 100 + " entries");
}
// Write Zip file archive to the stream.
response.setBufferSize(4*1024);
response.setContentType("application/zip");
response.addHeader("Content-Disposition", String.format("attachment; filename=\"genesys-images-%1$s.zip\"", filterCode));
try (final OutputStream outputStream = response.getOutputStream()) {
try {
downloadService.writeAccessionImageArchive(filter, outputStream);
outputStream.flush();
response.flushBuffer();
} catch (Throwable e) {
outputStream.write(e.getMessage().getBytes());
throw e;
}
}
}
/**
* List accessions by filterCode or filter
*
* @param page the page
* @param filterCode short filter code
* @param filter the filter
* @return the page
* @throws IOException
*/
@PostMapping(value = "/filter", produces = { MediaType.APPLICATION_JSON_VALUE })
public AccessionService.AccessionSuggestionPage<AccessionDTO, AccessionFilter> filter(@RequestParam(name = "f", required = false) String filterCode, @ParameterObject final Pagination page,
@RequestBody(required = false) AccessionFilter filter) throws IOException, SearchException {
LOG.debug("Received filter: {}", filter);
ShortFilterService.FilterInfo<AccessionFilter> filterInfo = shortFilterProcessor.processFilter(filterCode, filter, AccessionFilter.class);
LOG.debug("Processed filter: {}", filterInfo.filter);
var pageRes = new FilteredPage<>(filterInfo.filterCode, filterInfo.filter, accessionApiService.list(filterInfo.filter, page.toPageRequest(MAX_PAGE_SIZE, DEFAULT_PAGE_SIZE, Sort.Direction.ASC, "seqNo")));
Map<String, ElasticsearchService.TermResult> suggestionRes = accessionApiService.getSuggestions(filterInfo.filter);
return new AccessionService.AccessionSuggestionPage<>(pageRes, suggestionRes);
}
/**
* Get term overview for filters
*
* @param filterCode short filter code
* @param filter the filter
* @return the page
* @throws SearchException
*/
@PostMapping(value = "/overview", produces = { MediaType.APPLICATION_JSON_VALUE })
public AccessionService.AccessionOverview overview(@RequestParam(name = "f", required = false) final String filterCode, @RequestBody(required = false) final AccessionFilter filter,
@RequestParam(name = "limit", defaultValue = "10", required = false) final int limit) throws IOException, SearchException, ExecutionException, InterruptedException {
ShortFilterService.FilterInfo<AccessionFilter> filterInfo = shortFilterProcessor.processFilter(filterCode, filter, AccessionFilter.class);
return accessionOverviewCache.get(filterInfo.filterCode + ",limit=" + limit, () -> {
var stopWatch = StopWatch.createStarted();
Map<String, ElasticsearchService.TermResult> overview = elasticsearchService.termStatisticsAuto(Accession.class, filterInfo.filter, Math.min(50, limit), terms.toArray(new String[] {}));
LOG.info("overview termStatisticsAuto {}ms", stopWatch.getTime());
ElasticsearchService.TermResult result = elasticsearchService.recountResult(Accession.class, QAccession.accession.accessionId().storage, filterInfo.filter, overview.get("storage"), "storage");
LOG.info("overview recountResult {}ms", stopWatch.getTime());
overview.put("storage", result);
long accessionCount = accessionApiService.countAccessions(filterInfo.filter);
LOG.info("overview countAccessions {}ms", stopWatch.getTime());
Map<String, ElasticsearchService.TermResult> suggestions = accessionApiService.getSuggestions(filterInfo.filter);
LOG.info("overview getSuggestions {}ms", stopWatch.getTime());
return AccessionService.AccessionOverview.from(filterInfo.filterCode, filterInfo.filter, overview, accessionCount, suggestions);
});
}
/**
* Get overview tree for filters and provided terms
*
* @param filterCode short filter code
* @param filter the filter
* @param terms the terms (order is important!)
* @return the overview tree
*
* @throws SearchException the search exception
* @throws IOException the IO exception
*/
@PostMapping(value = "/overview-tree", produces = { MediaType.APPLICATION_JSON_VALUE })
public ElasticsearchService.TreeNode overviewTree(@RequestParam(name = "f", required = false) final String filterCode,
@RequestBody(required = false) final AccessionFilter filter,
@RequestParam(name = "terms", required = true) final String[] terms) throws SearchException, IOException {
if (ArrayUtils.isEmpty(terms)) {
throw new InvalidApiUsageException("Terms must be provided!");
}
ShortFilterService.FilterInfo<AccessionFilter> filterInfo = shortFilterProcessor.processFilter(filterCode, filter, AccessionFilter.class);
var rootNode = elasticsearchService.treeNodeStatistics(Accession.class, filterInfo.filter, terms);
rootNode.filterCode = filterInfo.filterCode;
return rootNode;
}
/**
* Load more data for the specified term
*
* @param filterCode short filter code
* @param filter the filter
* @param term the term
* @return the term result
* @throws SearchException the search exception
* @throws IOException signals that an I/O exception has occurred
*/
@PostMapping(value = "/overview/{term}", produces = { MediaType.APPLICATION_JSON_VALUE })
public ElasticsearchService.TermResult loadMoreTerms(@PathVariable(name = "term") final String term, @RequestBody(required = false) final AccessionFilter filter,
@RequestParam(name = "f", required = false) final String filterCode, @RequestParam(name = "limit", defaultValue = "20", required = false) final int limit)
throws IOException, SearchException, ExecutionException, InterruptedException {
if (! terms.contains(term)) {
throw new InvalidApiUsageException("No such term. Will not search.");
}
ShortFilterService.FilterInfo<AccessionFilter> filterInfo = shortFilterProcessor.processFilter(filterCode, filter, AccessionFilter.class);
ElasticsearchService.TermResult termResult = elasticsearchService.termStatisticsAuto(Accession.class, filterInfo.filter, Math.min(200, limit), term);
if (term.equals("storage")) {
termResult = elasticsearchService.recountResult(Accession.class, QAccession.accession.accessionId().storage, filterInfo.filter, termResult, "storage");
}
return termResult;
}
/**
* Gets accessions by list of uuid-s
*
* @param uuids accession identifi`ers to lookup in DB
* @return list of Accessions
*/
@PostMapping(value = "/for-uuid", produces = { MediaType.APPLICATION_JSON_VALUE, CSVMessageConverter.TEXT_CSV_VALUE })
public List<AccessionDTO> forUuids(@RequestBody Set<UUID> uuids) {
return accessionApiService.forUuids(uuids);
}
/**
* Converts AccessionIdentifiers to UUID
*
* @param identifiers accession identifiers to lookup in DB
* @return map with UUIDs and related AccessionIdentifiers
*/
@PostMapping(value = "/toUUID", produces = { MediaType.APPLICATION_JSON_VALUE })
public Map<UUID, AccessionIdentifier3> toUUID(@RequestBody List<AccessionHeaderJson> identifiers) {
return accessionApiService.toUUID(identifiers);
}
@GetMapping(value = "/details/10.{doi1:[0-9]+}/**", produces = MediaType.APPLICATION_JSON_VALUE)
public AccessionApiService.AccessionDetails getAccessionDetailsByDoi(final HttpServletRequest request) {
final String doi = ((String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE)).substring((CONTROLLER_URL + "/details/").length());
return accessionApiService.getAccessionDetailsByDoi(doi);
}
@GetMapping(value = "/details/{uuid:\\w{8}\\-\\w{4}.+}", produces = MediaType.APPLICATION_JSON_VALUE)
public AccessionApiService.AccessionDetails getAccessionDetailsByUUID(@PathVariable("uuid") final UUID uuid) {
return accessionApiService.getAccessionDetailsByUuid(uuid);
}
@PreAuthorize("isAuthenticated()")
@GetMapping(value = "/similar/{uuid:\\w{8}\\-\\w{4}.+}", produces = MediaType.APPLICATION_JSON_VALUE)
public List<DuplicateFinder.Hit<AccessionDTO>> getSimilarAccessionsForUUID(@PathVariable("uuid") final UUID uuid) {
return accessionApiService.getSimilarAccessionsForUUID(uuid);
}
@PreAuthorize("isAuthenticated()")
@PostMapping(value = "/similar/{uuid:\\w{8}\\-\\w{4}.+}", produces = MediaType.APPLICATION_JSON_VALUE)
public List<DuplicateFinder.Hit<AccessionDTO>> getSimilarAccessionsForUUID(@PathVariable("uuid") final UUID uuid, @RequestBody(required = false) AccessionFilter filter) {
return accessionApiService.getSimilarAccessionsForUUID(uuid, filter);
}
@PreAuthorize("isAuthenticated()")
@GetMapping(value = "/auditlog/10.{doi1:[0-9]+}/**", produces = MediaType.APPLICATION_JSON_VALUE)
public AccessionService.AccessionAuditLog getAccessionAuditLogByDoi(final HttpServletRequest request) {
final String doi = ((String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE)).substring((CONTROLLER_URL + "/auditlog/").length());
return accessionApiService.getAccessionAuditLogByDoi(doi);
}
@PreAuthorize("isAuthenticated()")
@GetMapping(value = "/auditlog/{uuid:\\w{8}\\-\\w{4}.+}", produces = MediaType.APPLICATION_JSON_VALUE)
public AccessionService.AccessionAuditLog getAccessionAuditLogByUUID(@PathVariable("uuid") final UUID uuid) {
return accessionApiService.getAccessionAuditLogByUUID(uuid);
}
@PostMapping(value = "/mapinfo", produces = MediaType.APPLICATION_JSON_VALUE)
public AccessionService.AccessionMapInfo mapInfo(@RequestParam(value = "f", required = false) String filterCode, @RequestBody(required = false) AccessionFilter filter) throws IOException, SearchException, ExecutionException {
ShortFilterService.FilterInfo<AccessionFilter> filterInfo = shortFilterProcessor.processFilter(filterCode, filter, AccessionFilter.class);
return accessionMapinfoCache.get(filterInfo.filterCode, () -> {
// Force only georeferenced accessions
AccessionFilter georefFilter = filterInfo.filter.copy(AccessionFilter.class);
georefFilter.geo().referenced(true);
AccessionService.AccessionMapInfo mapInfo = new AccessionService.AccessionMapInfo();
mapInfo.filterCode = filterInfo.filterCode;
mapInfo.filter = filterInfo.filter;
if (StringUtils.isBlank(filterInfo.filterCode)) {
// Entire map
mapInfo.bounds = AccessionService.DEFAULT_GEOBOUNDS;
} else {
mapInfo.bounds = accessionApiService.getGeoBounds(georefFilter);
}
mapInfo.accessionCount = accessionApiService.countAccessions(georefFilter);
mapInfo.tileServers = cdnServers;
mapInfo.suggestions= accessionApiService.getSuggestions(filterInfo.filter);
return mapInfo;
});
}
/**
* Returns accession json by filter
*
* @param limit - max count of accession returned
* @param filter - filter
* @return json with minimal accession data
*/
@PostMapping(value = "/geoJson", produces = MediaType.APPLICATION_JSON_VALUE)
public ObjectNode geoJson(@RequestParam(value = "limit", required = false, defaultValue = "100") Long limit,
@RequestBody AccessionFilter filter) throws Exception {
final ObjectNode geoJson = mapper.createObjectNode();
final ArrayNode featuresArray = geoJson.arrayNode();
accessionProcessor.process(filter, (accessions) -> {
for (Accession accession: accessions) {
final ObjectNode feature = featuresArray.objectNode();
feature.put("type", "Feature");
feature.put("id", accession.getId());
ObjectNode geometry;
feature.set("geometry", geometry = feature.objectNode());
geometry.put("type", "Point");
ArrayNode coordArray;
geometry.set("coordinates", coordArray = geometry.arrayNode());
coordArray.add(accession.getAccessionId().getLongitude());
coordArray.add(accession.getAccessionId().getLatitude());
ObjectNode properties;
feature.set("properties", properties = feature.objectNode());
properties.put("uuid", accession.getAccessionId().getUuid().toString());
properties.put("doi", accession.getDoi());
properties.put("accessionNumber", accession.getAccessionNumber());
properties.put("instCode", accession.getInstCode());
properties.put("datum", accession.getAccessionId().getCoordinateDatum());
properties.put("uncertainty", accession.getAccessionId().getCoordinateUncertainty());
featuresArray.add(feature);
}
}, limit);
geoJson.set("geoJson", featuresArray);
long accessionCount = accessionApiService.countAccessions(filter);
if (accessionCount > limit) {
geoJson.put("otherCount", accessionCount - limit);
}
return geoJson;
}
@GetMapping(value = "/autocomplete/{field:.+}", produces = MediaType.APPLICATION_JSON_VALUE)
public List<AccessionService.LabelValue<String>> autocomplete(@PathVariable("field") String field,
@RequestParam(value = "term", required = true) String term,
@RequestParam(name = "f", required = false) String filterCode, @RequestBody(required = false) AccessionFilter filter) throws IOException {
ShortFilterService.FilterInfo<AccessionFilter> filterInfo = shortFilterProcessor.processFilter(filterCode, filter, AccessionFilter.class);
return accessionApiService.autocomplete(filterInfo.filter, field, term);
}
@DownloadEndpoint
@RequestMapping(value = "/downloadKml", produces = "application/vnd.google-earth.kml+xml", method = RequestMethod.POST)
public void downloadKml(@RequestParam(value = "f", required = false, defaultValue = "") String filterCode, @RequestParam(value="filter", required = false) AccessionFilter filter, HttpServletResponse response) throws Exception {
// get AccessionFilter from filterCode
ShortFilterService.FilterInfo<AccessionFilter> filterInfo = shortFilterProcessor.processFilter(filterCode, filter, AccessionFilter.class);
AccessionGeoFilter geoFilter = filterInfo.filter.geo;
if (geoFilter == null) {
filterInfo.filter.geo = geoFilter = new AccessionGeoFilter();
}
geoFilter.referenced(true);
final long countFiltered = accessionApiService.countAccessions(filterInfo.filter);
// LOG.info("Attempting to download KML for {} accessions", countFiltered);
if (countFiltered > DOWNLOAD_LIMIT) {
throw new InvalidApiUsageException("Refusing to export more than " + DOWNLOAD_LIMIT + " entries");
}
response.setContentType("application/vnd.google-earth.kml+xml");
response.addHeader("Content-Disposition", String.format("attachment; filename=\"genesys-kml-%1s.kml\"", filterInfo.filterCode));
DecimalFormatSymbols dfs = new DecimalFormatSymbols();
dfs.setDecimalSeparator('.');
DecimalFormat decimalFormat = new DecimalFormat("0.#", dfs);
decimalFormat.setMinimumIntegerDigits(1);
decimalFormat.setMinimumFractionDigits(6);
decimalFormat.setGroupingUsed(false);
// Write KML to the stream.
final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(response.getOutputStream()));
writer.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
writer.write("<kml xmlns=\"http://www.opengis.net/kml/2.2\">\n");
writer.write("<Document>\n");
try {
accessionProcessor.process(filterInfo.filter, (accessions) -> {
for (Accession accession: accessions) {
AccessionId aid = accession.getAccessionId();
if (aid != null && aid.getLongitude() != null && aid.getLatitude() != null) {
writer.append("<Placemark>");
writer.append("<name>").append(accession.getAccessionNumber()).append("</name>");
writer.append("<description><![CDATA[\n");
writer.append("<p>").append(accession.getTaxonomy().getTaxonNameHtml()).append("</p>");
writer.append("<p>").append(accession.getInstitute().getCode()).append(" ").append(accession.getInstitute().getFullName()).append("</p>");
writer.append("<p><a href=\"").append(frontendUrl).append("/a/").append(accession.getUuid().toString()).append("\">Passport data</a></p>");
writer.append("\n]]></description>");
writer.append("<Point><coordinates>");
writer.append(decimalFormat.format(aid.getLongitude())).append(",").append(decimalFormat.format(aid.getLatitude()));
writer.append("</coordinates></Point>");
writer.append("</Placemark>\n");
writer.flush();
}
}
});
writer.write("</Document>\n</kml>\n");
} catch (EOFException e) {
LOG.warn("Download was aborted: {}", e.getMessage());
throw e;
} catch (Exception e) {
LOG.warn("Error generating KML: {}", e.getMessage());
throw e;
} finally {
writer.flush();
writer.close();
response.flushBuffer();
}
}
@DownloadEndpoint
@RequestMapping(value = "/download-selected", method = RequestMethod.POST, params = { "mcpd" })
public void downloadMcpdByUuids(@RequestParam(value="uuids", required = true) Set<UUID> uuids, HttpServletResponse response) throws IOException, SearchException {
// Create JSON filter
AccessionFilter filter = new AccessionFilter();
filter.historic(null);
filter.uuid(new HashSet<>(uuids));
final long countFiltered = accessionApiService.countAccessions(filter);
if (countFiltered > DOWNLOAD_LIMIT) {
throw new InvalidApiUsageException("Refusing to export more than " + DOWNLOAD_LIMIT + " entries");
}
// Write MCPD to the stream.
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.addHeader("Content-Disposition", String.format("attachment; filename=\"genesys-accessions-selected-%1s.xlsx\"", System.currentTimeMillis()));
final OutputStream outputStream = response.getOutputStream();
try {
downloadService.writeXlsxMCPD(filter, outputStream, null, "/sel");
response.flushBuffer();
} catch (EOFException e) {
LOG.warn("Download was aborted: {}", e.getMessage());
throw e;
}
}
@DownloadEndpoint
@RequestMapping(value = "/download-selected", method = RequestMethod.POST, params = { "dwca" })
public void downloadDwcaByUuids(@RequestParam(value="uuids", required = true) Set<UUID> uuids, HttpServletResponse response) throws Exception {
// Create JSON filter
AccessionFilter filter = new AccessionFilter();
filter
.historic(null)
.uuid(new HashSet<>(uuids));
final long countFiltered = accessionApiService.countAccessions(filter);
if (countFiltered > DOWNLOAD_LIMIT) {
throw new InvalidApiUsageException("Refusing to export more than " + DOWNLOAD_LIMIT + " entries");
}
// Write Darwin Core Archive to the stream.
response.setContentType("application/zip");
response.addHeader("Content-Disposition", String.format("attachment; filename=\"genesys-accessions-selected-%1$s.zip\"", System.currentTimeMillis()));
final OutputStream outputStream = response.getOutputStream();
genesysService.writeAccessions(filter, outputStream, null, "/sel");
response.flushBuffer();
}
@DownloadEndpoint
@RequestMapping(value = "/download", method = RequestMethod.POST, params = { "mcpd" })
public void downloadMcpd(@RequestParam(value = "f", required = false, defaultValue = "") String filterCode, @RequestParam(value="filter", required = false) AccessionFilter filter, HttpServletResponse response) throws IOException, SearchException {
// get AccessionFilter from filterCode
ShortFilterService.FilterInfo<AccessionFilter> filterInfo = shortFilterProcessor.processFilter(filterCode, filter, AccessionFilter.class);
final long countFiltered = accessionApiService.countAccessions(filterInfo.filter);
if (countFiltered > DOWNLOAD_LIMIT) {
throw new InvalidApiUsageException("Refusing to export more than " + DOWNLOAD_LIMIT + " entries");
}
// Write MCPD to the stream.
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.addHeader("Content-Disposition", String.format("attachment; filename=\"genesys-accessions-%1s.xlsx\"", filterInfo.filterCode));
// response.flushBuffer();
final OutputStream outputStream = response.getOutputStream();
try {
downloadService.writeXlsxMCPD(filterInfo.filter, outputStream, filterInfo.filterCode, "/a/" + filterInfo.filterCode);
response.flushBuffer();
} catch (EOFException e) {
LOG.warn("Download was aborted: {}", e.getMessage());
throw e;
}
}
@DownloadEndpoint
@RequestMapping(value = "/download", method = RequestMethod.POST, params = { "pdci" })
public void downloadPdci(@RequestParam(value = "f", required = false, defaultValue = "") String filterCode, @RequestParam(value="filter", required = false) AccessionFilter filter, HttpServletResponse response) throws IOException, SearchException {
// get AccessionFilter from filterCode
ShortFilterService.FilterInfo<AccessionFilter> filterInfo = shortFilterProcessor.processFilter(filterCode, filter, AccessionFilter.class);
final long countFiltered = accessionApiService.countAccessions(filterInfo.filter);
if (countFiltered > DOWNLOAD_LIMIT) {
throw new InvalidApiUsageException("Refusing to export more than " + DOWNLOAD_LIMIT + " entries");
}
// Write PDCI to the stream.
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.addHeader("Content-Disposition", String.format("attachment; filename=\"genesys-PDCI-%1s.xlsx\"", filterInfo.filterCode));
// response.flushBuffer();
final OutputStream outputStream = response.getOutputStream();
try {
downloadService.writeXlsxPDCI(filterInfo.filter, outputStream, filterInfo.filterCode, "/a/" + filterInfo.filterCode);
response.flushBuffer();
} catch (EOFException e) {
LOG.warn("Download was aborted: {}", e.getMessage());
throw e;
}
}
@DownloadEndpoint
@RequestMapping(value = "/download", method = RequestMethod.POST, params = { "dwca" })
public void downloadDwca(@RequestParam(value = "f", required = false, defaultValue = "") String filterCode, @RequestParam(value="filter", required = false) AccessionFilter filter, HttpServletResponse response) throws Exception {
// get AccessionFilter from filterCode
ShortFilterService.FilterInfo<AccessionFilter> filterInfo = shortFilterProcessor.processFilter(filterCode, filter, AccessionFilter.class);
final long countFiltered = accessionApiService.countAccessions(filterInfo.filter);
if (countFiltered > DOWNLOAD_LIMIT) {
throw new InvalidApiUsageException("Refusing to export more than " + DOWNLOAD_LIMIT + " entries");
}
// Write Darwin Core Archive to the stream.
response.setContentType("application/zip");
response.addHeader("Content-Disposition", String.format("attachment; filename=\"genesys-accessions-%1$s.zip\"", filterInfo.filterCode));
final OutputStream outputStream = response.getOutputStream();
genesysService.writeAccessions(filterInfo.filter, outputStream, filterInfo.filterCode, "/a/" + filterInfo.filterCode);
response.flushBuffer();
}
/**
* Returns accession json by filter
*
* @param params - similarity search params {@link org.genesys.server.api.v1.AccessionController.SimilaritySearchParams}
* @return json with minimal accession data
*/
@PostMapping(value = "/find-similar", produces = MediaType.APPLICATION_JSON_VALUE)
public List<DuplicateFinder.SimilarityHit<Accession>> findSimilar(@RequestBody org.genesys.server.api.v1.AccessionController.SimilaritySearchParams params) throws Exception {
List<DuplicateFinder.SimilarityHit<Accession>> results = new ArrayList<>();
final long countFiltered = accessionApiService.countAccessions(params.select);
if (countFiltered > 100) {
throw new InvalidApiUsageException("Too many matches for similarity search!");
}
accessionProcessor.process(params.select, (accessions) -> {
results.addAll(duplicateFinder.findSimilar(accessions, params.target));
});
return results;
}
@Accessors(fluent = true, chain = true)
@Getter
@Setter
public static class SimilaritySearchParams {
public AccessionFilter select; // Which accessions to process
public AccessionFilter target; // What target filter to apply
}
/**
* Get term overview for filters
*
* @param filterCode short filter code
* @param filter the filter
* @return the page
* @throws Exception
*/
@PostMapping(value = "/tileIndex3min", produces = { MediaType.APPLICATION_JSON_VALUE })
public Set<Integer> allTileIndex3min(@RequestParam(name = "f", required = false) final String filterCode, @RequestBody(required = false) final AccessionFilter filter) throws Exception {
ShortFilterService.FilterInfo<AccessionFilter> filterInfo = shortFilterProcessor.processFilter(filterCode, filter, AccessionFilter.class);
return accessionApiService.listTileIndex3min(filterInfo.filter);
}
/**
* Get term overview for filters
*
* @param filterCode short filter code
* @param filter the filter
* @return the page
* @throws Exception
*/
@PostMapping(value = "/tileIndex3min", produces = { MediaType.APPLICATION_JSON_VALUE }, params = { "crop" })
public Map<String, Set<Integer>> allTileIndex3minByCrop(@RequestParam(name = "f", required = false) final String filterCode, @RequestBody(required = false) final AccessionFilter filter) throws Exception {
ShortFilterService.FilterInfo<AccessionFilter> filterInfo = shortFilterProcessor.processFilter(filterCode, filter, AccessionFilter.class);
return accessionApiService.listTileIndex3minByCrop(filterInfo.filter);
}
@GetMapping(value = "/{uuid:\\w{8}\\-\\w{4}.+}/observations", produces = MediaType.APPLICATION_JSON_VALUE)
public AmphibianService.AccessionObservations getAccessionObservations(@PathVariable("uuid") final UUID uuid) throws Exception {
return amphibianService.getAccessionObservations(uuid);
}
}