====== Reports module - Creating a report ======
{{tag> report}}
[[devel:documentation:modules_rpt | Reports]] module was added in CzechIdM 7.6.0.
The aim of this tutorial is to show the way, how to **create** your own **report**. We will be creating a new report with all identities in ''xlsx'' format and we want to have report with enabled / disabled identities, so we want to search identities by their enabled status. This filter parameter will be optional - all identities will be in report, if activity is not selected.
Source codes for this tutorial can be found in the [[https://github.com/bcvsolutions/CzechIdMng/tree/develop/Realization/backend/example/src/main/java/eu/bcvsolutions/idm/example/report|example module]]
===== What do you need before you start =====
* You need to install CzechIdM 7.6.0 (and higher). We have CzechIdM installed for this tutorial on server ''http://localhost:8080/idm''.
* Create identity, which will have permission to create and read reports. We are using demo ''admin:admin'' identity.
===== 01 Create report =====
We need to implement ''ReportExecutor'' (~report), which generates output report data in json format. This data are saved as attachment. Report loads identities secured be read permission - identity, which will use this report has to have permission to read identities. This can be used e.g. for managers - they will see only identities, which can read => their subordinates. Identity without permission to read identities will see empty report only (if this identity will have permission to create reports). Loaded identities are streamed into temp file and after the end attachment is created and returned. Methods from ''AbstractReportExecutor'' super class are used.
@Component("exampleIdentityReportExecutor")
@Description("Identities - example")
public class IdentityReportExecutor extends AbstractReportExecutor {
public static final String REPORT_NAME = "example-identity-report"; // report ~ executor name
//
@Autowired private IdmIdentityService identityService;
/**
* Report ~ executor name
*/
@Override
public String getName() {
return REPORT_NAME;
}
/**
* Filter form attributes:
* - enabled / disabled identities
*/
@Override
public List getFormAttributes() {
IdmFormAttributeDto disabled = new IdmFormAttributeDto(
IdmIdentityFilter.PARAMETER_DISABLED,
"Disabled identities",
PersistentType.BOOLEAN);
// we want select box instead simple checkbox (null value is needed)
disabled.setFaceType(BaseFaceType.BOOLEAN_SELECT);
disabled.setPlaceholder("All identities or select ...");
return Lists.newArrayList(disabled);
}
@Override
protected IdmAttachmentDto generateData(RptReportDto report) {
// prepare temp file for json stream
File temp = getAttachmentManager().createTempFile();
//
try (FileOutputStream outputStream = new FileOutputStream(temp)) {
// write into json stream
JsonGenerator jGenerator = getMapper().getFactory().createGenerator(outputStream, JsonEncoding.UTF8);
try {
// json will be array of identities
jGenerator.writeStartArray();
// form instance has useful methods to transform form values
IdmFormInstanceDto formInstance = new IdmFormInstanceDto(report, getFormDefinition(), report.getFilter());
// initialize filter by given form - transform to multi value map
// => form attribute defined above will be automaticaly mapped to identity filter
IdmIdentityFilter filter = new IdmIdentityFilter(formInstance.toMultiValueMap());
// report extends long running task - show progress by count and counter lrt attributes
counter = 0L;
// find a first page of identities
Pageable pageable = PageRequest.of(0, 100, new Sort(Direction.ASC, IdmIdentity_.username.getName()));
do {
Page identities = identityService.find(filter, pageable, IdmBasePermission.READ);
if (count == null) {
// report extends long running task - show progress by count and counter lrt attributes
count = identities.getTotalElements();
}
boolean canContinue = true;
for (Iterator i = identities.iterator(); i.hasNext() && canContinue;) {
// write single identity into json
getMapper().writeValue(jGenerator, i.next());
//
// supports cancel report generating (report extends long running task)
++counter;
canContinue = updateState();
}
// iterate while next page of identities is available
pageable = identities.hasNext() && canContinue ? identities.nextPageable() : null;
} while (pageable != null);
//
// close array of identities
jGenerator.writeEndArray();
} finally {
// close json stream
jGenerator.close();
}
// save create temp file with array of identities in json as attachment
return createAttachment(report, new FileInputStream(temp));
} catch (IOException ex) {
throw new ReportGenerateException(report.getName(), ex);
} finally {
FileUtils.deleteQuietly(temp);
}
}
}
Generated output json data in attachment will be used in renderer.
===== 02 Create renderer =====
Renderer loads generated data prepared in previous step and transform them into ''xlsx'' format. This output will be provided to download by report module rest controller.
@Component("exampleIdentityReportRenderer")
@Description(AbstractXlsxRenderer.RENDERER_EXTENSION) // will be show as format for download
public class IdentityReportRenderer
extends AbstractXlsxRenderer {
@Override
public InputStream render(RptReportDto report) {
try {
// read json stream
JsonParser jParser = getMapper().getFactory().createParser(getReportData(report));
XSSFWorkbook workbook = new XSSFWorkbook();
XSSFSheet sheet = workbook.createSheet("Report");
// header
Row row = sheet.createRow(0);
Cell cell = row.createCell(0);
cell.setCellValue("Id");
cell = row.createCell(1);
cell.setCellValue("Username");
cell = row.createCell(2);
cell.setCellValue("First name");
cell = row.createCell(3);
cell.setCellValue("Last name");
cell = row.createCell(4);
cell.setCellValue("Disabled");
int rowNum = 1;
//
// json is array of identities
if (jParser.nextToken() == JsonToken.START_ARRAY) {
// write single identity
while (jParser.nextToken() == JsonToken.START_OBJECT) {
IdmIdentityDto identity = getMapper().readValue(jParser, IdmIdentityDto.class);
row = sheet.createRow(rowNum++);
cell = row.createCell(0);
cell.setCellValue(identity.getId().toString());
cell = row.createCell(1);
cell.setCellValue(identity.getUsername());
cell = row.createCell(2);
cell.setCellValue(identity.getFirstName());
cell = row.createCell(3);
cell.setCellValue(identity.getLastName());
cell = row.createCell(4);
cell.setCellValue(identity.isDisabled());
}
}
// close json stream
jParser.close();
//
// close and return input stream
return getInputStream(workbook);
} catch (IOException ex) {
throw new ReportRenderException(report.getName(), ex);
}
}
}
===== 03 Register renderer =====
We have report and renderer, but we need to register renterer with report => renderer can render this report. We will use ''RendererRegistrar'' to register renderer to report - we only implement this interface directly in renderer - see ''register'' method.
@Component("exampleIdentityReportRenderer")
@Description(AbstractXlsxRenderer.RENDERER_EXTENSION) // will be show as format for download
public class IdentityReportRenderer
extends AbstractXlsxRenderer
implements RendererRegistrar {
...
/**
* Register renderer to example report
*/
@Override
public String[] register(String reportName) {
if (IdentityReportExecutor.REPORT_NAME.equals(reportName)) {
return new String[]{ getName() };
}
return new String[]{};
}
}
The other way is to create standalone registrar class e.g. by extending ''AbstractRendererRegistrar''.
===== 04 Test on frontend =====
Log in to application, go to the report agenda and click on add button on the right:
{{:tutorial:dev:example-report.png|}}
Congratulations, your own report is created.
===== 05 Integration test =====
Create integration test for report.
public class IdentityReportExecutorIntegrationTest extends AbstractIntegrationTest {
@Autowired private TestHelper helper;
@Autowired private IdentityReportExecutor reportExecutor;
@Autowired private IdmIdentityService identityService;
@Autowired private AttachmentManager attachmentManager;
@Qualifier("objectMapper")
@Autowired private ObjectMapper mapper;
@Autowired private LoginService loginService;
@Autowired private IdentityReportRenderer xlsxRenderer;
@Before
public void before() {
// report checks authorization policies - we need to log in
loginService.login(new LoginDto(InitTestData.TEST_ADMIN_USERNAME, new GuardedString(InitTestData.TEST_ADMIN_PASSWORD)));
}
@After
public void after() {
super.logout();
}
@Test
@Transactional
public void testDisabledIdentity() throws IOException {
// prepare test identities
IdmIdentityDto identityOne = helper.createIdentity();
IdmIdentityDto identityDisabled = helper.createIdentity();
identityService.disable(identityDisabled.getId());
//
// prepare report filter
RptReportDto report = new RptReportDto(UUID.randomUUID());
report.setExecutorName(reportExecutor.getName());
IdmFormDto filter = new IdmFormDto();
IdmFormDefinitionDto definition = reportExecutor.getFormDefinition();
IdmFormValueDto disabled = new IdmFormValueDto(definition.getMappedAttributeByCode(IdmIdentityFilter.PARAMETER_DISABLED));
disabled.setValue(false);
filter.getValues().add(disabled);
filter.setFormDefinition(definition.getId());
report.setFilter(filter);
//
// generate report
report = reportExecutor.generate(report);
Assert.assertNotNull(report.getData());
List identityRoles = mapper.readValue(
attachmentManager.getAttachmentData(report.getData()),
new TypeReference>(){});
//
// test
Assert.assertTrue(identityRoles.stream().anyMatch(i -> i.equals(identityOne)));
Assert.assertFalse(identityRoles.stream().anyMatch(i -> i.equals(identityDisabled)));
//
attachmentManager.deleteAttachments(report);
}
@Test
@Transactional
public void testRenderers() {
helper.createIdentity();
//
// prepare report filter
RptReportDto report = new RptReportDto(UUID.randomUUID());
report.setExecutorName(reportExecutor.getName());
//
// generate report
report = reportExecutor.generate(report);
//
Assert.assertNotNull(xlsxRenderer.render(report));
}
}
===== Advanced =====
==== 06 Add @Enabled annotation ====
When your report and his renderer is created in custom module, which can be disabled, then you need to add ''@Enabled(YourModuleId)'' annotation on report and renderer classes.
@Enabled(ExampleModuleDescriptor.MODULE_ID)
@Component("exampleIdentityReportExecutor")
@Description("Identities - example")
public class IdentityReportExecutor extends AbstractReportExecutor {
...
}
@Enabled(ExampleModuleDescriptor.MODULE_ID)
@Component("exampleIdentityReportRenderer")
@Description(AbstractXlsxRenderer.RENDERER_EXTENSION) // will be show as format for download
public class IdentityReportRenderer
extends AbstractXlsxRenderer
implements RendererRegistrar {
...
}
==== 07 Send notification ====
@since 10.6.0
Notification with rendered report can be sent. All reports have configuration property ''Notification'' - send notification to given topic (by notification configuration) after report is successfuly created. Notification is sent to report creator (identity) by default. If report is scheduled, recipient has to be configured in notification configuration (report creator is system).
All report renderes (except default json renderer) are used and generated reports are attached to notification => e.g. email is sent with ''xlsx'' attachment included.