Improve error handling if reporting service implementation is missing [FINERACT-1173]

This commit is contained in:
Michael Vorburger
2020-11-26 06:13:15 +08:00
committed by Petri Tuomola
parent a3ab467278
commit b96061613b
11 changed files with 218 additions and 222 deletions
@@ -51,6 +51,7 @@ import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSeria
import org.apache.fineract.infrastructure.core.serialization.ToApiJsonSerializer; import org.apache.fineract.infrastructure.core.serialization.ToApiJsonSerializer;
import org.apache.fineract.infrastructure.dataqueries.data.ReportData; import org.apache.fineract.infrastructure.dataqueries.data.ReportData;
import org.apache.fineract.infrastructure.dataqueries.service.ReadReportingService; import org.apache.fineract.infrastructure.dataqueries.service.ReadReportingService;
import org.apache.fineract.infrastructure.report.provider.ReportingProcessServiceProvider;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope; import org.springframework.context.annotation.Scope;
@@ -71,17 +72,19 @@ public class ReportsApiResource {
private final ReadReportingService readReportingService; private final ReadReportingService readReportingService;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
private final ApiRequestParameterHelper apiRequestParameterHelper; private final ApiRequestParameterHelper apiRequestParameterHelper;
private final ReportingProcessServiceProvider reportingProcessServiceProvider;
@Autowired @Autowired
public ReportsApiResource(final PlatformSecurityContext context, final ReadReportingService readReportingService, public ReportsApiResource(final PlatformSecurityContext context, final ReadReportingService readReportingService,
final ToApiJsonSerializer<ReportData> toApiJsonSerializer, final ToApiJsonSerializer<ReportData> toApiJsonSerializer,
final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService, final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService,
final ApiRequestParameterHelper apiRequestParameterHelper) { final ApiRequestParameterHelper apiRequestParameterHelper, ReportingProcessServiceProvider reportingProcessServiceProvider) {
this.context = context; this.context = context;
this.readReportingService = readReportingService; this.readReportingService = readReportingService;
this.toApiJsonSerializer = toApiJsonSerializer; this.toApiJsonSerializer = toApiJsonSerializer;
this.commandsSourceWritePlatformService = commandsSourceWritePlatformService; this.commandsSourceWritePlatformService = commandsSourceWritePlatformService;
this.apiRequestParameterHelper = apiRequestParameterHelper; this.apiRequestParameterHelper = apiRequestParameterHelper;
this.reportingProcessServiceProvider = reportingProcessServiceProvider;
} }
@GET @GET
@@ -117,7 +120,8 @@ public class ReportsApiResource {
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
if (settings.isTemplate()) { if (settings.isTemplate()) {
result.appendedTemplate(this.readReportingService.getAllowedParameters(), this.readReportingService.getAllowedReportTypes()); result.appendedTemplate(this.readReportingService.getAllowedParameters(),
this.reportingProcessServiceProvider.findAllReportingTypes());
} }
return this.toApiJsonSerializer.serialize(settings, result, this.responseDataParameters); return this.toApiJsonSerializer.serialize(settings, result, this.responseDataParameters);
} }
@@ -135,7 +139,8 @@ public class ReportsApiResource {
this.context.authenticatedUser().validateHasReadPermission(this.resourceNameForPermissions); this.context.authenticatedUser().validateHasReadPermission(this.resourceNameForPermissions);
final ReportData result = new ReportData(); final ReportData result = new ReportData();
result.appendedTemplate(this.readReportingService.getAllowedParameters(), this.readReportingService.getAllowedReportTypes()); result.appendedTemplate(this.readReportingService.getAllowedParameters(),
this.reportingProcessServiceProvider.findAllReportingTypes());
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return this.toApiJsonSerializer.serialize(settings, result, this.responseDataParameters); return this.toApiJsonSerializer.serialize(settings, result, this.responseDataParameters);
@@ -25,10 +25,6 @@ import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import javax.ws.rs.Consumes; import javax.ws.rs.Consumes;
import javax.ws.rs.DefaultValue; import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET; import javax.ws.rs.GET;
@@ -40,20 +36,14 @@ import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.ResponseBuilder;
import javax.ws.rs.core.StreamingOutput;
import javax.ws.rs.core.UriInfo; import javax.ws.rs.core.UriInfo;
import org.apache.fineract.infrastructure.core.api.ApiParameterHelper; import org.apache.fineract.infrastructure.core.api.ApiParameterHelper;
import org.apache.fineract.infrastructure.core.serialization.ToApiJsonSerializer; import org.apache.fineract.infrastructure.core.exception.PlatformServiceUnavailableException;
import org.apache.fineract.infrastructure.dataqueries.data.GenericResultsetData;
import org.apache.fineract.infrastructure.dataqueries.data.ReportData;
import org.apache.fineract.infrastructure.dataqueries.service.GenericDataService;
import org.apache.fineract.infrastructure.dataqueries.service.ReadReportingService; import org.apache.fineract.infrastructure.dataqueries.service.ReadReportingService;
import org.apache.fineract.infrastructure.report.provider.ReportingProcessServiceProvider; import org.apache.fineract.infrastructure.report.provider.ReportingProcessServiceProvider;
import org.apache.fineract.infrastructure.report.service.ReportingProcessService; import org.apache.fineract.infrastructure.report.service.ReportingProcessService;
import org.apache.fineract.infrastructure.security.exception.NoAuthorizationException; import org.apache.fineract.infrastructure.security.exception.NoAuthorizationException;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.infrastructure.security.utils.SQLInjectionValidator;
import org.apache.fineract.useradministration.domain.AppUser; import org.apache.fineract.useradministration.domain.AppUser;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope; import org.springframework.context.annotation.Scope;
@@ -65,20 +55,17 @@ import org.springframework.stereotype.Component;
@Tag(name = "Run Reports", description = "") @Tag(name = "Run Reports", description = "")
public class RunreportsApiResource { public class RunreportsApiResource {
public static final String IS_SELF_SERVICE_USER_REPORT_PARAMETER = "isSelfServiceUserReport";
private final PlatformSecurityContext context; private final PlatformSecurityContext context;
private final ToApiJsonSerializer<ReportData> toApiJsonSerializer;
private final ReadReportingService readExtraDataAndReportingService; private final ReadReportingService readExtraDataAndReportingService;
private final GenericDataService genericDataService;
private final ReportingProcessServiceProvider reportingProcessServiceProvider; private final ReportingProcessServiceProvider reportingProcessServiceProvider;
@Autowired @Autowired
public RunreportsApiResource(final PlatformSecurityContext context, final ReadReportingService readExtraDataAndReportingService, public RunreportsApiResource(final PlatformSecurityContext context, final ReadReportingService readExtraDataAndReportingService,
final GenericDataService genericDataService, final ToApiJsonSerializer<ReportData> toApiJsonSerializer,
final ReportingProcessServiceProvider reportingProcessServiceProvider) { final ReportingProcessServiceProvider reportingProcessServiceProvider) {
this.context = context; this.context = context;
this.readExtraDataAndReportingService = readExtraDataAndReportingService; this.readExtraDataAndReportingService = readExtraDataAndReportingService;
this.genericDataService = genericDataService;
this.toApiJsonSerializer = toApiJsonSerializer;
this.reportingProcessServiceProvider = reportingProcessServiceProvider; this.reportingProcessServiceProvider = reportingProcessServiceProvider;
} }
@@ -108,79 +95,26 @@ public class RunreportsApiResource {
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = RunreportsApiResourceSwagger.RunReportsResponse.class))) }) @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = RunreportsApiResourceSwagger.RunReportsResponse.class))) })
public Response runReport(@PathParam("reportName") @Parameter(description = "reportName") final String reportName, public Response runReport(@PathParam("reportName") @Parameter(description = "reportName") final String reportName,
@Context final UriInfo uriInfo, @Context final UriInfo uriInfo,
@DefaultValue("false") @QueryParam("isSelfServiceUserReport") @Parameter(description = "isSelfServiceUserReport") final boolean isSelfServiceUserReport) { @DefaultValue("false") @QueryParam(IS_SELF_SERVICE_USER_REPORT_PARAMETER) @Parameter(description = IS_SELF_SERVICE_USER_REPORT_PARAMETER) final boolean isSelfServiceUserReport) {
final MultivaluedMap<String, String> queryParams = uriInfo.getQueryParameters(); final MultivaluedMap<String, String> queryParams = uriInfo.getQueryParameters();
final boolean parameterType = ApiParameterHelper.parameterType(queryParams);
final boolean prettyPrint = ApiParameterHelper.prettyPrint(uriInfo.getQueryParameters());
final boolean exportCsv = ApiParameterHelper.exportCsv(uriInfo.getQueryParameters());
final boolean parameterType = ApiParameterHelper.parameterType(uriInfo.getQueryParameters());
final boolean exportPdf = ApiParameterHelper.exportPdf(uriInfo.getQueryParameters());
checkUserPermissionForReport(reportName, parameterType); checkUserPermissionForReport(reportName, parameterType);
String parameterTypeValue = null; // Pass through isSelfServiceUserReport so that ReportingProcessService implementations can use it
if (!parameterType) { queryParams.putSingle(IS_SELF_SERVICE_USER_REPORT_PARAMETER, Boolean.toString(isSelfServiceUserReport));
parameterTypeValue = "report";
String reportType = this.readExtraDataAndReportingService.getReportType(reportName, isSelfServiceUserReport); String reportType = this.readExtraDataAndReportingService.getReportType(reportName, isSelfServiceUserReport);
ReportingProcessService reportingProcessService = this.reportingProcessServiceProvider.findReportingProcessService(reportType); ReportingProcessService reportingProcessService = this.reportingProcessServiceProvider.findReportingProcessService(reportType);
if (reportingProcessService != null) { if (reportingProcessService == null) {
return reportingProcessService.processRequest(reportName, queryParams); throw new PlatformServiceUnavailableException("err.msg.report.service.implementation.missing",
} ReportingProcessServiceProvider.SERVICE_MISSING + reportType, reportType);
} else {
parameterTypeValue = "parameter";
} }
return reportingProcessService.processRequest(reportName, queryParams);
// PDF format
if (exportPdf) {
final Map<String, String> reportParams = getReportParams(queryParams);
final String pdfFileName = this.readExtraDataAndReportingService.retrieveReportPDF(reportName, parameterTypeValue, reportParams,
isSelfServiceUserReport);
final File file = new File(pdfFileName);
final ResponseBuilder response = Response.ok(file);
response.header("Content-Disposition", "attachment; filename=\"" + pdfFileName + "\"");
response.header("content-Type", "application/pdf");
return response.build();
}
if (!exportCsv) {
final Map<String, String> reportParams = getReportParams(queryParams);
final GenericResultsetData result = this.readExtraDataAndReportingService.retrieveGenericResultset(reportName,
parameterTypeValue, reportParams, isSelfServiceUserReport);
String json = "";
final boolean genericResultSetIsPassed = ApiParameterHelper.genericResultSetPassed(uriInfo.getQueryParameters());
final boolean genericResultSet = ApiParameterHelper.genericResultSet(uriInfo.getQueryParameters());
if (genericResultSetIsPassed) {
if (genericResultSet) {
json = this.toApiJsonSerializer.serializePretty(prettyPrint, result);
} else {
json = this.genericDataService.generateJsonFromGenericResultsetData(result);
}
} else {
json = this.toApiJsonSerializer.serializePretty(prettyPrint, result);
}
return Response.ok().entity(json).type(MediaType.APPLICATION_JSON).build();
}
// CSV Export
final Map<String, String> reportParams = getReportParams(queryParams);
final StreamingOutput result = this.readExtraDataAndReportingService.retrieveReportCSV(reportName, parameterTypeValue, reportParams,
isSelfServiceUserReport);
return Response.ok().entity(result).type("text/csv")
.header("Content-Disposition", "attachment;filename=" + reportName.replaceAll(" ", "") + ".csv").build();
} }
private void checkUserPermissionForReport(final String reportName, final boolean parameterType) { private void checkUserPermissionForReport(final String reportName, final boolean parameterType) {
// Anyone can run a 'report' that is simply getting possible parameter // Anyone can run a 'report' that is simply getting possible parameter
// (dropdown listbox) values. // (dropdown listbox) values.
if (!parameterType) { if (!parameterType) {
@@ -190,22 +124,4 @@ public class RunreportsApiResource {
} }
} }
} }
private Map<String, String> getReportParams(final MultivaluedMap<String, String> queryParams) {
final Map<String, String> reportParams = new HashMap<>();
final Set<String> keys = queryParams.keySet();
String pKey;
String pValue;
for (final String k : keys) {
if (k.startsWith("R_")) {
pKey = "${" + k.substring(2) + "}";
pValue = queryParams.get(k).get(0);
SQLInjectionValidator.validateSQLInput(pValue);
reportParams.put(pKey, pValue);
}
}
return reportParams;
}
} }
@@ -0,0 +1,110 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.fineract.infrastructure.dataqueries.service;
import java.io.File;
import java.util.List;
import java.util.Map;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.ResponseBuilder;
import javax.ws.rs.core.StreamingOutput;
import org.apache.fineract.infrastructure.core.api.ApiParameterHelper;
import org.apache.fineract.infrastructure.core.serialization.ToApiJsonSerializer;
import org.apache.fineract.infrastructure.dataqueries.api.RunreportsApiResource;
import org.apache.fineract.infrastructure.dataqueries.data.GenericResultsetData;
import org.apache.fineract.infrastructure.dataqueries.data.ReportData;
import org.apache.fineract.infrastructure.report.annotation.ReportService;
import org.apache.fineract.infrastructure.report.service.ReportingProcessService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
@ReportService(type = { "Table", "Chart", "SMS" })
public class DatatableReportingProcessService implements ReportingProcessService {
private final ReadReportingService readExtraDataAndReportingService;
private final ToApiJsonSerializer<ReportData> toApiJsonSerializer;
private final GenericDataService genericDataService;
@Autowired
public DatatableReportingProcessService(final ReadReportingService readExtraDataAndReportingService,
final GenericDataService genericDataService, final ToApiJsonSerializer<ReportData> toApiJsonSerializer) {
this.readExtraDataAndReportingService = readExtraDataAndReportingService;
this.toApiJsonSerializer = toApiJsonSerializer;
this.genericDataService = genericDataService;
}
@Override
public Response processRequest(String reportName, MultivaluedMap<String, String> queryParams) {
boolean isSelfServiceUserReport = Boolean.parseBoolean(
queryParams.getOrDefault(RunreportsApiResource.IS_SELF_SERVICE_USER_REPORT_PARAMETER, List.of("false")).get(0));
final boolean prettyPrint = ApiParameterHelper.prettyPrint(queryParams);
final boolean exportCsv = ApiParameterHelper.exportCsv(queryParams);
final boolean exportPdf = ApiParameterHelper.exportPdf(queryParams);
final String parameterTypeValue = ApiParameterHelper.parameterType(queryParams) ? "parameter" : "report";
// PDF format
if (exportPdf) {
final Map<String, String> reportParams = getReportParams(queryParams);
final String pdfFileName = this.readExtraDataAndReportingService.retrieveReportPDF(reportName, parameterTypeValue, reportParams,
isSelfServiceUserReport);
final File file = new File(pdfFileName);
final ResponseBuilder response = Response.ok(file);
response.header("Content-Disposition", "attachment; filename=\"" + pdfFileName + "\"");
response.header("content-Type", "application/pdf");
return response.build();
}
// JSON format
if (!exportCsv) {
final Map<String, String> reportParams = getReportParams(queryParams);
final GenericResultsetData result = this.readExtraDataAndReportingService.retrieveGenericResultset(reportName,
parameterTypeValue, reportParams, isSelfServiceUserReport);
String json;
final boolean genericResultSetIsPassed = ApiParameterHelper.genericResultSetPassed(queryParams);
final boolean genericResultSet = ApiParameterHelper.genericResultSet(queryParams);
if (genericResultSetIsPassed) {
if (genericResultSet) {
json = this.toApiJsonSerializer.serializePretty(prettyPrint, result);
} else {
json = this.genericDataService.generateJsonFromGenericResultsetData(result);
}
} else {
json = this.toApiJsonSerializer.serializePretty(prettyPrint, result);
}
return Response.ok().entity(json).type(MediaType.APPLICATION_JSON).build();
}
// CSV format
final Map<String, String> reportParams = getReportParams(queryParams);
final StreamingOutput result = this.readExtraDataAndReportingService.retrieveReportCSV(reportName, parameterTypeValue, reportParams,
isSelfServiceUserReport);
return Response.ok().entity(result).type("text/csv")
.header("Content-Disposition", "attachment;filename=" + reportName.replaceAll(" ", "") + ".csv").build();
}
}
@@ -30,29 +30,28 @@ import org.apache.fineract.useradministration.domain.AppUser;
public interface ReadReportingService { public interface ReadReportingService {
Collection<ReportData> retrieveReportList();
ReportData retrieveReport(Long id);
String getReportType(String reportName, boolean isSelfServiceUserReport);
Collection<ReportParameterData> getAllowedParameters();
// TODO Move the following x3 methods into the (new; FINERACT-1173) DatatableReportingProcessService?
String retrieveReportPDF(String name, String type, Map<String, String> extractedQueryParams, boolean isSelfServiceUserReport);
StreamingOutput retrieveReportCSV(String name, String type, Map<String, String> extractedQueryParams, boolean isSelfServiceUserReport); StreamingOutput retrieveReportCSV(String name, String type, Map<String, String> extractedQueryParams, boolean isSelfServiceUserReport);
GenericResultsetData retrieveGenericResultset(String name, String type, Map<String, String> extractedQueryParams, GenericResultsetData retrieveGenericResultset(String name, String type, Map<String, String> extractedQueryParams,
boolean isSelfServiceUserReport); boolean isSelfServiceUserReport);
String retrieveReportPDF(String name, String type, Map<String, String> extractedQueryParams, boolean isSelfServiceUserReport); // TODO This is weird, could they not be using the retrieveGenericResultset() above after all?
// needed for smsCampaign and emailCampaign jobs where securityContext is null
String getReportType(String reportName, boolean isSelfServiceUserReport);
Collection<ReportData> retrieveReportList();
Collection<ReportParameterData> getAllowedParameters();
ReportData retrieveReport(Long id);
Collection<String> getAllowedReportTypes();
// needed for smsCampaign and emailCampaign jobs where securityContext is
// null
GenericResultsetData retrieveGenericResultSetForSmsEmailCampaign(String name, String type, Map<String, String> extractedQueryParams); GenericResultsetData retrieveGenericResultSetForSmsEmailCampaign(String name, String type, Map<String, String> extractedQueryParams);
String sqlToRunForSmsEmailCampaign(String name, String type, Map<String, String> queryParams); // TODO kill this when tackling https://issues.apache.org/jira/browse/FINERACT-1264
ByteArrayOutputStream generatePentahoReportAsOutputStream(String reportName, String outputTypeParam, Map<String, String> queryParams, ByteArrayOutputStream generatePentahoReportAsOutputStream(String reportName, String outputTypeParam, Map<String, String> queryParams,
Locale locale, AppUser runReportAsUser, StringBuilder errorLog); Locale locale, AppUser runReportAsUser, StringBuilder errorLog);
} }
@@ -27,7 +27,6 @@ import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
@@ -51,7 +50,6 @@ import org.apache.fineract.infrastructure.dataqueries.data.ResultsetColumnHeader
import org.apache.fineract.infrastructure.dataqueries.data.ResultsetRowData; import org.apache.fineract.infrastructure.dataqueries.data.ResultsetRowData;
import org.apache.fineract.infrastructure.dataqueries.exception.ReportNotFoundException; import org.apache.fineract.infrastructure.dataqueries.exception.ReportNotFoundException;
import org.apache.fineract.infrastructure.documentmanagement.contentrepository.FileSystemContentRepository; import org.apache.fineract.infrastructure.documentmanagement.contentrepository.FileSystemContentRepository;
import org.apache.fineract.infrastructure.report.provider.ReportingProcessServiceProvider;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.infrastructure.security.utils.ColumnValidator; import org.apache.fineract.infrastructure.security.utils.ColumnValidator;
import org.apache.fineract.infrastructure.security.utils.SQLInjectionException; import org.apache.fineract.infrastructure.security.utils.SQLInjectionException;
@@ -74,53 +72,43 @@ public class ReadReportingServiceImpl implements ReadReportingService {
private final DataSource dataSource; private final DataSource dataSource;
private final PlatformSecurityContext context; private final PlatformSecurityContext context;
private final GenericDataService genericDataService; private final GenericDataService genericDataService;
private final ReportingProcessServiceProvider reportingProcessServiceProvider;
private final ColumnValidator columnValidator; private final ColumnValidator columnValidator;
@Autowired @Autowired
public ReadReportingServiceImpl(final PlatformSecurityContext context, final RoutingDataSource dataSource, public ReadReportingServiceImpl(final PlatformSecurityContext context, final RoutingDataSource dataSource,
final GenericDataService genericDataService, final ReportingProcessServiceProvider reportingProcessServiceProvider, final GenericDataService genericDataService, final ColumnValidator columnValidator) {
final ColumnValidator columnValidator) {
this.context = context; this.context = context;
this.dataSource = dataSource; this.dataSource = dataSource;
this.jdbcTemplate = new JdbcTemplate(this.dataSource); this.jdbcTemplate = new JdbcTemplate(this.dataSource);
this.genericDataService = genericDataService; this.genericDataService = genericDataService;
this.reportingProcessServiceProvider = reportingProcessServiceProvider;
this.columnValidator = columnValidator; this.columnValidator = columnValidator;
} }
@Override @Override
public StreamingOutput retrieveReportCSV(final String name, final String type, final Map<String, String> queryParams, public StreamingOutput retrieveReportCSV(final String name, final String type, final Map<String, String> queryParams,
final boolean isSelfServiceUserReport) { final boolean isSelfServiceUserReport) {
return out -> {
try {
return new StreamingOutput() { final GenericResultsetData result = retrieveGenericResultset(name, type, queryParams, isSelfServiceUserReport);
final StringBuilder sb = generateCsvFileBuffer(result);
@Override final InputStream in = new ByteArrayInputStream(sb.toString().getBytes(StandardCharsets.UTF_8));
public void write(final OutputStream out) {
try {
final GenericResultsetData result = retrieveGenericResultset(name, type, queryParams, isSelfServiceUserReport); final byte[] outputByte = new byte[4096];
final StringBuilder sb = generateCsvFileBuffer(result); Integer readLen = in.read(outputByte, 0, 4096);
final InputStream in = new ByteArrayInputStream(sb.toString().getBytes(StandardCharsets.UTF_8)); while (readLen != -1) {
out.write(outputByte, 0, readLen);
final byte[] outputByte = new byte[4096]; readLen = in.read(outputByte, 0, 4096);
Integer readLen = in.read(outputByte, 0, 4096);
while (readLen != -1) {
out.write(outputByte, 0, readLen);
readLen = in.read(outputByte, 0, 4096);
}
// in.close();
// out.flush();
// out.close();
} catch (final Exception e) {
throw new PlatformDataIntegrityException("error.msg.exception.error", e.getMessage(), e);
} }
// in.close();
// out.flush();
// out.close();
} catch (final Exception e) {
throw new PlatformDataIntegrityException("error.msg.exception.error", e.getMessage(), e);
} }
}; };
} }
private StringBuilder generateCsvFileBuffer(final GenericResultsetData result) { private StringBuilder generateCsvFileBuffer(final GenericResultsetData result) {
@@ -146,8 +134,8 @@ public class ReadReportingServiceImpl implements ReadReportingService {
final String doubleQuote = "\""; final String doubleQuote = "\"";
final String twoDoubleQuotes = doubleQuote + doubleQuote; final String twoDoubleQuotes = doubleQuote + doubleQuote;
LOG.info("NO. of Rows: {}", data.size()); LOG.info("NO. of Rows: {}", data.size());
for (int i = 0; i < data.size(); i++) { for (ResultsetRowData element : data) {
row = data.get(i).getRow(); row = element.getRow();
rSize = row.size(); rSize = row.size();
for (int j = 0; j < rSize; j++) { for (int j = 0; j < rSize; j++) {
// currCol = columnHeaders.get(j).getColumnName(); // currCol = columnHeaders.get(j).getColumnName();
@@ -184,7 +172,7 @@ public class ReadReportingServiceImpl implements ReadReportingService {
final GenericResultsetData result = this.genericDataService.fillGenericResultSet(sql); final GenericResultsetData result = this.genericDataService.fillGenericResultSet(sql);
final long elapsed = System.currentTimeMillis() - startTime; final long elapsed = System.currentTimeMillis() - startTime;
LOG.info("FINISHING Report/Request Name: {} - {} Elapsed Time: {}", new Object[] { name, type, elapsed }); LOG.info("FINISHING Report/Request Name: {} - {} Elapsed Time: {}", name, type, elapsed);
return result; return result;
} }
@@ -213,11 +201,9 @@ public class ReadReportingServiceImpl implements ReadReportingService {
sql = this.genericDataService.wrapSQL(sql); sql = this.genericDataService.wrapSQL(sql);
return sql; return sql;
} }
private String getSql(final String name, final String type) { private String getSql(final String name, final String type) {
final String inputSql = "select " + type + "_sql as the_sql from stretchy_" + type + " where " + type + "_name = '" + name + "'"; final String inputSql = "select " + type + "_sql as the_sql from stretchy_" + type + " where " + type + "_name = '" + name + "'";
validateReportName(name); validateReportName(name);
@@ -234,7 +220,6 @@ public class ReadReportingServiceImpl implements ReadReportingService {
@Override @Override
public String getReportType(final String reportName, final boolean isSelfServiceUserReport) { public String getReportType(final String reportName, final boolean isSelfServiceUserReport) {
final String sql = "SELECT ifnull(report_type,'') as report_type FROM `stretchy_report` where report_name = '" + reportName final String sql = "SELECT ifnull(report_type,'') as report_type FROM `stretchy_report` where report_name = '" + reportName
+ "' and self_service_user_report = ?"; + "' and self_service_user_report = ?";
validateReportName(reportName); validateReportName(reportName);
@@ -242,7 +227,7 @@ public class ReadReportingServiceImpl implements ReadReportingService {
final String sqlWrapped = this.genericDataService.wrapSQL(sql); final String sqlWrapped = this.genericDataService.wrapSQL(sql);
final SqlRowSet rs = this.jdbcTemplate.queryForRowSet(sqlWrapped, new Object[] { isSelfServiceUserReport }); final SqlRowSet rs = this.jdbcTemplate.queryForRowSet(sqlWrapped, isSelfServiceUserReport);
if (rs.next()) { if (rs.next()) {
return rs.getString("report_type"); return rs.getString("report_type");
@@ -290,8 +275,8 @@ public class ReadReportingServiceImpl implements ReadReportingService {
String currColType; String currColType;
String currVal; String currVal;
LOG.info("NO. of Rows: {}", data.size()); LOG.info("NO. of Rows: {}", data.size());
for (int i = 0; i < data.size(); i++) { for (ResultsetRowData element : data) {
row = data.get(i).getRow(); row = element.getRow();
rSize = row.size(); rSize = row.size();
for (int j = 0; j < rSize; j++) { for (int j = 0; j < rSize; j++) {
currColType = columnHeaders.get(j).getColumnType(); currColType = columnHeaders.get(j).getColumnType();
@@ -338,7 +323,7 @@ public class ReadReportingServiceImpl implements ReadReportingService {
final String sql = rm.schema(id); final String sql = rm.schema(id);
final Collection<ReportParameterJoinData> rpJoins = this.jdbcTemplate.query(sql, rm, new Object[] {}); final Collection<ReportParameterJoinData> rpJoins = this.jdbcTemplate.query(sql, rm);
final Collection<ReportData> reportList = new ArrayList<>(); final Collection<ReportData> reportList = new ArrayList<>();
if (rpJoins == null || rpJoins.size() == 0) { if (rpJoins == null || rpJoins.size() == 0) {
@@ -410,22 +395,12 @@ public class ReadReportingServiceImpl implements ReadReportingService {
@Override @Override
public Collection<ReportParameterData> getAllowedParameters() { public Collection<ReportParameterData> getAllowedParameters() {
final ReportParameterMapper rm = new ReportParameterMapper(); final ReportParameterMapper rm = new ReportParameterMapper();
final String sql = rm.schema(); final String sql = rm.schema();
final Collection<ReportParameterData> parameters = this.jdbcTemplate.query(sql, rm, new Object[] {}); final Collection<ReportParameterData> parameters = this.jdbcTemplate.query(sql, rm);
return parameters; return parameters;
} }
@Override
public Collection<String> getAllowedReportTypes() {
final List<String> reportTypes = new ArrayList<>();
reportTypes.add("Table");
reportTypes.add("Chart");
reportTypes.addAll(this.reportingProcessServiceProvider.findAllReportingTypes());
return reportTypes;
}
private static final class ReportParameterJoinMapper implements RowMapper<ReportParameterJoinData> { private static final class ReportParameterJoinMapper implements RowMapper<ReportParameterJoinData> {
public String schema(final Long reportId) { public String schema(final Long reportId) {
@@ -460,7 +435,6 @@ public class ReadReportingServiceImpl implements ReadReportingService {
@Override @Override
public ReportParameterJoinData mapRow(final ResultSet rs, final int rowNum) throws SQLException { public ReportParameterJoinData mapRow(final ResultSet rs, final int rowNum) throws SQLException {
final Long reportId = rs.getLong("reportId"); final Long reportId = rs.getLong("reportId");
final String reportName = rs.getString("reportName"); final String reportName = rs.getString("reportName");
final String reportType = rs.getString("reportType"); final String reportType = rs.getString("reportType");
@@ -491,9 +465,7 @@ public class ReadReportingServiceImpl implements ReadReportingService {
private static final class ReportParameterMapper implements RowMapper<ReportParameterData> { private static final class ReportParameterMapper implements RowMapper<ReportParameterData> {
public String schema() { public String schema() {
return "select p.id as id, p.parameter_name as parameterName from stretchy_parameter p where ifnull(p.special,'') != 'Y' order by p.id"; return "select p.id as id, p.parameter_name as parameterName from stretchy_parameter p where ifnull(p.special,'') != 'Y' order by p.id";
} }
@Override @Override
@@ -516,12 +488,11 @@ public class ReadReportingServiceImpl implements ReadReportingService {
final GenericResultsetData result = this.genericDataService.fillGenericResultSet(sql); final GenericResultsetData result = this.genericDataService.fillGenericResultSet(sql);
final long elapsed = System.currentTimeMillis() - startTime; final long elapsed = System.currentTimeMillis() - startTime;
LOG.info("FINISHING Report/Request Name: {} - {} Elapsed Time: {}", new Object[] { name, type, elapsed }); LOG.info("FINISHING Report/Request Name: {} - {} Elapsed Time: {}", name, type, elapsed);
return result; return result;
} }
@Override private String sqlToRunForSmsEmailCampaign(final String name, final String type, final Map<String, String> queryParams) {
public String sqlToRunForSmsEmailCampaign(final String name, final String type, final Map<String, String> queryParams) {
String sql = getSql(name, type); String sql = getSql(name, type);
final Set<String> keys = queryParams.keySet(); final Set<String> keys = queryParams.keySet();
@@ -599,7 +570,6 @@ public class ReadReportingServiceImpl implements ReadReportingService {
} }
private void validateReportName(final String name) { private void validateReportName(final String name) {
if (!StringUtils.isBlank(name) && !name.matches(REPORT_NAME_REGEX_PATTERN)) { if (!StringUtils.isBlank(name) && !name.matches(REPORT_NAME_REGEX_PATTERN)) {
throw new SQLInjectionException(); throw new SQLInjectionException();
} }
@@ -39,6 +39,7 @@ import org.apache.fineract.infrastructure.dataqueries.domain.ReportRepository;
import org.apache.fineract.infrastructure.dataqueries.exception.ReportNotFoundException; import org.apache.fineract.infrastructure.dataqueries.exception.ReportNotFoundException;
import org.apache.fineract.infrastructure.dataqueries.exception.ReportParameterNotFoundException; import org.apache.fineract.infrastructure.dataqueries.exception.ReportParameterNotFoundException;
import org.apache.fineract.infrastructure.dataqueries.serialization.ReportCommandFromApiJsonDeserializer; import org.apache.fineract.infrastructure.dataqueries.serialization.ReportCommandFromApiJsonDeserializer;
import org.apache.fineract.infrastructure.report.provider.ReportingProcessServiceProvider;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.useradministration.domain.Permission; import org.apache.fineract.useradministration.domain.Permission;
import org.apache.fineract.useradministration.domain.PermissionRepository; import org.apache.fineract.useradministration.domain.PermissionRepository;
@@ -62,20 +63,20 @@ public class ReportWritePlatformServiceImpl implements ReportWritePlatformServic
private final ReportParameterUsageRepository reportParameterUsageRepository; private final ReportParameterUsageRepository reportParameterUsageRepository;
private final ReportParameterRepository reportParameterRepository; private final ReportParameterRepository reportParameterRepository;
private final PermissionRepository permissionRepository; private final PermissionRepository permissionRepository;
private final ReadReportingService readReportingService; private final ReportingProcessServiceProvider reportingProcessServiceProvider;
@Autowired @Autowired
public ReportWritePlatformServiceImpl(final PlatformSecurityContext context, public ReportWritePlatformServiceImpl(final PlatformSecurityContext context,
final ReportCommandFromApiJsonDeserializer fromApiJsonDeserializer, final ReportRepository reportRepository, final ReportCommandFromApiJsonDeserializer fromApiJsonDeserializer, final ReportRepository reportRepository,
final ReportParameterRepository reportParameterRepository, final ReportParameterUsageRepository reportParameterUsageRepository, final ReportParameterRepository reportParameterRepository, final ReportParameterUsageRepository reportParameterUsageRepository,
final PermissionRepository permissionRepository, final ReadReportingService readReportingService) { final PermissionRepository permissionRepository, final ReportingProcessServiceProvider reportingProcessServiceProvider) {
this.context = context; this.context = context;
this.fromApiJsonDeserializer = fromApiJsonDeserializer; this.fromApiJsonDeserializer = fromApiJsonDeserializer;
this.reportRepository = reportRepository; this.reportRepository = reportRepository;
this.reportParameterRepository = reportParameterRepository; this.reportParameterRepository = reportParameterRepository;
this.reportParameterUsageRepository = reportParameterUsageRepository; this.reportParameterUsageRepository = reportParameterUsageRepository;
this.permissionRepository = permissionRepository; this.permissionRepository = permissionRepository;
this.readReportingService = readReportingService; this.reportingProcessServiceProvider = reportingProcessServiceProvider;
} }
@Transactional @Transactional
@@ -87,7 +88,7 @@ public class ReportWritePlatformServiceImpl implements ReportWritePlatformServic
this.fromApiJsonDeserializer.validate(command.json()); this.fromApiJsonDeserializer.validate(command.json());
final Report report = Report.fromJson(command, this.readReportingService.getAllowedReportTypes()); final Report report = Report.fromJson(command, this.reportingProcessServiceProvider.findAllReportingTypes());
final Set<ReportParameterUsage> reportParameterUsages = assembleSetOfReportParameterUsages(report, command); final Set<ReportParameterUsage> reportParameterUsages = assembleSetOfReportParameterUsages(report, command);
report.update(reportParameterUsages); report.update(reportParameterUsages);
@@ -121,7 +122,7 @@ public class ReportWritePlatformServiceImpl implements ReportWritePlatformServic
final Report report = this.reportRepository.findById(reportId).orElseThrow(() -> new ReportNotFoundException(reportId)); final Report report = this.reportRepository.findById(reportId).orElseThrow(() -> new ReportNotFoundException(reportId));
final Map<String, Object> changes = report.update(command, this.readReportingService.getAllowedReportTypes()); final Map<String, Object> changes = report.update(command, this.reportingProcessServiceProvider.findAllReportingTypes());
if (changes.containsKey("reportParameters")) { if (changes.containsKey("reportParameters")) {
final Set<ReportParameterUsage> reportParameterUsages = assembleSetOfReportParameterUsages(report, command); final Set<ReportParameterUsage> reportParameterUsages = assembleSetOfReportParameterUsages(report, command);
@@ -32,5 +32,5 @@ public @interface ReportService {
/** /**
* @return the type of the report * @return the type of the report
*/ */
String type(); String[] type();
} }
@@ -18,68 +18,46 @@
*/ */
package org.apache.fineract.infrastructure.report.provider; package org.apache.fineract.infrastructure.report.provider;
import com.google.common.collect.ImmutableMap;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap; import java.util.List;
import java.util.Map; import java.util.Map;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.fineract.infrastructure.report.annotation.ReportService; import org.apache.fineract.infrastructure.report.annotation.ReportService;
import org.apache.fineract.infrastructure.report.service.ReportingProcessService; import org.apache.fineract.infrastructure.report.service.ReportingProcessService;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Scope; import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@Component @Component
@Scope("singleton") @Scope("singleton")
public class ReportingProcessServiceProvider implements ApplicationContextAware { public class ReportingProcessServiceProvider {
public static final String SERVICE_MISSING = "There is no ReportingProcessService registered in the ReportingProcessServiceProvider for this report type: ";
private static final Logger LOGGER = LoggerFactory.getLogger(ReportingProcessServiceProvider.class); private static final Logger LOGGER = LoggerFactory.getLogger(ReportingProcessServiceProvider.class);
private ApplicationContext applicationContext; private final Map<String, ReportingProcessService> reportingProcessServices;
Map<String, String> reportingProcessServices = null;
ReportingProcessServiceProvider() {
@Autowired
public ReportingProcessServiceProvider(List<ReportingProcessService> reportingProcessServices) {
var mapBuilder = ImmutableMap.<String, ReportingProcessService>builder();
for (ReportingProcessService s : reportingProcessServices) {
String[] reportTypes = s.getClass().getAnnotation(ReportService.class).type();
for (String type : reportTypes) {
mapBuilder.put(type, s);
}
LOGGER.info("Registered report service '{}' for type/s '{}'", s, reportTypes);
}
this.reportingProcessServices = mapBuilder.build();
} }
public ReportingProcessService findReportingProcessService(final String reportType) { public ReportingProcessService findReportingProcessService(final String reportType) {
if (this.reportingProcessServices.containsKey(reportType)) { return reportingProcessServices.getOrDefault(reportType, null);
return (ReportingProcessService) this.applicationContext.getBean(this.reportingProcessServices.get(reportType));
}
return null;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
this.initializeRegistry();
} }
public Collection<String> findAllReportingTypes() { public Collection<String> findAllReportingTypes() {
return this.reportingProcessServices.keySet(); return this.reportingProcessServices.keySet();
}
private void initializeRegistry() {
if (this.reportingProcessServices == null) {
this.reportingProcessServices = new HashMap<>();
final String[] reportServiceBeans = this.applicationContext.getBeanNamesForAnnotation(ReportService.class);
if (ArrayUtils.isNotEmpty(reportServiceBeans)) {
for (final String reportName : reportServiceBeans) {
LOGGER.info("Register report service '{}' ...", reportName);
final ReportService service = this.applicationContext.findAnnotationOnBean(reportName, ReportService.class);
try {
this.reportingProcessServices.put(service.type(), reportName);
} catch (final Throwable th) {
LOGGER.error("Unable to register reporting service '{}'!", reportName, th);
}
}
}
}
} }
} }
@@ -18,11 +18,28 @@
*/ */
package org.apache.fineract.infrastructure.report.service; package org.apache.fineract.infrastructure.report.service;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import org.apache.fineract.infrastructure.security.utils.SQLInjectionValidator;
public interface ReportingProcessService { public interface ReportingProcessService {
Response processRequest(String reportName, MultivaluedMap<String, String> queryParams); Response processRequest(String reportName, MultivaluedMap<String, String> queryParams);
default Map<String, String> getReportParams(final MultivaluedMap<String, String> queryParams) {
final Map<String, String> reportParams = new HashMap<>();
final Set<String> keys = queryParams.keySet();
for (final String k : keys) {
if (k.startsWith("R_")) {
String pKey = "${" + k.substring(2) + "}";
String pValue = queryParams.get(k).get(0);
SQLInjectionValidator.validateSQLInput(pValue);
reportParams.put(pKey, pValue);
}
}
return reportParams;
}
} }
@@ -471,7 +471,7 @@ public class ReportMailingJobWritePlatformServiceImpl implements ReportMailingJo
errorLog.append("Response object entity is not equal to ByteArrayOutputStream ---------- "); errorLog.append("Response object entity is not equal to ByteArrayOutputStream ---------- ");
} }
} else { } else {
errorLog.append("ReportingProcessService object is null ---------- "); errorLog.append(ReportingProcessServiceProvider.SERVICE_MISSING + reportType);
} }
} catch (Exception e) { } catch (Exception e) {
errorLog.append("The ReportMailingJobWritePlatformServiceImpl.generateReportOutputStream method threw an Exception: " + e errorLog.append("The ReportMailingJobWritePlatformServiceImpl.generateReportOutputStream method threw an Exception: " + e
@@ -45,7 +45,7 @@ public class ReportsTest extends IntegrationTest {
@Test @Test
void runExpectedPaymentsPentahoReportWithoutPlugin() { void runExpectedPaymentsPentahoReportWithoutPlugin() {
assertThat(fineract().reportsRun.runReportGetFile("Expected Payments By Date - Formatted", Map.of("R_endDate", "2013-04-30", assertThat(fineract().reportsRun.runReportGetFile("Expected Payments By Date - Formatted", Map.of("R_endDate", "2013-04-30",
"R_loanOfficerId", "-1", "R_officeId", "1", "R_startDate", "2013-04-16", "output-type", "PDF"), false)).hasHttpStatus(404); "R_loanOfficerId", "-1", "R_officeId", "1", "R_startDate", "2013-04-16", "output-type", "PDF"), false)).hasHttpStatus(503);
} }
@Test @Test