InventoryViabilityController.java

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

import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

import io.swagger.v3.oas.annotations.Operation;
import org.apache.commons.lang3.ArrayUtils;
import org.genesys.filerepository.InvalidRepositoryFileDataException;
import org.genesys.filerepository.InvalidRepositoryPathException;
import org.gringlobal.api.exception.InvalidApiUsageException;
import org.gringlobal.api.v1.ActionController;
import org.gringlobal.api.v1.FilteredCRUDController;
import org.gringlobal.api.v1.FilteredPage;
import org.gringlobal.api.v1.Pagination;
import org.gringlobal.custom.elasticsearch.SearchException;
import org.gringlobal.model.AbstractAction.ActionState;
import org.gringlobal.model.Cooperator;
import org.gringlobal.model.Inventory;
import org.gringlobal.model.InventoryAction;
import org.gringlobal.model.InventoryViability;
import org.gringlobal.model.InventoryViabilityAction;
import org.gringlobal.model.InventoryViabilityAttach;
import org.gringlobal.model.InventoryViabilityData;
import org.gringlobal.model.InventoryViabilityRule;
import org.gringlobal.model.OrderRequest;
import org.gringlobal.model.QInventoryAction;
import org.gringlobal.model.QInventoryViability;
import org.gringlobal.model.QInventoryViabilityAction;
import org.gringlobal.model.Site;
import org.gringlobal.model.community.CommunityCodeValues;
import org.gringlobal.persistence.InventoryRepository;
import org.gringlobal.persistence.InventoryViabilityRuleRepository;
import org.gringlobal.service.InventoryActionService;
import org.gringlobal.service.InventoryService;
import org.gringlobal.service.InventoryViabilityActionService;
import org.gringlobal.service.InventoryViabilityActionService.InventoryViabilityActionRequest;
import org.gringlobal.service.InventoryViabilityActionService.InventoryViabilityActionScheduleFilter;
import org.gringlobal.service.InventoryViabilityAttachmentService;
import org.gringlobal.service.InventoryViabilityDataService;
import org.gringlobal.service.InventoryViabilityService;
import org.gringlobal.service.InventoryViabilityService.InventoryViabilityDetails;
import org.gringlobal.service.filter.InventoryActionFilter;
import org.gringlobal.service.filter.InventoryFilter;
import org.gringlobal.service.filter.InventoryViabilityActionFilter;
import org.gringlobal.service.filter.InventoryViabilityAttachFilter;
import org.gringlobal.service.filter.InventoryViabilityDataFilter;
import org.gringlobal.service.filter.InventoryViabilityFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Pageable;
import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
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.RequestPart;
import org.springframework.web.bind.annotation.RestController;

import com.querydsl.core.types.OrderSpecifier;

import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;

import org.springframework.web.multipart.MultipartFile;


@RestController("inventoryViabilityApi1")
@RequestMapping(InventoryViabilityController.API_URL)
@PreAuthorize("isAuthenticated()")
@Tag(name = InventoryViabilityController.API_TAG)
public class InventoryViabilityController extends FilteredCRUDController<InventoryViability, InventoryViabilityService, InventoryViabilityFilter> {

	/** The Constant API_URL. */
	public static final String API_URL = InventoryController.API_URL + "/viability";
	public static final String API_TAG = "InventoryViability";

	@Autowired
	private InventoryService inventoryService;
	@Autowired
	private InventoryRepository inventoryRepository;
	@Autowired
	private InventoryActionService inventoryActionService;
	@Autowired
	private InventoryViabilityRuleRepository inventoryViabilityRuleRepository;
	@Autowired
	private InventoryViabilityAttachmentService attachmentFileService;

	@Override
	protected Class<InventoryViabilityFilter> filterType() {
		return InventoryViabilityFilter.class;
	}

	@Override
	protected OrderSpecifier<?>[] defaultSort() {
		return new OrderSpecifier[] { QInventoryViability.inventoryViability.testedDate.desc() };
	}

	@Override
	public InventoryViability create(@RequestBody InventoryViability entity) {
		entity.setInventory(inventoryService.get(entity.getInventory().getId()));
		if (entity.getInventoryViabilityRule() != null) {
			entity.setInventoryViabilityRule(inventoryViabilityRuleRepository.findById(entity.getInventoryViabilityRule().getId()).orElseThrow());
		}
		return super.create(entity);
	}

