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.model.community.CommunityCodeValues.ACCESSION_NAME_TYPE_PROGDOI;
import static org.gringlobal.model.community.CommunityCodeValues.ACCESSION_SOURCE_TYPE;
import static org.gringlobal.model.community.CommunityCodeValues.ACCESSION_SOURCE_TYPE_COLLECTED;
import static org.gringlobal.model.community.CommunityCodeValues.ACCESSION_SOURCE_TYPE_COPY;
import static org.gringlobal.model.community.CommunityCodeValues.ACCESSION_SOURCE_TYPE_DEVELOPED;
import static org.gringlobal.model.community.CommunityCodeValues.ACCESSION_SOURCE_TYPE_DONATED;
import static org.gringlobal.model.community.CommunityCodeValues.ACCESSION_SOURCE_TYPE_VARIANT;
import static org.gringlobal.service.glis.impl.GlisSMTAReportingManager.*;

import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import com.fasterxml.jackson.core.JsonProcessingException;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Strings;
import org.genesys.blocks.security.SecurityContextUtil;
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.application.config.GGCESecurityConfig;
import org.gringlobal.custom.elasticsearch.SearchException;
import org.gringlobal.model.Accession;
import org.gringlobal.model.AccessionInvName;
import org.gringlobal.model.AccessionSource;
import org.gringlobal.model.community.AccessionMCPD;
import org.gringlobal.model.community.CommunityCodeValues;
import org.gringlobal.persistence.AccessionRepository;
import org.gringlobal.persistence.InventoryRepository;
import org.gringlobal.service.AccessionService;
import org.gringlobal.service.AppSettingsService;
import org.gringlobal.service.CodeValueService;
import org.gringlobal.service.InventoryService;
import org.gringlobal.service.ShortFilterService;
import org.gringlobal.service.filter.AccessionFilter;
import org.genesys.blocks.util.TransactionHelper;
import org.gringlobal.service.filter.CooperatorOwnedModelFilter;
import org.gringlobal.service.filter.InventoryFilter;
import org.gringlobal.util.MCPDDate;
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.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.web.client.HttpClientErrorException;

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

import javax.validation.constraints.NotNull;

@Component
@Slf4j
public class GlisDOIRegistrationManager {

	private static final String APPSETTINGS_ITPGRFA_GLIS = "ITPGRFA_GLIS";

	private static final String ASSIGN_DOI_TO_MANY_ERRORS = "The DOI assignment operation was stopped due to multiple processing errors.";

	@Autowired
	private AccessionService accessionService;

	@Autowired
	private AccessionRepository accessionRepository;

	@Autowired
	private InventoryService inventoryService;

	@Autowired
	private InventoryRepository inventoryRepository;

	@Autowired
	private AppSettingsService appSettingsService;

	@Autowired
	private AccessionMCPDConverter accessionMCPDConverter;

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

	@Autowired
	private ThreadPoolTaskExecutor executor;

	@Autowired
	private ShortFilterService shortFilterService;

	@Autowired
	private TransactionHelper transactionHelper;

	@Autowired
	private CodeValueService codeValueService;

	@Autowired
	private GGCESecurityConfig.GgceSec ggceSec;

	private final AtomicReference<AssignDoiState> assignDoiState = new AtomicReference<>();

	public static class AssignDoiState {
		public String filterCode;
		public AssignDoiProgressData progress;
		public boolean canceled;

		public AssignDoiState(String filterCode, AssignDoiProgressData progress) {
			this.filterCode = filterCode;
			this.progress = progress;
		}

		public AssignDoiState updateProgress(int total, List<GlisDoiResponse> processed, AssignDoiProgressData.Status status, String error) {
			this.progress.status = status;
			this.progress.total = total;
			this.progress.processed = processed;
			this.progress.error = error;
			return this;
		}

		public AssignDoiState updateProgress(int total, AssignDoiProgressData.Status status) {
			this.progress.status = status;
			this.progress.total = total;
			return this;
		}

