AccessionController.java

  1. /*
  2.  * Copyright 2024 Global Crop Diversity Trust
  3.  *
  4.  * Licensed under the Apache License, Version 2.0 (the "License");
  5.  * you may not use this file except in compliance with the License.
  6.  * You may obtain a copy of the License at
  7.  *
  8.  *   http://www.apache.org/licenses/LICENSE-2.0
  9.  *
  10.  * Unless required by applicable law or agreed to in writing, software
  11.  * distributed under the License is distributed on an "AS IS" BASIS,
  12.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13.  * See the License for the specific language governing permissions and
  14.  * limitations under the License.
  15.  */

  16. package org.genesys.server.api.v2.impl;

  17. import com.fasterxml.jackson.databind.ObjectMapper;
  18. import com.fasterxml.jackson.databind.node.ArrayNode;
  19. import com.fasterxml.jackson.databind.node.ObjectNode;
  20. import com.google.common.cache.Cache;
  21. import com.google.common.cache.CacheBuilder;
  22. import com.google.common.collect.Sets;
  23. import com.opencsv.CSVWriter;
  24. import io.swagger.annotations.Api;
  25. import lombok.Getter;
  26. import lombok.Setter;
  27. import lombok.experimental.Accessors;
  28. import org.apache.commons.lang3.ArrayUtils;
  29. import org.apache.commons.lang3.StringUtils;
  30. import org.apache.commons.lang3.time.StopWatch;
  31. import org.genesys.blocks.model.JsonViews;
  32. import org.genesys.blocks.security.serialization.CurrentPermissionsWriter;
  33. import org.genesys.server.api.ApiBaseController;
  34. import org.genesys.server.api.FilteredPage;
  35. import org.genesys.server.api.FilteredSlice;
  36. import org.genesys.server.api.Pagination;
  37. import org.genesys.server.api.ScrollPagination;
  38. import org.genesys.server.api.model.AccessionHeaderJson;
  39. import org.genesys.server.api.v2.facade.AccessionApiService;
  40. import org.genesys.server.api.v2.model.impl.AccessionDTO;
  41. import org.genesys.server.api.v2.model.impl.Taxonomy2Info;
  42. import org.genesys.server.component.aspect.DownloadEndpoint;
  43. import org.genesys.server.exception.ClientDisconnectedException;
  44. import org.genesys.server.exception.InvalidApiUsageException;
  45. import org.genesys.server.exception.SearchException;
  46. import org.genesys.server.model.genesys.Accession;
  47. import org.genesys.server.model.genesys.AccessionId;
  48. import org.genesys.server.model.genesys.QAccession;
  49. import org.genesys.server.model.impl.AccessionIdentifier3;
  50. import org.genesys.server.service.AccessionService;
  51. import org.genesys.server.service.AmphibianService;
  52. import org.genesys.server.service.DownloadService;
  53. import org.genesys.server.service.ElasticsearchService;
  54. import org.genesys.server.service.GenesysService;
  55. import org.genesys.server.service.ShortFilterService;
  56. import org.genesys.server.service.filter.AccessionFilter;
  57. import org.genesys.server.service.filter.AccessionGeoFilter;
  58. import org.genesys.server.service.impl.AmphibianServiceImpl;
  59. import org.genesys.server.service.worker.AccessionProcessor;
  60. import org.genesys.server.service.worker.ShortFilterProcessor;
  61. import org.genesys.server.service.worker.dupe.AccessionDuplicateFinder;
  62. import org.genesys.server.service.worker.dupe.DuplicateFinder;
  63. import org.genesys.spring.CSVMessageConverter;
  64. import org.slf4j.Logger;
  65. import org.slf4j.LoggerFactory;
  66. import org.springdoc.api.annotations.ParameterObject;
  67. import org.springframework.beans.factory.annotation.Autowired;
  68. import org.springframework.beans.factory.annotation.Value;
  69. import org.springframework.data.domain.Sort;
  70. import org.springframework.http.MediaType;
  71. import org.springframework.security.access.prepost.PreAuthorize;
  72. import org.springframework.web.bind.annotation.GetMapping;
  73. import org.springframework.web.bind.annotation.PathVariable;
  74. import org.springframework.web.bind.annotation.PostMapping;
  75. import org.springframework.web.bind.annotation.RequestBody;
  76. import org.springframework.web.bind.annotation.RequestMapping;
  77. import org.springframework.web.bind.annotation.RequestMethod;
  78. import org.springframework.web.bind.annotation.RequestParam;
  79. import org.springframework.web.bind.annotation.RestController;
  80. import org.springframework.web.servlet.HandlerMapping;

  81. import javax.servlet.http.HttpServletRequest;
  82. import javax.servlet.http.HttpServletResponse;
  83. import java.io.BufferedWriter;
  84. import java.io.EOFException;
  85. import java.io.IOException;
  86. import java.io.OutputStream;
  87. import java.io.OutputStreamWriter;
  88. import java.text.DecimalFormat;
  89. import java.text.DecimalFormatSymbols;
  90. import java.util.ArrayList;
  91. import java.util.HashSet;
  92. import java.util.List;
  93. import java.util.Map;
  94. import java.util.Objects;
  95. import java.util.Set;
  96. import java.util.UUID;
  97. import java.util.concurrent.ExecutionException;
  98. import java.util.concurrent.TimeUnit;
  99. import java.util.concurrent.atomic.AtomicInteger;
  100. import java.util.concurrent.atomic.AtomicReference;

  101. /**
  102.  * Accession API v2
  103.  */
  104. @RestController("accessionApi2")
  105. @PreAuthorize("isAuthenticated()")
  106. @RequestMapping(AccessionController.CONTROLLER_URL)
  107. @Api(tags = { "accession" })
  108. public class AccessionController extends ApiBaseController {

  109.     private static final Logger LOG = LoggerFactory.getLogger(AccessionController.class);

  110.     /** The Constant CONTROLLER_URL. */
  111.     public static final String CONTROLLER_URL = ApiBaseController.APIv2_BASE + "/acn";

  112.     private static final int DOWNLOAD_LIMIT = 300000;

  113.     @Autowired
  114.     private AccessionApiService accessionApiService;

  115.     @Autowired
  116.     protected ShortFilterProcessor shortFilterProcessor;

  117.     @Autowired(required = false)
  118.     private ElasticsearchService elasticsearchService;

  119.     @Autowired
  120.     private DownloadService downloadService;

  121.     @Autowired
  122.     private GenesysService genesysService;

  123.     @Autowired
  124.     private AmphibianService amphibianService;

  125.     @Autowired
  126.     private AccessionProcessor accessionProcessor;

  127.     @Autowired
  128.     private ObjectMapper objectMapper;

  129.     private final Cache<String, AccessionService.AccessionOverview> accessionOverviewCache = CacheBuilder.newBuilder().maximumSize(50).expireAfterWrite(5, TimeUnit.MINUTES).build();
  130.     private final Cache<String, AccessionService.AccessionMapInfo> accessionMapinfoCache = CacheBuilder.newBuilder().maximumSize(50).expireAfterWrite(5, TimeUnit.MINUTES).build();

  131.     private final ObjectMapper mapper = new ObjectMapper();

  132.     @Value("${frontend.url}")
  133.     private String frontendUrl;

  134.     @Value("${cdn.servers}")
  135.     private String[] cdnServers;

  136.     public static final Set<String> terms = Sets.newHashSet("institute.code", "institute.country.code3", "cropName", "crop.shortName", "taxonomy.genus", "taxonomy.species",
  137.         "taxonomy.genusSpecies", "taxonomy.grinTaxonomySpecies.name", "taxonomy.currentTaxonomySpecies.name", "countryOfOrigin.code3", "sampStat", "available", "mlsStatus",
  138.         "donorCode", "sgsv", "storage", "duplSite", "breederCode", "aegis", "curationType");

  139.     @Autowired
  140.     private AccessionDuplicateFinder duplicateFinder;

  141.     @GetMapping(value = "/id/{id}", produces = { MediaType.APPLICATION_JSON_VALUE })
  142.     public UUID uuidFromId(@PathVariable("id") final long id) {
  143.         return accessionApiService.uuidFromId(id);
  144.     }

  145.     @PostMapping(value = "/id", produces = { MediaType.APPLICATION_JSON_VALUE })
  146.     public List<UUID> uuidFromIds(@RequestBody List<Long> ids) {
  147.         return accessionApiService.uuidsFromIds(ids);
  148.     }


  149.     @GetMapping(value = "/acce-number/{acceNumber}", produces = { MediaType.APPLICATION_JSON_VALUE })
  150.     public UUID uuidFromAcceNumber(@RequestParam(value = "instCode", required = false) String instCode , @PathVariable("acceNumber") String acceNumber) {
  151.         return accessionApiService.uuidFromAcceNumber(instCode, acceNumber);
  152.     }

  153.     @PostMapping(value = "/acce-number", produces = { MediaType.APPLICATION_JSON_VALUE })
  154.     public List<UUID> uuidsFromAcceNumbers(@RequestParam(value = "instCode", required = false) String instCode, @RequestBody List<String> acceNumbers) {
  155.         return accessionApiService.uuidsFromAcceNumbers(instCode, acceNumbers);
  156.     }

  157.     /**
  158.      * Gets the accession
  159.      *
  160.      * @param uuid the uuid
  161.      * @return the subset
  162.      */
  163.     @GetMapping(value = "/{uuid:\\w{8}\\-\\w{4}.+}", produces = { MediaType.APPLICATION_JSON_VALUE })
  164.     public AccessionDTO getByUuid(@PathVariable("uuid") final UUID uuid) {
  165.         return accessionApiService.getByUuid(uuid);
  166.     }

  167.     /**
  168.      * Gets the accession
  169.      *
  170.      * @return the subset
  171.      */
  172.     @GetMapping(value = "/10.{doi1:[0-9]+}/**", produces = { MediaType.APPLICATION_JSON_VALUE })
  173.     public AccessionDTO getByDoi(final HttpServletRequest request) {
  174.         final String doi = ((String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE)).substring((CONTROLLER_URL + "/").length());
  175.         return accessionApiService.getByDoi(doi);
  176.     }

  177.     /**
  178.      * List accessions by filterCode or filter
  179.      *
  180.      * @param page the page
  181.      * @param filterCode short filter code
  182.      * @param filter the filter
  183.      * @return the page
  184.      * @throws IOException
  185.      */
  186.     @PostMapping(value = "/list", produces = { MediaType.APPLICATION_JSON_VALUE, CSVMessageConverter.TEXT_CSV_VALUE })
  187.     public FilteredPage<AccessionDTO, AccessionFilter> list(@RequestParam(name = "f", required = false) String filterCode, @ParameterObject final Pagination page,
  188.         @RequestBody(required = false) AccessionFilter filter) throws IOException, SearchException {

  189.         ShortFilterService.FilterInfo<AccessionFilter> filterInfo = shortFilterProcessor.processFilter(filterCode, filter, AccessionFilter.class);
  190.         return new FilteredPage<>(filterInfo.filterCode, filterInfo.filter, accessionApiService.list(filterInfo.filter, page.toPageRequest(MAX_PAGE_SIZE, DEFAULT_PAGE_SIZE, Sort.Direction.ASC, "seqNo")));
  191.     }


  192.     /**
  193.      * Query accession fields
  194.      *
  195.      * @param page the page
  196.      * @param filterCode short filter code
  197.      * @param filter the filter
  198.      * @param select list of {@code Accession} fields to include in the response
  199.      * @param mcpd Concatenate arrays with ";". Default is <b>{@code false}</b>
  200.      * @return the page
  201.      * @throws Exception
  202.      */
  203.     @PostMapping(value = "/query", produces = { MediaType.APPLICATION_JSON_VALUE })
  204.     public FilteredPage<?, AccessionFilter> query(
  205.         final Pagination page,
  206.         @RequestParam(name = "f", required = false) String filterCode,
  207.         @RequestBody(required = false) AccessionFilter filter,
  208.         @RequestParam List<String> select,
  209.         @RequestParam(required = false, defaultValue = "false") boolean mcpd,
  210.         HttpServletRequest request
  211.     ) throws Exception {
  212.         LOG.warn("Using MCPD style? {}", mcpd);
  213.         ShortFilterService.FilterInfo<AccessionFilter> filterInfo = shortFilterProcessor.processFilter(filterCode, filter, AccessionFilter.class);
  214. //      return new FilteredPage<>(filterInfo.filterCode, filterInfo.filter, accessionService.query(filterInfo.filter, select, page.toPageRequest(MAX_PAGE_SIZE, DEFAULT_PAGE_SIZE), mcpd));
  215.         return new FilteredPage<>(filterInfo.filterCode, filterInfo.filter, accessionApiService.query(filterInfo.filter, select, page.toPageRequest(10000, DEFAULT_PAGE_SIZE), mcpd));
  216.     }

  217.     /**
  218.      * Query accession fields (for HTTP Form POST)
  219.      *
  220.      * @param page the page
  221.      * @param filterCode short filter code
  222.      * @param filter the filter
  223.      * @param select list of {@code Accession} fields to include in the response
  224.      * @param mcpd Concatenate arrays with ";". Default is <b>{@code true}</b>
  225.      * @throws Exception
  226.      */
  227.     @PostMapping(value = "/query", produces = { CSVMessageConverter.TEXT_CSV_VALUE, CSVMessageConverter.TEXT_TSV_VALUE }, consumes = { MediaType.APPLICATION_FORM_URLENCODED_VALUE, MediaType.MULTIPART_FORM_DATA_VALUE })
  228.     public void queryCsvForm(
  229.         final Pagination page,
  230.         @RequestParam(name = "f", required = false) String filterCode,
  231.         @RequestParam String filter,
  232.         @RequestParam List<String> select,
  233.         @RequestParam(required = false, defaultValue = "true") boolean mcpd,
  234.         HttpServletRequest request,
  235.         HttpServletResponse response
  236.     ) throws Exception {
  237.         queryCsv(page, filterCode, objectMapper.readValue(filter, AccessionFilter.class), select, mcpd, request, response);
  238.     }
  239.     /**
  240.      * Query accession fields.
  241.      *
  242.      * @param page the page
  243.      * @param filterCode short filter code
  244.      * @param filter the filter
  245.      * @param select list of {@code Accession} fields to include in the response
  246.      * @param mcpd Concatenate arrays with ";". Default is <b>{@code true}</b>
  247.      * @throws Exception
  248.      */
  249.     @PostMapping(value = "/query", produces = { CSVMessageConverter.TEXT_CSV_VALUE, CSVMessageConverter.TEXT_TSV_VALUE })
  250.     public void queryCsv(
  251.         final Pagination page,
  252.         @RequestParam(name = "f", required = false) String filterCode,
  253.         @RequestBody(required = false) AccessionFilter filter,
  254.         @RequestParam List<String> select,
  255.         @RequestParam(required = false, defaultValue = "true") boolean mcpd,
  256.         HttpServletRequest request,
  257.         HttpServletResponse response
  258.     ) throws Exception {
  259.         LOG.debug("Using MCPD style? {}", mcpd);
  260.         ShortFilterService.FilterInfo<AccessionFilter> filterInfo = shortFilterProcessor.processFilter(filterCode, filter, AccessionFilter.class);
  261.         response.addHeader("Content-Type", CSVMessageConverter.TEXT_TSV_VALUE);
  262.         String fileName = "query" + (StringUtils.isNotBlank(filterInfo.filterCode) ? "-".concat(filterInfo.filterCode) : "");
  263.         response.addHeader("Content-Disposition", "attachment; filename=\"" + fileName + ".csv\"");
  264. //      Does not work: response.getOutputStream().write('\uFEFF'); // UTF-8 BOM for Excel
  265.         try (CSVWriter csvWriter = new CSVWriter(new OutputStreamWriter(response.getOutputStream(), "UTF8"), '\t', '"', '\\', "\n")) {
  266.             var keysArr = new ArrayList<>();
  267.             var row = new AtomicReference<String[]>();
  268.             var counter = new AtomicInteger(0);

  269.             accessionApiService.query(filterInfo.filter, select, page.toPageRequest(1000000, 10000, Sort.Direction.ASC, "seqNo"), mcpd, (one) -> {
  270.                 if (counter.getAndIncrement() == 0) {
  271.                     var row1 = (Map<?, ?>) one;
  272.                     var keys = row1.keySet();
  273.                     keysArr.addAll(keys);
  274.                     csvWriter.writeNext(row1.keySet().toArray(new String[keys.size()]), false);
  275.                     row.set(new String[keys.size()]);
  276.                 }
  277.                 var r = row.get();
  278.                 for (var i = 0; i < r.length; i++) {
  279.                     var val = one.get(keysArr.get(i));
  280.                     r[i] = val == null ? null : Objects.toString(val);
  281.                 }
  282.                 csvWriter.writeNext(r, false);
  283.                 try {
  284.                     if (counter.get() % 50 == 0) csvWriter.flush(); // Flush every 50 rows to check if the client is still connected
  285.                 } catch (IOException e) {
  286.                     throw new ClientDisconnectedException(e);
  287.                 }
  288.             });
  289.             csvWriter.flush();
  290.         };
  291.     }


  292.     /**
  293.      * List distinct taxonomic data for filtered accessions
  294.      *
  295.      * @param filter the accession filter
  296.      * @throws IOException
  297.      */
  298.     @PostMapping(value = "/species", produces = { MediaType.APPLICATION_JSON_VALUE, CSVMessageConverter.TEXT_CSV_VALUE })
  299.     public List<Taxonomy2Info> listSpecies(@RequestBody(required = false) AccessionFilter filter) throws IOException, SearchException {

  300.         return accessionApiService.listSpecies(filter);
  301.     }

  302.     static interface RootNoPermissions extends JsonViews.Root, CurrentPermissionsWriter.NoPermissions { }

  303.     /**
  304.      * List accessions by filterCode or filter
  305.      *
  306.      * @param page the page
  307.      * @param filterCode short filter code
  308.      * @param filter the filter
  309.      * @return the page
  310.      * @throws IOException
  311.      * @throws SearchException
  312.      */
  313.     @PostMapping(value = "/images", params = { "p" }, produces = { MediaType.APPLICATION_JSON_VALUE })
  314.     public AccessionService.AccessionSuggestionPage<AccessionApiService.AccessionDetails, AccessionFilter> images(@RequestParam(name = "f", required = false) final String filterCode, @ParameterObject final Pagination page,
  315.         @RequestBody(required = false) final AccessionFilter filter) throws IOException, SearchException {

  316.         ShortFilterService.FilterInfo<AccessionFilter> filterInfo = shortFilterProcessor.processFilter(filterCode, filter, AccessionFilter.class);

  317. //      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")));
  318.         FilteredPage<AccessionApiService.AccessionDetails, AccessionFilter> pageRes = new FilteredPage<>(filterInfo.filterCode, filterInfo.filter, accessionApiService.withImages(filterInfo.filter, page.toPageRequest(20, 20, Sort.Direction.ASC, "seqNo")));

  319.         filterInfo.filter.images(true);
  320.         Map<String, ElasticsearchService.TermResult> suggestionRes = accessionApiService.getSuggestions(filterInfo.filter);

  321.         return new AccessionService.AccessionSuggestionPage<>(pageRes, suggestionRes);
  322.     }

  323.     @PostMapping(value = "/images", params = { "o" }, produces = { MediaType.APPLICATION_JSON_VALUE })
  324.     public AccessionService.AccessionSuggestionSlice<AccessionApiService.AccessionDetails> images(@RequestParam(name = "f", required = false) final String filterCode,
  325.         final ScrollPagination page, @RequestBody(required = false) final AccessionFilter filter) throws IOException, SearchException {

  326.         ShortFilterService.FilterInfo<AccessionFilter> filterInfo = shortFilterProcessor.processFilter(filterCode, filter, AccessionFilter.class);

  327.         FilteredSlice<AccessionApiService.AccessionDetails> pageRes = new FilteredSlice<>(filterInfo.filterCode, filterInfo.filter, accessionApiService.withImagesSlice(filterInfo.filter, page.toPageRequest(20, Sort.Direction.ASC, "seqNo")));

  328.         filterInfo.filter.images(true);
  329.         Map<String, ElasticsearchService.TermResult> suggestionRes = accessionApiService.getSuggestions(filterInfo.filter);

  330.         return new AccessionService.AccessionSuggestionSlice<>(pageRes, suggestionRes);
  331.     }

  332.     @PostMapping(value = "/images/count", produces = { MediaType.APPLICATION_JSON_VALUE })
  333.     public int countImages(@RequestParam(name = "f", required = false) final String filterCode,
  334.         @RequestBody(required = false) final AccessionFilter filter) throws SearchException, Exception {

  335.         ShortFilterService.FilterInfo<AccessionFilter> filterInfo = shortFilterProcessor.processFilter(filterCode, filter, AccessionFilter.class);

  336.         return accessionApiService.countAccessionsImages(filterInfo.filter);
  337.     }

  338.     @PreAuthorize("hasRole('USER')")
  339.     @DownloadEndpoint
  340.     @RequestMapping(value = "/downloadImages", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE, method = RequestMethod.POST)
  341.     public void downloadImages(@RequestParam(value = "f", required = false, defaultValue = "") String filterCode, HttpServletResponse response) throws Exception {

  342.         // get AccessionFilter from filterCode
  343.         AccessionFilter filter = shortFilterProcessor.filterByCode(filterCode, AccessionFilter.class);
  344.         filter.images(true);

  345.         final long countFiltered = accessionApiService.countAccessions(filter);
  346.         // LOG.info("Attempting to download images for {} accessions", countFiltered);
  347.         if (countFiltered > 100) {
  348.             throw new InvalidApiUsageException("Refusing to export more than " + 100 + " entries");
  349.         }

  350.         // Write Zip file archive to the stream.
  351.         response.setBufferSize(4*1024);
  352.         response.setContentType("application/zip");
  353.         response.addHeader("Content-Disposition", String.format("attachment; filename=\"genesys-images-%1$s.zip\"", filterCode));

  354.         try (final OutputStream outputStream = response.getOutputStream()) {
  355.             try {
  356.                 downloadService.writeAccessionImageArchive(filter, outputStream);
  357.                 outputStream.flush();
  358.                 response.flushBuffer();
  359.             } catch (Throwable e) {
  360.                 outputStream.write(e.getMessage().getBytes());
  361.                 throw e;
  362.             }
  363.         }
  364.     }

  365.     /**
  366.      * List accessions by filterCode or filter
  367.      *
  368.      * @param page the page
  369.      * @param filterCode short filter code
  370.      * @param filter the filter
  371.      * @return the page
  372.      * @throws IOException
  373.      */
  374.     @PostMapping(value = "/filter", produces = { MediaType.APPLICATION_JSON_VALUE })
  375.     public AccessionService.AccessionSuggestionPage<AccessionDTO, AccessionFilter> filter(@RequestParam(name = "f", required = false) String filterCode, @ParameterObject final Pagination page,
  376.         @RequestBody(required = false) AccessionFilter filter) throws IOException, SearchException {

  377.         LOG.debug("Received filter: {}", filter);
  378.         ShortFilterService.FilterInfo<AccessionFilter> filterInfo = shortFilterProcessor.processFilter(filterCode, filter, AccessionFilter.class);

  379.         LOG.debug("Processed filter: {}", filterInfo.filter);
  380.         var pageRes = new FilteredPage<>(filterInfo.filterCode, filterInfo.filter, accessionApiService.list(filterInfo.filter, page.toPageRequest(MAX_PAGE_SIZE, DEFAULT_PAGE_SIZE, Sort.Direction.ASC, "seqNo")));
  381.         Map<String, ElasticsearchService.TermResult> suggestionRes = accessionApiService.getSuggestions(filterInfo.filter);

  382.         return new AccessionService.AccessionSuggestionPage<>(pageRes, suggestionRes);
  383.     }

  384.     /**
  385.      * Get term overview for filters
  386.      *
  387.      * @param filterCode short filter code
  388.      * @param filter the filter
  389.      * @return the page
  390.      * @throws SearchException
  391.      */
  392.     @PostMapping(value = "/overview", produces = { MediaType.APPLICATION_JSON_VALUE })
  393.     public AccessionService.AccessionOverview overview(@RequestParam(name = "f", required = false) final String filterCode, @RequestBody(required = false) final AccessionFilter filter,
  394.         @RequestParam(name = "limit", defaultValue = "10", required = false) final int limit) throws IOException, SearchException, ExecutionException, InterruptedException {

  395.         ShortFilterService.FilterInfo<AccessionFilter> filterInfo = shortFilterProcessor.processFilter(filterCode, filter, AccessionFilter.class);

  396.         return accessionOverviewCache.get(filterInfo.filterCode + ",limit=" + limit, () -> {
  397.             var stopWatch = StopWatch.createStarted();
  398.             Map<String, ElasticsearchService.TermResult> overview = elasticsearchService.termStatisticsAuto(Accession.class, filterInfo.filter, Math.min(50, limit), terms.toArray(new String[] {}));
  399.             LOG.info("overview termStatisticsAuto {}ms", stopWatch.getTime());

  400.             ElasticsearchService.TermResult result = elasticsearchService.recountResult(Accession.class, QAccession.accession.accessionId().storage, filterInfo.filter, overview.get("storage"), "storage");
  401.             LOG.info("overview recountResult {}ms", stopWatch.getTime());
  402.             overview.put("storage", result);

  403.             long accessionCount = accessionApiService.countAccessions(filterInfo.filter);
  404.             LOG.info("overview countAccessions {}ms", stopWatch.getTime());
  405.             Map<String, ElasticsearchService.TermResult> suggestions = accessionApiService.getSuggestions(filterInfo.filter);
  406.             LOG.info("overview getSuggestions {}ms", stopWatch.getTime());

  407.             return AccessionService.AccessionOverview.from(filterInfo.filterCode, filterInfo.filter, overview, accessionCount, suggestions);
  408.         });
  409.     }

  410.     /**
  411.      * Get overview tree for filters and provided terms
  412.      *
  413.      * @param filterCode short filter code
  414.      * @param filter the filter
  415.      * @param terms the terms (order is important!)
  416.      * @return the overview tree
  417.      *
  418.      * @throws SearchException the search exception
  419.      * @throws IOException the IO exception
  420.      */
  421.     @PostMapping(value = "/overview-tree", produces = { MediaType.APPLICATION_JSON_VALUE })
  422.     public ElasticsearchService.TreeNode overviewTree(@RequestParam(name = "f", required = false) final String filterCode,
  423.         @RequestBody(required = false) final AccessionFilter filter,
  424.         @RequestParam(name = "terms", required = true) final String[] terms) throws SearchException, IOException {

  425.         if (ArrayUtils.isEmpty(terms)) {
  426.             throw new InvalidApiUsageException("Terms must be provided!");
  427.         }

  428.         ShortFilterService.FilterInfo<AccessionFilter> filterInfo = shortFilterProcessor.processFilter(filterCode, filter, AccessionFilter.class);
  429.         var rootNode = elasticsearchService.treeNodeStatistics(Accession.class, filterInfo.filter, terms);
  430.         rootNode.filterCode = filterInfo.filterCode;
  431.         return rootNode;
  432.     }

  433.     /**
  434.      * Load more data for the specified term
  435.      *
  436.      * @param filterCode short filter code
  437.      * @param filter the filter
  438.      * @param term the term
  439.      * @return the term result
  440.      * @throws SearchException the search exception
  441.      * @throws IOException signals that an I/O exception has occurred
  442.      */
  443.     @PostMapping(value = "/overview/{term}", produces = { MediaType.APPLICATION_JSON_VALUE })
  444.     public ElasticsearchService.TermResult loadMoreTerms(@PathVariable(name = "term") final String term, @RequestBody(required = false) final AccessionFilter filter,
  445.         @RequestParam(name = "f", required = false) final String filterCode, @RequestParam(name = "limit", defaultValue = "20", required = false) final int limit)
  446.         throws IOException, SearchException, ExecutionException, InterruptedException {

  447.         if (! terms.contains(term)) {
  448.             throw new InvalidApiUsageException("No such term. Will not search.");
  449.         }

  450.         ShortFilterService.FilterInfo<AccessionFilter> filterInfo = shortFilterProcessor.processFilter(filterCode, filter, AccessionFilter.class);

  451.         ElasticsearchService.TermResult termResult = elasticsearchService.termStatisticsAuto(Accession.class, filterInfo.filter, Math.min(200, limit), term);
  452.         if (term.equals("storage")) {
  453.             termResult = elasticsearchService.recountResult(Accession.class, QAccession.accession.accessionId().storage, filterInfo.filter, termResult, "storage");
  454.         }
  455.         return termResult;
  456.     }

  457.     /**
  458.      * Gets accessions by list of uuid-s
  459.      *
  460.      * @param uuids accession identifi`ers to lookup in DB
  461.      * @return list of Accessions
  462.      */
  463.     @PostMapping(value = "/for-uuid", produces = { MediaType.APPLICATION_JSON_VALUE, CSVMessageConverter.TEXT_CSV_VALUE })
  464.     public List<AccessionDTO> forUuids(@RequestBody Set<UUID> uuids) {
  465.         return accessionApiService.forUuids(uuids);
  466.     }

  467.     /**
  468.      * Converts AccessionIdentifiers to UUID
  469.      *
  470.      * @param identifiers accession identifiers to lookup in DB
  471.      * @return map with UUIDs and related AccessionIdentifiers
  472.      */
  473.     @PostMapping(value = "/toUUID", produces = { MediaType.APPLICATION_JSON_VALUE })
  474.     public Map<UUID, AccessionIdentifier3> toUUID(@RequestBody List<AccessionHeaderJson> identifiers) {
  475.         return accessionApiService.toUUID(identifiers);
  476.     }

  477.     @GetMapping(value = "/details/10.{doi1:[0-9]+}/**", produces = MediaType.APPLICATION_JSON_VALUE)
  478.     public AccessionApiService.AccessionDetails getAccessionDetailsByDoi(final HttpServletRequest request) {
  479.         final String doi = ((String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE)).substring((CONTROLLER_URL + "/details/").length());
  480.         return accessionApiService.getAccessionDetailsByDoi(doi);
  481.     }

  482.     @GetMapping(value = "/details/{uuid:\\w{8}\\-\\w{4}.+}", produces = MediaType.APPLICATION_JSON_VALUE)
  483.     public AccessionApiService.AccessionDetails getAccessionDetailsByUUID(@PathVariable("uuid") final UUID uuid) {
  484.         return accessionApiService.getAccessionDetailsByUuid(uuid);
  485.     }

  486.     @PreAuthorize("isAuthenticated()")
  487.     @GetMapping(value = "/similar/{uuid:\\w{8}\\-\\w{4}.+}", produces = MediaType.APPLICATION_JSON_VALUE)
  488.     public List<DuplicateFinder.Hit<AccessionDTO>> getSimilarAccessionsForUUID(@PathVariable("uuid") final UUID uuid) {
  489.         return accessionApiService.getSimilarAccessionsForUUID(uuid);
  490.     }

  491.     @PreAuthorize("isAuthenticated()")
  492.     @PostMapping(value = "/similar/{uuid:\\w{8}\\-\\w{4}.+}", produces = MediaType.APPLICATION_JSON_VALUE)
  493.     public List<DuplicateFinder.Hit<AccessionDTO>> getSimilarAccessionsForUUID(@PathVariable("uuid") final UUID uuid, @RequestBody(required = false) AccessionFilter filter) {
  494.         return accessionApiService.getSimilarAccessionsForUUID(uuid, filter);
  495.     }

  496.     @PreAuthorize("isAuthenticated()")
  497.     @GetMapping(value = "/auditlog/10.{doi1:[0-9]+}/**", produces = MediaType.APPLICATION_JSON_VALUE)
  498.     public AccessionService.AccessionAuditLog getAccessionAuditLogByDoi(final HttpServletRequest request) {
  499.         final String doi = ((String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE)).substring((CONTROLLER_URL + "/auditlog/").length());
  500.         return accessionApiService.getAccessionAuditLogByDoi(doi);
  501.     }

  502.     @PreAuthorize("isAuthenticated()")
  503.     @GetMapping(value = "/auditlog/{uuid:\\w{8}\\-\\w{4}.+}", produces = MediaType.APPLICATION_JSON_VALUE)
  504.     public AccessionService.AccessionAuditLog getAccessionAuditLogByUUID(@PathVariable("uuid") final UUID uuid) {
  505.         return accessionApiService.getAccessionAuditLogByUUID(uuid);
  506.     }

  507.     @PostMapping(value = "/mapinfo", produces = MediaType.APPLICATION_JSON_VALUE)
  508.     public AccessionService.AccessionMapInfo mapInfo(@RequestParam(value = "f", required = false) String filterCode, @RequestBody(required = false) AccessionFilter filter) throws IOException, SearchException, ExecutionException {

  509.         ShortFilterService.FilterInfo<AccessionFilter> filterInfo = shortFilterProcessor.processFilter(filterCode, filter, AccessionFilter.class);

  510.         return accessionMapinfoCache.get(filterInfo.filterCode, () -> {

  511.             // Force only georeferenced accessions
  512.             AccessionFilter georefFilter = filterInfo.filter.copy(AccessionFilter.class);
  513.             georefFilter.geo().referenced(true);

  514.             AccessionService.AccessionMapInfo mapInfo = new AccessionService.AccessionMapInfo();
  515.             mapInfo.filterCode = filterInfo.filterCode;
  516.             mapInfo.filter = filterInfo.filter;

  517.             if (StringUtils.isBlank(filterInfo.filterCode)) {
  518.                 // Entire map
  519.                 mapInfo.bounds = AccessionService.DEFAULT_GEOBOUNDS;
  520.             } else {
  521.                 mapInfo.bounds = accessionApiService.getGeoBounds(georefFilter);
  522.             }
  523.             mapInfo.accessionCount = accessionApiService.countAccessions(georefFilter);
  524.             mapInfo.tileServers = cdnServers;
  525.             mapInfo.suggestions= accessionApiService.getSuggestions(filterInfo.filter);

  526.             return mapInfo;
  527.         });
  528.     }

  529.     /**
  530.      * Returns accession json by filter
  531.      *
  532.      * @param limit - max count of accession returned
  533.      * @param filter - filter
  534.      * @return json with minimal accession data
  535.      */
  536.     @PostMapping(value = "/geoJson", produces = MediaType.APPLICATION_JSON_VALUE)
  537.     public ObjectNode geoJson(@RequestParam(value = "limit", required = false, defaultValue = "100") Long limit,
  538.         @RequestBody AccessionFilter filter) throws Exception {

  539.         final ObjectNode geoJson = mapper.createObjectNode();
  540.         final ArrayNode featuresArray = geoJson.arrayNode();

  541.         accessionProcessor.process(filter, (accessions) -> {
  542.             for (Accession accession: accessions) {
  543.                 final ObjectNode feature = featuresArray.objectNode();
  544.                 feature.put("type", "Feature");
  545.                 feature.put("id", accession.getId());

  546.                 ObjectNode geometry;
  547.                 feature.set("geometry", geometry = feature.objectNode());
  548.                 geometry.put("type", "Point");

  549.                 ArrayNode coordArray;
  550.                 geometry.set("coordinates", coordArray = geometry.arrayNode());
  551.                 coordArray.add(accession.getAccessionId().getLongitude());
  552.                 coordArray.add(accession.getAccessionId().getLatitude());

  553.                 ObjectNode properties;
  554.                 feature.set("properties", properties = feature.objectNode());
  555.                 properties.put("uuid", accession.getAccessionId().getUuid().toString());
  556.                 properties.put("doi", accession.getDoi());
  557.                 properties.put("accessionNumber", accession.getAccessionNumber());
  558.                 properties.put("instCode", accession.getInstCode());
  559.                 properties.put("datum", accession.getAccessionId().getCoordinateDatum());
  560.                 properties.put("uncertainty",  accession.getAccessionId().getCoordinateUncertainty());

  561.                 featuresArray.add(feature);
  562.             }
  563.         }, limit);

  564.         geoJson.set("geoJson", featuresArray);

  565.         long accessionCount = accessionApiService.countAccessions(filter);
  566.         if (accessionCount > limit) {
  567.             geoJson.put("otherCount", accessionCount - limit);
  568.         }

  569.         return geoJson;
  570.     }

  571.     @GetMapping(value = "/autocomplete/{field:.+}", produces = MediaType.APPLICATION_JSON_VALUE)
  572.     public List<AccessionService.LabelValue<String>> autocomplete(@PathVariable("field") String field,
  573.         @RequestParam(value = "term", required = true) String term,
  574.         @RequestParam(name = "f", required = false) String filterCode, @RequestBody(required = false) AccessionFilter filter) throws IOException {

  575.         ShortFilterService.FilterInfo<AccessionFilter> filterInfo = shortFilterProcessor.processFilter(filterCode, filter, AccessionFilter.class);
  576.         return accessionApiService.autocomplete(filterInfo.filter, field, term);
  577.     }

  578.     @DownloadEndpoint
  579.     @RequestMapping(value = "/downloadKml", produces = "application/vnd.google-earth.kml+xml", method = RequestMethod.POST)
  580.     public void downloadKml(@RequestParam(value = "f", required = false, defaultValue = "") String filterCode, @RequestParam(value="filter", required = false) AccessionFilter filter, HttpServletResponse response) throws Exception {

  581.         // get AccessionFilter from filterCode
  582.         ShortFilterService.FilterInfo<AccessionFilter> filterInfo = shortFilterProcessor.processFilter(filterCode, filter, AccessionFilter.class);
  583.         AccessionGeoFilter geoFilter = filterInfo.filter.geo;
  584.         if (geoFilter == null) {
  585.             filterInfo.filter.geo = geoFilter = new AccessionGeoFilter();
  586.         }
  587.         geoFilter.referenced(true);

  588.         final long countFiltered = accessionApiService.countAccessions(filterInfo.filter);
  589.         // LOG.info("Attempting to download KML for {} accessions", countFiltered);
  590.         if (countFiltered > DOWNLOAD_LIMIT) {
  591.             throw new InvalidApiUsageException("Refusing to export more than " + DOWNLOAD_LIMIT + " entries");
  592.         }

  593.         response.setContentType("application/vnd.google-earth.kml+xml");
  594.         response.addHeader("Content-Disposition", String.format("attachment; filename=\"genesys-kml-%1s.kml\"", filterInfo.filterCode));

  595.         DecimalFormatSymbols dfs = new DecimalFormatSymbols();
  596.         dfs.setDecimalSeparator('.');
  597.         DecimalFormat decimalFormat = new DecimalFormat("0.#", dfs);
  598.         decimalFormat.setMinimumIntegerDigits(1);
  599.         decimalFormat.setMinimumFractionDigits(6);
  600.         decimalFormat.setGroupingUsed(false);

  601.         // Write KML to the stream.
  602.         final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(response.getOutputStream()));
  603.         writer.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
  604.         writer.write("<kml xmlns=\"http://www.opengis.net/kml/2.2\">\n");
  605.         writer.write("<Document>\n");
  606.         try {
  607.             accessionProcessor.process(filterInfo.filter, (accessions) -> {
  608.                 for (Accession accession: accessions) {
  609.                     AccessionId aid = accession.getAccessionId();
  610.                     if (aid != null && aid.getLongitude() != null && aid.getLatitude() != null) {
  611.                         writer.append("<Placemark>");
  612.                         writer.append("<name>").append(accession.getAccessionNumber()).append("</name>");

  613.                         writer.append("<description><![CDATA[\n");
  614.                         writer.append("<p>").append(accession.getTaxonomy().getTaxonNameHtml()).append("</p>");
  615.                         writer.append("<p>").append(accession.getInstitute().getCode()).append(" ").append(accession.getInstitute().getFullName()).append("</p>");
  616.                         writer.append("<p><a href=\"").append(frontendUrl).append("/a/").append(accession.getUuid().toString()).append("\">Passport data</a></p>");
  617.                         writer.append("\n]]></description>");

  618.                         writer.append("<Point><coordinates>");
  619.                         writer.append(decimalFormat.format(aid.getLongitude())).append(",").append(decimalFormat.format(aid.getLatitude()));
  620.                         writer.append("</coordinates></Point>");
  621.                         writer.append("</Placemark>\n");
  622.                         writer.flush();
  623.                     }
  624.                 }
  625.             });
  626.             writer.write("</Document>\n</kml>\n");

  627.         } catch (EOFException e) {
  628.             LOG.warn("Download was aborted: {}", e.getMessage());
  629.             throw e;
  630.         } catch (Exception e) {
  631.             LOG.warn("Error generating KML: {}", e.getMessage());
  632.             throw e;
  633.         } finally {
  634.             writer.flush();
  635.             writer.close();
  636.             response.flushBuffer();
  637.         }
  638.     }

  639.     @DownloadEndpoint
  640.     @RequestMapping(value = "/download-selected", method = RequestMethod.POST, params = { "mcpd" })
  641.     public void downloadMcpdByUuids(@RequestParam(value="uuids", required = true) Set<UUID> uuids, HttpServletResponse response) throws IOException, SearchException {
  642.         // Create JSON filter
  643.         AccessionFilter filter = new AccessionFilter();
  644.         filter.historic(null);
  645.         filter.uuid(new HashSet<>(uuids));

  646.         final long countFiltered = accessionApiService.countAccessions(filter);
  647.         if (countFiltered > DOWNLOAD_LIMIT) {
  648.             throw new InvalidApiUsageException("Refusing to export more than " + DOWNLOAD_LIMIT + " entries");
  649.         }

  650.         // Write MCPD to the stream.
  651.         response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
  652.         response.addHeader("Content-Disposition", String.format("attachment; filename=\"genesys-accessions-selected-%1s.xlsx\"", System.currentTimeMillis()));

  653.         final OutputStream outputStream = response.getOutputStream();
  654.         try {
  655.             downloadService.writeXlsxMCPD(filter, outputStream, null, "/sel");
  656.             response.flushBuffer();
  657.         } catch (EOFException e) {
  658.             LOG.warn("Download was aborted: {}", e.getMessage());
  659.             throw e;
  660.         }
  661.     }

  662.     @DownloadEndpoint
  663.     @RequestMapping(value = "/download-selected", method = RequestMethod.POST, params = { "dwca" })
  664.     public void downloadDwcaByUuids(@RequestParam(value="uuids", required = true) Set<UUID> uuids, HttpServletResponse response) throws Exception {
  665.         // Create JSON filter
  666.         AccessionFilter filter = new AccessionFilter();
  667.         filter
  668.             .historic(null)
  669.             .uuid(new HashSet<>(uuids));

  670.         final long countFiltered = accessionApiService.countAccessions(filter);
  671.         if (countFiltered > DOWNLOAD_LIMIT) {
  672.             throw new InvalidApiUsageException("Refusing to export more than " + DOWNLOAD_LIMIT + " entries");
  673.         }

  674.         // Write Darwin Core Archive to the stream.
  675.         response.setContentType("application/zip");
  676.         response.addHeader("Content-Disposition", String.format("attachment; filename=\"genesys-accessions-selected-%1$s.zip\"", System.currentTimeMillis()));

  677.         final OutputStream outputStream = response.getOutputStream();
  678.         genesysService.writeAccessions(filter, outputStream, null, "/sel");
  679.         response.flushBuffer();
  680.     }

  681.     @DownloadEndpoint
  682.     @RequestMapping(value = "/download", method = RequestMethod.POST, params = { "mcpd" })
  683.     public void downloadMcpd(@RequestParam(value = "f", required = false, defaultValue = "") String filterCode, @RequestParam(value="filter", required = false) AccessionFilter filter, HttpServletResponse response) throws IOException, SearchException {

  684.         // get AccessionFilter from filterCode
  685.         ShortFilterService.FilterInfo<AccessionFilter> filterInfo = shortFilterProcessor.processFilter(filterCode, filter, AccessionFilter.class);

  686.         final long countFiltered = accessionApiService.countAccessions(filterInfo.filter);
  687.         if (countFiltered > DOWNLOAD_LIMIT) {
  688.             throw new InvalidApiUsageException("Refusing to export more than " + DOWNLOAD_LIMIT + " entries");
  689.         }

  690.         // Write MCPD to the stream.
  691.         response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
  692.         response.addHeader("Content-Disposition", String.format("attachment; filename=\"genesys-accessions-%1s.xlsx\"", filterInfo.filterCode));
  693.         // response.flushBuffer();

  694.         final OutputStream outputStream = response.getOutputStream();
  695.         try {
  696.             downloadService.writeXlsxMCPD(filterInfo.filter, outputStream, filterInfo.filterCode, "/a/" + filterInfo.filterCode);
  697.             response.flushBuffer();
  698.         } catch (EOFException e) {
  699.             LOG.warn("Download was aborted: {}", e.getMessage());
  700.             throw e;
  701.         }
  702.     }


  703.     @DownloadEndpoint
  704.     @RequestMapping(value = "/download", method = RequestMethod.POST, params = { "pdci" })
  705.     public void downloadPdci(@RequestParam(value = "f", required = false, defaultValue = "") String filterCode, @RequestParam(value="filter", required = false) AccessionFilter filter, HttpServletResponse response) throws IOException, SearchException {

  706.         // get AccessionFilter from filterCode
  707.         ShortFilterService.FilterInfo<AccessionFilter> filterInfo = shortFilterProcessor.processFilter(filterCode, filter, AccessionFilter.class);

  708.         final long countFiltered = accessionApiService.countAccessions(filterInfo.filter);
  709.         if (countFiltered > DOWNLOAD_LIMIT) {
  710.             throw new InvalidApiUsageException("Refusing to export more than " + DOWNLOAD_LIMIT + " entries");
  711.         }

  712.         // Write PDCI to the stream.
  713.         response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
  714.         response.addHeader("Content-Disposition", String.format("attachment; filename=\"genesys-PDCI-%1s.xlsx\"", filterInfo.filterCode));
  715.         // response.flushBuffer();

  716.         final OutputStream outputStream = response.getOutputStream();
  717.         try {
  718.             downloadService.writeXlsxPDCI(filterInfo.filter, outputStream, filterInfo.filterCode, "/a/" + filterInfo.filterCode);
  719.             response.flushBuffer();
  720.         } catch (EOFException e) {
  721.             LOG.warn("Download was aborted: {}", e.getMessage());
  722.             throw e;
  723.         }
  724.     }

  725.     @DownloadEndpoint
  726.     @RequestMapping(value = "/download", method = RequestMethod.POST, params = { "dwca" })
  727.     public void downloadDwca(@RequestParam(value = "f", required = false, defaultValue = "") String filterCode, @RequestParam(value="filter", required = false) AccessionFilter filter, HttpServletResponse response) throws Exception {

  728.         // get AccessionFilter from filterCode
  729.         ShortFilterService.FilterInfo<AccessionFilter> filterInfo = shortFilterProcessor.processFilter(filterCode, filter, AccessionFilter.class);

  730.         final long countFiltered = accessionApiService.countAccessions(filterInfo.filter);
  731.         if (countFiltered > DOWNLOAD_LIMIT) {
  732.             throw new InvalidApiUsageException("Refusing to export more than " + DOWNLOAD_LIMIT + " entries");
  733.         }

  734.         // Write Darwin Core Archive to the stream.
  735.         response.setContentType("application/zip");
  736.         response.addHeader("Content-Disposition", String.format("attachment; filename=\"genesys-accessions-%1$s.zip\"", filterInfo.filterCode));

  737.         final OutputStream outputStream = response.getOutputStream();
  738.         genesysService.writeAccessions(filterInfo.filter, outputStream, filterInfo.filterCode, "/a/" + filterInfo.filterCode);
  739.         response.flushBuffer();
  740.     }


  741.     /**
  742.      * Returns accession json by filter
  743.      *
  744.      * @param params - similarity search params {@link org.genesys.server.api.v1.AccessionController.SimilaritySearchParams}
  745.      * @return json with minimal accession data
  746.      */
  747.     @PostMapping(value = "/find-similar", produces = MediaType.APPLICATION_JSON_VALUE)
  748.     public List<DuplicateFinder.SimilarityHit<Accession>> findSimilar(@RequestBody org.genesys.server.api.v1.AccessionController.SimilaritySearchParams params) throws Exception {
  749.         List<DuplicateFinder.SimilarityHit<Accession>> results = new ArrayList<>();

  750.         final long countFiltered = accessionApiService.countAccessions(params.select);

  751.         if (countFiltered > 100) {
  752.             throw new InvalidApiUsageException("Too many matches for similarity search!");
  753.         }

  754.         accessionProcessor.process(params.select, (accessions) -> {
  755.             results.addAll(duplicateFinder.findSimilar(accessions, params.target));
  756.         });

  757.         return results;
  758.     }

  759.     @Accessors(fluent = true, chain = true)
  760.     @Getter
  761.     @Setter
  762.     public static class SimilaritySearchParams {
  763.         public AccessionFilter select; // Which accessions to process
  764.         public AccessionFilter target; // What target filter to apply
  765.     }

  766.     /**
  767.      * Get term overview for filters
  768.      *
  769.      * @param filterCode short filter code
  770.      * @param filter the filter
  771.      * @return the page
  772.      * @throws Exception
  773.      */
  774.     @PostMapping(value = "/tileIndex3min", produces = { MediaType.APPLICATION_JSON_VALUE })
  775.     public Set<Integer> allTileIndex3min(@RequestParam(name = "f", required = false) final String filterCode, @RequestBody(required = false) final AccessionFilter filter) throws Exception {

  776.         ShortFilterService.FilterInfo<AccessionFilter> filterInfo = shortFilterProcessor.processFilter(filterCode, filter, AccessionFilter.class);
  777.         return accessionApiService.listTileIndex3min(filterInfo.filter);
  778.     }

  779.     /**
  780.      * Get term overview for filters
  781.      *
  782.      * @param filterCode short filter code
  783.      * @param filter the filter
  784.      * @return the page
  785.      * @throws Exception
  786.      */
  787.     @PostMapping(value = "/tileIndex3min", produces = { MediaType.APPLICATION_JSON_VALUE }, params = { "crop" })
  788.     public Map<String, Set<Integer>> allTileIndex3minByCrop(@RequestParam(name = "f", required = false) final String filterCode, @RequestBody(required = false) final AccessionFilter filter) throws Exception {

  789.         ShortFilterService.FilterInfo<AccessionFilter> filterInfo = shortFilterProcessor.processFilter(filterCode, filter, AccessionFilter.class);
  790.         return accessionApiService.listTileIndex3minByCrop(filterInfo.filter);
  791.     }

  792.     @GetMapping(value = "/{uuid:\\w{8}\\-\\w{4}.+}/observations", produces = MediaType.APPLICATION_JSON_VALUE)
  793.     public AmphibianService.AccessionObservations getAccessionObservations(@PathVariable("uuid") final UUID uuid) throws Exception {
  794.         return amphibianService.getAccessionObservations(uuid);
  795.     }
  796. }