	@PostMapping("/scheduled")
	public FilteredPage<InventoryAction, InventoryFilter> listScheduledInventories(@Parameter(hidden = true) final Pagination page, @RequestBody(required = false) final InventoryFilter inventoryFilter) throws SearchException, IOException {

		InventoryFilter normalizedFilter = shortFilterService.normalizeFilter(inventoryFilter, InventoryFilter.class);

		InventoryActionFilter actionFilter = (InventoryActionFilter) new InventoryActionFilter()
			.inventory(normalizedFilter)
			.actionNameCode(Set.of(CommunityCodeValues.INVENTORY_ACTION_VABILITYTEST.value))
			.states(Set.of(ActionState.PENDING, ActionState.SCHEDULED));

		Pageable pageable = ArrayUtils.isEmpty(page.getS()) ? page.toPageRequest(MAX_PAGE_SIZE, DEFAULT_PAGE_SIZE, QInventoryAction.inventoryAction.notBeforeDate.asc().nullsFirst()) : page.toPageRequest(MAX_PAGE_SIZE, DEFAULT_PAGE_SIZE);
		return new FilteredPage<>(normalizedFilter, inventoryActionService.listActions(actionFilter, pageable));
	}

	@PostMapping("/order")
	public OrderRequest orderViabilityTest(@RequestBody @Valid ViabilityOrderRequest viabilityOrderRequest) {
		List<Inventory> inventories = inventoryRepository.findAllById(viabilityOrderRequest.inventories.keySet());
		if (inventories.size() != viabilityOrderRequest.inventories.size()) {
			throw new InvalidApiUsageException("Specified inventory does not exist");
		}
		var viabilityRuleIds = Set.copyOf(viabilityOrderRequest.inventories.values());
		List<InventoryViabilityRule> viabilityRules = inventoryViabilityRuleRepository.findAllById(viabilityRuleIds);
		if (viabilityRules.size() != viabilityRuleIds.size()) {
			throw new InvalidApiUsageException("Specified vability rule does not exist");
		}

		var inventoryLookup = inventories.stream().collect(Collectors.toMap(Inventory::getId, Function.identity()));
		var ruleLookup = viabilityRules.stream().collect(Collectors.toMap(InventoryViabilityRule::getId, Function.identity()));

		Map<Inventory, InventoryViabilityRule> inventoriesAndRules = viabilityOrderRequest.inventories.entrySet().stream()
				// map
				.collect(Collectors.toMap((entry) -> inventoryLookup.get(entry.getKey()), (entry) -> ruleLookup.get(entry.getValue())));

		return crudService.orderViabilityTest(viabilityOrderRequest.site, inventoriesAndRules, viabilityOrderRequest.cooperator);
	}

	/**
	 * Calculate results across all replicates.
	 *
	 * @param id the id of InventoryViability
	 * @return object of InventoryViability (not persisted) with calculated results
	 */
	@PostMapping("/{id:\\d+}/calculate")
	public InventoryViabilityDetails calculate(@PathVariable("id") final long id, @RequestBody(required = false) List<Integer> selectedReplicationNumbers) {
		return crudService.calculateResult(crudService.get(id), selectedReplicationNumbers);
	}

	/**
	 * Returns fully loaded viability + data records
	 * @param id InventoryViability#id
	 * @return fully loaded viability
	 */
	@GetMapping(ENDPOINT_ID + "/details")
	public InventoryViabilityDetails getDetails(@PathVariable("id") final long id) {
		return crudService.loadDetails(crudService.get(id));
	}

	@RestController("inventoryViabilityActionApi1")
	@RequestMapping(InventoryViabilityActionController.API_URL)
	@PreAuthorize("isAuthenticated()")
	@Tag(name = InventoryViabilityController.API_TAG)
	public static class InventoryViabilityActionController extends ActionController<InventoryViabilityAction, InventoryViabilityActionFilter, InventoryViabilityActionRequest, InventoryViabilityActionScheduleFilter, InventoryViabilityActionService> {
		public static final String API_URL = InventoryViabilityController.API_URL;

		@Override
		protected Class<InventoryViabilityActionFilter> filterType() {
			return InventoryViabilityActionFilter.class;
		}

