Using documents in code

When working with dynamic documents directly from code, the main component you need to use is a DynamicDocumentWorkspace. It manages a single document, and allows you to:

  • load a specific version of the document

  • create a new version

  • work with individual fields of the current document version

  • change the definition for the document version

  • import data from other documents

  • validating a document version

  • save the document version

You create a DynamicDocumentWorkspace component using the DynamicDocumentService.

DynamicDocumentWorkspace instances are stateful and not thread-safe, do not share or cache them.

Create a new document

You create a new document by creating a workspace from either a DynamicDefinition or DynamicDefinitionVersion for the document type. If you use a DynamicDefinition, the attached latest version will be used for creating your document.

@Autowired
private DynamicDocumentService documentService;

DynamicDocumentWorkspace workspace = documentService.createDocumentWorkspace( documentType ); (1)
workspace.setDocumentName( "My document" ); (2)
workspace.setFieldValue( "some.field", 123 ); (3)

DynamicDocumentVersion documentVersion = workspace.save(); (4)
1 Create a new document workspace for the document definition
2 Set a custom name for your document, a random string will be assigned if you do not set a name manually
3 Modify document fields, the value you provide must match with the specific Java type of the field
4 Call save() on the workspace to persist the document in the database, the return value is the DynamicDocumentVersion that has been created

Reading document fields

You can use the workspace to read values from a document, optionally with type conversion.

DynamicDocumentWorkspace workspace = documentService.createDocumentWorkspace( document ); (1)
String number = workspace.getFieldValue( "number.as.string" ); (2)
long count = workspace.getFieldValue( "number.as.string", Long.class ); (2)
1 Create a new workspace for the document, this will load the latest document version in the workspace
2 Retrieve field values. If you do not specify a second type parameter, the return type will be the exact java type from the document definition. If you do specify a type parameter, a ConversionService will be used to convert from the source type to your desired output type.

Update a document

Updating a document is done by creating a new version of that document.

DynamicDocumentWorkspace workspace = documentService.createDocumentWorkspace( document ); (1)
workspace.createNewVersion( true ); (2)
workspace.setFieldValue( "some.field", 123 );

DynamicDocumentVersion newDocumentversion = document.save();
1 Create a new workspace for the document (or specific document version)
2 Initialize a new document version, keeping the current field values. In this example the new version will use the latest document definition. If we had supplied false as method argument, a new document version would be created using the same definition version as the currently loaded document version.

Updating calculated fields

Calculated fields are by default only updated when saving a document. Manually triggering a recalculation can be done with:

workspace.updateCalculatedFields();

If you want to have real-time calculated fields (updated immediately), you should set:

workspace.setCalculatedFields( true );

This will recalculate a field whenever its value is fetched.

Enabling real-time calculated fields can have negative performance impact, use with care.

Update an existing document version

By default an exception will be thrown if you attempt to call save() on an already saved document version. This is a safeguard for accidentally re-writing version history.

If you want to update an existing document version, you must tell the workspace to allow it.

DynamicDocumentWorkspace workspace = documentService.createDocumentWorkspace( documentVersion ); (1)
workspace.setAllowVersionUpdates( true ); (2)
workspace.setFieldValue( "some.field", 123 );
DynamicDocumentVersion updateVersion = document.save(); (3)
workspace.setAllowVersionUpdates( false ); (4)
1 Create a new workspace based on the specific document version
2 Configure the workspace to allow saving the current - existing - version
3 The save() method will now return the same, but updated document version
4 Reset the workspace behaviour if you want to keep using it

Using calculated fields

DynamicFormsModule supports the use of calculated fields, which are read-only fields whose value depends on the specified formula. These fields will by default only be updated when the document itself is saved.

Aside of updating calculated fields when the value is saved, those fields can immediately be recalculated for the entire document or be calculated whenever the field is fetched.

In the following example, we’ll assume our definition has the following fields:

Fields present in the definition
content:
- id: price
  type: number
- id: priceWithPaxes
  type: currency(EUR, 2)
  formula: $(price) * 1.25
Fetching a calculated field
DynamicDocumentWorkspace workspace = documentService.createDocumentWorkspace( definition );

// set the value of the field that we have used in our formula
workspace.setFieldValue( "price", 100 );
// fetch the calculated field, no value will be present.
workspace.getFieldValue( "priceWithTaxes", BigDecimal.class );

// update all calculated fields in the document
workspace.updateCalculatedFields();
// fetch the calculated field, the value will be 125.
workspace.getFieldValue( "priceWithTaxes", BigDecimal.class );

// always calculate the value when a calculated field is retrieved
workspace.setCalculatedFieldsEnabled( true );

workspace.setFieldValue( "price", 200 );
// fetch the calculated value, the value will be 250.
workspace.getFieldValue( "priceWithTaxes", BigDecimal.class );

Validating a document

Validating a document is done by calling the validate() method on the DynamicDocumentWorkspace. Validation will execute the different field validators and report violations in an Errors object.

