JasperReportServiceImpl.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.service.impl;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.PropertyResourceBundle;
import java.util.ResourceBundle;
import java.util.UUID;

import lombok.extern.slf4j.Slf4j;
import net.sf.jasperreports.engine.JRParameter;
import net.sf.jasperreports.functions.FunctionsBundle;
import net.sf.jasperreports.repo.ResourceBundleResource;
import org.apache.commons.io.FileUtils;
import org.genesys.filerepository.InvalidRepositoryPathException;
import org.genesys.filerepository.NoSuchRepositoryFileException;
import org.genesys.filerepository.model.RepositoryFile;
import org.genesys.filerepository.model.RepositoryFolder;
import org.genesys.filerepository.service.RepositoryService;
import org.gringlobal.api.exception.NotFoundElement;
import org.gringlobal.custom.jasper.CodeValueFunctions;
import org.gringlobal.service.JasperReportService;
import org.gringlobal.spring.TransactionHelper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;

import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.JasperCompileManager;
import net.sf.jasperreports.engine.JasperExportManager;
import net.sf.jasperreports.engine.JasperFillManager;
import net.sf.jasperreports.engine.JasperPrint;
import net.sf.jasperreports.engine.JasperReport;
import net.sf.jasperreports.engine.SimpleJasperReportsContext;
import net.sf.jasperreports.engine.data.JRBeanCollectionDataSource;
import net.sf.jasperreports.engine.util.JRLoader;
import net.sf.jasperreports.repo.InputStreamResource;
import net.sf.jasperreports.repo.ReportResource;
import net.sf.jasperreports.repo.Resource;

@Service
@Transactional(readOnly = true)
@Slf4j
public class JasperReportServiceImpl implements JasperReportService {

	@Autowired
	private RepositoryService repositoryService;

	@Override
	public void generatePdfReport(String entityName, String reportTemplate,
		Collection<Object> entities, Locale locale, Map<String, Object> parameters, OutputStream reportOutput) throws Exception {
		JasperPrint generatedReport = generateReport(entityName, reportTemplate, entities, locale, parameters);
		JasperExportManager.exportReportToPdfStream(generatedReport, reportOutput);
	}

	@Override
	public ResponseEntity<StreamingResponseBody> generatePdfReport(String entityName, String reportTemplate, Collection<Object> entities, Locale locale, Map<String, Object> parameters) throws Exception {
		JasperPrint generatedReport = generateReport(entityName, reportTemplate, entities, locale, parameters);

		StreamingResponseBody responseStream = out -> {
			try {
				JasperExportManager.exportReportToPdfStream(generatedReport, out);
			} catch (Exception e) {
				log.warn("Generating PDF failed: {}", e.getMessage());
				throw new IOException(e);
			}
		};

		return ResponseEntity.ok()
			.contentType(MediaType.APPLICATION_PDF)
			.header(HttpHeaders.CONTENT_DISPOSITION, String.format("attachment; filename=%sReport.pdf", entityName))
			.body(responseStream);
	}

	private JasperPrint generateReport(String entityName, String reportTemplate, Collection<Object> entities, Locale locale,
		Map<String, Object> parameters) throws JRException, IOException {
		log.debug("Generating pdf report for {} entities", entityName);

		if (parameters == null) {
			parameters = new HashMap<>();
		}
		parameters.put(JRParameter.REPORT_LOCALE, locale);
		
		Path path = Path.of(REPORT_PATH, entityName).toAbsolutePath();

		// load report template
		JasperReport jasperReport = loadReport(path, reportTemplate);

		// add ResourceRepository to the reports context
		SimpleJasperReportsContext jasperReportsContext = new SimpleJasperReportsContext();
		jasperReportsContext.setExtensions(
			net.sf.jasperreports.repo.RepositoryService.class, List.of(new ResourceRepository(path, SecurityContextHolder.getContext().getAuthentication()))
		);

		final JRBeanCollectionDataSource sourceEntities = new JRBeanCollectionDataSource(entities);

		// fill report using report context with ResourceRepository
		return JasperFillManager.getInstance(jasperReportsContext).fill(jasperReport, parameters, sourceEntities);
	}

