This example demonstrates how to configure Cordra to behave like a Registry of People. To showcase that such a registry could be used to manage transactions related to people, such as loans taken by them, we also show how loan digital objects could be managed and associated with person digital objects. This example is a basic starting point for a potential application.
In addition to showing how to configure Cordra, we will demonstrate how to interact with Cordra using the DOIP Java SDK.
This sample application supports the following narrative:
Person
digital objects are added to the registry by administrators. The registry can track various biographical
information, as well as information related to official documents.
When creating or updating a person digital object, the request should fail if it would create a duplicate Person object. Duplicates are flagged if either the government issued ids match, or the last name and the date of birth match.
Loan
digital objects can be added, including information about its payment status as well as borrowers.
There are operations for getting a list of all households, getting a list of all living persons with expired fingerprints, and getting a list of all living persons with defaulted loans.
Information is not publicly accessible. User accounts can be created and added to one of two groups: an admin group that has read and write access to all records, and a users group that has read-only access.
If you would like to follow along without having to manually create the objects described below, you can be download a
file containing sample objects here
. Once downloaded, you can
then load this information into Cordra using the Cordra UI. Sign into Cordra as admin
and select the
Admin->Types dropdown menu. Click the “Load from file” button. In the dialog that pops up, select the file you
downloaded and check the box to delete existing objects. Click “Load” to import the types into Cordra.
These sample objects are for testing only. They include users “admin1”, “admin2”, “user1”, and “user2”, all with the default password of “password”.
The schemas for the Person and Loan types of digital objects are show below. Notice that identifiers of digital objects are flagged to be auto-generated. The various timestamp attributes in the objects are also flagged to be auto-populated.
Person Schema:
{
"type": "object",
"title": "Person",
"required": [
"id",
"name",
"birth",
"gender",
"address",
"issuedIds"
],
"properties": {
"id": {
"type": "string",
"cordra": {
"type": {
"autoGeneratedField": "handle"
}
}
},
"name": {
"type": "object",
"title": "Name",
"required": [
"first",
"last"
],
"properties": {
"last": {
"type": "string",
"title": "Surname",
"cordra": {
"preview": {
"showInPreview": true,
"isPrimary": true
}
}
},
"first": {
"type": "string",
"title": "First Name",
"cordra": {
"preview": {
"showInPreview": true
}
}
},
"middle": {
"type": "string",
"title": "Middle Name",
"cordra": {
"preview": {
"showInPreview": true
}
}
}
}
},
"birth": {
"type": "object",
"title": "Birth Information",
"required": [
"date"
],
"properties": {
"date": {
"type": "string",
"pattern": "^[1-2]{1}[0-9]{7}$",
"title": "Date of Birth (YYYYMMDD)"
},
"certificate": {
"type": "object",
"title": "Birth Certificate",
"required": [
"id",
"source"
],
"properties": {
"id": {
"type": "string",
"title": "Certificate ID"
},
"source": {
"type": "string",
"title": "Certificate Source"
}
}
}
}
},
"death": {
"type": "object",
"title": "Death Information",
"required": [
"date"
],
"properties": {
"date": {
"type": "string",
"pattern": "^[1-2]{1}[0-9]{7}$",
"title": "Date of Death (YYYYMMDD)"
},
"certificate": {
"type": "object",
"title": "Death Certificate",
"required": [
"id",
"source"
],
"properties": {
"id": {
"type": "string",
"title": "Certificate ID"
},
"source": {
"type": "string",
"title": "Certificate Source"
}
}
}
}
},
"gender": {
"type": "string",
"title": "Gender",
"enum": [
"female",
"male",
"other"
]
},
"address": {
"type": "object",
"title": "Address",
"required": [
"line1",
"line2"
],
"properties": {
"line1": {
"type": "string",
"title": "Line 1"
},
"line2": {
"type": "string",
"title": "Line 2"
},
"line3": {
"type": "string",
"title": "Line 3"
}
}
},
"issuedIds": {
"type": "array",
"title": "Government Issued Ids",
"format": "table",
"uniqueItems": true,
"minItems": 1,
"items": {
"type": "object",
"required": [
"type",
"id"
],
"properties": {
"id": {
"type": "string",
"title": "ID"
},
"type": {
"type": "string",
"title": "ID Type"
}
}
}
},
"fingerprints": {
"type": "object",
"title": "Finger Print External Reference",
"required": [
"id",
"source",
"lastCapturedDate"
],
"properties": {
"id": {
"type": "string",
"title": "Certificate ID"
},
"source": {
"type": "string",
"title": "Certificate Source"
},
"lastCapturedDate": {
"type": "string",
"pattern": "^[1-2]{1}[0-9]{7}$",
"title": "Last Captured Date (YYYYMMDD)"
}
}
},
"recordCreatedOn": {
"type": "string",
"title": "Record Creation Date",
"cordra": {
"type": {
"autoGeneratedField": "creationDate"
}
}
},
"recordModifiedOn": {
"type": "string",
"title": "Record Modification Date",
"cordra": {
"type": {
"autoGeneratedField": "modificationDate"
}
}
},
"recordCreatedBy": {
"type": "string",
"title": "Record Created By",
"cordra": {
"type": {
"autoGeneratedField": "createdBy"
}
}
},
"recordModifiedBy": {
"type": "string",
"title": "Record Modified By",
"cordra": {
"type": {
"autoGeneratedField": "modifiedBy"
}
}
}
}
}
Loan Schema:
{
"type": "object",
"title": "Loan",
"required": [
"id",
"status",
"lender",
"start",
"end",
"borrowers",
"amount"
],
"properties": {
"id": {
"type": "string",
"cordra": {
"type": {
"autoGeneratedField": "handle"
}
}
},
"lender": {
"type": "string",
"title": "Lender"
},
"start": {
"type": "string",
"pattern": "^[1-2]{1}[0-9]{7}$",
"title": "Term Start Date (YYYYMMDD)"
},
"end": {
"type": "string",
"pattern": "^[1-2]{1}[0-9]{7}$",
"title": "Term End Date (YYYYMMDD)"
},
"status": {
"type": "string",
"title": "Status",
"enum": [
"defaulted",
"performing",
"paid"
]
},
"amount": {
"type": "object",
"title": "Loan Amount",
"required": [
"amount",
"currency"
],
"properties": {
"amount": {
"type": "number",
"minimum": 0,
"title": "Amount"
},
"currency": {
"type": "string",
"title": "Currency"
}
}
},
"borrowers": {
"type": "array",
"title": "Borrowers",
"uniqueItems": true,
"minItems": 1,
"items": {
"type": "string",
"title": "Borrower",
"cordra": {
"type": {
"handleReference": {
"types": [
"Person"
]
}
}
}
}
},
"recordCreatedOn": {
"type": "string",
"title": "Record Creation Date",
"cordra": {
"type": {
"autoGeneratedField": "creationDate"
}
}
},
"recordModifiedOn": {
"type": "string",
"title": "Record Modification Date",
"cordra": {
"type": {
"autoGeneratedField": "modificationDate"
}
}
},
"recordCreatedBy": {
"type": "string",
"title": "Record Created By",
"cordra": {
"type": {
"autoGeneratedField": "createdBy"
}
}
},
"recordModifiedBy": {
"type": "string",
"title": "Record Modified By",
"cordra": {
"type": {
"autoGeneratedField": "modifiedBy"
}
}
}
}
}
We will use the beforeSchemaValidation
lifecycle hook to de-duplicate persons
on create and update. Additionally, we will add two static type methods to the Person type for
getting households and persons with expired fingerprints.
Person Javascript:
const cordra = require('cordra');
let prefix = cordra.get("design").content.handleMintingConfig.prefix;
if (!prefix) prefix = 'test';
exports.beforeSchemaValidation = beforeSchemaValidation;
exports.staticMethods = {};
exports.staticMethods[prefix+'/Op.GetHouseholdMembers'] = getHouseholdMembers;
exports.staticMethods[prefix+'/Op.GetExpiredFingerPrints'] = getExpiredFingerPrints;
function beforeSchemaValidation(obj, context) {
// dedupe and rejects creations or updates based on the following criteria:
// - If the government issued ids match.
// - If the last name and the date of birth match
let query = '+type:Person ';
query += `+((+/name/last:"${obj.content.name.last}" +/birth/date:"${obj.content.birth.date}")`;
obj.content.issuedIds.forEach(id => {
query += ` OR (+/issuedIds/_/id:"${id.id}" +/issuedIds/_/type:"${id.type}")`;
});
query += ')';
const res = cordra.search(query);
if (res.size > 0) {
throw "Either Issued Id or Last Name + Birthday is duplicated.";
}
return obj;
}
function getHouseholdMembers(context) {
// Returns groups of Person Ids consisting of people who are still alive and from the same address.
const households = {};
const livingPersons = cordra.search('+type:"Person" -/death/date:[* TO *]');
livingPersons.results.forEach(person => {
const addressHash = getAddressHash(person.content.address);
if (addressHash) {
if (!households.hasOwnProperty(addressHash)) {
households[addressHash] = [];
}
households[addressHash].push(person.id);
}
});
return Object.keys(households).map(i => households[i]);
}
function getAddressHash(address) {
if (!address) return null;
const lines = address.line1 + address.line2 + address.line3;
return lines.hashCode();
}
function getExpiredFingerPrints(context) {
// Lists all Ids of living people that have lastCapturedDate before the given expirationDate.
let expirationDate = context.params.expirationDate;
if (!expirationDate) expirationDate = '*';
const persons = cordra.search('+type:"Person" -/death/date:[* TO *] +/fingerprints/lastCapturedDate:[* TO ' + expirationDate + '}');
return persons.results.map(p => p.id);
}
Note: The de-duplication function shown is best-effort using search and only checking a few properties. A production system would need to be more thorough (to disallow any simultaneous create/update requests that might be potential duplicates), but this is sufficient for demonstration purposes.
We also need to add a static type method to the Loan type, for getting a list of persons with defaulted loans.
Loan Javascript:
const cordra = require('cordra');
let prefix = cordra.get("design").content.handleMintingConfig.prefix;
if (!prefix) prefix = 'test';
exports.staticMethods = {};
exports.staticMethods[prefix+'/Op.GetDefaulters'] = getDefaulters;
function getDefaulters(context) {
const livingPersons = cordra.search('+type:"Person" -/death/date:[* TO *]');
const personIds = livingPersons.results.map(p => p.id);
const defaultedLoans = cordra.search('+type:"Loan" +/status:"defaulted"');
const defaultedBorrowers = [];
defaultedLoans.results.forEach(loan => {
loan.content.borrowers.forEach(borrower => {
if (personIds.indexOf(borrower) !== -1) {
defaultedBorrowers.push(borrower);
}
})
});
return defaultedBorrowers.filter(b => personIds.indexOf(b) !== -1);
}
We will use the DOIP Java SDK to demonstrate how to use the system. Although not shown here, the Cordra UI or any other Cordra client library may be used to perform equivalent actions.
All identifiers shown are the randomly-generated identifiers included in the sample objects. If you have created your own objects instead of importing the sample ones, you will need to substitute these values with the appropriate identifiers in your local system.
Before proceeding, be sure you have configured your Java project with doip-sdk jar file on the classpath. You can find instructions for doing so here.
First, we must create a DOIP client Java instance, along with authentication and service information for the client to use.
String serviceId = "test/service";
DoipClient client = new DoipClient();
ServiceInfo serviceInfo = new ServiceInfo(serviceId, "localhost", 9000);
AuthenticationInfo adminAuthInfo = new PasswordAuthenticationInfo("admin1", "password");
Change the authentication and service information as appropriate to your system. You may prefer to use a PKI based setup.
Create a Person:
DigitalObject personDobj = new DigitalObject();
personDobj.type = "Person";
JsonObject personContent = new JsonObject();
JsonObject name = new JsonObject();
name.addProperty("first", "Jane");
name.addProperty("last", "Doe");
personContent.add("name", name);
personContent.addProperty("gender", "female");
JsonObject address = new JsonObject();
address.addProperty("line1", "123 Elm St");
address.addProperty("line2", "Apt A");
address.addProperty("line3", "Charlottesville, VA 22902");
personContent.add("address", address);
JsonObject birth = new JsonObject();
birth.addProperty("date", "19330202");
personContent.add("birth", birth);
JsonObject death = new JsonObject();
death.addProperty("date", "20200115");
personContent.add("death", death);
JsonArray issueIds = new JsonArray();
JsonObject id = new JsonObject();
id.addProperty("type", "license");
id.addProperty("id", "123456-789");
issueIds.add(id);
personContent.add("issuedIds", issueIds);
JsonObject prints = new JsonObject();
prints.addProperty("id", "qwerty123456");
prints.addProperty("source", "govt");
prints.addProperty("lastCapturedDate", "19750306");
personContent.add("fingerprints", prints);
personDobj.setAttribute("content", personContent);
personDobj = client.create(personDobj, adminAuthInfo, serviceInfo);
System.out.println("Created Person: " + personDobj.id);
Create a Loan:
DigitalObject loanDobj = new DigitalObject();
loanDobj.id = "test/a4a33fed1bba7e8752ea";
loanDobj.type = "Loan";
JsonObject loanContent = new JsonObject();
loanContent.addProperty("lender", "ABC Bank");
loanContent.addProperty("start", "20180101");
loanContent.addProperty("end", "20220101");
loanContent.addProperty("status", "performing");
JsonObject amount = new JsonObject();
amount.addProperty("amount", 10000);
amount.addProperty("currency", "USD");
loanContent.add("amount", amount);
JsonArray borrowers = new JsonArray();
borrowers.add(personDobj.id);
loanContent.add("borrowers", borrowers);
loanDobj.setAttribute("content", loanContent);
loanDobj = client.create(loanDobj, adminAuthInfo, serviceInfo);
System.out.println("\nCreated Loan: " + loanDobj.id);
Retrieve:
loanDobj = client.retrieve(loanDobj.id, adminAuthInfo, serviceInfo);
Update an object:
loanDobj.attributes.get("content").getAsJsonObject().addProperty("lender", "XYZ Bank");
loanDobj = client.update(loanDobj, adminAuthInfo, serviceInfo);
Delete: See below, as these code blocks are organized such that they can be transferred in order into a Java method.
Search:
QueryParams queryParams = new QueryParams(0, 50);
String query = "*:*";
try (SearchResults<DigitalObject> results = client.search(serviceId, query, queryParams, adminAuthInfo, serviceInfo)) {
System.out.println("\nSearch: Found " + results.size() + " objects:");
for (DigitalObject result : results) {
System.out.println("\t" + result.id + ": " + result.type);
}
}
List Operations:
List<String> ops = client.listOperations("test/8c41ae88467fe5bbad09", adminAuthInfo, serviceInfo);
System.out.println("Operations available on Person schema:");
ops.forEach(System.out::println);
ops = client.listOperations("test/36c71e41f2d1e438ced9", adminAuthInfo, serviceInfo);
System.out.println("\nOperations available on Loan schema:");
ops.forEach(System.out::println);
Op.GetExpiredFingerPrints:
JsonObject params = new JsonObject();
params.addProperty("expirationDate", "20200101");
try (DoipClientResponse resp = client.performOperation("test/8c41ae88467fe5bbad09", "test/Op.GetExpiredFingerPrints", adminAuthInfo, null, params, serviceInfo)) {
if (resp.getStatus().equals(DoipConstants.STATUS_OK)) {
try (InDoipMessage in = resp.getOutput()) {
InDoipSegment firstSegment = InDoipMessageUtil.getFirstSegment(in);
if (firstSegment == null) {
throw new DoipException("Missing first segment in response");
}
System.out.println("\nOp.GetExpiredFingerPrints response:");
Gson gson = new GsonBuilder().setPrettyPrinting().create();
System.out.println(gson.toJson(firstSegment.getJson()));
}
} else {
throw DoipClient.doipExceptionFromDoipResponse(resp);
}
}
Op.GetHouseholdMembers:
try (DoipClientResponse resp = client.performOperation("test/8c41ae88467fe5bbad09", "test/Op.GetHouseholdMembers", adminAuthInfo, null, serviceInfo)) {
if (resp.getStatus().equals(DoipConstants.STATUS_OK)) {
try (InDoipMessage in = resp.getOutput()) {
InDoipSegment firstSegment = InDoipMessageUtil.getFirstSegment(in);
if (firstSegment == null) {
throw new DoipException("Missing first segment in response");
}
System.out.println("Op.GetHouseholdMembers response:");
Gson gson = new GsonBuilder().setPrettyPrinting().create();
System.out.println(gson.toJson(firstSegment.getJson()));
}
} else {
throw DoipClient.doipExceptionFromDoipResponse(resp);
}
}
Op.GetDefaulters:
try (DoipClientResponse resp = client.performOperation("test/36c71e41f2d1e438ced9", "test/Op.GetDefaulters", adminAuthInfo, null, serviceInfo)) {
if (resp.getStatus().equals(DoipConstants.STATUS_OK)) {
try (InDoipMessage in = resp.getOutput()) {
InDoipSegment firstSegment = InDoipMessageUtil.getFirstSegment(in);
if (firstSegment == null) {
throw new DoipException("Missing first segment in response");
}
System.out.println("Op.GetDefaulters response:");
Gson gson = new GsonBuilder().setPrettyPrinting().create();
System.out.println(gson.toJson(firstSegment.getJson()));
}
} else {
throw DoipClient.doipExceptionFromDoipResponse(resp);
}
}
Binary files can be attached to Person or Loan objects as payloads. Payloads can be files such as pictures, scans of certificates or government issued ids, or documents.
For example, to add a payload to an existing Loan object:
DigitalObject loanDobj = client.retrieve("test/a4a33fed1bba7e8752ea", adminAuthInfo, serviceInfo);
Element payload = new Element();
payload.type = "application/pdf";
payload.id = "contract.pdf";
payload.in = Files.newInputStream(Paths.get("contract.pdf")); // Change this to a file on your local system
payload.attributes = new JsonObject();
payload.attributes.addProperty("filename", "contract.pdf");
loanDobj.elements = new ArrayList<>();
loanDobj.elements.add(payload);
client.update(loanDobj, adminAuthInfo, serviceInfo);
If you try to create a duplicate person, the beforeSchemaValidation
lifecycle hook will block the change. For
example, running this code (which duplicates one of the sample persons):
DigitalObject personDobj = new DigitalObject();
personDobj.type = "Person";
JsonObject personContent = new JsonObject();
JsonObject name = new JsonObject();
name.addProperty("first", "Jane");
name.addProperty("last", "Hoeger");
personContent.add("name", name);
personContent.addProperty("gender", "female");
JsonObject address = new JsonObject();
address.addProperty("line1", "123 Elm St");
address.addProperty("line2", "Apt A");
address.addProperty("line3", "Charlottesville, VA 22902");
personContent.add("address", address);
JsonObject birth = new JsonObject();
birth.addProperty("date", "19941221");
personContent.add("birth", birth);
JsonArray issueIds = new JsonArray();
JsonObject id = new JsonObject();
id.addProperty("type", "license");
id.addProperty("id", "123456-789");
issueIds.add(id);
personContent.add("issuedIds", issueIds);
JsonObject prints = new JsonObject();
prints.addProperty("id", "qwerty123456");
prints.addProperty("source", "govt");
prints.addProperty("lastCapturedDate", "19750306");
personContent.add("fingerprints", prints);
personDobj.setAttribute("content", personContent);
client.create(personDobj, adminAuthInfo, serviceInfo);
Will produce an error like this:
Exception in thread "main" net.dona.doip.client.DoipException: Bad request: 400; Either Issued Id or Last Name + Birthday is duplicated.
Trying to create objects as a user will also fail, due to the Authorization configuration. For example, this Loan creation attempt:
DigitalObject loanDobj = new DigitalObject();
loanDobj.type = "Loan";
JsonObject loanContent = new JsonObject();
loanContent.addProperty("lender", "ABC Bank");
loanContent.addProperty("start", "20180101");
loanContent.addProperty("end", "20220101");
loanContent.addProperty("status", "performing");
JsonObject amount = new JsonObject();
amount.addProperty("amount", "10000");
amount.addProperty("currency", "USD");
loanContent.add("amount", amount);
JsonArray borrowers = new JsonArray();
borrowers.add("test/987a25ef7a329664b150");
loanContent.add("borrowers", borrowers);
loanDobj.setAttribute("content", loanContent);
// Using user authentication info instead of admin
AuthenticationInfo userAuthInfo = new PasswordAuthenticationInfo("user1", "password");
client.create(loanDobj, userAuthInfo, serviceInfo);
Will produce an error like this:
Exception in thread "main" net.dona.doip.client.DoipException: Forbidden: 403
However, users can still read object and call read-only static methods. Unauthenticated users who try to read objects
instead get a Unauthorized: 401
error.
Delete objects:
client.delete(personDobj.id, adminAuthInfo, serviceInfo);
client.delete(loanDobj.id, adminAuthInfo, serviceInfo);