Passing a list of arguments to a Jasper Reports query

5

I'm redoing reports in Jasper Reports here from the company. I am migrating to have an agnostic reporting service the structure of the report's SQL query. Before, the system was deeply aware of how such a query was made, but this ran into some of the limitations I have.

I need to query with a IN clause; for example, know the orders going to the US, Italy and Germany.

  

A case similar to what TIBCO itself put in documentation of parameters in queries

So my query would be this:

SELECT * FROM ORDERS WHERE SHIPCOUNTRY IN ('USA','Italy','Germany')

In JRXML, I had the query like this:

<parameter name="P_WHERE_CLAUSE" class="java.lang.String"/>
<queryString>
    <![CDATA[
        SELECT * FROM ORDERS $P!{P_WHERE_CLAUSE}
    ]]>
</queryString>

And in the report call, the old Java code looks like this:

// public class RelatorioDownloadServlet extends HttpServlet {...
// ...
// protected void doPost(HttpServletRequest request,
//    HttpServletResponse response) throws ServletException, IOException { ...

String whereClause = " ";

String shippingCountries = request.getParameter("shipping");
if (shippingCountries  != null && !shippingCountries.isEmpty()) {
  whereClause = " WHERE SHIPCOUNTRY IN (" + shippingCountries + ")";
}

HashMap<String, Object> parametros = new HashMap<String, Object>();
parametros.put("P_WHERE_CLAUSE", whereClause );
String templatePath = servletContext.getRealPath("shipping.jasper");
byte[] bytes;
try (Connection connection = getConnection()) {
  bytes = JasperRunManager.runReportToPdf(templatePath, parametros, connection);
}
ServletOutputStream servletOutputStream = response.getOutputStream();
response.setContentType("application/pdf");
response.setContentLength(bytes.length);
servletOutputStream.write(bytes, 0, bytes.length);
servletOutputStream.flush();

But this model does not satisfy me. I build the (partially) external query to the JRXML itself and the execution of this query successfully depends on the caller to know the report in depth a priori.

As I said before, I am moving to a generic service to receive the parameters of an abstract caller, and then mount the report. I'm doing this service in Springboot, for other reports it does the following:

@RestController
public class RelatorioController {

  @Autowired HikariDataSource dataSource;
  /**
   * CompileJasperRecursive é uma classe minha, ela possui dois métodos principais:
   * - getJasperReport: retorna um objeto JasperReport baseado no .jasper; não existindo o .jasper, compila-o chamando !compileIfShould"
   * - compileIfShould: verifica se deve gerar um novo .jasper baseado no .jrxml; também tenta procurar recursivamente por subrelatórios e compilá-los com "compileIfShould"
   *
   * não vejo esse cara como o problema, ele só faz isso
   */
  @Autowired CompileJasperRecursive compiler;

  @RequestMapping(path = "/{relat}.pdf", method = RequestMethod.POST, produces = "application/pdf")
  public void requisicaoPdf(@PathVariable("relat") String relat,
           @RequestParam Map<String, String> params,
           HttpServletRequest req, HttpServletResponse resp) throws JRException, SQLException, IOException {
    compilaRelatPDF(relat, params, resp.getOutputStream());
    resp.setHeader("Content-Disposition", "attachment; filename=" + relat + ".pdf");
  }

  private void compilaRelatPDF(String relat, Map<String, String> params, OutputStream outputStream) throws JRException, SQLException, IOException {
    JasperReport jasperReport = compiler.getJasperReport("/path/relatorios/", relat + ".jrxml");
    HashMap<String, Object> map = new HashMap<>();
    map.putAll(params);
    map.put("P_CAMINHO_SUB_RELAT", "/path/relatorios/"); // ocasional caminho dos subrelatórios

    JasperPrint print = fillReport(jasperReport, map);
    JRPdfExporter exporter = new JRPdfExporter();

    exporter.setExporterInput(new SimpleExporterInput(print));
    exporter.setExporterOutput(new SimpleOutputStreamExporterOutput(outputStream));

    SimplePdfReportConfiguration reportConfig = new SimplePdfReportConfiguration();
    reportConfig.setSizePageToContent(true);
    reportConfig.setForceLineBreakPolicy(false);

    SimplePdfExporterConfiguration exportConfig = new SimplePdfExporterConfiguration();
    exportConfig.setMetadataAuthor("Jeff Coelho Quesado");
    exportConfig.setEncrypted(false);
    exportConfig.setAllowedPermissionsHint("PRINTING");

    exporter.setConfiguration(reportConfig);
    exporter.setConfiguration(exportConfig);
    exporter.exportReport();
  }
}

In this scheme, without worrying about where the request comes from or the parameters a priori , I can mount all the reports that do not need lists (passing the parameters as String). But I'm still not in the mood to send those lists.

Looking at the a>, apparently the $X expansion does what I need. My query would look like this:

SELECT * FROM ORDERS WHERE $X{IN, SHIPCOUNTRY, myCountries}

But I do not know how to declare the parameters in JRXML. In case, would it look something like this?

<parameter name="myCountries" class="java.util.Collection"/>
<queryString>
    <![CDATA[
        SELECT * FROM ORDERS WHERE $X{IN, SHIPCOUNTRY, myCountries}
    ]]>
</queryString>

