GlisDOIRegistrationManager.java

/*
 * 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 static org.gringlobal.service.glis.impl.GlisSMTAReportingManager.*;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;

import lombok.extern.slf4j.Slf4j;
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.PGRFARegistrationResponse;
import org.genesys.glis.v1.model.PGRFAUpdate;
import org.gringlobal.api.exception.InvalidApiUsageException;
import org.gringlobal.api.exception.NotFoundElement;
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.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.domain.Pageable;
import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.client.HttpClientErrorException;

import com.fasterxml.jackson.dataformat.xml.XmlMapper;

@Component
@Slf4j
public class GlisDOIRegistrationManager {

	private static final String APPSETTINGS_ITPGRFA_GLIS = "ITPGRFA_GLIS";

	@Autowired
	private AccessionService accessionService;

	@Autowired
	private AccessionRepository accessionRepository;

	@Autowired
	private AppSettingsService appSettingsService;

	@Autowired
	private AccessionMCPDConverter accessionMCPDConverter;

	@Autowired
	@Qualifier("glisDOIManagerApiFactory")
	private FactoryBean<ManagerApi> managerApiFactory;

	public static class GlisDoiResponse {
		public long accessionId;
		public String accessionNumber;
		public String doi;
		public String error;

		public GlisDoiResponse(long accessionId, String accessionNumber, String doi) {
			this.accessionId = accessionId;
			this.accessionNumber = accessionNumber;
			this.doi = doi;
		}
	}

	/**
	 * 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 GlisDoiResponse updateDoiRegistration(Accession accession) throws Throwable {
		accession = accessionRepository.getReferenceById(accession.getId()); // Reload

		var result = updateGlisRegistration(List.of(accession)).get(0);

		if (StringUtils.isNotBlank(result.error)) {
			throw new InvalidApiUsageException(result.error);
		} else {
			return result;
		}
	}

	/**
	 * 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 List<GlisDoiResponse> updateDoiRegistration(AccessionFilter filter) throws Exception {
		var accessions = accessionService.list(filter, Pageable.unpaged()).getContent();
		return updateGlisRegistration(accessions);
	}
	
	private List<GlisDoiResponse> updateGlisRegistration(List<Accession> accessions) throws Exception {
		var managerApi = createGlisManager();

		String glisUsername = appSettingsService.getSetting(APPSETTINGS_ITPGRFA_GLIS, SETTING_USERNAME).getValue();
		String glisPassword = appSettingsService.getSetting(APPSETTINGS_ITPGRFA_GLIS, SETTING_PASSWORD).getValue();
		String glisInstitutePid = appSettingsService.getSetting(APPSETTINGS_ITPGRFA_GLIS, SETTING_PID).getValue();
		String glisInstituteName = appSettingsService.getSetting(APPSETTINGS_ITPGRFA_GLIS, SETTING_INSTITUTE_NAME).getValue();
		String glisInstituteAddress = appSettingsService.getSetting(APPSETTINGS_ITPGRFA_GLIS, SETTING_INSTITUTE_ADDRESS).getValue();
		String glisInstituteCountry = appSettingsService.getSetting(APPSETTINGS_ITPGRFA_GLIS, SETTING_INSTITUTE_COUNTRY_CODE).getValue();

		var result = new ArrayList<GlisDoiResponse>();

		AtomicInteger errorCounter = new AtomicInteger(0);

		XmlMapper xmlMapper = new XmlMapper(); // For reading HTTP 400 errors

		for (var accession : accessions) {
			var doiUpdate = new GlisDoiResponse(accession.getId(), accession.getAccessionNumber(), accession.getDoi());
			result.add(doiUpdate);

			// Skip if not web visible
			if (! Objects.equals("Y", accession.getIsWebVisible())) {
				doiUpdate.error = "Accession is not visible to external users";
				continue;
			}
			// Skip if acquisition date missing
			if (accession.getInitialReceivedDate() == null) {
				doiUpdate.error = "Accession initialReceiedDate is not declared";
				continue;
			}

			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 && accessionMCPD.decLongitude != 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);

			try {
				if (request instanceof PGRFARegistration) {
					var response = managerApi.registerPGRFA((PGRFARegistration) request);

					// GLIS DOI API returned 200 OK
					assert(response.getDoi() != null);
					assert(response.getSampleid().equals(accession.getAccessionNumber()));
					// Assign DOI
					try {
						var saved = TransactionHelper.executeInTransaction(false, () -> {
							var a = accessionRepository.getReferenceById(accession.getId());
							a.setDoi(response.getDoi());
							return accessionRepository.save(a);
						});
						doiUpdate.doi = saved.getDoi();
						doiUpdate.error = null; // Just in case

					} catch (Throwable e) {
						doiUpdate.error = e.getMessage();
					}

				} else if (request instanceof PGRFAUpdate) {
					var response = managerApi.updatePGRFA((PGRFAUpdate) request);

					// GLIS DOI API returned 200 OK
					assert(response.getDoi() != null);
					assert(response.getSampleid().equals(accession.getAccessionNumber()));
					doiUpdate.error = null; // Just in case
				}

			} catch (HttpClientErrorException e) {
				errorCounter.incrementAndGet();
				log.error("Error calling GLIS API {}: {}", e.getClass(), e.getMessage());

				if (e.getResponseHeaders().getContentType().isCompatibleWith(MediaType.APPLICATION_XML)) {
					var glisResponse = xmlMapper.readValue(e.getResponseBodyAsString(), PGRFARegistrationResponse.class);
					doiUpdate.error = glisResponse.getError();
				} else {
					doiUpdate.error = e.getMessage();
				}

			} catch (Throwable e) {
				errorCounter.incrementAndGet();
				log.error("Error interacting with GLIS API {}: {}", e.getClass(), e.getMessage());
				doiUpdate.error = e.getMessage();
			}

			if (errorCounter.get() > 10) {
				log.warn("Stopping after too many (10) errors.");
				break;
			}
		}

		return result;
	}

	private ManagerApi createGlisManager() throws Exception {
		var managerApi = managerApiFactory.getObject();
		if (managerApi == null) {
			throw new InvalidApiUsageException("GLIS client not available.");
		}

		if (log.isDebugEnabled() || log.isTraceEnabled()) {
			managerApi.getApiClient().setDebugging(true);
		}

		try {
			String glisUsername = appSettingsService.getSetting(APPSETTINGS_ITPGRFA_GLIS, SETTING_USERNAME).getValue();
			appSettingsService.getSetting(APPSETTINGS_ITPGRFA_GLIS, SETTING_PASSWORD).getValue();
			String glisInstitutePid = appSettingsService.getSetting(APPSETTINGS_ITPGRFA_GLIS, SETTING_PID).getValue();
			appSettingsService.getSetting(APPSETTINGS_ITPGRFA_GLIS, SETTING_INSTITUTE_NAME).getValue();
			appSettingsService.getSetting(APPSETTINGS_ITPGRFA_GLIS, SETTING_INSTITUTE_ADDRESS).getValue();
			appSettingsService.getSetting(APPSETTINGS_ITPGRFA_GLIS, SETTING_INSTITUTE_COUNTRY_CODE).getValue();
	
			if (StringUtils.isBlank(glisUsername) || StringUtils.isBlank(glisInstitutePid)) {
				throw new InvalidApiUsageException("Missing credentials for GLIS");
			}
			return managerApi;

		} catch (NotFoundElement e) {
			throw new InvalidApiUsageException("GLIS 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) {
		if (mcpdDate == null) return null;
		String requestDate = null;

		if (mcpdDate.matches("\\d{4}[0-]{4}")) {
			requestDate = mcpdDate.substring(0, 4);
		} else if (mcpdDate.matches("\\d{6}[0-]{2}")) {
			requestDate = String.format("%s-%s", mcpdDate.substring(0, 4), mcpdDate.substring(4, 6));
		} else if (mcpdDate.matches("\\d{8}")) {
			requestDate = String.format("%s-%s-%s", mcpdDate.substring(0, 4), mcpdDate.substring(4, 6), mcpdDate.substring(6, 8));
		}
		return requestDate;
	}
}