RequestServiceImpl.java
/*
* Copyright 2019 Global Crop Diversity Trust
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.genesys.server.service.impl;
import static org.genesys.server.model.genesys.MaterialSubRequest.State.CONFIRMED;
import static org.genesys.server.model.genesys.MaterialSubRequest.State.DRAFT;
import static org.genesys.server.model.genesys.MaterialSubRequest.State.NOTCONFIRMED;
import java.io.IOException;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import javax.validation.Valid;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.genesys.blocks.security.SecurityContextUtil;
import org.genesys.server.exception.EasySMTAException;
import org.genesys.server.exception.InvalidApiUsageException;
import org.genesys.server.exception.NotFoundElement;
import org.genesys.server.model.genesys.Accession;
import org.genesys.server.model.genesys.MaterialRequest;
import org.genesys.server.model.genesys.MaterialSubRequest;
import org.genesys.server.model.genesys.QMaterialRequest;
import org.genesys.server.model.genesys.QMaterialSubRequest;
import org.genesys.server.model.impl.Country;
import org.genesys.server.model.impl.FaoInstitute;
import org.genesys.server.model.impl.FaoInstituteSetting;
import org.genesys.server.model.impl.User;
import org.genesys.server.model.impl.VerificationToken;
import org.genesys.server.persistence.CountryRepository;
import org.genesys.server.persistence.MaterialRequestRepository;
import org.genesys.server.persistence.MaterialSubRequestRepository;
import org.genesys.server.service.AccessionService;
import org.genesys.server.service.ArticleService;
import org.genesys.server.service.ArticleTranslationService;
import org.genesys.server.service.ContentService;
import org.genesys.server.service.EMailService;
import org.genesys.server.service.EasySMTA;
import org.genesys.server.service.GenesysService;
import org.genesys.server.service.InstituteService;
import org.genesys.server.service.RequestService;
import org.genesys.server.service.TokenVerificationService;
import org.genesys.server.service.TokenVerificationService.NoSuchVerificationTokenException;
import org.genesys.server.service.TokenVerificationService.TokenExpiredException;
import org.genesys.server.service.UserService;
import org.genesys.server.service.filter.MaterialRequestFilter;
import org.genesys.server.service.filter.MaterialSubRequestFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.dao.ConcurrencyFailureException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.querydsl.core.BooleanBuilder;
import com.querydsl.jpa.impl.JPAQueryFactory;
import net.javacrumbs.shedlock.spring.annotation.SchedulerLock;
@Service
@Transactional(readOnly = true)
@Validated
public class RequestServiceImpl implements RequestService {
private static final String REQUEST_TOKENTYPE = "confirm-request";
private static final String RECEIPT_TOKENTYPE = "confirm-receipt";
private static final Logger LOG = LoggerFactory.getLogger(RequestServiceImpl.class);
@Autowired
private EasySMTA pidChecker;
@Autowired
private TokenVerificationService tokenVerificationService;
@Autowired
private EMailService emailService;
@Autowired
private ContentService contentService;
@Autowired
private ArticleService articleService;
@Autowired
private GenesysService genesysService;
@Autowired
private AccessionService accessionService;
@Autowired
private InstituteService instituteService;
@Autowired
private MaterialRequestRepository requestRepository;
@Autowired
private MaterialSubRequestRepository subRequestRepository;
@Autowired
private CountryRepository countryRepository;
@Autowired
private JPAQueryFactory jpaQueryFactory;
@Value("${frontend.url}")
private String frontendUrl;
@Value("${mail.requests.to}")
private String requestsEmail;
@Autowired
private UserService userService;
private final ObjectMapper mapper = new ObjectMapper();
@Override
@Transactional
public MaterialRequest initiateRequest(RequestInfo requestInfo, Set<Long> accessionIds, String origin, String sid, String lang) throws RequestException {
final Set<Long> availableAccessionIds = genesysService.filterAvailableForDistribution(accessionIds);
return initiateRequest(availableAccessionIds, requestInfo, origin, sid, lang);
}
@Override
@Transactional
public MaterialRequest initiateRequestByUuids(RequestInfo requestInfo, Set<UUID> accessionUuids, String origin, String sid, String lang) throws RequestException {
final Set<Long> availableAccessionIds = accessionService.filterAvailableForDistributionByUuid(accessionUuids);
return initiateRequest(availableAccessionIds, requestInfo, origin, sid, lang);
}
private MaterialRequest initiateRequest(Set<Long> availableAccessionIds, RequestInfo requestInfo, String origin, String sid, String lang) throws RequestException {
final Locale locale = LocaleContextHolder.getLocale();
LOG.error("Current locale: {}", locale);
if (availableAccessionIds == null || availableAccessionIds.size() == 0) {
throw new RequestException("None of the selected accessions are available for distribution");
}
EasySMTA.EasySMTAUserData pid;
if (!requestInfo.isInternalRequest()) {
// Check Easy-SMTA for PID
try {
pid = pidChecker.getUserData(requestInfo.getEmail());
} catch (EasySMTAException e) {
throw new RequestException(e.getMessage(), e);
}
} else {
pid = requestInfo.getUserData();
requestInfo.setEmail(pid.getEmail());
Country country = countryRepository.findByCode3(pid.getCountry());
if (country != null) {
pid.setCountryName(country.getName(locale));
}
}
final MaterialRequest request = createRequest(requestInfo, pid, availableAccessionIds, origin, sid, lang);
return sendValidationEmail(request);
}
/**
* Create requests to holding institutes if no subrequests exist for this
* request.
*
* <p>This is part of the original API and is called from {@link #validateRequest(MaterialRequest)}</p>
*
* @param materialRequest
* @return
* @throws NoPidException
*/
private List<MaterialSubRequest> breakup(MaterialRequest materialRequest) throws NoPidException {
if (StringUtils.isBlank(materialRequest.getPid()) && !materialRequest.isInternalRequest()) {
LOG.warn("Material request has no PID, will not break it up.");
throw new NoPidException("Not breaking up request without PID.");
}
final List<MaterialSubRequest> existingRequests = subRequestRepository.findBySourceRequest(materialRequest);
if (existingRequests.size() > 0) {
// check for subrequests
LOG.warn("Subrequests exists, will not recreate them.");
return existingRequests;
}
RequestBody rb = null;
try {
rb = mapper.readValue(materialRequest.getBody(), RequestBody.class);
} catch (final IOException e) {
// FIXME Some other exception?
throw new RuntimeException("Could not handle request JSON", e);
}
final ArrayList<MaterialSubRequest> subrequests = new ArrayList<MaterialSubRequest>();
final List<FaoInstitute> holdingInstitutes = genesysService.findHoldingInstitutes(rb.accessionIds);
for (final FaoInstitute holdingInstitute : holdingInstitutes) {
final MaterialSubRequest subRequest = new MaterialSubRequest();
subRequest.setInstCode(holdingInstitute.getCode());
subRequest.setState(DRAFT.getValue());
subRequest.setSourceRequest(materialRequest);
final RequestBody srb = new RequestBody();
srb.pid = rb.pid;
srb.requestInfo = rb.requestInfo;
srb.accessionIds = genesysService.listAccessions(holdingInstitute, rb.accessionIds);
if (srb.accessionIds.size() == 0) {
LOG.warn("No accessions for this institute??");
}
subRequest.setBody(serialize(srb));
subrequests.add(subRequest);
}
return subRequestRepository.saveAll(subrequests);
}
MaterialRequest createRequest(RequestInfo requestInfo, EasySMTA.EasySMTAUserData pid, Set<Long> accessionIds, String origin, String sid, String lang) throws RequestException {
final Set<Long> availableAccessionIds = genesysService.filterAvailableForDistribution(accessionIds);
if (availableAccessionIds == null || availableAccessionIds.size() == 0) {
throw new RequestException("None of the selected accessions are available for distribution");
}
MaterialRequest request = new MaterialRequest();
request.setState(MaterialRequest.NOTVALIDATED);
request.setEmail(requestInfo.getEmail());
if (pid != null) {
if (requestInfo.isInternalRequest()) {
request.setInternalRequest(true);
// set to null to avoid duplication of user data in the RequestBody for MaterialRequest#body
// we don't need to save this data in the storage
requestInfo.setUserData(null);
}
request.setPid(pid.getPid());
}
final RequestBody rb = new RequestBody(requestInfo, pid, availableAccessionIds);
request.setBody(serialize(rb));
request.setOrigin(origin);
request.setSid(sid);
request.setLanguage(lang);
request = requestRepository.save(request);
LOG.info("Persisted new material request: {}", request);
return request;
}
@Override
@Transactional
public MaterialRequest createDraftRequest(Set<Long> accessionIds) throws RequestException {
var availableAccessionIds = accessionService.filterAvailableById(accessionIds);
if (availableAccessionIds == null || availableAccessionIds.size() == 0) {
throw new RequestException("None of the selected accessions are available for distribution");
}
MaterialRequest request = new MaterialRequest();
request.setState(MaterialRequest.DRAFT);
RequestInfo requestInfo = null;
if (SecurityContextUtil.getMe() != null) {
User user = SecurityContextUtil.getCurrentUser();
String email = user.getEmail();
request.setEmail(email);
}
if (request.getEmail() == null) {
// todo
request.setEmail("Guest");
}
final RequestBody rb = new RequestBody(requestInfo, null, availableAccessionIds);
request.setBody(serialize(rb));
request = requestRepository.save(request);
LOG.info("Persisted new DRAFT material request: {}", request);
var subRequests = createDraftSubRequests(request);
var savedRequest = requestRepository.getReferenceById(request.getId());
savedRequest.setSubRequests(subRequests);
return savedRequest;
}
@Override
@Transactional
public MaterialRequest processRequest(RequestInfo requestInfo, Long requestId) throws RequestException {
// remove unavailable accessions from sub requests and remove sub requests with no available accessions
MaterialRequest request = refreshRequestAccessions(requestId);
List<MaterialSubRequest> subRequests = request.getSubRequests();
if (CollectionUtils.isEmpty(subRequests)) {
throw new RequestException("None of the MaterialSubRequests are available for distribution");
}
List<MaterialSubRequest> availableSubRequests = subRequests.stream()
.filter(subRequest -> instituteService.getInstitute(subRequest.getInstCode()).isAllowMaterialRequests()).collect(Collectors.toList());
if (availableSubRequests.size() == 0) {
throw new RequestException("None of the MaterialSubRequests are available for distribution");
}
RequestBody rb;
try {
rb = mapper.readValue(request.getBody(), RequestBody.class);
} catch (final IOException e) {
// FIXME Some other exception?
throw new RuntimeException("Could not handle request JSON", e);
}
EasySMTA.EasySMTAUserData pid;
if (rb.pid != null) {
pid = rb.pid;
} else if (!requestInfo.isInternalRequest()) {
// Check Easy-SMTA for PID
try {
pid = pidChecker.getUserData(requestInfo.getEmail());
} catch (EasySMTAException e) {
throw new RequestException(e.getMessage(), e);
}
} else {
final Locale locale = LocaleContextHolder.getLocale();
pid = requestInfo.getUserData();
Country country = countryRepository.findByCode3(pid.getCountry());
if (country != null) {
pid.setCountryName(country.getName(locale));
}
}
request.setEmail(requestInfo.getEmail());
if (requestInfo.isInternalRequest()) {
request.setInternalRequest(true);
// set to null to avoid duplication of user data in the RequestBody for MaterialRequest#body
// we don't need to save this data in the storage
requestInfo.setUserData(null);
}
request.setPid(pid.getPid());
rb.requestInfo = requestInfo;
rb.pid = pid;
request.setBody(serialize(rb));
request.setState(MaterialRequest.NOTVALIDATED);
request = requestRepository.save(request);
for (var subRequest : request.getSubRequests()) {
try {
RequestBody srb = mapper.readValue(subRequest.getBody(), RequestBody.class);
srb.requestInfo = rb.requestInfo;
srb.pid = rb.pid;
subRequest.setBody(mapper.writeValueAsString(srb));
subRequestRepository.save(subRequest);
} catch (final IOException e) {
// FIXME Some other exception?
throw new RuntimeException("Could not handle request JSON", e);
}
}
//todo create a new validation email related to sub requests instead of a list of accessions?
return sendValidationEmail(request);
}
@Transactional
private List<MaterialSubRequest> createDraftSubRequests(MaterialRequest materialRequest) {
RequestBody rb = null;
try {
rb = mapper.readValue(materialRequest.getBody(), RequestBody.class);
} catch (final IOException e) {
// FIXME Some other exception?
throw new RuntimeException("Could not handle request JSON", e);
}
final ArrayList<MaterialSubRequest> subrequests = new ArrayList<MaterialSubRequest>();
final List<FaoInstitute> holdingInstitutes = genesysService.findHoldingInstitutes(rb.accessionIds);
for (final FaoInstitute holdingInstitute : holdingInstitutes) {
final MaterialSubRequest subRequest = new MaterialSubRequest();
subRequest.setInstCode(holdingInstitute.getCode());
subRequest.setSourceRequest(materialRequest);
subRequest.setState(DRAFT.getValue());
final RequestBody srb = new RequestBody();
srb.pid = rb.pid;
srb.requestInfo = rb.requestInfo;
srb.accessionIds = genesysService.listAccessions(holdingInstitute, rb.accessionIds);
if (srb.accessionIds.size() == 0) {
LOG.warn("No accessions for this institute??");
}
subRequest.setBody(serialize(srb));
subrequests.add(subRequest);
}
return subRequestRepository.saveAllAndFlush(subrequests);
}
private MaterialRequest refreshRequestAccessions(Long requestId) throws RequestException {
MaterialRequest request = get(requestId);
// if request is not in DRAFT mode then reject
if (request.getState() != MaterialRequest.DRAFT) {
throw new InvalidApiUsageException("Request was already processed");
}
Set<Long> availableAccessionIds;
try {
RequestBody rb = mapper.readValue(request.getBody(), RequestBody.class);
availableAccessionIds = accessionService.filterAvailableById(rb.accessionIds);
rb.accessionIds = availableAccessionIds;
request.setBody(mapper.writeValueAsString(rb));
request = requestRepository.save(request);
} catch (final IOException e) {
// FIXME Some other exception?
throw new RuntimeException("Could not handle request JSON", e);
}
if (availableAccessionIds == null || availableAccessionIds.size() == 0) {
// todo update sub requests ?
throw new RequestException("None of the selected accessions are available for distribution");
}
var subRequests = subRequestRepository.findAll(QMaterialSubRequest.materialSubRequest.sourceRequest().id.eq(request.getId()));
for (var subRequest : subRequests) {
try {
RequestBody srb = mapper.readValue(subRequest.getBody(), RequestBody.class);
srb.accessionIds.removeIf(srbAccessionId -> !availableAccessionIds.contains(srbAccessionId));
if (!srb.accessionIds.isEmpty()) {
subRequest.setBody(mapper.writeValueAsString(srb));
subRequestRepository.save(subRequest);
} else {
subRequestRepository.delete(subRequest);
}
} catch (final IOException e) {
// FIXME Some other exception?
throw new RuntimeException("Could not handle request JSON", e);
}
}
return requestRepository.getReferenceById(requestId);
}
// Send email to the user to confirm the request
@Override
@Transactional
@PreAuthorize("hasRole('ADMINISTRATOR') || (@userService.me != null && (@userService.me.id.equals(#materialRequest.createdBy) || @userService.me.email.equalsIgnoreCase(#materialRequest.email)))")
public MaterialRequest sendValidationEmail(MaterialRequest materialRequest) {
if (materialRequest.getState() != MaterialRequest.NOTVALIDATED) {
LOG.warn("Not sending validation email. Request {} state {} is not NOTVALIDATED={}.", materialRequest.getId(), materialRequest.getState(), MaterialRequest.NOTVALIDATED);
return materialRequest;
}
RequestBody rb = null;
try {
rb = mapper.readValue(materialRequest.getBody(), RequestBody.class);
} catch (final IOException e) {
// FIXME Some other exception?
throw new RuntimeException("Could not handle request JSON", e);
}
ArticleTranslationService.TranslatedArticle article;
if (StringUtils.isNotBlank(materialRequest.getPid()) || materialRequest.isInternalRequest()) {
article = articleService.getGlobalArticle(ContentService.SMTP_MATERIAL_CONFIRM, Locale.ENGLISH);
} else {
LOG.warn("No such user in ITPGRFA system");
article = articleService.getGlobalArticle(ContentService.SMTP_MATERIAL_CONFIRM_NO_PID, Locale.ENGLISH);
}
// Generate verification token+key
final VerificationToken verificationToken = tokenVerificationService.generateToken(REQUEST_TOKENTYPE, materialRequest.getUuid());
final Page<Accession> accessions = genesysService.listAccessions(rb.accessionIds, PageRequest.of(0, Integer.MAX_VALUE));
// Create the root hash
final Map<String, Object> root = new HashMap<String, Object>();
root.put("baseUrl", frontendUrl);
root.put("verificationToken", verificationToken);
root.put("internal", materialRequest.isInternalRequest());
root.put("pid", rb.pid);
root.put("accessions", accessions.getContent());
var body = article.getTranslation() != null ? article.getTranslation().getBody() : article.getEntity().getBody();
var title = article.getTranslation() != null ? article.getTranslation().getTitle() : article.getEntity().getTitle();
final String mailBody = contentService.processTemplate(body, root);
final String mailSubject = "[" + materialRequest.getUuid() + "] " + title;
LOG.debug(">>>{}", mailBody);
// send to user
emailService.sendMail(mailSubject, mailBody, materialRequest.getEmail());
materialRequest.setLastReminderDate(Instant.now());
return requestRepository.save(materialRequest);
}
private String serialize(RequestBody rb) {
String x = null;
try {
x = mapper.writeValueAsString(rb);
LOG.debug("Request JSON: {}" + x);
} catch (final JsonProcessingException e) {
e.printStackTrace();
}
return x;
}
// Rollback for any exception
@Override
@Transactional(rollbackFor= { NoPidException.class, EasySMTAException.class })
public MaterialRequest validateClientRequest(String tokenUuid, String key) throws NoSuchVerificationTokenException, NoPidException, EasySMTAException, TokenExpiredException, RequestException {
final VerificationToken consumedToken = tokenVerificationService.consumeToken(REQUEST_TOKENTYPE, tokenUuid, key);
final MaterialRequest materialRequest = requestRepository.findByUuid(consumedToken.getData());
if (materialRequest.getState() == MaterialRequest.DRAFT) {
throw new RequestException("Request is DRAFT.");
}
if (materialRequest.getState() != MaterialRequest.NOTVALIDATED) {
throw new RequestException("Request is already validated or dispatched.");
}
return validateRequest(materialRequest);
}
/**
* Send email notifications about each SubRequest to respective holding genebanks.
*/
@Transactional
private void relayRequests(MaterialRequest materialRequest) {
final List<MaterialSubRequest> subRequests = subRequestRepository.findBySourceRequest(materialRequest).stream()
.filter(subRequest -> instituteService.getInstitute(subRequest.getInstCode()).isAllowMaterialRequests()).collect(Collectors.toList());
if (subRequests.size() == 0) {
LOG.info("Nothing to relay.");
return;
}
LOG.info("Material subrequests {}", subRequests.size());
materialRequest.setState(MaterialRequest.DISPATCHED);
requestRepository.save(materialRequest);
for (final MaterialSubRequest msr : subRequests) {
relayRequest(msr);
}
}
@Override
@Transactional
@PreAuthorize("hasRole('ADMINISTRATOR') || (@userService.me != null && (@userService.me.id.equals(#materialRequest.createdBy) || @userService.me.email.equalsIgnoreCase(#materialRequest.email)))")
public MaterialRequest recheckPid(MaterialRequest materialRequest) throws NoPidException, EasySMTAException {
// re-test email for PID
EasySMTA.EasySMTAUserData pid = pidChecker.getUserData(materialRequest.getEmail());
if (pid == null) {
throw new NoPidException("Email not registered with PID server");
}
materialRequest.setPid(pid.getPid());
requestRepository.save(materialRequest);
try {
final RequestBody rb = mapper.readValue(materialRequest.getBody(), RequestBody.class);
rb.pid = pid;
materialRequest.setBody(serialize(rb));
// Need to update all subrequests
for (final MaterialSubRequest subrequest : materialRequest.getSubRequests()) {
final RequestBody rbs = mapper.readValue(subrequest.getBody(), RequestBody.class);
rbs.pid = pid;
subrequest.setBody(serialize(rbs));
LOG.info("Updating subrequest: {}", subrequest);
subRequestRepository.save(subrequest);
}
return requestRepository.save(materialRequest);
} catch (final IOException e) {
// FIXME Some other exception?
throw new RuntimeException("Could not handle request JSON", e);
}
}
/**
* Mark request as validated if PID checks out.
*
* @param materialRequest
* @return
* @throws EasySMTAException
* @throws NoPidException
*/
@Override
@Transactional
@PreAuthorize("hasRole('ADMINISTRATOR')")
public MaterialRequest validateRequest(MaterialRequest materialRequest) throws NoPidException, EasySMTAException {
if (materialRequest.getState() != MaterialRequest.NOTVALIDATED) {
LOG.warn("Cannot validate request {} with state {}. It is not NOTVALIDATED={}.", materialRequest.getId(), materialRequest.getState(), MaterialRequest.NOTVALIDATED);
return materialRequest;
}
if (!materialRequest.isInternalRequest()) {
materialRequest = recheckPid(materialRequest);
}
// Client email is confirmed
materialRequest.setState(MaterialRequest.VALIDATED);
materialRequest = requestRepository.save(materialRequest);
breakup(materialRequest); // does nothing if MaterialSubRequests exist
relayRequests(materialRequest);
return materialRequest;
}
/**
* Relay sub-request to holding institute
*/
@Override
@Transactional
@PreAuthorize("hasRole('ADMINISTRATOR')")
public MaterialSubRequest relayRequest(MaterialSubRequest materialSubRequest) {
LOG.info("Relaying request {}", materialSubRequest);
if (! (materialSubRequest.getState() == DRAFT.getValue() || materialSubRequest.getState() == NOTCONFIRMED.getValue())) {
throw new InvalidApiUsageException("The sub-request is not in DRAFT or NOTCONFIRMED state.");
}
final VerificationToken verificationToken = tokenVerificationService.generateToken(RECEIPT_TOKENTYPE, materialSubRequest.getUuid());
// Recipient
String recipient = null;
final Locale recipientLocale = Locale.ENGLISH;
final FaoInstitute institute = instituteService.findInstitute(materialSubRequest.getInstCode());
final FaoInstituteSetting instMailToSetting = institute.getSettings().get("requests.mailto");
if (instMailToSetting != null) {
recipient = StringUtils.defaultIfBlank(instMailToSetting.getValue(), null);
}
String[] emailCc = null;
if (recipient != null) {
emailCc = new String[] { requestsEmail };
// TODO If such user is registered, use to user's selected locale
} else {
LOG.info("Using default email to relay request for institute {}", materialSubRequest.getInstCode());
recipient = requestsEmail;
}
materialSubRequest.setInstEmail(recipient);
materialSubRequest = subRequestRepository.save(materialSubRequest);
final ArticleTranslationService.TranslatedArticle article = articleService.getGlobalArticle(ContentService.SMTP_MATERIAL_REQUEST, recipientLocale);
RequestBody rb;
try {
rb = mapper.readValue(materialSubRequest.getBody(), RequestBody.class);
} catch (final IOException e) {
throw new RuntimeException(e);
}
final Page<Accession> accessions = genesysService.listAccessions(rb.accessionIds, PageRequest.of(0, Integer.MAX_VALUE));
// Create the root hash
final Map<String, Object> root = new HashMap<String, Object>();
root.put("baseUrl", frontendUrl);
root.put("verificationToken", verificationToken);
root.put("pid", rb.pid);
root.put("requestInfo", rb.requestInfo);
root.put("accessions", accessions.getContent());
var body = article.getTranslation() != null ? article.getTranslation().getBody() : article.getEntity().getBody();
var title = article.getTranslation() != null ? article.getTranslation().getTitle() : article.getEntity().getTitle();
final String mailBody = contentService.processTemplate(body, root);
final String mailSubject = "[" + materialSubRequest.getInstCode() + "] " + "[" + materialSubRequest.getUuid() + "] " + title;
LOG.debug("Subrequest: {}\n\n{}", materialSubRequest.getInstEmail(), mailBody);
// send to recipient(s)
emailService.sendMail(mailSubject, mailBody, emailCc, emailService.toEmails(materialSubRequest.getInstEmail()));
materialSubRequest.setLastReminderDate(Instant.now());
materialSubRequest.setState(NOTCONFIRMED.getValue());
return subRequestRepository.save(materialSubRequest);
}
@Override
@Transactional
public MaterialSubRequest validateReceipt(String tokenUuid, String key) throws NoSuchVerificationTokenException, TokenExpiredException {
final VerificationToken consumedToken = tokenVerificationService.consumeToken(RECEIPT_TOKENTYPE, tokenUuid, key);
final MaterialSubRequest materialSubRequest = subRequestRepository.findByUuid(consumedToken.getData());
if (materialSubRequest.getState() == CONFIRMED.getValue()) {
throw new InvalidApiUsageException("The request has already confirmed.");
}
materialSubRequest.setState(CONFIRMED.getValue());
subRequestRepository.save(materialSubRequest);
sendSubRequestConfirmedEmail(materialSubRequest);
return materialSubRequest;
}
private void sendSubRequestConfirmedEmail(MaterialSubRequest materialSubRequest) {
try {
final ArticleTranslationService.TranslatedArticle article = articleService.getGlobalArticle(ContentService.SMTP_REQUEST_CONFIRMED, Locale.ENGLISH);
if (article == null) {
return;
}
RequestBody rb;
try {
rb = mapper.readValue(materialSubRequest.getBody(), RequestBody.class);
} catch (final IOException e) {
throw new RuntimeException(e);
}
final Page<Accession> accessions = genesysService.listAccessions(rb.accessionIds, PageRequest.of(0, Integer.MAX_VALUE));
final FaoInstitute institute = instituteService.findInstitute(materialSubRequest.getInstCode());
// Create the root hash
final Map<String, Object> root = new HashMap<String, Object>();
root.put("baseUrl", frontendUrl);
root.put("pid", rb.pid);
root.put("requestInfo", rb.requestInfo);
root.put("accessions", accessions.getContent());
root.put("institute", institute);
var body = article.getTranslation() != null ? article.getTranslation().getBody() : article.getEntity().getBody();
final String mailBody = contentService.processTemplate(body, root);
final String mailSubject = String.format("[%s]: Request for material from %s", materialSubRequest.getUuid(), StringUtils.defaultIfBlank(institute.getAcronym(), institute.getFullName()));
// send to recipient(s)
emailService.sendMail(mailSubject, mailBody, emailService.toEmails(materialSubRequest.getInstEmail()), materialSubRequest.getSourceRequest().getEmail());
} catch (Exception e) {
LOG.warn("Exception in sending \"MaterialSubRequest confirmed\" email", e);
}
}
public static final class RequestBody {
public RequestInfo requestInfo;
public Set<Long> accessionIds;
public EasySMTA.EasySMTAUserData pid;
public RequestBody() {
}
public RequestBody(RequestInfo requestInfo, EasySMTA.EasySMTAUserData pid, Set<Long> accessionIds2) {
this.requestInfo = requestInfo;
this.pid = pid;
this.accessionIds = new HashSet<Long>(accessionIds2);
}
}
@Override
@PreAuthorize("hasRole('ADMINISTRATOR')")
public Page<MaterialRequest> list(MaterialRequestFilter filter, Pageable pageable) {
BooleanBuilder predicate = new BooleanBuilder();
if (filter!=null) {
predicate.and(filter.buildPredicate());
}
return requestRepository.findAll(predicate, pageable);
}
@Override
@PreAuthorize("hasRole('REQUESTS')")
public Page<MaterialSubRequest> listMineSubRequests(MaterialSubRequestFilter filter, Pageable pageable) {
BooleanBuilder predicate = new BooleanBuilder();
if (filter != null) {
predicate.and(filter.buildPredicate());
}
List<FaoInstitute> institutes = instituteService.listMyInstitutes(Sort.by("code"));
if (institutes != null) {
Set<String> codes = institutes.stream().map(FaoInstitute::getCode).collect(Collectors.toSet());
predicate.and(QMaterialSubRequest.materialSubRequest.instCode.in(codes));
}
return subRequestRepository.findAll(predicate, pageable);
}
@Override
@PreAuthorize("hasRole('ADMINISTRATOR') or hasPermission(#faoInstitute, 'ADMINISTRATION') or hasPermission(#faoInstitute, 'WRITE')")
public Page<MaterialSubRequest> list(FaoInstitute faoInstitute, MaterialSubRequestFilter filter, Pageable pageable) {
BooleanBuilder predicate = new BooleanBuilder(QMaterialSubRequest.materialSubRequest.instCode.eq(faoInstitute.getCode()));
if (filter != null) {
predicate.and(filter.buildPredicate());
}
return subRequestRepository.findAll(predicate, pageable);
}
@Override
@PreAuthorize("hasRole('ADMINISTRATOR')")
public Page<MaterialSubRequest> listSubRequests(MaterialSubRequestFilter filter, Pageable pageable) {
final BooleanBuilder predicate = new BooleanBuilder();
if (filter != null) {
predicate.and(filter.buildPredicate());
}
return subRequestRepository.findAll(predicate, pageable);
}
@Override
@PostAuthorize("hasRole('ADMINISTRATOR') || (@userService.me != null && (@userService.me.id.equals(returnObject.createdBy) || @userService.me.email.equalsIgnoreCase(returnObject.email)))")
public MaterialRequest get(String uuid) {
MaterialRequest request = requestRepository.findByUuid(uuid);
if (request == null) {
throw new NotFoundElement("Record not found by uuid=" + uuid);
}
return request;
}
@Override
@PostAuthorize("hasRole('ADMINISTRATOR') || (@userService.me != null && (@userService.me.id.equals(returnObject.createdBy) || @userService.me.email.equalsIgnoreCase(returnObject.email)))")
public MaterialRequest get(Long requestId) {
return requestRepository.getReferenceById(requestId);
}
@Override
@PreAuthorize("hasRole('ADMINISTRATOR') or hasPermission(#institute, 'ADMINISTRATION')")
public MaterialSubRequest get(FaoInstitute institute, String uuid) {
return subRequestRepository.findByInstCodeAndUuid(institute.getCode(), uuid);
}
@Override
@PreAuthorize("hasRole('ADMINISTRATOR')")
public MaterialSubRequest getSubrequest(String uuid) {
return subRequestRepository.findByUuid(uuid);
}
@Override
@PostAuthorize("returnObject==null || hasRole('ADMINISTRATOR')")
public MaterialSubRequest getSubrequest(UUID uuid, Integer version) {
if (uuid == null) {
throw new InvalidApiUsageException("UUID of sub-request isn't provided.");
}
final MaterialSubRequest subRequest = subRequestRepository.findByUuid(uuid.toString());
if (subRequest == null) {
throw new NotFoundElement("Record not found by uuid=" + uuid);
}
if (version != null && !subRequest.getVersion().equals(version)) {
LOG.warn("Sub-requests versions don't match anymore");
throw new ConcurrencyFailureException("Object version changed to " + subRequest.getVersion() + ", you provided " + version);
}
return subRequest;
}
@Override
@Transactional
@PreAuthorize("hasRole('ADMINISTRATOR')")
public MaterialRequest remove(String uuid) {
MaterialRequest request = requestRepository.findByUuid(uuid);
subRequestRepository.deleteAll(request.getSubRequests());
requestRepository.delete(request);
request.setId(0L);
return request;
}
@Override
public long changeInstitute(FaoInstitute currentInstitute, FaoInstitute newInstitute) {
LOG.warn("Migrating requests from {} to {}", currentInstitute.getCode(), newInstitute.getCode());
// Update accession references
var qSubRequest = QMaterialSubRequest.materialSubRequest;
return jpaQueryFactory.update(qSubRequest)
// Update instCode
.set(qSubRequest.instCode, newInstitute.getCode())
// WHERE
.where(qSubRequest.instCode.eq(currentInstitute.getCode()))
// Execute
.execute();
}
@Override
@Transactional
@PreAuthorize("hasRole('ADMINISTRATOR') or hasPermission(#materialSubRequest, 'ADMINISTRATION') or hasPermission(#faoInstitute, 'ADMINISTRATION') or hasPermission(#faoInstitute, 'WRITE')")
public MaterialSubRequest setProviderInfo(MaterialSubRequest materialSubRequest, FaoInstitute faoInstitute, @Valid RequestService.ProviderInfoRequest info) {
if (materialSubRequest == null) {
throw new InvalidApiUsageException("MaterialSubRequest isn't provided.");
}
if (faoInstitute == null) {
throw new InvalidApiUsageException("FaoInstitute isn't provided.");
}
if (!materialSubRequest.getInstCode().equals(faoInstitute.getCode())) {
throw new InvalidApiUsageException("Given MaterialSubRequest is not related to FaoInstitute.");
}
if (info.getState() == null) { // @Valid and @NotNull should take care of this
throw new InvalidApiUsageException("State isn't provided.");
}
if (List.of(MaterialSubRequest.State.DRAFT, MaterialSubRequest.State.NOTCONFIRMED).contains(info.getState())) {
throw new InvalidApiUsageException("Invalid state.");
}
materialSubRequest.setProviderId(info.getProviderId());
materialSubRequest.setProviderNote(info.getProviderNote());
materialSubRequest.setState(info.getState().getValue());
return subRequestRepository.save(materialSubRequest);
}
@Override
public MaterialRequest getRequestStatus(UUID uuid) {
MaterialRequest request = requestRepository.findByUuid(uuid.toString());
if (request == null) {
throw new NotFoundElement("Record not found by uuid=" + uuid);
}
MaterialRequest requestStatus = new MaterialRequest();
requestStatus.setId(request.getId());
requestStatus.setUuid(request.getUuid());
requestStatus.setState(request.getState());
requestStatus.setInternalRequest(request.isInternalRequest());
requestStatus.setActive(request.isActive());
requestStatus.setCreatedDate(request.getCreatedDate());
requestStatus.setLastModifiedDate(request.getLastModifiedDate());
requestStatus.setLastReminderDate(request.getLastReminderDate());
var subRequests = request.getSubRequests();
if (subRequests != null) {
requestStatus.setSubRequests(subRequests.stream()
.map(sub -> {
var subRequest = new MaterialSubRequest();
subRequest.setUuid(sub.getUuid());
subRequest.setInstCode(sub.getInstCode());
subRequest.setLastReminderDate(sub.getLastReminderDate());
subRequest.setState(sub.getState());
return subRequest;
}).collect(Collectors.toList())
);
}
RequestBody rb;
try {
rb = mapper.readValue(request.getBody(), RequestBody.class);
} catch (final IOException e) {
throw new RuntimeException(e);
}
var body = new RequestBody();
body.accessionIds = rb.accessionIds;
if (rb.pid != null) {
body.pid = new EasySMTA.EasySMTAUserData();
body.pid.setType(rb.pid.getType());
body.pid.setCountry(rb.pid.getCountry());
body.pid.setOrgCountry(rb.pid.getOrgCountry());
body.pid.setShipAddrFlag(rb.pid.getShipAddrFlag());
body.pid.setShipCountry(rb.pid.getShipCountry());
}
if (rb.requestInfo != null) {
body.requestInfo = new RequestInfo();
body.requestInfo.setPurposeType(rb.requestInfo.getPurposeType());
body.requestInfo.setPreacceptSMTA(rb.requestInfo.isPreacceptSMTA());
}
requestStatus.setBody(serialize(body));
return requestStatus;
}
@Override
@PreAuthorize("isFullyAuthenticated()")
public Page<MaterialRequest> listMyRequests(MaterialRequestFilter filter, Pageable page) {
BooleanBuilder predicate = new BooleanBuilder();
if (filter != null) {
predicate.and(filter.buildPredicate());
}
final User user = userService.getMe();
if (user == null) {
throw new InvalidApiUsageException("User not found in the system");
}
predicate.and(QMaterialRequest.materialRequest.createdBy.eq(user.getId()).or(QMaterialRequest.materialRequest.email.eq(user.getEmail())));
return requestRepository.findAll(predicate, page);
}
@Override
@Scheduled(cron = "0 0 */4 ? * MON-FRI") // Run every four hours MON-FRI
// DEV @Scheduled(cron = "0 */4 * ? * *") // Run every four minutes
@SchedulerLock(lockAtLeastFor = "PT10M", name = "org.genesys.server.service.impl.RequestServiceImpl.resendNotConfirmedSubRequests")
@Transactional
public List<MaterialSubRequest> resendNotConfirmedSubRequests() {
LOG.warn("Resending email notifications to genebanks");
var createdAfter = OffsetDateTime.now().minus(1, ChronoUnit.MONTHS).toInstant();
var remindedBefore = OffsetDateTime.now().minus(5, ChronoUnit.DAYS).toInstant();
QMaterialSubRequest msr = QMaterialSubRequest.materialSubRequest;
var predicate = msr.state.eq(MaterialSubRequest.State.NOTCONFIRMED.getValue())
.and(msr.createdDate.after(createdAfter)) // created within one month
.and(msr.lastReminderDate.before(remindedBefore)); // but sent more than 5 days ago
var notifiedSubRequests = new LinkedList<MaterialSubRequest>();
subRequestRepository.findAll(predicate).forEach(subRequest-> {
try {
LOG.info("Sending reminder for subRequest id={} {}", subRequest.getId(), subRequest.getInstCode());
notifiedSubRequests.add(relayRequest(subRequest));
} catch (Throwable e) {
LOG.warn("Could not send reminder for subRequest id={}: {}", subRequest.getId(), e.getMessage());
}
});
return notifiedSubRequests;
}
}