And how would I handle the data in my parameter request? I would like to handle receiving application/x-www-form-urlencoded to avoid any problems with SOP, but I have not yet tested passing vectors through this format.

    
asked by anonymous 13.04.2018 / 22:16

1 answer

1

The .jrxml part is correct. The use of $X{} expansion was done properly. But the receipt of the form was not done properly.

In the javadoc of @RequestParam has the following:

  

If the method parameter is Map<String, String> or MultiValueMap<String, String> and a parameter name is not specified, then the parameter map is populated with all request parameter names and values.

Free translation:

  

If the method parameter is Map<String, String> or MultiValueMap<String, String> and a parameter name is not specified, then the map parameter will be populated with all the values of the request parameters.

When you try to use Map , Spring Boot interprets as a normal map and therefore will only have a single value for the key. It's as if the requisition is arriving.

myCountries=USA
myCountries=Italy
myCountries=Germany

And Spring did the following:

Map<String, String> params = new HashMap<>();
params.put("myCountries", "USA");
params.put("myCountries", "Italy");
params.put("myCountries", "Germany");

So when I do params.get("myCountries") I get "Germany" response, so I can not pass the query parameters properly.

When using MultiValueMap<String, String> , we are working with a multimap , which in the specific case is a specialization of the Map<String,List<String>> interface . So if I were to go over the arguments directly to Jasper to work, I would have to adapt all my report parameters to be java.util.List , even though I guarantee that it contains a maximum of only one string.

So how to treat? I made a convention that if my argument is multivalued then it should end with [] . In HTML, it would look something like this:

<input type="text" name="myCountries[]" placeholder="primeiro país"></input>
<input type="text" name="myCountries[]" placeholder="segundo país"></input>

Then the values would be appended verbatim into the Jasper parameters by removing the square brackets. The remaining values would be added only the first element:

params.forEach((k, v) -> {
  if (k.endsWith("[]")) {
    map.put(k.substring(0, k.length() - 2), v);
  } else {
    map.put(k, v.get(0));
  }
});

The idea of doing this pattern of names came after reading that article ; although it is in PHP, I ended up using this standardization in the end. It also comes in handy that many of the articles that appear on Google about sending vectors through HTML forms is in PHP, so it will be less work for the team to try to internalize this.

No special treatment has been made to pass arguments as positional vectors (ie, form elements have name="myCountries[1]" , name="myCountries[2]" etc) or associative (in case you use name="form[address]" or name="form[user]" ). So, although it is a solution that deals with most of the cases that I come across in the company, it is not yet the general solution.

Summary

No .jrxml :

<parameter name="myCountries" class="java.util.Collection"/>
<queryString>
    <![CDATA[
        SELECT * FROM ORDERS WHERE $X{IN, SHIPCOUNTRY, myCountries}
    ]]>
</queryString>

In form :

<input type="text" name="myCountries[]" placeholder="parâmtro multivalorado"></input>
<input type="text" name="myCountries[]" placeholder="parâmtro multivalorado"></input>
<input type="text" name="otherParam" placeholder="parâmtro escalar"></input>

On the server side:

@RestController
public class RelatorioController {

  @Autowired HikariDataSource dataSource;
  @Autowired CompileJasperRecursive compiler;

  @RequestMapping(path = "/{relat}.pdf", method = RequestMethod.POST, produces = "application/pdf")
  public void requisicaoPdf(@PathVariable("relat") String relat,
           @RequestParam MultiValueMap<String, String> params params,
           HttpServletRequest req, HttpServletResponse resp) throws JRException, SQLException, IOException {
    compilaRelatPDF(relat, params, resp.getOutputStream());
    resp.setHeader("Content-Disposition", "attachment; filename=" + relat + ".pdf");
  }

  private void compilaRelatPDF(String relat, MultiValueMap<String, String> params, OutputStream outputStream) throws JRException, SQLException, IOException {
    JasperReport jasperReport = compiler.getJasperReport("/path/relatorios/", relat + ".jrxml");
    HashMap<String, Object> map = new HashMap<>();

    params.forEach((k, v) -> {
      if (k.endsWith("[]")) {
        map.put(k.substring(0, k.length() - 2), v);
      } else {
        map.put(k, v.get(0));
      }
    });
    map.put("P_CAMINHO_SUB_RELAT", "/path/relatorios/"); // ocasional caminho dos subrelatórios

    JasperPrint print = fillReport(jasperReport, map);
    JRPdfExporter exporter = new JRPdfExporter();

    exporter.setExporterInput(new SimpleExporterInput(print));
    exporter.setExporterOutput(new SimpleOutputStreamExporterOutput(outputStream));

    SimplePdfReportConfiguration reportConfig = new SimplePdfReportConfiguration();
    reportConfig.setSizePageToContent(true);
    reportConfig.setForceLineBreakPolicy(false);

    SimplePdfExporterConfiguration exportConfig = new SimplePdfExporterConfiguration();
    exportConfig.setMetadataAuthor("Jeff Coelho Quesado");
    exportConfig.setEncrypted(false);
    exportConfig.setAllowedPermissionsHint("PRINTING");

    exporter.setConfiguration(reportConfig);
    exporter.setConfiguration(exportConfig);
    exporter.exportReport();
  }
  • Scalar Values Only
  • Unable to send value in specific position
  • Associative vector not treated
23.04.2018 / 01:30