		public AssignDoiState updateProgress(GlisDoiResponse processedToAdd, AssignDoiProgressData.Status status) {
			this.progress.status = status;
			this.progress.processed.add(processedToAdd);
			return this;
		}

		public AssignDoiState updateProgress(AssignDoiProgressData.Status status, String error) {
			this.progress.status = status;
			this.progress.error = error;
			return this;
		}

		public AssignDoiState updateProgress(AssignDoiProgressData.Status status) {
			this.progress.status = status;
			return this;
		}

		public AssignDoiState cancel() {
			this.canceled = true;
			return this;
		}
	}

	@Data
	public static class AssignDoiProgressData {
		public enum Status {UPLOADING, ABORTED, DONE}

		private Status status;
		private long total;
		private List<GlisDoiResponse> processed;
		private String error;

		public AssignDoiProgressData(Status status, long total, List<GlisDoiResponse> processed) {
			this.status = status;
			this.total = total;
			this.processed = processed;
		}
	}

	public static class InventoryGlisDoiResponse extends GlisDoiResponse {
		public long inventoryId;
		public String inventoryNumber;

		public InventoryGlisDoiResponse(long inventoryId, String inventoryNumber, String doi) {
			super(doi);
			this.inventoryId = inventoryId;
			this.inventoryNumber = inventoryNumber;
		}
	}

	public static class AccessionGlisDoiResponse extends GlisDoiResponse {
		public long accessionId;
		public String accessionNumber;

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

	public static abstract class GlisDoiResponse {
		public String doi;
		public String error;

		public GlisDoiResponse(String doi) {
			this.doi = doi;
		}
	}

	public ResponseEntity<HttpStatus> cancelDoiAssignment(@NotNull String filterCode) {

		if (assignDoiState.get() != null && assignDoiState.get().filterCode.equals(filterCode)) {
			// cancel only active uploading
			if (assignDoiState.get().progress.status == AssignDoiProgressData.Status.UPLOADING) {
				var sid = SecurityContextUtil.getCurrentUser() != null ? SecurityContextUtil.getCurrentUser().getSid() : null;
				log.warn("DOI assignment was canceled by {}", sid);
				assignDoiState.set(assignDoiState.get().cancel());
			}
		} else {
			throw new InvalidApiUsageException("No active assigning is currently running for requested ids.");
		}
		return ResponseEntity.ok().build();
	}

	@PreAuthorize("hasAuthority('GROUP_ADMINS')")
	public void stopDoiAssignmentProcess() {
		if (assignDoiState.get() != null) {
			// cancel only active uploading
			if (assignDoiState.get().progress.status == AssignDoiProgressData.Status.UPLOADING) {
				var sid = SecurityContextUtil.getCurrentUser() != null ? SecurityContextUtil.getCurrentUser().getSid() : null;
				log.warn("DOI assignment was stoped by {}", sid);
				assignDoiState.set(null);
			} else {
				throw new InvalidApiUsageException("No active process is currently running.");
			}
		}
	}

	/**
	 * Update GLIS DOI Registration service to register or update PGRFA. Registration of a new PGRFA will
	 * result in generation of a new DOI.
	 *
	 * @param filterCode filter code
	 * @param filter AccessionFilter
	 * @return AssignDoiProgressData result
	 */
	@Transactional(readOnly = true)
	public List<GlisDoiResponse> updateAccessionDoiRegistration(String filterCode, AccessionFilter filter) throws Exception {
		final var filterInfo = shortFilterService.processFilter(filterCode, filter, AccessionFilter.class);
		if (filterInfo.filter.toString().equals("{}"))
			throw new InvalidApiUsageException("Refusing to upload accessions by empty filter.");

		return updateGlisRegistration(filterInfo);
	}