		@Override
		protected OrderSpecifier<?>[] defaultSort() {
			return new OrderSpecifier[] { QInventoryViabilityAction.inventoryViabilityAction.startedDate.desc() };
		}
	}

	@RestController("inventoryViabilityDataApi1")
	@RequestMapping(InventoryViabilityDataController.API_URL)
	@PreAuthorize("isAuthenticated()")
	@Tag(name = InventoryViabilityController.API_TAG)
	public static class InventoryViabilityDataController extends FilteredCRUDController<InventoryViabilityData, InventoryViabilityDataService, InventoryViabilityDataFilter> {
		public static final String API_URL = InventoryViabilityController.API_URL + "/data";

	}

	@NoArgsConstructor
	@SuperBuilder
	public static class ViabilityOrderRequest {
		public Site site;

		@NotNull
		public Cooperator cooperator;

		@NotNull
		@Size(min = 1)
		public Map<Long, Long> inventories;
	}

	@PostMapping(value = "/attach/{inventoryViabilityId}", produces = { MediaType.APPLICATION_JSON_VALUE })
	@Operation(operationId = "uploadFile", description = "Attach file", summary = "Attach file")
	public InventoryViabilityAttach uploadFile(@PathVariable(name = "inventoryViabilityId") final Long inventoryViabilityId, @RequestPart(name = "file") final MultipartFile file,
		@RequestPart(name = "metadata") final InventoryViabilityAttachmentService.InventoryViabilityAttachmentRequest metadata) throws InvalidRepositoryPathException, InvalidRepositoryFileDataException, IOException {

		return attachmentFileService.uploadFile(crudService.get(inventoryViabilityId), file, metadata);
	}

	@DeleteMapping(value = "/attach/{inventoryViabilityId}/{attachmentId}", produces = { MediaType.APPLICATION_JSON_VALUE })
	@Operation(operationId = "removeFile", description = "Remove attached file", summary = "Remove file")
	public InventoryViabilityAttach removeFile(@PathVariable(name = "inventoryViabilityId") final Long inventoryViabilityId, @PathVariable(name = "attachmentId") final Long attachmentId) {
		return attachmentFileService.removeFile(crudService.get(inventoryViabilityId), attachmentId);
	}

	@GetMapping(value = "/attach/{inventoryViabilityId}", produces = { MediaType.APPLICATION_JSON_VALUE })
	@Operation(operationId = "listAttachments", description = "List attached files", summary = "list files")
	public List<InventoryViabilityAttach> listFiles(@PathVariable(name = "inventoryViabilityId") final Long inventoryViabilityId) {
		return attachmentFileService.getAllAttachments(crudService.get(inventoryViabilityId));
	}
	
	@RestController("inventoryViabilityAttachApi1")
	@RequestMapping(InventoryViabilityController.InventoryViabilityAttachController.API_URL)
	@PreAuthorize("isAuthenticated()")
	@Tag(name = InventoryViabilityController.API_TAG)
	public static class InventoryViabilityAttachController extends FilteredCRUDController<InventoryViabilityAttach, InventoryViabilityAttachmentService, InventoryViabilityAttachFilter> {

		/** The Constant API_URL. */
		public static final String API_URL = InventoryViabilityController.API_URL + "/attach/meta";

		@Override
		@Operation(operationId = "createInventoryViabilityAttach", description = "Create InventoryViabilityAttach", summary = "Create")
		public InventoryViabilityAttach create(@RequestBody InventoryViabilityAttach entity) {
			return super.create(entity);
		}

		@Override
		@Operation(operationId = "updateInventoryViabilityAttach", description = "Update an existing record", summary = "Update")
		public InventoryViabilityAttach update(@RequestBody InventoryViabilityAttach entity) {
			return super.update(entity);
		}

		@Override
		@Operation(operationId = "getInventoryViabilityAttach", description = "Get record by ID", summary = "Get")
		public InventoryViabilityAttach get(@PathVariable long id) {
			return super.get(id);
		}

		@Override
		@Operation(operationId = "deleteInventoryViabilityAttach", description = "Delete existing record by ID", summary = "Delete")
		public InventoryViabilityAttach remove(@PathVariable long id) {
			return super.remove(id);
		}
	}
}