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.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;
}
}
}
}