	private synchronized JasperReport loadReport(Path path, String uri) throws JRException, IOException {
		log.debug("Loading report template path={} uri={}", path, uri);
		Path filepath = Path.of(path.toString(), uri);
		String fileName = filepath.getFileName().toString();
		path = filepath.getParent();
		RepositoryFile repositoryTemplateFile;
		try {
			repositoryTemplateFile = repositoryService.getFile(path, fileName);
		} catch (NoSuchRepositoryFileException | InvalidRepositoryPathException e) {
			throw new NotFoundElement(e.getMessage(), e);
		}
		JasperReport report;

		String compiledFileName = String.format("%s.jasper", repositoryTemplateFile.getMd5Sum());
		File compiledTemplate = new File(FileUtils.getTempDirectory(), compiledFileName);
		log.debug("Looking for compiled report {}", compiledTemplate.getAbsolutePath());

		if (!compiledTemplate.exists()) {
			byte[] template = repositoryService.getFileBytes(repositoryTemplateFile);
			log.debug("Compiling report bytes of {}{}", path, fileName);
			try (InputStream baos = new ByteArrayInputStream(template)) {
				if (!compiledTemplate.getParentFile().mkdirs()) {
					log.warn("Folder for compiled template was not created");
				}
				if (!compiledTemplate.createNewFile()) {
					log.warn("Compiled template file was not created");
				}

				try (var compiledOutputStream = Files.newOutputStream(compiledTemplate.toPath(), StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)) {
					SimpleJasperReportsContext jasperReportsContext = new SimpleJasperReportsContext();
					jasperReportsContext.getExtensions(FunctionsBundle.class).get(0).addFunctionClass(CodeValueFunctions.class);
					JasperCompileManager.getInstance(jasperReportsContext).compileToStream(baos, compiledOutputStream);
				} catch (JRException e) {
					if (!compiledTemplate.delete()) { // delete prepared file (with 0 bytes)
						log.warn("Compiled template file was not deleted");
					}
					throw e;
				}
			}
		}
		report = (JasperReport) JRLoader.loadObject(compiledTemplate);
		return report;
	}

	private class ResourceRepository implements net.sf.jasperreports.repo.RepositoryService {

		private final Path reportPath;
		private final Authentication authentication;

		public ResourceRepository(Path reportPath, Authentication authentication) {
			this.reportPath = reportPath;
			this.authentication = authentication;
		}

		@Override
		public Resource getResource(final String uri) {
			return null;
		}

		@Override
		public void saveResource(final String uri, final Resource resource) {
			throw new UnsupportedOperationException();
		}

		@Override
		public <K extends Resource> K getResource(final String uri, final Class<K> resourceType) {
			log.debug("Resolving getResource {} of type {}", uri, resourceType);
			// check the type of resource
			try {
				return TransactionHelper.asUser(authentication, () -> {
					if (resourceType.isAssignableFrom(ReportResource.class)) {
						return resourceType.cast(getReport(uri));
					} else if (resourceType.isAssignableFrom(InputStreamResource.class)) {
						return resourceType.cast(getImage(uri));
					} else if (resourceType.isAssignableFrom(ResourceBundleResource.class)) {
						return resourceType.cast(getBundle(uri));
					} else {
						return null;
					}
				});
			} catch (Exception e) {
				throw new Error(e);
			}
		}

		private ReportResource getReport(final String uri) throws Exception {
			log.debug("Resolving getReport {} path={}", uri, this.reportPath);
			final ReportResource reportResource = new ReportResource();
			reportResource.setReport(loadReport(reportPath, uri));
			return reportResource;
		}

		private InputStreamResource getImage(final String uri) throws Exception {
			log.debug("Resolving getImage {} path={}", uri, this.reportPath);
			InputStreamResource inputStreamResource = new InputStreamResource();
			try {
				UUID fileUuid = UUID.fromString(uri);
				RepositoryFile file = repositoryService.getFile(fileUuid);
				inputStreamResource.setInputStream(new ByteArrayInputStream(repositoryService.getFileBytes(file)));
			} catch (IllegalArgumentException e) {
				Path filepath = Path.of(reportPath.toString(), uri);
				String fileName = filepath.getFileName().toString();
				RepositoryFile file = repositoryService.getFile(filepath.getParent(), fileName);
				inputStreamResource.setInputStream(new ByteArrayInputStream(repositoryService.getFileBytes(file)));
			}
			return inputStreamResource;
		}

		private ResourceBundleResource getBundle(String uri) throws IOException {
			Path basePath = reportPath.resolve("resources");
			ResourceBundleResource resourceBundleResource = null;
			var resourceBytes = searchForResource(basePath, uri);
			if (resourceBytes != null) {
				var bundleInputStream = new ByteArrayInputStream(resourceBytes);
				ResourceBundle bundle = new PropertyResourceBundle(bundleInputStream);
				resourceBundleResource = new ResourceBundleResource();
				resourceBundleResource.setResourceBundle(bundle);
			}
			return resourceBundleResource;
		}

		private byte[] searchForResource(Path path, String name) {
			try {
				byte[] resourceBytes = getResourceBytes(path, name);
				if (resourceBytes != null) {
					return resourceBytes;
				}
				var folders = repositoryService.listPathsRecursively(path);
				for (RepositoryFolder folder : folders) {
					resourceBytes = getResourceBytes(folder.getFolderPath(), name);
					if (resourceBytes != null) {
						break;
					}
				}
				return resourceBytes;
			} catch (InvalidRepositoryPathException e) {
				log.warn("Invalid repository path", e);
				return null;
			}
		}

		private byte[] getResourceBytes(Path path, String fileName) {
			try {
				var file = repositoryService.getFile(path, fileName);
				return repositoryService.getFileBytes(file);
			} catch (Exception e) {
				return null;
			}
		}
	}
}