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.

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

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

DynamicDocumentWorkspace s 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.