RequestsController.java

/*
 * Copyright 2018 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.api.v1;

import java.io.IOException;
import java.util.UUID;

import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import org.genesys.blocks.model.JsonViews;
import org.genesys.blocks.security.SecurityContextUtil;
import org.genesys.server.api.ApiBaseController;
import org.genesys.server.api.FilteredPage;
import org.genesys.server.api.Pagination;
import org.genesys.server.api.v1.facade.RequestApiService;
import org.genesys.server.api.v1.model.MaterialRequest;
import org.genesys.server.api.v1.model.MaterialSubRequest;
import org.genesys.server.exception.InvalidApiUsageException;
import org.genesys.server.model.impl.FaoInstitute;
import org.genesys.server.service.InstituteService;
import org.genesys.server.service.RequestService;
import org.genesys.server.service.ShortFilterService;
import org.genesys.server.service.RequestService.NoPidException;
import org.genesys.server.service.ShortFilterService.FilterInfo;
import org.genesys.server.service.TokenVerificationService;
import org.genesys.server.service.filter.MaterialRequestFilter;
import org.genesys.server.service.filter.MaterialSubRequestFilter;
import org.genesys.server.exception.EasySMTAException;
import org.genesys.server.service.worker.ShortFilterProcessor;
import org.genesys.spring.CaptchaChecker;
import org.springdoc.api.annotations.ParameterObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Sort;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.fasterxml.jackson.annotation.JsonView;

import io.swagger.annotations.Api;

@RestController("requestsApi1")
@PreAuthorize("isAuthenticated()")
@RequestMapping(value = { RequestsController.CONTROLLER_URL })
@Api(tags = { "request" })
@Validated
public class RequestsController extends ApiBaseController {

	// Rest controller base URL
	public static final String CONTROLLER_URL = ApiBaseController.APIv1_BASE + "/requests";

	public static final String PARAM_KEY = "key";
	public static final String PARAM_TOKENUUID = "tokenUuid";

	@Autowired
	private RequestApiService requestService;

	@Autowired
	private InstituteService instituteService;

	/** The short filter service. */
	@Autowired
	protected ShortFilterProcessor shortFilterProcessor;

	@Autowired
	private CaptchaChecker captchaChecker;

	/**
	 * List sub-requests
	 *
	 * @return sub-requests
	 */
	@PreAuthorize("hasRole('ADMINISTRATOR')")
	@PostMapping(value = "/r/list", produces = { MediaType.APPLICATION_JSON_VALUE })
	public FilteredPage<MaterialSubRequest, MaterialSubRequestFilter> list(@RequestParam(name = "f", required = false) String filterCode, @ParameterObject final Pagination page,
		@RequestBody(required = false) MaterialSubRequestFilter filter) throws IOException {

		ShortFilterService.FilterInfo<MaterialSubRequestFilter> filterInfo = shortFilterProcessor.processFilter(filterCode, filter, MaterialSubRequestFilter.class);
		return new FilteredPage<>(filterInfo.filterCode, filterInfo.filter, requestService.listSubRequests(filterInfo.filter, page.toPageRequest(MAX_PAGE_SIZE, DEFAULT_PAGE_SIZE, Sort.Direction.ASC, "id")));
	}

	/**
	 * List sub-requests for current user
	 *
	 * @return sub-requests
	 */
	@PreAuthorize("hasRole('REQUESTS')")
	@PostMapping(value = "/r/list-mine", produces = { MediaType.APPLICATION_JSON_VALUE })
	@JsonView({ JsonViews.Public.class })
	public FilteredPage<MaterialSubRequest, MaterialSubRequestFilter> listMine(@RequestParam(name = "f", required = false) String filterCode, @ParameterObject final Pagination page,
		@RequestBody(required = false) MaterialSubRequestFilter filter) throws IOException {

		ShortFilterService.FilterInfo<MaterialSubRequestFilter> filterInfo = shortFilterProcessor.processFilter(filterCode, filter, MaterialSubRequestFilter.class);
		return new FilteredPage<>(filterInfo.filterCode, filterInfo.filter, requestService.listMineSubRequests(filterInfo.filter, page.toPageRequest(MAX_PAGE_SIZE, DEFAULT_PAGE_SIZE, Sort.Direction.DESC, "createdDate")));
	}

	/**
	 * List requests by filterCode or filter
	 *
	 * @param page the page
	 * @param filterCode short filter code
	 * @param filter the filter
	 * @return the page
	 * @throws IOException
	 */
	@PreAuthorize("hasRole('ADMINISTRATOR')")
	@PostMapping(value = "/list", produces = { MediaType.APPLICATION_JSON_VALUE })
	@JsonView({ JsonViews.Public.class })
	public FilteredPage<MaterialRequest, MaterialRequestFilter> list(@RequestParam(name = "f", required = false) String filterCode, @ParameterObject final Pagination page,
			@RequestBody(required = false) MaterialRequestFilter filter) throws IOException {

		FilterInfo<MaterialRequestFilter> filterInfo = shortFilterProcessor.processFilter(filterCode, filter, MaterialRequestFilter.class);
		return new FilteredPage<>(filterInfo.filterCode, filterInfo.filter, requestService.list(filterInfo.filter, page.toPageRequest(MAX_PAGE_SIZE, DEFAULT_PAGE_SIZE, Sort.Direction.ASC, "id")));
	}

	/**
	 * Remove request
	 *
	 * @return
	 */
	@PreAuthorize("hasRole('ADMINISTRATOR')")
	@RequestMapping(value = "/r/{uuid:.{36}}/remove", method = RequestMethod.POST, produces = { MediaType.APPLICATION_JSON_VALUE })
	public MaterialRequest removeRequest(@PathVariable("uuid") UUID uuid) {
		LOG.info("Removing request uuid={}", uuid);
		return requestService.remove(uuid.toString());
	}


	/**
	 * Get request
	 *
	 * @return
	 */
	@RequestMapping(value = "/r/{uuid:.{36}}", method = RequestMethod.GET, produces = { MediaType.APPLICATION_JSON_VALUE })
	public MaterialRequest getRequest(@PathVariable("uuid") UUID uuid) {
		LOG.info("Loading request uuid={}", uuid);
		final MaterialRequest request = requestService.get(uuid.toString());
		return request;
	}

	/**
	 * Validate request
	 *
	 * @return
	 */
	@PostMapping(value = "/r/{uuid:.{36}}/reconfirm", produces = { MediaType.APPLICATION_JSON_VALUE })
	public MaterialRequest sendValidationEmail(@PathVariable("uuid") UUID uuid) {
		LOG.info("Loading request uuid={}", uuid);
		final MaterialRequest materialRequest = requestService.get(uuid.toString());
		return requestService.sendValidationEmail(materialRequest);
	}

	/**
	 * Relay sub-request to holding institute
	 *
	 * @return updated sub-request
	 */
	@PreAuthorize("hasRole('ADMINISTRATOR')")
	@PostMapping(value = "/r/{uuid:.{36}}/resend", produces = { MediaType.APPLICATION_JSON_VALUE })
	public MaterialSubRequest relaySubRequest(@PathVariable("uuid") UUID uuid) {
		LOG.info("Loading sub-request uuid={}", uuid);
		final MaterialSubRequest materialSubRequest = requestService.getSubrequest(uuid.toString());
		return requestService.relayRequest(materialSubRequest);
	}

	/**
	 * Initiate a new request for material.
	 *
	 * @return requests
	 */
	@PostMapping(value = "/r/initiate", produces = { MediaType.APPLICATION_JSON_VALUE })
	public MaterialRequest initiateRequest(@RequestBody RequestService.RequestData requestData, final HttpServletRequest request) throws RequestService.RequestException, IOException {

		if (SecurityContextUtil.getMe() == null) {
			// Validate the reCAPTCHA only for anonymous users
			captchaChecker.assureValidResponseForClient(requestData.captchaResponse, request.getRemoteAddr());
		}

		String origin = request.getHeader("Origin");
		var languages = request.getHeaders(HttpHeaders.ACCEPT_LANGUAGE); 
		String lang = languages.hasMoreElements() ? languages.nextElement() : null;
		var currentUser = SecurityContextUtil.getCurrentUser();
		String sid = currentUser != null ? currentUser.getSid() : null;
		LOG.info("Initiating request: origin={} sid={} lang={} info={} accessionUuids={}", origin, sid, lang, requestData.requestInfo, requestData.accessionUuids);

		return requestService.initiateRequestByUuids(requestData.requestInfo, requestData.accessionUuids, origin, sid, lang);
	}

	/**
	 * Validate request
	 *
	 * @return
	 * @throws EasySMTAException 
	 * @throws NoPidException 
	 */
	@PostMapping(value = "/r/{uuid:.{36}}/validate", produces = { MediaType.APPLICATION_JSON_VALUE })
	public MaterialRequest validateRequest(@PathVariable("uuid") UUID uuid) throws NoPidException, EasySMTAException {
		LOG.info("Loading request uuid={}", uuid);
		final MaterialRequest materialRequest = requestService.get(uuid.toString());
		return requestService.validateRequest(materialRequest);
	}

	/**
	 * Validate request
	 *
	 * @return
	 * @throws EasySMTAException
	 * @throws NoPidException
	 */
	@PostMapping(value = "/r/validate", produces = { MediaType.APPLICATION_JSON_VALUE })
	public MaterialRequest validateRequest(@RequestParam(PARAM_TOKENUUID) String tokenUuid, @RequestParam(PARAM_KEY) String key) {
		LOG.info("Validating request tokenUuid={}, key={}", tokenUuid, key);
		try {
			return requestService.validateClientRequest(tokenUuid, key);
		} catch (NoPidException | EasySMTAException | RequestService.RequestException e) {
			throw new InvalidApiUsageException(e.getMessage(), e);
		} catch (TokenVerificationService.NoSuchVerificationTokenException e) {
			throw new InvalidApiUsageException("Verification token was already used or is not valid!", e);
		} catch (TokenVerificationService.TokenExpiredException e) {
			throw new InvalidApiUsageException("Verification token is expired!", e);
		}
	}

	/**
	 * Confirm receipt of request
	 *
	 * @return
	 * @throws InvalidApiUsageException
	 */
	@PostMapping(value = "/r/confirm", produces = { MediaType.APPLICATION_JSON_VALUE })
	public MaterialSubRequest confirmRequest(
			@RequestParam(value = "g-recaptcha-response") final String captchaResponse,
			@RequestParam(PARAM_TOKENUUID) String tokenUuid,
			@RequestParam(PARAM_KEY) String key,
			final HttpServletRequest request) throws IOException {

		// Validate the reCAPTCHA
		captchaChecker.assureValidResponseForClient(captchaResponse, request.getRemoteAddr());

		LOG.info("Validating request tokenUuid={}, key={}", tokenUuid, key);
		try {
			return requestService.validateReceipt(tokenUuid, key);
		} catch (TokenVerificationService.NoSuchVerificationTokenException e) {
			throw new InvalidApiUsageException("Verification token was already used or is not valid!", e);
		} catch (TokenVerificationService.TokenExpiredException e) {
			throw new InvalidApiUsageException("Verification token is expired!", e);
		}
	}
	/**
	 * Reload PID data
	 *
	 * @return
	 * @throws EasySMTAException 
	 * @throws NoPidException 
	 */
	@PostMapping(value = "/r/{uuid:.{36}}/update-pid", produces = { MediaType.APPLICATION_JSON_VALUE })
	public MaterialRequest recheckPid(@PathVariable("uuid") UUID uuid) throws NoPidException, EasySMTAException {
		LOG.info("Loading request uuid={}", uuid);
		final MaterialRequest materialRequest = requestService.get(uuid.toString());
		return requestService.recheckPid(materialRequest);
	}

	/**
	 * List institute requests
	 *
	 * @return
	 */
	@PostMapping(value = "/{instCode}", produces = { MediaType.APPLICATION_JSON_VALUE })
	public FilteredPage<MaterialSubRequest, MaterialSubRequestFilter> listInstituteRequests(@PathVariable("instCode") String instCode, @ParameterObject final Pagination page,
			@RequestParam(name = "f", required = false) String filterCode,
			@RequestBody(required = false) MaterialSubRequestFilter filter) throws IOException {

		LOG.info("Listing requests for {}", instCode);
		final FaoInstitute institute = instituteService.getInstitute(instCode);

		FilterInfo<MaterialSubRequestFilter> filterInfo = shortFilterProcessor.processFilter(filterCode, filter, MaterialSubRequestFilter.class);
		return new FilteredPage<>(filterInfo.filterCode, filterInfo.filter, requestService.list(institute, filterInfo.filter, page.toPageRequest(MAX_PAGE_SIZE, DEFAULT_PAGE_SIZE, Sort.Direction.ASC, "id")));
	}

	/**
	 * Get institute request
	 *
	 * @return
	 */
	@GetMapping(value = "/{instCode}/r/{uuid:.{36}}", produces = { MediaType.APPLICATION_JSON_VALUE })
	public MaterialSubRequest getInstituteRequest(@PathVariable("instCode") String instCode, @PathVariable("uuid") UUID uuid) {
		LOG.info("Loading request for {} uuid={}", instCode, uuid);
		final FaoInstitute institute = instituteService.getInstitute(instCode);
		final MaterialSubRequest request = requestService.get(institute, uuid.toString());
		return request;
	}
	
	@PostMapping(value = "/r/sub/{uuid:.{36}}/provider/info", produces = { MediaType.APPLICATION_JSON_VALUE })
	public MaterialSubRequest updateProviderInfo(@PathVariable("uuid") UUID uuid, @RequestBody @Valid RequestService.ProviderInfoRequest info) {
		return requestService.setProviderInfo(uuid, info);
	}

	/**
	 * Get request without confidential information
	 *
	 * @return RequestStatusResponse
	 */
	@RequestMapping(value = "/r/{uuid:.{36}}/status", method = RequestMethod.GET, produces = { MediaType.APPLICATION_JSON_VALUE })
	public MaterialRequest getRequestStatus(@PathVariable("uuid") UUID uuid) {
		LOG.info("Loading request uuid={}", uuid);
		return requestService.getRequestStatus(uuid);
	}

	/**
	 * List user requests by filterCode or filter
	 *
	 * @param page the page
	 * @param filterCode short filter code
	 * @param filter the filter
	 * @return the page
	 * @throws IOException
	 */
	@PostMapping(value = "/list-mine", produces = { MediaType.APPLICATION_JSON_VALUE })
	@JsonView({ JsonViews.Public.class })
	public FilteredPage<MaterialRequest, MaterialRequestFilter> listMyRequests(@RequestParam(name = "f", required = false) String filterCode, @ParameterObject final Pagination page,
		@RequestBody(required = false) MaterialRequestFilter filter) throws IOException {

		FilterInfo<MaterialRequestFilter> filterInfo = shortFilterProcessor.processFilter(filterCode, filter, MaterialRequestFilter.class);
		return new FilteredPage<>(filterInfo.filterCode, filterInfo.filter, requestService.listMyRequests(filterInfo.filter, page.toPageRequest(MAX_PAGE_SIZE, DEFAULT_PAGE_SIZE, Sort.Direction.ASC, "id")));
	}
}