tutorial:dev:create_report

Reports module - Creating a report

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 example module

  • 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.

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<IdmFormAttributeDto> 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<IdmIdentityDto> 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<IdmIdentityDto> 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.

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

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.

Log in to application, go to the report agenda and click on add button on the right:

Congratulations, your own report is created.

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<IdmIdentityDto> identityRoles = mapper.readValue(
				attachmentManager.getAttachmentData(report.getData()), 
				new TypeReference<List<IdmIdentityDto>>(){});
		//
		// 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));
	}
}

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

@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.