	/**
	 * Update GLIS DOI Registration service to register or update PGRFA. Registration of a new PGRFA will
	 * result in generation of a new DOI.
	 *
	 * @param filterCode filter code
	 * @param filter AccessionFilter
	 * @return AssignDoiProgressData result
	 */
	@Transactional(readOnly = true)
	public AssignDoiState updateAccessionDoiRegistrationWithProgress(String filterCode, AccessionFilter filter) throws Exception {
		final var filterInfo = shortFilterService.processFilter(filterCode, filter, AccessionFilter.class);
		if (filterInfo.filter.toString().equals("{}"))
			throw new InvalidApiUsageException("Refusing to upload accessions by empty filter.");

		return updateGlisRegistrationWithProgress(filterInfo);
	}

	/**
	 * Update GLIS DOI Registration service to register or update PGRFA. Registration of a new PGRFA will
	 * result in a generation of a new DOI.
	 *
	 * @param filterCode filter code
	 * @param filter InventoryFilter
	 * @return AssignDoiProgressData result
	 */
	@Transactional(readOnly = true)
	public List<GlisDoiResponse> updateInventoryDoiRegistration(String filterCode, InventoryFilter filter) throws Exception {
		final var filterInfo = shortFilterService.processFilter(filterCode, filter, InventoryFilter.class);
		if (filterInfo.filter.toString().equals("{}")) {
			throw new InvalidApiUsageException("Refusing to upload inventories by empty filter.");
		}

		return updateGlisRegistration(filterInfo);
	}

	/**
	 * Update GLIS DOI Registration service to register or update PGRFA. Registration of a new PGRFA will
	 * result in a generation of a new DOI.
	 *
	 * @param filterCode filter code
	 * @param filter AccessionFilter
	 * @return AssignDoiProgressData result
	 */
	@Transactional(readOnly = true)
	public AssignDoiState updateInventoryDoiRegistrationWithProgress(String filterCode, InventoryFilter filter) throws Exception {
		final var filterInfo = shortFilterService.processFilter(filterCode, filter, InventoryFilter.class);
		if (filterInfo.filter.toString().equals("{}")) {
			throw new InvalidApiUsageException("Refusing to upload inventories by empty filter.");
		}

		return updateGlisRegistrationWithProgress(filterInfo);
	}

	private List<GlisDoiResponse> updateGlisRegistration(ShortFilterService.FilterInfo<? extends CooperatorOwnedModelFilter<?, ?, ?>> filterInfo) {

		if (assignDoiState.get() != null && assignDoiState.get().progress.status == AssignDoiProgressData.Status.UPLOADING) {
			// only one active upload at a time!
			throw new InvalidApiUsageException("Another assign DOI process is currently running.");
		}

		assignDoiState.set(new AssignDoiState(filterInfo.filterCode, new AssignDoiProgressData(AssignDoiProgressData.Status.UPLOADING, 0, new LinkedList<>())));
		try {
			return assignDOI(filterInfo.filter);
		} catch (Throwable e) {
			log.warn("Error while DOI assignment: {}", e.getMessage());
			assignDoiState.get().updateProgress(AssignDoiProgressData.Status.ABORTED, e.getMessage());
			throw new InvalidApiUsageException("An error occured while submitting to GLIS", e);
		}
	}

	
	private AssignDoiState updateGlisRegistrationWithProgress(ShortFilterService.FilterInfo<? extends CooperatorOwnedModelFilter<?, ?, ?>> filterInfo) {
		if (assignDoiState.get() != null && assignDoiState.get().filterCode.equals(filterInfo.filterCode)) {
			if (assignDoiState.get().progress.status != AssignDoiProgressData.Status.UPLOADING) {
				// return progress if it's DONE or ABORTED, then clear it
				return assignDoiState.getAndSet(null);
			} else {
				return assignDoiState.get(); // return progress, still uploading...
			}
		} else if (assignDoiState.get() != null && !assignDoiState.get().filterCode.equals(filterInfo.filterCode)) {
			if (assignDoiState.get().progress.status == AssignDoiProgressData.Status.UPLOADING) {
				// only one active upload at a time!
				throw new InvalidApiUsageException("Another assign DOI process is currently running.");
			}
		}

		final Authentication currentAuth = SecurityContextHolder.getContext().getAuthentication();
		executor.submit(() -> {
			try {
				transactionHelper.asUser(currentAuth, () -> {
					assignDoiState.set(new AssignDoiState(filterInfo.filterCode, new AssignDoiProgressData(AssignDoiProgressData.Status.UPLOADING, 0, new LinkedList<>())));
					try {
						assignDOI(filterInfo.filter);
					} catch (Throwable e) {
						log.warn("Error while DOI assignment: {}", e.getMessage());
						assignDoiState.get().updateProgress(AssignDoiProgressData.Status.ABORTED, e.getMessage());
					}
					return true;
				});
			} catch (Exception e) {
				log.warn("Error while updating GLIS registration with progress: {}", e.getMessage());
				assignDoiState.get().updateProgress(AssignDoiProgressData.Status.ABORTED, e.getMessage());
			}
		});
		return new AssignDoiState(filterInfo.filterCode, new AssignDoiProgressData(AssignDoiProgressData.Status.UPLOADING, 0, new LinkedList<>()));
	}

