RepositoryApiServiceImpl.java

/*
 * Copyright 2024 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.v2.facade.impl;

import lombok.extern.slf4j.Slf4j;
import org.genesys.filerepository.FolderNotEmptyException;
import org.genesys.filerepository.InvalidRepositoryFileDataException;
import org.genesys.filerepository.InvalidRepositoryPathException;
import org.genesys.filerepository.NoSuchRepositoryFileException;
import org.genesys.filerepository.NoSuchRepositoryFolderException;
import org.genesys.filerepository.model.RepositoryFile;
import org.genesys.filerepository.model.RepositoryFolder;
import org.genesys.filerepository.service.ImageGalleryService;
import org.genesys.filerepository.service.RepositoryService;
import org.genesys.server.api.Pagination;
import org.genesys.server.api.v2.facade.RepositoryApiService;
import org.genesys.server.api.v2.mapper.MapstructMapper;
import org.genesys.server.api.v2.model.impl.RepositoryFileDTO;
import org.genesys.server.api.v2.model.impl.RepositoryFolderDTO;
import org.genesys.server.exception.NotFoundElement;
import org.genesys.server.service.impl.FilesMetadataInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.HandlerMapping;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.UUID;
import java.util.stream.Stream;

@Service
@Transactional(readOnly = true)
@Slf4j
public class RepositoryApiServiceImpl implements RepositoryApiService {

	@Autowired
	private RepositoryService service;

	@Autowired
	private ImageGalleryService imageGalleryService;

	@Autowired
	private FilesMetadataInfo filesMetadataInfo;

	@Autowired
	private MapstructMapper mapper;

	@Override
	@Transactional
	public <T extends RepositoryFileDTO> T addFile(Path repositoryPath, String originalFilename, String contentType, InputStream inputStream, T metaData) throws InvalidRepositoryPathException, InvalidRepositoryFileDataException, IOException {
		return (T) mapper.map(service.addFile(repositoryPath, originalFilename, contentType, inputStream, mapper.map(metaData)));
	}

	@Override
	public <T extends RepositoryFileDTO> T getFile(UUID fileUuid) throws NoSuchRepositoryFileException {
		return (T) mapper.map((RepositoryFile) service.getFile(fileUuid));
	}

	@Override
	public byte[] getFileBytes(RepositoryFileDTO repositoryFileDTO) throws IOException {
		return service.getFileBytes(mapper.map(repositoryFileDTO));
	}

	@Override
	public Page<RepositoryFileDTO> listFiles(Path folderPath, Pageable page) throws InvalidRepositoryPathException {
		return mapper.map(service.listFiles(folderPath, page), mapper::map);
	}

	@Override
	@Transactional
	public <T extends RepositoryFileDTO> T updateMetadata(T repositoryFileDTO) throws NoSuchRepositoryFileException {
		return (T) mapper.map(service.updateMetadata(mapper.map(repositoryFileDTO)));
	}

	@Override
	@Transactional
	public RepositoryFileDTO updateFileBytes(MultipartFile file, RepositoryFileDTO metadata, UUID fileUuid) throws NoSuchRepositoryFileException, IOException {
		if (metadata != null) {
			service.updateMetadata(List.of(mapper.map(metadata)));
		}
		var repositoryFile = service.getFile(fileUuid);

		log.info("Update bytes for file {} in {}", repositoryFile.getOriginalFilename(), repositoryFile.getFolder().getPath());
		return mapper.map(service.updateBytes(repositoryFile, file.getContentType(), file.getInputStream()));
	}

	@Override
	public RepositoryFolderDTO getFolder(Path folderPath) throws InvalidRepositoryPathException {
		return mapper.map(service.getFolder(folderPath));
	}

	@Override
	@Transactional
	public FolderDetails updateFolder(RepositoryFolderDTO folder) throws NoSuchRepositoryFolderException, InvalidRepositoryPathException {
		var repositoryFolder = service.updateFolder(mapper.map(folder));
		return folderDetails(Paths.get(repositoryFolder.getPath()));
	}

	@Override
	@Transactional
	public RepositoryFolderDTO renamePath(Path currentPath, Path newPath) throws InvalidRepositoryPathException {
		return mapper.map(service.renamePath(currentPath, newPath));
	}

	@Override
	@Transactional
	public RepositoryFolderDTO ensureFolder(Path path) throws InvalidRepositoryPathException {
		return mapper.map(service.ensureFolder(path));
	}

	@Override
	@Transactional(rollbackFor = Throwable.class)
	public RepositoryFolderDTO deleteFolder(Path path) throws FolderNotEmptyException, InvalidRepositoryPathException {
		var folder = service.getFolder(path);
		service.deleteFolder(path);
		return mapper.map(folder);
	}

	@Override
	public Page<RepositoryFolderDTO> listFolders(Path root, Pageable page) throws InvalidRepositoryPathException {
		return mapper.map(service.listFolders(root, page), mapper::map);
	}

	@Override
	@Transactional
	public FolderDetails renameFolder(final UUID folderUuid, String fullPath) throws InvalidRepositoryPathException {
		RepositoryFolder folder = service.getFolder(folderUuid);
		if (folder == null) {
			throw new NotFoundElement("No folder with uuid=" + folderUuid);
		}
		return folderDetails(Paths.get(renamePath(folder.getFolderPath(), Paths.get(fullPath)).getPath()));
	}

	/**
	 * Folder details.
	 *
	 * @param path the path
	 * @return the folder details
	 * @throws InvalidRepositoryPathException the invalid repository path exception
	 */
	@Transactional
	@Override
	public FolderDetails folderDetails(final Path path) throws InvalidRepositoryPathException {
		FolderDetails fd = new FolderDetails();
		fd.folder = getFolder(path);
		fd.subFolders = listFolders(path, Pagination.toPageRequest(50, RepositoryFolder.DEFAULT_SORT));
		if (fd.folder == null && !path.toAbsolutePath().toString().equals("/")) {
			throw new NotFoundElement("No such folder");
		}
		fd.files = listFiles(path, Pagination.toPageRequest(50, RepositoryFile.DEFAULT_SORT));
		fd.gallery = mapper.map(imageGalleryService.loadImageGallery(path));
		return fd;
	}

	@Override
	public RepositoryFileDTO getMetadata(String path, String uuid, String ext) throws NoSuchRepositoryFileException {
		final RepositoryFile repositoryFile = service.getFile(UUID.fromString(uuid));
		sanityCheck(Paths.get(path), ext, repositoryFile);
		return mapper.map(repositoryFile);
	}

	@Override
	@Transactional
	public RepositoryFileDTO removeFile(UUID fileUuid) throws NoSuchRepositoryFileException, IOException {
		return mapper.map((RepositoryFile) service.removeFile(service.getFile(fileUuid)));
	}

	@Override
	@Transactional
	public List<RepositoryFileDTO> extractZip(UUID fileUuid) throws NoSuchRepositoryFileException, IOException, InvalidRepositoryPathException, InvalidRepositoryFileDataException {
		return mapper.map(service.extractZip(service.getFile(fileUuid)), mapper::map);
	}

	@Override
	@Transactional
	public RepositoryFileDTO moveAndRenameFile(UUID fileUuid, Path fullPath) throws NoSuchRepositoryFileException, InvalidRepositoryPathException, InvalidRepositoryFileDataException {
		return mapper.map((RepositoryFile) service.moveAndRenameFile(service.getFile(fileUuid), fullPath));
	}

	@Override
	@Transactional
	public void deleteFile(UUID uuid) throws NoSuchRepositoryFileException, IOException {
		RepositoryFile repositoryFile = service.getFile(uuid);
		service.removeFile(repositoryFile);
	}

	@Override
	public List<RepositoryFileDTO> getFiles(Path path, Sort sort) throws InvalidRepositoryPathException {
		return mapper.map(service.getFiles(path, sort), mapper::map);
	}

	@Override
	public void sanityCheck(final Path path, final String ext, final RepositoryFile repositoryFile) {
		if (repositoryFile == null) {
			throw new NotFoundElement("No such thing");
		}

		if (!repositoryFile.getExtension().equals(ext) || (!repositoryFile.getStorageFolder().equals(path.toString()) && !repositoryFile.getFolder().getFolderPath().equals(path))) {
			log.warn("{}!={}", repositoryFile.getStorageFolder(), path);
			log.warn("{}!={}", repositoryFile.getExtension(), ext);
			throw new NotFoundElement("No such thing");
		}
	}

	@Override
	public void downloadFolderMetadata(HttpServletRequest request, HttpServletResponse response, String controllerUrl)
		throws NotFoundElement, IOException, InvalidRepositoryPathException {
		final String folderPath = ((String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE)).substring((controllerUrl + "/download/folder-metadata").length());
		final RepositoryFolderDTO folder = getFolder(Paths.get(folderPath));
		if (folder == null) {
			throw new NotFoundElement("No folder with path=" + folderPath);
		}
		response.setContentType("text/csv;charset=UTF-8");
		response.setHeader("Content-Disposition", "attachment; filename=" + folder.getName() + "_files_metadata.csv ");

		Stream<RepositoryFile> files = service.streamFiles(Paths.get(folder.getPath()), RepositoryFile.DEFAULT_SORT);
		filesMetadataInfo.downloadMetadata(files, response, '\t', '"', '\\', "\n", "UTF-8");
	}
}