Person Registry

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.

Narrative

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.

Sample Objects

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 in 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”.

Design

Authentication and Authorization

We will need two groups for access controls. One of the groups will have read/write access to the objects, and the other group will only have read access.

Edit the Cordra Authorization configuration in the Design Object, and replace the existing json with the following:

{
  "schemaAcls": {
    "User": {
      "defaultAclRead": [ "public" ],
      "defaultAclWrite": [ "self", "test/c5a2dc3dcb24a8c9c790" ],
      "aclCreate": [ "test/c5a2dc3dcb24a8c9c790" ]
    },
    "CordraDesign": {
      "defaultAclRead": [ "public" ],
      "defaultAclWrite": [],
      "aclCreate": []
    },
    "Schema": {
      "defaultAclRead": [ "public" ],
      "defaultAclWrite": [],
      "aclCreate": []
    },
    "Person": {
      "defaultAclRead": [ "authenticated" ],
      "defaultAclWrite": [ "test/c5a2dc3dcb24a8c9c790" ],
      "aclCreate": [ "test/c5a2dc3dcb24a8c9c790" ],
      "aclMethods": {
        "static": {
          "test/Op.GetHouseholdMembers": [ "authenticated" ],
          "test/Op.GetExpiredFingerPrints": [ "authenticated" ]
        },
        "default": {
          "static": [ "test/c5a2dc3dcb24a8c9c790" ],
          "instance": [ "test/c5a2dc3dcb24a8c9c790" ]
        }
      }
    },
    "Loan": {
      "defaultAclRead": [ "authenticated" ],
      "defaultAclWrite": [ "test/c5a2dc3dcb24a8c9c790" ],
      "aclCreate": [ "test/c5a2dc3dcb24a8c9c790" ],
      "aclMethods": {
        "static": {
          "test/Op.GetDefaulters": [ "authenticated" ]
        },
        "default": {
          "static": [ "test/c5a2dc3dcb24a8c9c790" ],
          "instance": [ "test/c5a2dc3dcb24a8c9c790" ]
        }
      }
    }
  },
  "defaultAcls": {
    "defaultAclRead": [ "authenticated" ],
    "defaultAclWrite": [ "test/c5a2dc3dcb24a8c9c790" ],
    "aclCreate": [ "test/c5a2dc3dcb24a8c9c790" ]
  }
}

Because the default read permission is set to authenticated above, users will be required to log in before being able to read digital objects in the system. The default write and create permissions are set to the admin group, so non-admin users are limited to read-only access. Note that, if you are not using the sample data provided along with this tutorial, you will need to change test/c5a2dc3dcb24a8c9c790 to the id of the admin group you created.

Schemas

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"
        }
      }
    }
  }
}

Rules and Type Methods

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

Using the Application

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.

Client Setup

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.

Basic Operations

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.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 objects:

client.delete(personDobj.id, adminAuthInfo, serviceInfo);
client.delete(loanDobj.id, adminAuthInfo, serviceInfo);

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

Extended Operations

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

Payloads

Binary files can be associated with Person or Loan objects using payloads. This can be used to attach files, such as, pictures or scans of certificates or government issued ids or load documents to a digital object.

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

Deduplication and Access Controls

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 this error:

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 this error:

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.