	private List<GlisDoiResponse> assignDOI(CooperatorOwnedModelFilter<?, ?, ?> filter) 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();

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

		boolean completed = transactionHelper.executeInTransaction(true, () -> {
			if (filter instanceof AccessionFilter) {
				return assignAccessionDoi((AccessionFilter) filter, glisUsername, glisPassword, glisInstitutePid, glisInstituteName, glisInstituteAddress, glisInstituteCountry, managerApi, errorCounter, xmlMapper);
			} else if (filter instanceof InventoryFilter) {
				return assignInventoryDoi((InventoryFilter) filter, glisUsername, glisPassword, glisInstitutePid, glisInstituteName, glisInstituteAddress, glisInstituteCountry, managerApi, errorCounter, xmlMapper);
			} else {
				throw new InvalidApiUsageException("Unsupported filter type: " + filter.getClass().getName());
			}
		});
		if (completed) {
			assignDoiState.get().updateProgress(AssignDoiProgressData.Status.DONE);
		}
		return assignDoiState.get().progress.processed;
	}

	private Boolean assignAccessionDoi(AccessionFilter filter, String glisUsername, String glisPassword, String glisInstitutePid, String glisInstituteName,
		String glisInstituteAddress, String glisInstituteCountry, ManagerApi managerApi, AtomicInteger errorCounter, XmlMapper xmlMapper) throws SearchException, JsonProcessingException {

		var page = Pageable.ofSize(100);
		var accessionsPage = accessionService.list(filter, page);
		int total = (int) accessionsPage.getTotalElements();
		var accessions = accessionsPage.getContent();

		// Update total
		assignDoiState.get().updateProgress(total, AssignDoiProgressData.Status.UPLOADING);

		while (!accessions.isEmpty()) {
			for (var accession : accessions) {
				if (assignDoiState.get() == null || assignDoiState.get().canceled) {
					throw new InvalidApiUsageException("Assignment has been canceled by the user.");
				}
				if (errorCounter.get() > 10) {
					log.warn("Stopping after too many (10) errors.");
					assignDoiState.get().updateProgress(AssignDoiProgressData.Status.ABORTED, ASSIGN_DOI_TO_MANY_ERRORS);
					return false;
				}

				var doiUpdate = new AccessionGlisDoiResponse(accession.getId(), accession.getAccessionNumber(), accession.getDoi());

				// Skip if not web visible
				if (!Objects.equals("Y", accession.getIsWebVisible())) {
					doiUpdate.error = "Accession is not visible to external users";
					assignDoiState.get().updateProgress(doiUpdate, AssignDoiProgressData.Status.UPLOADING);
					continue;
				}
				// Skip if acquisition date missing
				if (accession.getInitialReceivedDate() == null) {
					doiUpdate.error = "Accession initialReceivedDate is not declared";
					assignDoiState.get().updateProgress(doiUpdate, AssignDoiProgressData.Status.UPLOADING);
					continue;
				}
				// Skip if permission denied
				boolean permissionDenied;
				if (accession.getDoi() != null) {
					permissionDenied = !ggceSec.actionAllowed("AccessionDOI", "WRITE", accession.getSite());
				} else {
					permissionDenied = !ggceSec.actionAllowed("AccessionDOI", "CREATE", accession.getSite());
				}
				if (permissionDenied) {
					doiUpdate.error = "Don't have permission to update DOI registration for site: " + accession.getSite().getSiteLongName();
					assignDoiState.get().updateProgress(doiUpdate, AssignDoiProgressData.Status.UPLOADING);
					errorCounter.incrementAndGet();
					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;
				}

				buildPGRFARequestByAccession(request, accessionMCPD, glisUsername, glisPassword, glisInstitutePid, glisInstituteName, glisInstituteAddress, glisInstituteCountry);

				// Declare method
				request.setMethod(determineMethod(accession));
				// Progenitor DOIs
				if (accessionMCPD.progDoi != null) {
					request.setProgdoi(List.of(accessionMCPD.progDoi.split(";\\s*")));
				}

				sendPGRFARequest(request, managerApi, doiUpdate, errorCounter, xmlMapper, (response) -> {
					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
						assignDoiState.get().updateProgress(doiUpdate, AssignDoiProgressData.Status.UPLOADING);
					} catch (Throwable e) {
						doiUpdate.error = e.getMessage();
						assignDoiState.get().updateProgress(doiUpdate, AssignDoiProgressData.Status.UPLOADING);
					}
				});
			}
			if (accessionsPage.hasNext()) {
				accessionsPage = accessionService.list(filter, accessionsPage.nextPageable());
				accessions = accessionsPage.getContent();
			} else {
				break;
			}
		}
		return true;
	}

	private Boolean assignInventoryDoi(InventoryFilter filter, String glisUsername, String glisPassword, String glisInstitutePid, String glisInstituteName,
		String glisInstituteAddress, String glisInstituteCountry, ManagerApi managerApi, AtomicInteger errorCounter, XmlMapper xmlMapper) throws SearchException, JsonProcessingException {

		var noSystemInventoryFilter = new InventoryFilter().includeSystem(false);
		noSystemInventoryFilter.AND(filter);

		var page = Pageable.ofSize(100);
		var inventoriesPage = inventoryService.list(noSystemInventoryFilter, page);
		int total = (int) inventoriesPage.getTotalElements();
		var inventories = inventoriesPage.getContent();

		// Update total
		assignDoiState.get().updateProgress(total, AssignDoiProgressData.Status.UPLOADING);

		while (!inventories.isEmpty()) {
			for (var inventory : inventories) {
				if (assignDoiState.get() == null || assignDoiState.get().canceled) {
					throw new InvalidApiUsageException("Assignment has been canceled by the user.");
				}
				if (errorCounter.get() > 10) {
					log.warn("Stopping after too many (10) errors.");
					assignDoiState.get().updateProgress(AssignDoiProgressData.Status.ABORTED, ASSIGN_DOI_TO_MANY_ERRORS);
					return false;
				}

				var doiUpdate = new InventoryGlisDoiResponse(inventory.getId(), inventory.getInventoryNumber(), inventory.getDoi());
				
				var accession = inventory.getAccession();

				// Skip if accession DOI is not assigned
				if (accession.getDoi() == null) {
					doiUpdate.error = "Inventory accession is not registered";
					assignDoiState.get().updateProgress(doiUpdate, AssignDoiProgressData.Status.UPLOADING);
					continue;
				}
				// Skip if not web visible
				if (!Objects.equals("Y", accession.getIsWebVisible())) {
					doiUpdate.error = "Inventory accession is not visible to external users";
					assignDoiState.get().updateProgress(doiUpdate, AssignDoiProgressData.Status.UPLOADING);
					continue;
				}
				// Skip if propagation date missing
				if (inventory.getPropagationDate() == null) {
					doiUpdate.error = "Inventory propagationDate is not declared";
					assignDoiState.get().updateProgress(doiUpdate, AssignDoiProgressData.Status.UPLOADING);
					continue;
				}
				// Skip if permission denied
				boolean permissionDenied;
				if (inventory.getDoi() != null) {
					permissionDenied = !ggceSec.actionAllowed("InventoryDOI", "WRITE", inventory.getSite());
				} else {
					permissionDenied = !ggceSec.actionAllowed("InventoryDOI", "CREATE", inventory.getSite());
				}
				if (permissionDenied) {
					doiUpdate.error = "Don't have permission to update DOI registration for site: " + inventory.getSite().getSiteLongName();
					assignDoiState.get().updateProgress(doiUpdate, AssignDoiProgressData.Status.UPLOADING);
					errorCounter.incrementAndGet();
					continue;
				}

				var accessionMCPD = accessionMCPDConverter.convert(accession);
				BasePGRFA request;

				if (StringUtils.isNotBlank(inventory.getDoi())) {
					PGRFAUpdate update = new PGRFAUpdate();
					update.setSampledoi(inventory.getDoi()); // Current doi
					request = update;
				} else {
					// Build new registration request
					PGRFARegistration registration = new PGRFARegistration();
					request = registration;
				}

				buildPGRFARequestByAccession(request, accessionMCPD, glisUsername, glisPassword, glisInstitutePid, glisInstituteName, glisInstituteAddress, glisInstituteCountry);

				// Clear collecting, donor and breeding data
				request.setCollection(null);
				request.setAcquisition(null);
				request.setBreeding(null);

				request.setHistorical(inventory.getQuantityOnHand() != null && inventory.getQuantityOnHand() == 0d ? "y" : "n");
				request.setSampleid(inventory.getInventoryNumber());
				var mcpdDate = MCPDDate.convert(inventory.getPropagationDate(), inventory.getPropagationDateCode());
				request.setDate(convertMcpdDateToRequest(mcpdDate));
				request.setMethod(codeValueService.findMcpdOfCodeValue(ACCESSION_SOURCE_TYPE, CommunityCodeValues.ACCESSION_SOURCE_TYPE_VARIANT.value));

				// Add public inventory names
				var inventoryNames = inventory.getNames().stream().filter(n -> Objects.equals("Y", n.getIsWebVisible())).map(AccessionInvName::getPlantName).distinct().collect(Collectors.toList());
				if (inventoryNames.size() > 0) {
					var allNames = new LinkedList<String>();
					if (request.getNames() != null) {
						allNames.addAll(request.getNames());
					}
					allNames.addAll(inventoryNames);
					request.setNames(allNames.stream().distinct().collect(Collectors.toList()));
				}

				List<String> progdoi = new LinkedList<>();
				var parent = inventory.getParentInventory();
				while (parent != null) {
					parent = inventoryService.get(parent.getId());
					if (parent.getDoi() != null) {
						progdoi.add(parent.getDoi());
						break;
					}
				}
				if (progdoi.isEmpty()) {
					progdoi.add(accession.getDoi());
				}
				request.setProgdoi(progdoi);

				sendPGRFARequest(request, managerApi, doiUpdate, errorCounter, xmlMapper, (response) -> {
					try {
						var saved = transactionHelper.executeInTransaction(false, () -> {
							var a = inventoryRepository.getReferenceById(inventory.getId());
							a.setDoi(response.getDoi());
							return inventoryRepository.save(a);
						});
						doiUpdate.doi = saved.getDoi();
						doiUpdate.error = null; // Just in case
						assignDoiState.get().updateProgress(doiUpdate, AssignDoiProgressData.Status.UPLOADING);
					} catch (Throwable e) {
						doiUpdate.error = e.getMessage();
						assignDoiState.get().updateProgress(doiUpdate, AssignDoiProgressData.Status.UPLOADING);
					}
				});
			}
			if (inventoriesPage.hasNext()) {
				inventoriesPage = inventoryService.list(noSystemInventoryFilter, inventoriesPage.nextPageable());
				inventories = inventoriesPage.getContent();
			} else {
				break;
			}
		}
		return true;
	}

	private void sendPGRFARequest(BasePGRFA request, ManagerApi managerApi, GlisDoiResponse doiUpdate, AtomicInteger errorCounter, XmlMapper xmlMapper,
		Consumer<PGRFARegistrationResponse> responseProcessor) throws JsonProcessingException {

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

				// GLIS DOI API returned 200 OK
				assert (response.getDoi() != null);
				// Assign DOI
				responseProcessor.accept(response);

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

				// GLIS DOI API returned 200 OK
				assert (response.getDoi() != null);
				doiUpdate.error = null; // Just in case
				assignDoiState.get().updateProgress(doiUpdate, AssignDoiProgressData.Status.UPLOADING);
			}

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

			if (HttpStatus.UNAUTHORIZED.equals(e.getStatusCode())) {
				doiUpdate.error = e.getStatusCode().toString();
			} else 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();
			}
			assignDoiState.get().updateProgress(doiUpdate, AssignDoiProgressData.Status.UPLOADING);
		} catch (Throwable e) {
			errorCounter.incrementAndGet();
			log.error("Error interacting with GLIS API {}: {}", e.getClass(), e.getMessage());
			doiUpdate.error = e.getMessage();
			assignDoiState.get().updateProgress(doiUpdate, AssignDoiProgressData.Status.UPLOADING);
		}
	}

	private void buildPGRFARequestByAccession(BasePGRFA request, AccessionMCPD accessionMCPD, String glisUsername, String glisPassword, String glisInstitutePid, String glisInstituteName,
		String glisInstituteAddress, String glisInstituteCountry) {

		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));
		}

		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);

		var names = new LinkedList<String>();
		if (StringUtils.isNotBlank(accessionMCPD.acceName) && !Strings.CI.equals(accessionMCPD.acceNumb, accessionMCPD.acceName)) {
			names.add(accessionMCPD.acceName);
		}
		if (StringUtils.isNotBlank(accessionMCPD.otherNumb)) {
			names.addAll(List.of(accessionMCPD.otherNumb.split(";\\s*")));
		}
		request.setNames(names);
		request.setIds(null);

		// 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);
		acquisition.setProvenance(accessionMCPD.origCty);
		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);
	}

	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(Accession accession) {
		boolean hasProgdoiNames = accession.getNames() == null || accession.getNames().stream()
			.anyMatch(an -> an.getCategoryCode().equals(ACCESSION_NAME_TYPE_PROGDOI.value) && Strings.CI.equals("Y", an.getIsWebVisible()));
		var sources = accession.getAccessionSources();
		if (!hasProgdoiNames || CollectionUtils.isEmpty(sources)) {
			return "obin";
		}
		var sourceTypes = sources.stream().map(AccessionSource::getSourceTypeCode).collect(Collectors.toSet());
		String sourceType = null;
		if (sourceTypes.contains(ACCESSION_SOURCE_TYPE_COPY.value)) {
			sourceType = ACCESSION_SOURCE_TYPE_COPY.value;
		} else if (sourceTypes.contains(ACCESSION_SOURCE_TYPE_VARIANT.value)) {
			sourceType = ACCESSION_SOURCE_TYPE_VARIANT.value;
		} else if (sourceTypes.contains(ACCESSION_SOURCE_TYPE_DONATED.value)) {
			sourceType = ACCESSION_SOURCE_TYPE_DONATED.value;
		} else if (sourceTypes.contains(ACCESSION_SOURCE_TYPE_COLLECTED.value)) {
			sourceType = ACCESSION_SOURCE_TYPE_COLLECTED.value;
		} else if (sourceTypes.contains(ACCESSION_SOURCE_TYPE_DEVELOPED.value)) {
			sourceType = ACCESSION_SOURCE_TYPE_DEVELOPED.value;
		}

		if (sourceType == null) {
			return "obin";
		}

		return codeValueService.findMcpdOfCodeValue(ACCESSION_SOURCE_TYPE, sourceType);
	}

	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;
	}
}