Commit bcb43e73 authored by Matija Obreza's avatar Matija Obreza
Browse files

Merge branch '290-glis-doi-request' into 'main'

Resolve "GLIS DOI Request"

Closes #290

See merge request grin-global/grin-global-server!388
parents 08256190 4197d148
......@@ -59,7 +59,7 @@
<querydsl.version>5.0.0</querydsl.version>
<mysql.version>8.0.26</mysql.version>
<hsqldb.version>2.6.0</hsqldb.version>
<jackson.version>2.12.4</jackson.version>
<jackson.version>2.13.2</jackson.version>
<liquibase.version>4.4.0</liquibase.version>
<liquibase-hibernate5.version>4.3.5</liquibase-hibernate5.version>
......@@ -859,7 +859,7 @@
<dependency>
<groupId>org.jdom</groupId>
<artifactId>jdom2</artifactId>
<version>2.0.6</version>
<version>2.0.6.1</version>
</dependency>
<dependency>
<groupId>org.springframework.ws</groupId>
......@@ -1068,11 +1068,19 @@
<scope>test</scope>
</dependency>
<!--Jasper Reports-->
<dependency>
<groupId>net.sf.jasperreports</groupId>
<artifactId>jasperreports</artifactId>
<version>6.18.1</version>
</dependency>
<!--GLIS Spring REST template service-->
<dependency>
<groupId>org.genesys-pgr</groupId>
<artifactId>glis-client-resttemplate</artifactId>
<version>1.2-SNAPSHOT</version>
</dependency>
</dependencies>
<repositories>
......
......@@ -18,6 +18,7 @@ package org.gringlobal.api.v1.impl;
import java.io.EOFException;
import java.io.IOException;
import java.io.OutputStream;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletResponse;
......@@ -32,6 +33,7 @@ import org.gringlobal.api.v1.ActionController;
import org.gringlobal.api.v1.ApiBaseController;
import org.gringlobal.api.v1.FilteredCRUDController;
import org.gringlobal.api.v1.FilteredPage;
import org.gringlobal.api.v1.MultiOp;
import org.gringlobal.api.v1.Pagination;
import org.gringlobal.custom.elasticsearch.SearchException;
import org.gringlobal.model.Accession;
......@@ -48,6 +50,8 @@ import org.gringlobal.service.AccessionService.AcquisitionData;
import org.gringlobal.service.DownloadService;
import org.gringlobal.service.filter.AccessionActionFilter;
import org.gringlobal.service.filter.AccessionFilter;
import org.gringlobal.service.glis.impl.GlisDOIRegistrationManager;
import org.gringlobal.service.glis.impl.GlisDOIRegistrationManager.DoiUpdate;
import org.gringlobal.spring.CSVMessageConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
......@@ -86,6 +90,9 @@ public class AccessionController extends FilteredCRUDController<Accession, Acces
@Autowired
private DownloadService downloadService;
@Autowired
private GlisDOIRegistrationManager glisDOIRegistrationManager;
@Override
protected Class<AccessionFilter> filterType() {
return AccessionFilter.class;
......@@ -204,4 +211,32 @@ public class AccessionController extends FilteredCRUDController<Accession, Acces
throw e;
}
}
/**
* Update GLIS DOI Registration Service for one accession
*
* @param id Accession ID
* @return DOI Registration status
* @throws Throwable the problem error
*/
@PostMapping(value = ENDPOINT_ID + "/assign-doi")
public DoiUpdate assignDoiToAccessions(@PathVariable(required = true, name = "id") final long id) throws Throwable {
return glisDOIRegistrationManager.updateDoiRegistration(crudService.get(id));
}
/**
* Bulk update GLIS DOI Registration Service
*
* @param ids
* @return
* @throws Exception
*/
@PostMapping(value = "/assign-doi")
public MultiOp<DoiUpdate> assignDoiToAccessions(@RequestBody final List<Long> ids) throws Exception {
AccessionFilter filter = new AccessionFilter();
filter.id().addAll(ids);
return glisDOIRegistrationManager.updateDoiRegistration(filter);
}
}
......@@ -45,7 +45,7 @@ import javax.validation.ValidatorFactory;
@Configuration
@Import({ CommonConfig.class, HazelcastConfig.class, DatabaseConfig.class, SecurityConfig.class, GGCESecurityConfig.class, SpringMvcConfig.class, MailConfig.class,
SchedulerConfig.class, OpenAPIConfig.class, ElasticsearchConfig.class, FileRepositoryConfig.class, AuditConfig.class, HazelcastHttpSessionConfig.class, IntegrationConfig.class,
FirehoseConfig.class})
FirehoseConfig.class, GLISConfig.class})
@ComponentScan(basePackages = { "org.gringlobal.compatibility", "org.gringlobal.service", "org.gringlobal.worker" })
@EnableAspectJAutoProxy
public class ApplicationConfig {
......
/*
* Copyright 2022 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.gringlobal.application.config;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.genesys.glis.v1.GlisRateLimiter;
import org.genesys.glis.v1.api.ManagerApi;
import org.genesys.glis.v1.invoker.ApiClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.BufferingClientHttpRequestFactory;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter;
import org.springframework.web.client.RestTemplate;
@Configuration
public class GLISConfig {
public static final Logger LOG = LoggerFactory.getLogger(GLISConfig.class);
@Value("${itpgrfa.glis.basepath}")
private String basePath;
// @Value("${itpgrfa.glis.username:anonymous}")
// private String username;
//
// @Value("${itpgrfa.glis.password:}")
// private String password;
@Value("${itpgrfa.glis.ratelimit}")
private double glisRateLimit;
@Bean
public GlisRateLimiter glisRateLimiter() {
return new GlisRateLimiter(glisRateLimit);
}
private ApiClient glisClient() {
if (StringUtils.isNotBlank(basePath)) {
LOG.info("Creating a new GLIS API client for {}", basePath);
List<HttpMessageConverter<?>> messageConverters = new ArrayList<HttpMessageConverter<?>>();
// XML converter using JAXB2 (Jackson cannot handle wrapped xml lists!)
var jaxb2 = new Jaxb2RootElementHttpMessageConverter();
messageConverters.add(jaxb2);
RestTemplate restTemplate = new RestTemplate(messageConverters);
restTemplate.setRequestFactory(new BufferingClientHttpRequestFactory(restTemplate.getRequestFactory()));
ApiClient client = new ApiClient(restTemplate);
client.setDebugging("true".equals(System.getenv("GLIS_DEBUG")));
client.setBasePath(basePath);
// Basic HTTP auth is not required for /register or /update - credentials are embedded in the request object
// HttpBasicAuth easySmtaAuth = (HttpBasicAuth) client.getAuthentication("easySmta");
// easySmtaAuth.setUsername(username);
// easySmtaAuth.setPassword(password);
return client;
} else {
return null;
}
}
@Bean
public FactoryBean<ManagerApi> glisManagerApi() {
return new FactoryBean<ManagerApi>() {
@Override
public Class<?> getObjectType() {
return ManagerApi.class;
}
@Override
public ManagerApi getObject() throws Exception {
var glisClient = glisClient();
if (glisClient == null) {
return null;
}
LOG.warn("Preparing a new GLIS DOI Manager at {}", glisClient.getBasePath());
return new ManagerApi(glisClient);
}
};
}
}
/*
* Copyright 2022 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.gringlobal.service.glis.impl;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.genesys.glis.v1.api.ManagerApi;
import org.genesys.glis.v1.model.Acquisition;
import org.genesys.glis.v1.model.Actor;
import org.genesys.glis.v1.model.BasePGRFA;
import org.genesys.glis.v1.model.Breeder;
import org.genesys.glis.v1.model.Breeding;
import org.genesys.glis.v1.model.Collection;
import org.genesys.glis.v1.model.Collector;
import org.genesys.glis.v1.model.Location;
import org.genesys.glis.v1.model.PGRFARegistration;
import org.genesys.glis.v1.model.PGRFAUpdate;
import org.gringlobal.api.exception.InvalidApiUsageException;
import org.gringlobal.api.exception.NotFoundElement;
import org.gringlobal.api.v1.MultiOp;
import org.gringlobal.api.v1.MultiOp.MultiOpError;
import org.gringlobal.model.Accession;
import org.gringlobal.model.community.AccessionMCPD;
import org.gringlobal.persistence.AccessionRepository;
import org.gringlobal.service.AccessionService;
import org.gringlobal.service.AppSettingsService;
import org.gringlobal.service.filter.AccessionFilter;
import org.gringlobal.spring.TransactionHelper;
import org.gringlobal.worker.AccessionMCPDConverter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Pageable;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
@Component
public class GlisDOIRegistrationManager {
private final Logger LOG = LoggerFactory.getLogger(getClass());
private static final String APPSETTINGS_GLIS_DOI = "GLIS_DOI";
private final SimpleDateFormat mcpdDateFormat = new SimpleDateFormat("yyyyMMdd");
private final SimpleDateFormat requestFormat = new SimpleDateFormat("yyyy-MM-dd");
@Autowired
private AccessionService accessionService;
@Autowired
private AccessionRepository accessionRepository;
@Autowired
private AppSettingsService appSettingsService;
@Autowired
private AccessionMCPDConverter accessionMCPDConverter;
@Autowired
private FactoryBean<ManagerApi> managerApiFactory;
public static class DoiUpdate {
public long accessionId;
public String accessionNumber;
public String doi;
public DoiUpdate(long accessionId, String accessionNumber, String doi) {
this.accessionId = accessionId;
this.accessionNumber = accessionNumber;
this.doi = doi;
}
}
public static class DoiManagerError extends Exception {
private static final long serialVersionUID = 1L;
public DoiManagerError(String error) {
super(error);
}
}
/**
* Update GLIS DOI Registration Service for one Accession.
*
* @param accession The accession to update in GLIS
* @return DOI Registration Status
* @throws Throwable the problem
*/
@PreAuthorize("@ggceSec.actionAllowed('PassportData', 'ADMINISTRATION')")
@Transactional(readOnly = true)
public DoiUpdate updateDoiRegistration(Accession accession) throws Throwable {
accession = accessionRepository.getById(accession.getId()); // Reload
var result = updateGlisRegistration(List.of(accession));
if (CollectionUtils.isEmpty(result.success)) {
throw result.errors.get(0).error;
} else {
return result.success.get(0);
}
}
/**
* Update GLIS DOI Registration service to register or update PGRFA. Registration of a new PGRFA will
* result in generation of a new DOI.
*
* @param filter Accession filter
* @return MultiOp result
* @throws Exception When stuff goes wrong
*/
@PreAuthorize("@ggceSec.actionAllowed('PassportData', 'ADMINISTRATION')")
@Transactional(readOnly = true)
public MultiOp<DoiUpdate> updateDoiRegistration(AccessionFilter filter) throws Exception {
var accessions = accessionService.list(filter, Pageable.unpaged()).getContent();
return updateGlisRegistration(accessions);
}
private MultiOp<DoiUpdate> updateGlisRegistration(List<Accession> accessions) throws Exception {
var managerApi = createGlisManager();
String glisUsername = appSettingsService.getSetting(APPSETTINGS_GLIS_DOI, "username").getValue();
String glisPassword = appSettingsService.getSetting(APPSETTINGS_GLIS_DOI, "password").getValue();
String glisInstitutePid = appSettingsService.getSetting(APPSETTINGS_GLIS_DOI, "PID").getValue();
String glisInstituteName = appSettingsService.getSetting(APPSETTINGS_GLIS_DOI, "InstName").getValue();
String glisInstituteAddress = appSettingsService.getSetting(APPSETTINGS_GLIS_DOI, "InstituteAddress").getValue();
String glisInstituteCountry = appSettingsService.getSetting(APPSETTINGS_GLIS_DOI, "InstituteCtyCode").getValue();
MultiOp<DoiUpdate> result = new MultiOp<>();
result.success = new ArrayList<>();
result.errors = new ArrayList<>();
AtomicInteger errorCounter = new AtomicInteger(0);
accessions.forEach(accession -> {
// Skip if not web visible
if (! Objects.equals("Y", accession.getIsWebVisible())) {
result.errors.add(new MultiOpError(errorCounter.getAndIncrement(), new Exception("Accession " + accession.getAccessionNumber() + " is not visible to external users")));
return;
}
var accessionMCPD = accessionMCPDConverter.convert(accession);
BasePGRFA request;
if (StringUtils.isNotBlank(accessionMCPD.puid)) {
PGRFAUpdate update = new PGRFAUpdate();
update.setSampledoi(accessionMCPD.puid); // Current doi
request = update;
} else {
// Build new registration request
PGRFARegistration registration = new PGRFARegistration();
request = registration;
}
request.setUsername(glisUsername);
request.setPassword(glisPassword);
Location location = new Location();
location.setWiews(accessionMCPD.instCode);
// Set holding institute
location.setPid(glisInstitutePid);
location.setName(glisInstituteName);
location.setAddress(glisInstituteAddress);
location.setCountry(glisInstituteCountry);
request.setLocation(location);
request.setSampleid(accessionMCPD.acceNumb);
if (StringUtils.isNotBlank(accessionMCPD.acqDate)) {
request.setDate(convertMcpdDateToRequest(accessionMCPD.acqDate));
}
// Declare method
request.setMethod(determineMethod(accessionMCPD));
request.setGenus(accessionMCPD.genus);
request.setSpecies(accessionMCPD.species);
if (StringUtils.isNotBlank(accessionMCPD.cropName)) {
request.setCropnames(List.of(accessionMCPD.cropName));
}
if (StringUtils.isNotBlank(accessionMCPD.acceUrl)) {
// Skip accession URL
// Target target = new Target();
// target.setValue(accessionMCPD.acceUrl);
// // Define kws: 1-Passport data
// target.setKws(List.of("1"));
// request.setTargets(List.of(target));
}
request.setBiostatus(accessionMCPD.sampStat);
request.setSpauth(accessionMCPD.spAuthor);
request.setSubtaxa(accessionMCPD.subtaxa);
request.setStauth(accessionMCPD.subtAuthor);
if (StringUtils.isNotBlank(accessionMCPD.otherNumb)) {
request.setNames(List.of(accessionMCPD.otherNumb.split(";")));
}
// Add other IDs
// BasePGRFAId basePGRFAIds = new BasePGRFAId();
// request.setIds(List.of(basePGRFAIds));
request.setMlsstatus(accessionMCPD.mlsStat);
request.setHistorical(accessionMCPD.historical != null && accessionMCPD.historical ? "y" : "n");
Acquisition acquisition = new Acquisition();
Actor actor = new Actor();
actor.setWiews(accessionMCPD.donorCode);
actor.setName(accessionMCPD.donorName);
acquisition.setProvider(actor);
acquisition.setSampleid(accessionMCPD.donorNumb);
request.setAcquisition(acquisition);
Collection collection = new Collection();
if (StringUtils.isNotBlank(accessionMCPD.collName)) { // collector name is required
Collector collector = new Collector();
collector.setWiews(accessionMCPD.collCode);
collector.setName(accessionMCPD.collName);
collector.setAddress(accessionMCPD.collInstAddress);
collector.setCountry(accessionMCPD.origCty);
collection.setCollectors(List.of(collector));
}
collection.setSampleid(accessionMCPD.collNumb);
collection.setMissid(accessionMCPD.collMissid);
collection.setSite(accessionMCPD.collSite);
if (accessionMCPD.decLatitude != null) {
if (accessionMCPD.decLatitude != null) {
collection.setLat(String.valueOf(accessionMCPD.decLatitude));
collection.setLon(String.valueOf(accessionMCPD.decLongitude));
collection.setDatum(accessionMCPD.coordDatum);
collection.setGeoref(accessionMCPD.geoRefMeth);
if (accessionMCPD.coordUncert != null) {
collection.setUncert(String.valueOf(accessionMCPD.coordUncert));
}
}
}
collection.setElevation(accessionMCPD.elevation);
if (StringUtils.isNotBlank(accessionMCPD.collDate)) {
collection.setDate(convertMcpdDateToRequest(accessionMCPD.collDate));
}
collection.setSource(accessionMCPD.collSrc == null ? "" : String.valueOf(accessionMCPD.collSrc));
request.setCollection(collection);
Breeding breeding = new Breeding();
Breeder breeder = new Breeder();
breeder.setWiews(accessionMCPD.bredCode);
breeder.setName(accessionMCPD.bredName);
breeding.setBreeders(List.of(breeder));
breeding.setAncestry(accessionMCPD.ancest);
request.setBreeding(breeding);
if (request instanceof PGRFARegistration) {
var response = managerApi.registerPGRFA((PGRFARegistration) request);
if (response.getDoi() != null) {
assert(response.getSampleid().equals(accession.getAccessionNumber()));
// Assign DOI
try {
var saved = TransactionHelper.executeInTransaction(false, () -> {
var a = accessionRepository.getById(accession.getId());
a.setDoi(response.getDoi());
return accessionRepository.save(a);
});
result.success.add(new DoiUpdate(saved.getId(), saved.getAccessionNumber(), saved.getDoi()));
} catch (Throwable e) {
result.errors.add(new MultiOpError(errorCounter.getAndIncrement(), e));
}
} else {
LOG.warn("Exception in registration accession with acceNumb {}: {}.", accessionMCPD.acceNumb, response.getError());
result.errors.add(new MultiOpError(errorCounter.getAndIncrement(), new DoiManagerError(response.getError())));
}
} else if (request instanceof PGRFAUpdate) {
var response = managerApi.updatePGRFA((PGRFAUpdate) request);
if (response.getDoi() != null) {
assert(response.getSampleid().equals(accession.getAccessionNumber()));
result.success.add(new DoiUpdate(accession.getId(), accession.getAccessionNumber(), accession.getDoi()));
} else {
LOG.warn("Exception in registration accession with acceNumb {}: {}.", accessionMCPD.acceNumb, response.getError());
result.errors.add(new MultiOpError(errorCounter.getAndIncrement(), new DoiManagerError(response.getError())));
}
}
});
return result;
}
private ManagerApi createGlisManager() throws Exception {
var managerApi = managerApiFactory.getObject();
if (managerApi == null) {
throw new InvalidApiUsageException("GLIS client not available.");
}
managerApi.getApiClient().setDebugging(true);
try {
String glisUsername = appSettingsService.getSetting(APPSETTINGS_GLIS_DOI, "username").getValue();
appSettingsService.getSetting(APPSETTINGS_GLIS_DOI, "password").getValue();
String glisInstitutePid = appSettingsService.getSetting(APPSETTINGS_GLIS_DOI, "PID").getValue();
appSettingsService.getSetting(APPSETTINGS_GLIS_DOI, "InstName").getValue();
appSettingsService.getSetting(APPSETTINGS_GLIS_DOI, "InstituteAddress").getValue();
appSettingsService.getSetting(APPSETTINGS_GLIS_DOI, "InstituteCtyCode").getValue();
if (StringUtils.isBlank(glisUsername) || StringUtils.isBlank(glisInstitutePid)) {
throw new InvalidApiUsageException("Missing credentials for GLIS DOI Registration");
}
return managerApi;
} catch (NotFoundElement e) {
throw new InvalidApiUsageException("GLIS DOI Registration configuration is incomplete", e);
}
}
private String determineMethod(AccessionMCPD accessionMCPD) {
if (accessionMCPD.collDate != null) {
return "acqu"; // Acquisition
} else if (accessionMCPD.donorCode != null || accessionMCPD.donorName != null) {
return "acqu"; // Acquisition
} else if (accessionMCPD.bredCode != null || accessionMCPD.bredName != null) {
return "acqu"; // Acquisition
} else {
return "obin"; // Inherited
}
}
private String convertMcpdDateToRequest(String mcpdDate) {
try {
return requestFormat.format(mcpdDateFormat.parse(mcpdDate));
} catch (ParseException e) {
LOG.error("Exception in parsing mcpd date {}", mcpdDate, e);
return null;
}
}
}