Validation is always a manual step, how possible errors are handled afterwards is up to the caller.

DynamicDocumentWorkspace workspace = documentService.createDocumentWorkspace( documentVersion );
Errors validationErrors = workspace.validate();

if ( !validationErrors.hasErrors() ) {
    // save if the document data is valid
    workspace.save();
}
else {
    // report violations back to the user
}

In a web scenario where you are using a DynamicDocumentWorkspace as a WebDataBinder target, you can also call validate() with an existing BindingResult.

@ModelAttribute
DynamicDocumentWorkspace document() {
  return documentService.createDocumentWorkspace( ... );
}

@RequestMapping
String updateDocument( @ModelAttribute DynamicDocumentWorkspace document, BindingResult bindingResult ) {
  document.validate( bindingResult );

  if ( !bindingResult.hasErrors() ) {
   ...
  }

  ...
}

Importing document data

DynamicDocumentWorkspace has several methods that allow bulk setting of field values on a document. Bulk modifying methods do not throw exceptions, but return a DynamicDocumentDataLoader.Report object instead. The latter holds a list of all fields that have been updated, as well as the list of field errors that have occurred.

DynamicDocumentWorkspace workspace = documentService.createDocumentWorkspace( document );
workspace.createNewVersion( true );
DynamicDocumentDataLoader.Report report = workspace.importFields( dataToImport );

if ( report.hasErrors() ) {
    // the report object will contain an entry for every field that could not be set
    // containing details like the full path to the field, reason for failure and value that was set
}

There are several importFieldsXX methods available on a DynamicDocumentWorkspace. Please investigate the API for all options.

Using a data loader

If you want to perform multiple document updates, with possible type conversion, you can also use a DynamicDocumentDataLoader. This helper allows you to perform multiple actions sequentially, and retrieve the report separately when done. Additionally it allows for some configuration options regarding error reporting and type conversion.

Creating a data loader for your current document version
DynamicDocumentWorkspace workspace = documentService.createDocumentWorkspace( document );
DynamicDocumentDataLoader dataLoader = workspace.createDataLoader(); (1)
dataLoader.setFieldValue( "user.name", "john.doe" ); (2)
dataLoader.setFieldValue( "user.email", "john.doe@domain.com" );

DynamicDocumentDataLoader.Report report = dataLoader.getReport(); (3)
1 Create a data loader preconfigured for the current document version. Type conversion will be applied using a ConversionService and all errors will be added to the report instead of throwing the exceptions.
2 Perform document updates using the data loader.
3 When done, get the report and determine next steps.

Exporting document data

You can convert a single document version to another format by calling the exportFields() method on the workspace. This will convert the actual fields data of the document version to the format you’ve specified.

The export method signature is exportFields( String format ). For exports to work there must be a DynamicDocumentDataMapper registered for that format in the DynamicDocumentDataMapperFactory.
DynamicDocumentWorkspace workspace = documentService.createDocumentWorkspace( document );
String json = workspace.exportFields( DynamicDataObjectMapper.JSON ); (1)
1 Export the fields in the format specified. The return value depends on the format you used. In case of JSON we get a String returned.

Re-using a workspace

A DynamicDocumentWorkspace is always attached to a single document (version) at any point in time. You can change the document loaded or create a new document using the same workspace however. This will re-initialize the workspace for the new configuration.

Re-using the same workspace is a good solution when you want to perform sequential processing of multiple documents (eg. exports). A workspace uses a definition cache by default. Loading a new document in the same workspace often has better performance than creating a new workspace.

DynamicDocumentWorkspace workspace = documentService.createDocumentWorkspace( documentDefinition );

for ( DynamicDocumentVersion document : documents ) {
  workspace.loadDocument( document );
  String json = workspace.exportFields( DynamicDataObjectMapper.JSON );
  // write the json data to file
}

Performing bulk operations on documents

DynamicDocumentWorkspaces are always created through the DynamicDocumentService. These workspaces have their own definition cache, so that if a workspace is reused for various documents, a performance boost may occur for documents that use the same definition. When performing bulk operations on documents, sequential processing will become insufficient and using more than one workspace will require additional fetching and conversion of the same definitions.

Through the DynamicDocumentService, it is possible to create a CachingDynamicDocumentWorkspaceFactory, which has it’s own cache for definitions. All workspaces created through the caching workspace factory will then reuse that same cache, and as such cause a significant performance boost when creating multiple workspaces for documents with the same definition.

CachingDynamicDocumentWorkspaceFactory workspaceFactory = documentService.createCachingDocumentWorkspaceFactory();

documents.stream()
    .map(workspaceFacory::createDocumentWorkspace) (1)
    .forEach(ws -> {
        ws.exportFields(DynamicDataObjectMapper.JSON);
        // write the json data to file
        }
    )

}
1 Each document is converted to a workspace by the workspace factory. Definitions that are reused by various workspaces are converted once to a DynamicDocumentDefinition, instead of for every workspace that is created.