<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[Down the Code Mine]]></title><description><![CDATA[Ruminations of a software developer]]></description><link>https://blog.adamretter.org.uk/</link><image><url>https://blog.adamretter.org.uk/favicon.png</url><title>Down the Code Mine</title><link>https://blog.adamretter.org.uk/</link></image><generator>Ghost 2.29</generator><lastBuildDate>Thu, 09 Apr 2026 14:34:06 GMT</lastBuildDate><atom:link href="https://blog.adamretter.org.uk/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[Refreshing Apache XML Infrastructure]]></title><description><![CDATA[I have published new public versions of the Apache XML-RPC and Web Services Commons Open Source libraries. We have fixed a number of security issues, added embedded XML serialization/deserialization functionality, and addressed a long standing issue with namespace processing.]]></description><link>https://blog.adamretter.org.uk/refreshing-apache-xml-infrastructure/</link><guid isPermaLink="false">6912605b2beb3f025e6ac55a</guid><category><![CDATA[xml]]></category><category><![CDATA[Java]]></category><category><![CDATA[Maven]]></category><category><![CDATA[RPC]]></category><category><![CDATA[Open Source]]></category><category><![CDATA[XPath]]></category><category><![CDATA[Markup Monday]]></category><dc:creator><![CDATA[Adam Retter]]></dc:creator><pubDate>Mon, 10 Nov 2025 22:33:24 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1558939608-7e8f4c8336d2?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDh8fGplbmdhfGVufDB8fHx8MTc2MjgxMzYzOXww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1558939608-7e8f4c8336d2?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wxMTc3M3wwfDF8c2VhcmNofDh8fGplbmdhfGVufDB8fHx8MTc2MjgxMzYzOXww&ixlib=rb-4.1.0&q=80&w=1080" alt="Refreshing Apache XML Infrastructure"><p>At my company (<a href="https://www.evolvedbinary.com">Evolved Binary</a>) we recently had to address a series of bugs in <a href="https://www.elemental.xyz">Elemental</a> that involved the Serialization and Deserialization of <a href="https://www.w3.org/TR/xpath-datamodel-31/">XDM (XQuery and XPath Data Model)</a> values. The issues occurred when transferring the values of certain XDM types over the REST and XML-RPC APIs. As part of the <em>#markupmonday</em> effort from the XML community to post more articles, I decided to write up what we have been working on in this blog post.</p><p>The issues in the REST API were relatively easy for us to address as we control almost all of the code used there. However, Elemental inherited the XML:RPC API from eXist-db, and that relies heavily on the <a href="https://ws.apache.org/xmlrpc/">Apache XML-RPC</a> library. The last <em>official</em> release of Apache XML-RPC was version <a href="https://svn.apache.org/repos/asf/webservices/archive/xmlrpc/tags/xmlrpc-3.1.3/">3.1.3</a> back in February of 2010 - some 15+ years ago! Since then it has not been maintained by the Apache project.</p><p>In fact, we had already visited the topic of what to do with Apache XML-RPC in the past...</p><p>In June 2018, we identified that there were known security issues in Apache XML-RPC. I noted that a number of linux distributions were shipping security patches to protect against those issues, but that those patches were not included in the official Apache XML-RPC release. At that time we decided to create <a href="https://github.com/evolvedbinary/apache-xmlrpc">our own fork of the Apache XML-RPC project</a> because:</p><ol><li>The Apache XML-RPC project appeared to have been archived and was unmaintained.</li><li>We had a direct dependency on it.</li><li>It would not be easy to replace as we needed to maintain 100% forwards compatibility.</li></ol><p>At that time I imported the Apache XML-RPC code from Subversion to our GitHub, fixed the issues, and cut our own release. We did this work publicly and in accordance with the original license. We chose a new major version number to try and signal that this was a major departure from 3.1.3, i.e. a new fork. We published a new release of Apache XML-RPC version <a href="https://github.com/evolvedbinary/apache-xmlrpc/tree/xmlrpc-4.0.0">4.0.0</a> (in our own public namespace). This enabled anyone to use Apache XML-RPC with the security fixes already included.</p><p>In September 2022, I needed to switch a project over from Javax Servlet, to <a href="https://jakarta.ee/specifications/servlet/">Jakarta Servlet</a>. Unfortunately such a change in the Java world is not simple. Java Servlets (or those from any libraries you use) that utilise the Javax Servlet API have to be updated to compile against the newer Jakarta Servlet API. Apache XML-RPC was an example of one such library that delivers its Web Endpoint(s) using Javax Servlet. This time was easier as we already had own fork of Apache XML-RPC. From there we made the necessary changes, and chose a new major version number to indicate the breaking API change, and then we publicly released <a href="https://github.com/evolvedbinary/apache-xmlrpc/tree/xmlrpc-5.0.0">Apache XML-RPC version 5.0.0</a> that is compatible with Jakarta Servlet.</p><p>More recently, for Elemental, we wanted to be able to serialize and deserialize all XDM types over XML-RPC. If you squint a little, then the XDM Node types are a subset of the XML DOM (Document Object Model) types, and so we thought it might be nice if you could send and receive any XML over XML-RPC.</p><p>Now you might be forgiven for some confusion here if you are thinking: <em>Wait a minute this is XML-RPC! Why can't he send XML over his XML-RPC?</em> Let's step back for a moment. The XML in <a href="http://1998.xmlrpc.com/">XML-RPC</a> is just a wire-format for RPC, the focus is still on RPC (Remote Procedure Call), the XML is only used to describe function calls, their parameters, and their results.</p><p>For example, imagine we had a Java function like: <code>public String sayHello(String name)</code>, then an XML-RPC call to that function would produce the following XML document request that is sent from the client to the server:</p><!--kg-card-begin: code--><figure class="kg-card kg-code-card"><pre><code class="language-xml">&lt;methodCall&gt;
  &lt;methodName&gt;sayHello&lt;/methodName&gt;
  &lt;params&gt;
    &lt;param&gt;
      &lt;value&gt;Adam&lt;/value&gt;
    &lt;/param&gt;
  &lt;/params&gt;
&lt;/methodCall&gt;</code></pre><figcaption>Simple XML-RPC Request</figcaption></figure><!--kg-card-end: code--><p>The server would then execute the <code>sayHello</code> function, and all being well, might send this XML response document back to the client:</p><!--kg-card-begin: code--><figure class="kg-card kg-code-card"><pre><code class="language-xml">&lt;methodResponse&gt;
  &lt;params&gt;
    &lt;param&gt;
      &lt;value&gt;Hello Adam!&lt;/value&gt;
    &lt;/param&gt;
  &lt;/params&gt;
&lt;/methodResponse&gt;</code></pre><figcaption>Simple XML-RPC Response</figcaption></figure><!--kg-card-end: code--><p>So far so good, but our use-case is more complicated as we want to be able to send any XML DOM type (e.g. <code>org.w3c.dom.Document</code>, <code>org.w3c.dom.Element</code>, <code>org.w3c.dom.Text</code>, etc.) as either function parameters or the function result type. Imagine that we had another Java function like: <code>public Document createInvoice(Attr id, Element address, Element[] items)</code>. We now need to be able to put XML inside our XML-RPC request and/or response document. By default, XML-RPC does not support that. XML-RPC has a quite limited type system that supports just:</p><ol><li>32-bit signed integers.</li><li>64-bit double-precision signed floating point numbers</li><li>Boolean values.</li><li>Strings.</li><li>ISO 8601 date/time.</li><li>Base64</li><li>Arrays and Structures composed of the prior types.</li></ol><p>Apache XML-RPC allows you to add extensions in the form of custom XML serializers/deserializers for your own types. However, as our goal here was the serialization/deserialization of general XML and not some custom type, and furthermore as we found some incomplete support for this in Apache XML-RPC, we decided it was best to extend that prior work to completion. After our additions, an XML-RPC call to the <code>createInvoice</code> function now produces the following XML document request that is sent from the client to the server:</p><!--kg-card-begin: code--><figure class="kg-card kg-code-card"><pre><code class="language-xml">&lt;methodCall xmlns:ex="http://ws.apache.org/xmlrpc/namespaces/extensions" xmlns:dom="http://ws.apache.org/xmlrpc/namespaces/extensions/dom"&gt;
  &lt;methodName&gt;createInvoice&lt;/methodName&gt;
  &lt;params&gt;
    &lt;param&gt;
      &lt;value&gt;
        &lt;ex:dom dom:type="2"&gt;&lt;dom:attribute my-identifier="id1"/&gt;&lt;/ex:dom&gt;
      &lt;/value&gt;
    &lt;/param&gt;
    &lt;param&gt;
      &lt;value&gt;
        &lt;ex:dom dom:type="1"&gt;&lt;address&gt;&lt;number&gt;99&lt;/number&gt;&lt;street&gt;Via Medail&lt;/street&gt;&lt;city&gt;Bardonecchia&lt;/city&gt;&lt;province&gt;TO&lt;/province&gt;&lt;/address&gt;&lt;/ex:dom&gt;
      &lt;/value&gt;
    &lt;/param&gt;
    &lt;param&gt;
      &lt;value&gt;
        &lt;array&gt;
          &lt;data&gt;
            &lt;ex:dom dom:type="1"&gt;&lt;item&gt;&lt;name&gt;Sprockets&lt;/name&gt;&lt;quantity&gt;5&lt;/quantity&gt;&lt;unit-cost currency="GBP"&gt;7.50&lt;/unit-cost&gt;&lt;/item&gt;&lt;/ex:dom&gt;
            &lt;ex:dom dom:type="1"&gt;&lt;item&gt;&lt;name&gt;Springets&lt;/name&gt;&lt;quantity&gt;12&lt;/quantity&gt;&lt;unit-cost currency="EUR"&gt;19.20&lt;/unit-cost&gt;&lt;/item&gt;&lt;/ex:dom&gt;
          &lt;/data&gt;
        &lt;/array&gt;
      &lt;/value&gt;
    &lt;/param&gt;
  &lt;/params&gt;
&lt;/methodCall&gt;</code></pre><figcaption>XML-RPC Request containing embedded XML</figcaption></figure><!--kg-card-end: code--><p>The server would then execute the <code>createInvoice</code> function, and all being well, might send the back this XML response document to the client:</p><!--kg-card-begin: code--><figure class="kg-card kg-code-card"><pre><code class="language-xml">&lt;methodResponse xmlns:ex="http://ws.apache.org/xmlrpc/namespaces/extensions" xmlns:dom="http://ws.apache.org/xmlrpc/namespaces/extensions/dom"&gt;
  &lt;params&gt;
    &lt;param&gt;
      &lt;value&gt;
        &lt;ex:dom dom:type="9"&gt;&lt;invoice&gt; ... &lt;/invoice&gt;&lt;/ex:dom&gt;
      &lt;/value&gt;
    &lt;/param&gt;
  &lt;/params&gt;
&lt;/methodResponse&gt;</code></pre><figcaption>XML-RPC Response containing embedded XML</figcaption></figure><!--kg-card-end: code--><p>We wrapped that nice new feature up, and to signal that it could cause backward incompatibilities with previous versions, we released it as a major version <a href="https://github.com/evolvedbinary/apache-xmlrpc/tree/xmlrpc-6.0.0">6.0.0</a>.</p><p>Unfortunately during testing of 6.0.0 without our system, we found occasional exceptions being thrown with particular XML documents that we wanted to use as parameters to our functions. This initially puzzled me, as these were completely valid XML documents in their own right, and should have been handled correctly by our changes. Here is an example of such a document, that caused an exception when used as a parameter (or function return type) within our Apache XML-RPC 6.0.0:</p><!--kg-card-begin: code--><figure class="kg-card kg-code-card"><pre><code class="language-xml">&lt;c:Site xmlns="urn:content" xmlns:c="urn:content"&gt;
  &lt;config xmlns="urn:config"&gt;123&lt;/config&gt;
  &lt;serverconfig xmlns="urn:config"&gt;123&lt;/serverconfig&gt;
&lt;/c:Site&gt;</code></pre><figcaption>Innocuous looking XML document</figcaption></figure><!--kg-card-end: code--><p>Trying to use such a document within XML-RPC raised an exception like:</p><!--kg-card-begin: code--><figure class="kg-card kg-code-card"><pre><code class="language-java">java.lang.IllegalStateException: The prefix  isn't the prefix, which has been defined last.
	at org.apache.ws.commons.util.NamespaceContextImpl.endPrefixMapping(NamespaceContextImpl.java:95)
	at org.apache.ws.commons.util.test.NamespaceContextIT$NamespaceContextHandler.endPrefixMapping(NamespaceContextIT.java:90)
	...</code></pre><figcaption>Java Exception raised by processing the innocuous looking XML document</figcaption></figure><!--kg-card-end: code--><p>A quick look into <a href="https://github.com/evolvedbinary/apache-ws-commons/blob/ws-commons-util-1.0.2/modules/util/src/main/java/org/apache/ws/commons/util/NamespaceContextImpl.java#L75"><code>org.apache.ws.commons.util.NamespaceContextImpl#endPrefixMapping(String)</code></a> and we find this little nugget of Javadoc:</p><!--kg-card-begin: code--><figure class="kg-card kg-code-card"><pre><code class="language-java">* @throws IllegalStateException The prefix is not the prefix, which
* has been defined last. In other words, the calls to
* {@link #startPrefixMapping(String, String)}, and
* {@link #endPrefixMapping(String)} aren't in LIFO order.</code></pre><figcaption>Javadoc from NamespaceContextImpl</figcaption></figure><!--kg-card-end: code--><p>This and a further study of the complete code in <code>NamespaceContextImpl</code> confirmed that it expects the interleaved calls to <code>startPrefixMapping</code> and <code>endPrefixMapping</code> to happen in LIFO (Last in, First out) order. There's one big problem with that though, which is that that is not how prefix mapping might work in the real world! Typically such start/end prefix mapping methods are fired by an XML Parser or Serializer, and if we look at SAX (Simple API for XML) which is an approach used in many XML parsers and serializers, we see that it explicitly states <a href="https://docs.oracle.com/javase/6/docs/api/org/xml/sax/ContentHandler.html#startPrefixMapping(java.lang.String,%20java.lang.String)">here</a>:</p><!--kg-card-begin: code--><figure class="kg-card kg-code-card"><pre><code class="language-java">Note that start/endPrefixMapping events are not guaranteed to be properly nested relative to each other</code></pre><figcaption>Javadoc from SAX's ContentHandler</figcaption></figure><!--kg-card-end: code--><p>Through using XML-RPC to process XML Elements that have complex namespace arrangements, it seems we have found another bug. Unfortunately for us, this bug was not actually in Apache XML-RPC, but was in a library that it depends on - <code>ws-commons-util</code> (Apache Web Services Common Utilities). The Apache <code>ws-commons-util</code> library is even older than Apache XML-RPC, with its last <em>official</em> release version <a href="https://svn.apache.org/repos/asf/webservices/commons/tags/util/1.0.2/">1.0.2</a> being published back in August 2007 - some 18+ years ago! Since then it has not been maintained by the Apache project.</p><p>So we need to fix this bug too of course, but how should we do that?</p><p>With Apache XML-RPC we had felt comfortable forking the project as we had a direct dependency on it ourselves, but this is another level removed, where we have an indirect dependency on apache ws-commons-util through Apache XML-RPC. In addition, whilst Apache XML-RPC is quite application specific, ws-commons-util is known to be used as a general utility library in lots of other XML projects too. In the Java world, it is effectively <strong>XML infrastructure</strong>. Initially forking ws-commons-util felt like we were over reaching, and that any fix that we produced would be better upstreamed to Apache for all users to receive easily. Initially we contacted two of the main authors of ws-commons-util, and we received a very kind response from Jochen Wiedmann, who apologised that it was unmaintained and explained that whilst it may not be the response I hoped for, my best option was probably to fork it. Thank you very much Jochen :-)</p><p>Well with one of the original author's blessings in hand, and without any other option in sight, we forked the ws-commons-util project. I imported the Apache code from Subversion to our GitHub, fixed the bug by writing a completely new specification compliant implementation of <code>NamespaceContextImpl</code>, and cut our own release. We did this work publicly and in accordance with the original license. We bumped the feature version number to try and signal that this was an incremental release, i.e. a new implementation of <code>NamespaceContextImpl</code> that whilst maintaining the existing API, also adds a new and improved API. We published a new release of Apache ws-commons-util version <a href="https://github.com/evolvedbinary/apache-ws-commons/tree/ws-commons-1.1.0">1.1.0</a> (in our own public namespace).</p><p>Finally, we then released a new version of Apache XML-RPC version <a href="https://github.com/evolvedbinary/apache-xmlrpc/tree/xmlrpc-6.1.0">6.1.0</a>, where we changed its dependency from the official Apache ws-commons-util 1.0.2 to our newer fork: ws-commons-util 1.1.0.</p><p>If you are users of Apache XML-RPC or ws-commons-util, we would love to hear from you if this has been helpful for you too.</p><p>It seems that at my company, <a href="https://www.evolvedbinary.com">Evolved Binary</a>, we are creating a bit of a pattern for forking and maintaining XML infrastructure. I recognise that this is not without its own risks, and I hope to discuss those on this blog in detail soon.</p><p>In the meantime, for your perusal, Evolved Binary now maintain the following XML infrastructure projects:</p><ol><li>Builds of Apache Xerces, with and without XML Schema 1.1 support, and with and without Java 14+ support - <a href="https://central.sonatype.com/artifact/com.evolvedbinary.thirdparty.xerces/xercesImpl">https://central.sonatype.com/artifact/com.evolvedbinary.thirdparty.xerces/xercesImpl</a><br></li><li>Apache XML APIs - <a href="https://central.sonatype.com/artifact/com.evolvedbinary.thirdparty.xml-apis/xml-apis">https://central.sonatype.com/artifact/com.evolvedbinary.thirdparty.xml-apis/xml-apis</a><br></li><li>Eclipse XPath 2 Engine - <a href="https://central.sonatype.com/artifact/com.evolvedbinary.thirdparty.org.eclipse.wst.xml/xpath2">https://central.sonatype.com/artifact/com.evolvedbinary.thirdparty.org.eclipse.wst.xml/xpath2</a><br></li><li>Milton WebDAV library - <a href="https://central.sonatype.com/namespace/org.exist-db.thirdparty.com.ettrema">https://central.sonatype.com/namespace/org.exist-db.thirdparty.com.ettrema</a><br></li><li>Builds of XML Mind DITAC - <a href="https://central.sonatype.com/artifact/com.evolvedbinary.thirdparty.com.xmlmind/ditac">https://central.sonatype.com/artifact/com.evolvedbinary.thirdparty.com.xmlmind/ditac</a><br></li><li>JVNet JAXB Maven Plugin - <a href="https://github.com/evolvedbinary/jvnet-jaxb-maven-plugin">https://github.com/evolvedbinary/jvnet-jaxb-maven-plugin</a><br></li><li>Mojohaus JAXB Maven Plugin - <a href="https://github.com/evolvedbinary/mojohaus-jaxb-maven-plugin">https://github.com/evolvedbinary/mojohaus-jaxb-maven-plugin</a></li></ol>]]></content:encoded></item><item><title><![CDATA[Routing Success and Failure in XProc 3.0]]></title><description><![CDATA[An example of how to route success and failure from try/catch in XProc 3.0 to different Output Ports.]]></description><link>https://blog.adamretter.org.uk/routing-success-and-failure-in-xproc-3-0/</link><guid isPermaLink="false">67433c73decd9d025c804265</guid><category><![CDATA[XProc]]></category><category><![CDATA[xml]]></category><dc:creator><![CDATA[Adam Retter]]></dc:creator><pubDate>Sun, 24 Nov 2024 16:30:38 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1694674818352-f6061a0561a1?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDEwfHxwaXBlbGluZSUyMHxlbnwwfHx8fDE3MzI0NjU2ODN8MA&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=1080" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1694674818352-f6061a0561a1?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wxMTc3M3wwfDF8c2VhcmNofDEwfHxwaXBlbGluZSUyMHxlbnwwfHx8fDE3MzI0NjU2ODN8MA&ixlib=rb-4.0.3&q=80&w=1080" alt="Routing Success and Failure in XProc 3.0"><p>Recently on a new project where I needed to transform documents from a variety of source formats into a standard output format, I decided to try and orchestrate this by using XProc 3.0. In the past when faced with a similar challenge I have typically built the pipeline itself in Java or XQuery, but I felt that the goals and purpose of XProc should mean that it is a better fit for such projects.</p><p>One aspect that I have repeatedly been struggling with when building my XProc 3.0 pipeline is that of comprehending the exact flow of documents and/or data through the pipeline. Specifically, I want to be able to have separate success and failure flows in my pipeline, so that I can manage them with independent steps. To achieve this, I want to be able to capture any failures and route them to separate sub-pipelines that handle or recover from such failures.</p><p>I have tried to illustrate below an example of a simple pipeline where the main step of consequence is "Parse Document", whose tasks is to parse an XML document. If the parsing succeeds the pipeline should store the document to a location on the filesystem, and then return a copy of the document as the output of the pipeline. If the parsing fails, perhaps because the document is not well-formed, then it should create an error report of the details and store the error report to a different location on the filesystem.</p><!--kg-card-begin: image--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.adamretter.org.uk/content/images/2024/11/example-simple-parse-pipeline.png" class="kg-image" alt="Routing Success and Failure in XProc 3.0"><figcaption>Illustration of an example Parsing pipeline with separate Success and Failure routes</figcaption></figure><!--kg-card-end: image--><p>The point I am trying to illustrate is that I would like a different flow through the pipeline depending on whether the "Parse Document" step succeeds or fails.</p><p>In XProc 3.0 we can encapsulate any other step within a <em>try/catch</em> step. In this case we could encapsulate a <code>p:load</code> step within a <code>p:try</code> sub-pipeline. This enables us to catch and handle any error that is raised by the <code>p:load</code> step. XProc 3.0 also has the concept of Output Ports, and we can use them in this case to setup two Output Ports, one for success, and a separate one for any failure.</p><p>The theory, at least to me, of what we need to do is simple and straight-forward. Unfortunately, I spent a couple of days struggling to get this working in XProc 3.0; I experimented with many syntactic variations to try and implement this. Ultimately, after some kind pointers from both Norman Walsh and Achim Berndzen (thank you both!), I was able to construct an example that finally worked. I am reproducing this below for both my own reference, and for anyone else that might want to achieve the same thing.</p><h2 id="routing-from-try-catch-in-xproc-3-0">Routing from Try/Catch in XProc 3.0</h2><!--kg-card-begin: code--><figure class="kg-card kg-code-card"><pre><code class="language-xml">&lt;?xml version="1.0" encoding="UTF-8"?&gt;
&lt;p:declare-step
  xmlns:p="http://www.w3.org/ns/xproc"
  xmlns:xs="http://www.w3.org/2001/XMLSchema"
  xmlns:my="http://me"
  version="3.0"&gt;

  &lt;!-- TODO(AR) switch which one of these is commented out to see the result or the error --&gt;
  &lt;!--
  &lt;p:output port="result" sequence="true" primary="true" pipe="result@two-outputs-example"/&gt;
  --&gt;
  &lt;p:output port="result" sequence="true" primary="true" pipe="error@two-outputs-example"/&gt;



  &lt;!-- This step that has two output ports, only one will contain documents.
        * 'result' has documents if everything executes successfully.
        * 'error' has a document describing any error that occurred.
  --&gt;
  &lt;p:declare-step type="my:two-outputs-example"&gt;
    &lt;p:output port="result" sequence="true" primary="true"/&gt;
    &lt;p:output port="error"  sequence="true" pipe="failure@try1"/&gt;
    

    &lt;p:try name="try1"&gt;
      &lt;p:output port="result" sequence="true" primary="true"/&gt;

      &lt;!-- NOTE(AR) within this group could be a `p:load` or anything else that might raise an error --&gt;
      &lt;p:group&gt;
        &lt;p:identity&gt;
          &lt;p:with-input&gt;
            &lt;p:inline&gt;&lt;TRY-RESULT&gt;FROM TRY&lt;/TRY-RESULT&gt;&lt;/p:inline&gt;
          &lt;/p:with-input&gt;
        &lt;/p:identity&gt;
        
        &lt;!-- TODO(AR) comment out the line below out to instead get the 'result' of the try sub-pipeline --&gt;
        &lt;p:error code="ADAM-ERROR-01"/&gt;
      &lt;/p:group&gt;
      

      &lt;p:catch&gt;
        &lt;p:output port="result"  sequence="true" primary="true"/&gt;
        &lt;p:output port="failure" sequence="true" pipe="result@caught"/&gt;
        
        &lt;p:wrap name="caught"&gt;
          &lt;p:with-input port="source" pipe="error"/&gt;
          &lt;p:with-option name="wrapper" select="xs:QName('my:error-report')"/&gt;
          &lt;p:with-option name="match" select="'/'"/&gt;
        &lt;/p:wrap&gt;
        
        
        &lt;!-- NOTE(AR) we must discard output from catch sub-pipeline to stop it going to the primary 'result' output port --&gt;
        &lt;p:sink/&gt;
        
      &lt;/p:catch&gt;
    &lt;/p:try&gt;

  &lt;/p:declare-step&gt;
  
 
  &lt;my:two-outputs-example name="two-outputs-example"/&gt;


&lt;/p:declare-step&gt;</code></pre><figcaption>example-try-catch-routing.xproc - XProc 3.0 Pipeline with Success/Failure routing from Try/Catch</figcaption></figure><!--kg-card-end: code--><p>To experiment with this XProc 3.0 pipeline, you can run it using <a href="https://www.xml-project.com/morganaxproc-iiise.html">Morgana XProc IIIse</a>, by executing:</p><!--kg-card-begin: code--><pre><code>$ ./Morgana.sh example-try-catch-routing.xproc</code></pre><!--kg-card-end: code--><p>If you want to run and see the result of the Success flow:</p><ol><li>Uncomment the line: <code>&lt;p:output port="result" sequence="true" primary="true" pipe="result@two-outputs-example"/&gt;</code></li><li>Comment out the line: <code>&lt;p:output port="result" sequence="true" primary="true" pipe="error@two-outputs-example"/&gt;</code></li><li>Comment out the line: <code>&lt;p:error code="ADAM-ERROR-01"/&gt;</code></li></ol><p>If you want to run and see the result of the Failure flow:</p><ol><li>Comment out the line: <code>&lt;p:output port="result" sequence="true" primary="true" pipe="result@two-outputs-example"/&gt;</code></li><li>Uncomment the line: <code>&lt;p:output port="result" sequence="true" primary="true" pipe="error@two-outputs-example"/&gt;</code></li><li>Uncomment the line: <code>&lt;p:error code="ADAM-ERROR-01"/&gt;</code></li></ol><p>I have tried to show an illustration below of the data flow through such a pipeline. I admit that it is not a very good illustration and it might have been a better idea to flatten all of the steps and connect them by their ports only rather than showing them in the same nested fashion as that of the XProc syntax.</p><!--kg-card-begin: image--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.adamretter.org.uk/content/images/2024/11/example-try-catch-routing-pipeline.png" class="kg-image" alt="Routing Success and Failure in XProc 3.0"><figcaption>Illustration of an XProc 3.0 Pipeline with Success/Failure routing from Try/Catch</figcaption></figure><!--kg-card-end: image-->]]></content:encoded></item><item><title><![CDATA[Running OpenBSD 7.4 under UTM on macOS]]></title><description><![CDATA[<p>I had some difficulty with installing and running OpenBSD 7.4 (and previously 7.3) under UTM on macOS. My main issue appears to be that OpenBSD cannot access the Mouse or Keyboard from the Mac. I am unsure whether this is a problem caused by one or more of</p>]]></description><link>https://blog.adamretter.org.uk/running-openbsd-74-under-utm/</link><guid isPermaLink="false">6594787b312ce60ea93d74c6</guid><category><![CDATA[OpenBSD]]></category><category><![CDATA[macOS]]></category><category><![CDATA[UTM]]></category><category><![CDATA[Virtual Machine]]></category><dc:creator><![CDATA[Adam Retter]]></dc:creator><pubDate>Tue, 16 Jan 2024 19:07:30 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1557053503-0c252e5c8093?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDJ8fHB1ZmZlciUyMGZpc2h8ZW58MHx8fHwxNzA0MjMyMTI5fDA&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=1080" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1557053503-0c252e5c8093?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wxMTc3M3wwfDF8c2VhcmNofDJ8fHB1ZmZlciUyMGZpc2h8ZW58MHx8fHwxNzA0MjMyMTI5fDA&ixlib=rb-4.0.3&q=80&w=1080" alt="Running OpenBSD 7.4 under UTM on macOS"><p>I had some difficulty with installing and running OpenBSD 7.4 (and previously 7.3) under UTM on macOS. My main issue appears to be that OpenBSD cannot access the Mouse or Keyboard from the Mac. I am unsure whether this is a problem caused by one or more of UTM, QEMU, OpenBSD, or possibly macOS, but I can report that I have no problems with Ubuntu under UTM on macOS.</p><p>I have a MacBook Pro 2019 (Intel Core i9, 32 GB RAM) running macOS Sonoma 14.1.1. I am using UTM 4.4.5, and have downloaded the OpenBSD 7.4 image for the amd64 architecture.</p><p>Thankfully I have managed to find workarounds for each of the issues, which I am documenting below for my own memory, and because hopefully they may be useful for others.</p><p><strong>Update 2024-01-17:</strong> <a href="#update-2024-01-17-simpler-workaround-i440fx-instead-of-q35">A simpler workaround in UTM to fix the unresponsive Keyboard and Mouse</a>.</p><h2 id="how-to-boot-the-openbsd-install-image-in-utm">How to Boot the OpenBSD Install Image in UTM</h2><p>From the <a href="https://www.openbsd.org">openbsd.org</a> website, I downloaded the OpenBSD version 7.4 bootable image for amd64 systems from my closest mirror, the file is called: <code>install74.img</code>.</p><p>I created a new VM (Virtual Machine)  in UTM by selecting <em>Virtualize</em>, and choosing the <em>Other</em> category, I then assigned it 2 vCPU, 8 GB RAM, and a 40GB hard disk. However, as the <code>install74.img</code> file is not an ISO file, I had to click "<em>Skip ISO boot</em>" in the first stage of the configuration wizard, and then "<em>Open VM Settings</em>" in the last stage. From the VM settings I added a second drive of type <em>Removable USB</em>, set it to <em>Read-Only,</em> and selected the <code>install74.img</code> file as the source.</p><!--kg-card-begin: image--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.adamretter.org.uk/content/images/2024/01/Screenshot-2024-01-02-at-20.09.26.png" class="kg-image" alt="Running OpenBSD 7.4 under UTM on macOS"><figcaption>USB Disk Image for install74.img</figcaption></figure><!--kg-card-end: image--><h2 id="how-to-use-the-keyboard-in-openbsd-installer-in-utm">How to use the Keyboard in OpenBSD Installer in UTM</h2><p>Initially after booting the OpenBSD image described above, I found that I was unable to use the keyboard within the OpenBSD installer inside UTM; or at least OpenBSD did not seem to receiving any keystrokes!</p><!--kg-card-begin: image--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.adamretter.org.uk/content/images/2024/01/Screenshot-2024-01-02-at-20.26.14.png" class="kg-image" alt="Running OpenBSD 7.4 under UTM on macOS"><figcaption>Keyboard appears non-responsive within the OpenBSD Installer inside UTM</figcaption></figure><!--kg-card-end: image--><p>A little bit of experimentation showed however that before the installer starts, whilst at the initial OpenBSD kernel <code>boot&gt;</code> prompt, the keyboard was actually working. With that knowledge, I was then able to add a Serial Port to the VM in UTM. One of the nice features of UTM, is that it will start up a terminal in a window that is connected to the new serial port.</p><!--kg-card-begin: image--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.adamretter.org.uk/content/images/2024/01/Screenshot-2024-01-02-at-22.27.07.png" class="kg-image" alt="Running OpenBSD 7.4 under UTM on macOS"><figcaption>Configuring a Serial Port for the VM in UTM</figcaption></figure><!--kg-card-end: image--><p>After adding the Serial Port to the VM, I rebooted OpenBSD and told it to redirect its default console to the serial port by entering the command <code>set tty com0</code> at the <code>boot&gt;</code> prompt.</p><!--kg-card-begin: image--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.adamretter.org.uk/content/images/2024/01/Screenshot-2024-01-02-at-21.32.06.png" class="kg-image" alt="Running OpenBSD 7.4 under UTM on macOS"><figcaption>Switching the default console to the com0 Serial Port during OpenBSD boot</figcaption></figure><!--kg-card-end: image--><p>With that done, I was able to boot and install OpenBSD as normal albeit using the serial port terminal for interacting with it via the keyboard.</p><p>One more thing to note is that during the install process, the installer will ask whether to permanently switch the default console to the serial port, due to the issue in UTM we should answer <em>yes</em>, and accept the defaults for the serial port speed, i.e.:</p><!--kg-card-begin: code--><pre><code class="language-sh">Change the default console to com0? [yes] yes
Available speeds are: 9600 19200 38400 57600 115200.
Which speed should com0 use? (or 'done') [9600] 9600</code></pre><!--kg-card-end: code--><h2 id="how-to-use-the-keyboard-and-mouse-with-x-window-system-in-openbsd-in-utm">How to use the Keyboard and Mouse with X Window System in OpenBSD in UTM</h2><p>During the installation process I opted to install X, however after the install finished and the system had rebooted, I could see the X Windows System on the default console but I could not interact with it via the Mouse or Keyboard.</p><!--kg-card-begin: image--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.adamretter.org.uk/content/images/2024/01/Screenshot-2024-01-02-at-21.54.44.png" class="kg-image" alt="Running OpenBSD 7.4 under UTM on macOS"><figcaption>Mouse and Keyboard appear non-responsive within the OpenBSD X Windows System inside UTM</figcaption></figure><!--kg-card-end: image--><p>We can work around this by installing a VNC Server and then remotely controlling X Windows via a VNC Client. We can install and configure the VNC Server using the Serial Console that we earlier setup:</p><!--kg-card-begin: code--><pre><code>su -
pkg_add tigervnc
exit

vncpasswd

XAUTHORITY=/etc/X11/xenodm/authdir/authfiles/A:0-r4dlnM x0vncserver -display :0 -PasswordFile ~/.vnc/passwd</code></pre><!--kg-card-end: code--><p>You can then connect to the desktop via a VNC Client from the host Mac. Any VNC compatible client should work, personally I am using <a href="https://www.realvnc.com/en/">Real VNC</a> for this:</p><!--kg-card-begin: image--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.adamretter.org.uk/content/images/2024/01/Screenshot-2024-01-16-at-19.52.14.png" class="kg-image" alt="Running OpenBSD 7.4 under UTM on macOS"><figcaption>Connecting via VNC from Mac to OpenBSD X Windows System</figcaption></figure><!--kg-card-end: image--><h3 id="additional-utm-vm-settings-for-openbsd">Additional UTM VM Settings for OpenBSD</h3><p>Whilst the above is enough to get OpenBSD 7.4 running in UTM, I also made some additional changes to the VM configuration in UTM:</p><ul><li>Enable the <em>QEMU</em> -&gt; <em>Balloon Device.</em></li><li>Enable the <em>Display</em> -&gt; <em>Retina Mode.</em></li><li>Change the <em>Network</em> -&gt; <em>Emulated Network Card</em> to <code>virtio-net-pci</code>.</li><li>Change the <em>IDE Drive</em> -&gt; <em>Interface </em>from <code>IDE</code> to <code>VirtIO</code>.</li></ul><h2 id="update-2024-01-17-simpler-workaround-i440fx-instead-of-q35">Update 2024-01-17: Simpler Workaround - i440FX instead of Q35</h2><p>After submitting this article to <a href="https://lobste.rs/">Lobste.rs</a>, I had <a href="https://lobste.rs/s/oaqtsr/running_openbsd_7_4_under_utm_on_macos#c_penhdj">some feedback</a> from a user named <code>vtech</code> who suggested that changing the system type from Q35 (the default in UTM) to an i440FX would resolve the un-responsive Keyboard and Mouse issues I had been experiencing. I can confirm that this does appear to also workaround the issue in a simpler fashion. Thanks <code>vtech</code>!</p><!--kg-card-begin: image--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.adamretter.org.uk/content/images/2024/01/Screenshot-2024-01-17-at-16.10.34.png" class="kg-image" alt="Running OpenBSD 7.4 under UTM on macOS"><figcaption>Setting the System to i440FX instead of Q35 in UTM makes the Keyboard and Mouse responsive again</figcaption></figure><!--kg-card-end: image-->]]></content:encoded></item><item><title><![CDATA[Ubuntu RISC-V 64 Guest on an x86_64 KVM Host]]></title><description><![CDATA[<p>Ubuntu's <a href="https://ubuntu.com/server/docs/virtualization-uvt">uvtool</a> utility is fantastic for easily creating small Cloud VMs (Virtual Machines). Sadly, it didn't yet support creating VMs that provide <a href="https://en.wikipedia.org/wiki/RISC-V">RISC-V </a>emulation on x86_64 hosts. However, with a little bit of patching to Ubuntu 22.04's <code>uvtool</code> package, I was able to get it to create an</p>]]></description><link>https://blog.adamretter.org.uk/ubuntu-riscv64-guest-on-kvm/</link><guid isPermaLink="false">657759ec312ce60ea93d7334</guid><category><![CDATA[RISC-V]]></category><category><![CDATA[KVM]]></category><category><![CDATA[QEMU]]></category><category><![CDATA[hosting]]></category><dc:creator><![CDATA[Adam Retter]]></dc:creator><pubDate>Mon, 11 Dec 2023 21:09:01 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1598406444638-d4e842c982ab?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDh8fHNpbGljb258ZW58MHx8fHwxNzAyMzI4MzY4fDA&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=1080" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1598406444638-d4e842c982ab?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wxMTc3M3wwfDF8c2VhcmNofDh8fHNpbGljb258ZW58MHx8fHwxNzAyMzI4MzY4fDA&ixlib=rb-4.0.3&q=80&w=1080" alt="Ubuntu RISC-V 64 Guest on an x86_64 KVM Host"><p>Ubuntu's <a href="https://ubuntu.com/server/docs/virtualization-uvt">uvtool</a> utility is fantastic for easily creating small Cloud VMs (Virtual Machines). Sadly, it didn't yet support creating VMs that provide <a href="https://en.wikipedia.org/wiki/RISC-V">RISC-V </a>emulation on x86_64 hosts. However, with a little bit of patching to Ubuntu 22.04's <code>uvtool</code> package, I was able to get it to create an (emulated) RISC-V Ubuntu 22.04 KVM guest VM.</p><p>My Host system was:</p><ul><li>Intel Xeon E5-1650v3</li><li><a href="https://ubuntu.com/">Ubuntu</a> 22.04.3 LTS</li><li><a href="https://www.qemu.org/">QEMU</a> emulator version 6.2.0 (installed from Ubuntu's apt)</li><li><a href="https://libvirt.org/">libvirt</a> 8.0.0-1 (installed from Ubuntu's apt)</li><li><a href="https://launchpad.net/uvtool">uvtool</a> 0~git178-0 (installed from Ubuntu's apt)</li></ul><p>If you don't already have QEMU, KVM, and libvirt installed you will need to run:</p><!--kg-card-begin: code--><figure class="kg-card kg-code-card"><pre><code class="language-shell">sudo apt install -y cpu-checker qemu qemu-kvm libvirt-daemon libvirt-clients bridge-utils dnsmasq</code></pre><figcaption>Install QEMU, KVM, and libvirt</figcaption></figure><!--kg-card-end: code--><p>To install uvtool you will need to run:</p><!--kg-card-begin: code--><figure class="kg-card kg-code-card"><pre><code class="language-shell">sudo apt-get install -y uvtool</code></pre><figcaption>Install uvtool</figcaption></figure><!--kg-card-end: code--><p><br>To install support for RISC-V emulation you will need to run:</p><!--kg-card-begin: code--><figure class="kg-card kg-code-card"><pre><code class="language-shell">sudo apt-get install -y qemu-system-misc u-boot-qemu</code></pre><figcaption>Install RISC-V emulation support</figcaption></figure><!--kg-card-end: code--><h3 id="patching-uvtool-to-support-riscv64-emulation">Patching uvtool to Support riscv64 Emulation</h3><p>The latest version of uvtool (version 0~git178-0) at the time of writing does not support emulation of riscv64 via QEMU. However, by applying my patches you can get this work.</p><p>If you are familiar with Git and Python, you can find my patches here: <a href="https://code.launchpad.net/~adam-retter/ubuntu/+source/uvtool/+git/uvtool/+merge/457260">https://code.launchpad.net/~adam-retter/ubuntu/+source/uvtool/+git/uvtool/+merge/457260</a></p><p>If you are not, then you can take the following steps to apply my changes manually:</p><!--kg-card-begin: markdown--><ol>
<li>
<p>Make a backup of the file: <code>/usr/lib/python3/dist-packages/uvtool/libvirt/kvm.py</code>, and then make the following changes to the original by replacing:</p>
<pre><code class="language-python">if target_arch == 'armhf':
    return '/usr/share/uvtool/libvirt/template-emu-armhf.xml'
</code></pre>
<p>with:</p>
<pre><code class="language-python">if target_arch == 'armhf':
    return '/usr/share/uvtool/libvirt/template-emu-armhf.xml'
if target_arch == 'riscv64':
    return '/usr/share/uvtool/libvirt/template-emu-riscv64.xml'
</code></pre>
</li>
<li>
<p>Make a backup of the file: <code>/usr/lib/python3/dist-packages/uvtool/libvirt/__init__.py</code>, and then make the following changes to the original by replacing:</p>
<pre><code class="language-python">'ppc64le': 'ppc64el',
</code></pre>
<p>with:</p>
<pre><code class="language-python">'ppc64le': 'ppc64el',
'riscv64': 'riscv64',
</code></pre>
<p>and, by replacing:</p>
<pre><code class="language-python"># early exit on not supported emulations
if target_arch != 'armhf':
    return False
</code></pre>
<p>with:</p>
<pre><code class="language-python"># early exit on not supported emulations
if target_arch != 'armhf' and target_arch != 'riscv64':
    return False
</code></pre>
</li>
<li>
<p>Delete the folder <code>/usr/lib/python3/dist-packages/uvtool/libvirt/__pycache__</code></p>
</li>
<li>
<p>Create the following file at <code>/usr/share/uvtool/libvirt/template-emu-riscv64.xml</code>:</p>
<pre><code class="language-xml">&lt;domain type='qemu'&gt;
    &lt;os&gt;
        &lt;type arch='riscv64' machine='virt'&gt;hvm&lt;/type&gt;
        &lt;kernel&gt;/usr/lib/u-boot/qemu-riscv64_smode/uboot.elf&lt;/kernel&gt;
        &lt;cmdline&gt;root=/dev/vda1&lt;/cmdline&gt;
        &lt;boot dev='hd'/&gt;
    &lt;/os&gt;
    &lt;features&gt;
        &lt;acpi/&gt;
    &lt;/features&gt;
    &lt;devices&gt;
        &lt;emulator&gt;/usr/bin/qemu-system-riscv64&lt;/emulator&gt;
        &lt;interface type='network'&gt;
            &lt;source network='default'/&gt;
            &lt;target dev='vnet0'/&gt;
        &lt;/interface&gt;
        &lt;console type='pty'&gt;
            &lt;target type='serial' port='0'/&gt;
        &lt;/console&gt;
    &lt;/devices&gt;
&lt;/domain&gt;
</code></pre>
</li>
</ol>
<!--kg-card-end: markdown--><h3 id="creating-a-riscv64-emulated-kvm-guest-with-uvtool">Creating a riscv64 Emulated KVM Guest with uvtool</h3><p>First, we will use uvtool to download a Cloud Image of Ubuntu 22.04 (Jammy Jellyfish) for RISC-V 64, this can be done by running:</p><!--kg-card-begin: code--><figure class="kg-card kg-code-card"><pre><code class="language-shell">sudo uvt-simplestreams-libvirt sync arch=riscv64 release=jammy</code></pre><figcaption>Download the Cloud Image of Ubuntu 22.04 for RISC-V 64</figcaption></figure><!--kg-card-end: code--><p>After that completes, you can check that you have successfully downloaded the image by running the following command and observing the output:</p><!--kg-card-begin: code--><figure class="kg-card kg-code-card"><pre><code class="language-shell">sudo uvt-simplestreams-libvirt query

release=jammy arch=riscv64 label=release (20231211)</code></pre><figcaption>Show downloaded Ubuntu Cloud Images</figcaption></figure><!--kg-card-end: code--><p>To create an emulated RISC-V 64 KVM guest with <code>uvtool</code>, you need to pass the argument <code>--guest-arch riscv64</code> to its <code>uvt-kvm</code> command. For example:</p><!--kg-card-begin: code--><pre><code class="language-shell">sudo uvt-kvm create \
    --guest-arch riscv64 --cpu 2 --memory 16384 --disk 40 \
    --bridge virbr1 --network-config /tmp/myvm-net.yaml \
    --ssh-public-key-file /tmp/ssh/myvm.pub \
    myvm \
    arch=riscv64 release=jammy label=release</code></pre><!--kg-card-end: code--><p>You may need to vary the above configuration depending on your network settings:</p><ul><li>I already have <code>virbr1</code> configured as a virtual network bridge on the host.</li><li>I have already generated an SSH key-pair at <code>/tmp/ssh</code> using <code>ssh-keygen</code>.</li><li>I have created a <a href="https://netplan.io/">Netplan</a> configuration file at <code>/tmp/myvm-net.yaml</code> for <code>myvm</code> that will be deployed by <a href="https://cloud-init.io/">cloud-init</a> when the VM first boots:</li></ul><!--kg-card-begin: code--><figure class="kg-card kg-code-card"><pre><code class="language-yaml">version: 2
ethernets:
  enp1s0:
    addresses:
      - 10.0.0.10/32
      - fc00:10:0:0::10/128
    nameservers:
      addresses:
        - 10.0.0.253
        - fc00:10:0:0::253
      search:
        - home.dom
    routes:
      - to: 0.0.0.0/0
        via: 10.0.0.254
        on-link: true
      - to: "::/0"
        via: "fc00:10:0:0::254"
        on-link: true</code></pre><figcaption>Example Netplan configuration file for the VM</figcaption></figure><!--kg-card-end: code--><p>After <code>uvt-kvm</code> completes successfully we can check the status of the VM, by running:</p><!--kg-card-begin: code--><pre><code>virsh list --all

 Id   Name         State
-----------------------------
 1    myvm         running</code></pre><!--kg-card-end: code--><p>You can then observe the console of the VM by running: <code>virsh console myvm</code>, or by SSH'ing to the IP address of the VM using the SSH key-pair previously generated and the username <code>ubuntu</code>, e.g. <code>ssh -i /tmp/ssh/myvm ubuntu@10.0.0.10</code>. After logging, in you will see a banner like this:</p><!--kg-card-begin: code--><figure class="kg-card kg-code-card"><pre><code class="language-shell">Welcome to Ubuntu 22.04.3 LTS (GNU/Linux 5.19.0-1021-generic riscv64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

  System information as of Mon Dec 11 18:28:49 UTC 2023

  System load:             0.15380859375
  Usage of /:              4.1% of 38.60GB
  Memory usage:            1%
  Swap usage:              0%
  Processes:               101
  Users logged in:         0
  IPv4 address for enp1s0: 10.0.0.10
  IPv6 address for enp1s0: fc00:10:0:0::10</code></pre><figcaption>Ubuntu 22.04 RISC-V Login MOTD</figcaption></figure><!--kg-card-end: code--><p>Finally, running <code>uname -av</code> and <code>cat /proc/cpuinfo</code> confirms that this is indeed a RISC-V 64 system:</p><!--kg-card-begin: code--><figure class="kg-card kg-code-card"><pre><code class="language-shell">uname -av

Linux blackberry 5.19.0-1021-generic #23~22.04.1-Ubuntu SMP Thu Jun 22 12:49:35 UTC 2023 riscv64 riscv64 riscv64 GNU/Linux


cat /proc/cpuinfo

processor	: 0
hart		: 0
isa		: rv64imafdc
mmu		: sv48

processor	: 1
hart		: 1
isa		: rv64imafdc
mmu		: sv48</code></pre><figcaption>Ubuntu 22.04 VM Emulated RISC-V CPU Details</figcaption></figure><!--kg-card-end: code-->]]></content:encoded></item><item><title><![CDATA[AWS Client VPN with Manually Provisioned Certificates using Terraform]]></title><description><![CDATA[<p>This is a brief explanation of how to use Terraform to setup an AWS CVPN (Client VPN) where the certificates (for VPN authentication) are manually provisioned by yourself and then uploaded into ACM (AWS Certificate Manager).</p><p>The advantage of using a manually provisioned approach is that the cost is significantly</p>]]></description><link>https://blog.adamretter.org.uk/terraform-aws-client-vpn-with-openssl-certificates/</link><guid isPermaLink="false">6506dd07312ce60ea93d704a</guid><category><![CDATA[AWS]]></category><category><![CDATA[VPN]]></category><category><![CDATA[Terraform]]></category><category><![CDATA[OpenSSL]]></category><dc:creator><![CDATA[Adam Retter]]></dc:creator><pubDate>Mon, 25 Sep 2023 12:30:01 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1599959464432-458179a13352?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDEyfHxsb2NrfGVufDB8fHx8MTY5NTU4OTUyM3ww&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=1080" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1599959464432-458179a13352?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wxMTc3M3wwfDF8c2VhcmNofDEyfHxsb2NrfGVufDB8fHx8MTY5NTU4OTUyM3ww&ixlib=rb-4.0.3&q=80&w=1080" alt="AWS Client VPN with Manually Provisioned Certificates using Terraform"><p>This is a brief explanation of how to use Terraform to setup an AWS CVPN (Client VPN) where the certificates (for VPN authentication) are manually provisioned by yourself and then uploaded into ACM (AWS Certificate Manager).</p><p>The advantage of using a manually provisioned approach is that the cost is significantly lower than a managed approach that utilises AWS Private CA (Certificate Authority). Previously, the cost of operating AWS Private CA for us was $800 / Month. Apart from the initial manual steps required to generate the certificate chain, the main disadvantages of this approach over a fully managed approach are that: (1) you also have to store your certificates and keys outside of the cloud in a secure location, and (2) you have to remember to renew certificates before they expire and then replace the expiring certificates with their newer counterparts in ACM.</p><p>If you are looking for a fully managed approach instead, you can find this in a previous blog article that I wrote - <a href="https://blog.adamretter.org.uk/terraform-aws-client-vpn-with-managed-certificates/">AWS Client VPN with Managed Certificates using Terraform</a>.</p><p>The approach taken in this article was informed by the Timeular blog post: <a href="https://timeular.com/blog/creating-an-aws-client-vpn-with-terraform/">Creating an AWS Client VPN with Terraform</a>. Unfortunately, I could not get their approach to work correctly, and so the approach detailed in my article is an adaption/extension of their approach, but I want to thank them as my work builds upon theirs.</p><h2 id="chain-of-trust">Chain of Trust</h2><p>We will create a number of certificates that will form a chain of trust. Each subordinate certificate is signed by its parent certificate. Our chain of trust will look like:</p><ol><li>Root CA Certificate.</li><li>Intermediate CA Certificate - this CA certificate is used as an intermediary between the Root CA and our other CAs. This makes it easier to revoke subordinate CA certificates should we need to. This certificate is signed by the Root CA Certificate.</li><li>CVPN CA Certificate - this CA certificate is used for issuing and signing certificates that are used by the CVPN Server and its clients (e.g. users) to connect to the CVPN Server. This certificate is signed by the Intermediate CA Certificate.</li><li>CVPN Server Certificate - this endpoint certificate is for the CVPN server itself in AWS. This certificate is signed by the CVPN CA Certificate.</li><li>CVPN Client Certificate - we create one of these endpoint certificates for each client (e.g. user) that wishes to connect to the CPVN Server. These certificates will also be signed by the CVPN CA Certificate.</li></ol><!--kg-card-begin: image--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.adamretter.org.uk/content/images/2023/09/AWS-Client-VPN-Certificates---Approaches---Manually-Managed-Certificates.png" class="kg-image" alt="AWS Client VPN with Manually Provisioned Certificates using Terraform"><figcaption>Chain of Trust diagram</figcaption></figure><!--kg-card-end: image--><p>The purpose of having a separate Root CA, Intermediate CA, and CVPN CA is that we have delegated trust from the Root to the CVPN via the Intermediate. We can therefore easily isolate the CVPN if needed and manage CVPN certificate issuance and revocation completely separately to the Root. In future it may be desirable to create further delegated CAs from the Intermediate CA for other purposes.</p><h2 id="creating-the-certificates"><strong>Creating the Certificates</strong></h2><p>We will manually create all of the CA certificates and endpoint certificates. Whilst various tools are available for this purpose, for convenience, the first time I undertook this I used the GUI tool <a href="https://hohnstaedt.de/xca/">XCA</a> (which is available for macOS, Linux, and Windows). In this article I will show both the settings required for XCA, and also provide the equivalent <a href="https://www.openssl.org/">OpenSSL</a> commands. After the Certificates are generated we will deploy them and the Client VPN with Terraform further below.<br><br><strong>Note</strong>: For the OpenSSL commands, I tested against OpenSSL version 3.0.11.</p><h3 id="root-ca"><strong>Root CA</strong></h3><p>In XCA you need to navigate to the <code>Certificates</code> tab, and then press the <code>New Certificate</code> button. This presents a dialog with a number of tabs, that should be configured with the following information and settings, after which a new Root CA certificate will be created in the XCA database for you.</p><!--kg-card-begin: markdown--><ul>
<li><strong>Type:</strong> <code>x509 Certificate</code></li>
<li><strong>Source:</strong>
<ul>
<li><strong>Signing:</strong> Create a self signed certificate</li>
<li><strong>Signature algorithm:</strong> <code>SHA 512</code></li>
<li><strong>Template for new certificate:</strong> <code>[default] Empty template</code></li>
</ul>
</li>
<li><strong>Subject:</strong>
<ul>
<li><strong>Internal Name:</strong> <code>My Private Root CA</code></li>
<li><strong>Distinguished Name:</strong>
<ul>
<li><strong>countryName:</strong> <code>GB</code></li>
<li><strong>stateOrProvinceName:</strong> <code>My State</code></li>
<li><strong>localityName:</strong> <code>My Locality</code></li>
<li><strong>organizationName:</strong> <code>My Company</code></li>
<li><strong>organizationalUnitName:</strong> <code>DevOps</code></li>
<li><strong>commonName:</strong> <code>root.ca.cert.private.mydomain.com</code></li>
<li><strong>emailAddress:</strong> <code>devops@mydomain.com</code></li>
</ul>
</li>
<li><strong>Private Key:</strong>
<ul>
<li><strong>Name:</strong> <code>My Private Root CA</code></li>
<li><strong>Type:</strong> <code>RSA</code></li>
<li><strong>Size:</strong> <code>4096 bit</code></li>
</ul>
</li>
</ul>
</li>
<li><strong>Extensions:</strong>
<ul>
<li><strong>x509v3 Basic Constraints:</strong>
<ul>
<li><strong>Type:</strong> <code>Certification Authority</code></li>
</ul>
</li>
<li><strong>Key Identifier:</strong>
<ul>
<li><code>x509v3 Subject Key Identifier</code></li>
<li><code>x509v3 Authority Key Identifier</code></li>
</ul>
</li>
<li><strong>Validity:</strong> (10 Years)
<ul>
<li><strong>Not Before:</strong> <code>2023-09-01 00:00 GMT</code></li>
<li><strong>Not After:</strong> <code>2033-08-31 23:59 GMT</code></li>
</ul>
</li>
</ul>
</li>
<li><strong>Key usage</strong>:
<ul>
<li><strong>x509v3 Key Usage:</strong>
<ul>
<li><code>Certificate Sign</code></li>
<li><code>CRL Sign</code></li>
</ul>
</li>
</ul>
</li>
</ul>
<!--kg-card-end: markdown--><!--kg-card-begin: image--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.adamretter.org.uk/content/images/2023/09/Screenshot-2023-09-24-at-18.19.37.png" class="kg-image" alt="AWS Client VPN with Manually Provisioned Certificates using Terraform"><figcaption>Root CA visible in XCA</figcaption></figure><!--kg-card-end: image--><p>If you prefer to generate the Root CA with OpenSSL then you can use the following commands:</p><!--kg-card-begin: code--><figure class="kg-card kg-code-card"><pre><code class="language-bash">cat &lt;&lt; EOF &gt; openssl.conf
default_ca = my_ca

[ my_ca ]
unique_subject = yes
database = openssl.index.txt
serial = openssl.serial
default_md = sha1
default_crl_days = 730

[ req_ca ]
x509_extensions = v3_ca

[ v3_ca ]
basicConstraints = CA:TRUE
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid
keyUsage=Certificate Sign, CRL Sign

[ req_tls_server ]
x509_extensions = v3_tls_server

[ v3_tls_server ]
basicConstraints = CA:FALSE
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid
keyUsage=Digital Signature
extendedKeyUsage=TLS Web Server Authentication

[ req_tls_client ]
x509_extensions = v3_tls_client

[ v3_tls_client ]
basicConstraints = CA:FALSE
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid
keyUsage=Digital Signature
extendedKeyUsage=TLS Web Client Authentication
EOF

openssl genrsa -out root.ca.private.key.pem 4096

openssl req -new -x509 -config openssl.conf -section req_ca -sha512 \
    -subj "/C=GB/ST=My State/L=My Locality/O=My Company/OU=DevOps/CN=root.ca.cert.private.mydomain.com/emailAddress=devops@mycompany.com" \
    -days 3650 -set_serial 0x1 -key root.ca.private.key.pem -out root.ca.crt.pem</code></pre><figcaption>Generating a Root CA with OpenSSL</figcaption></figure><!--kg-card-end: code--><h3 id="intermediate-ca"><strong>Intermediate CA</strong></h3><p>To generate the subordinate Intermediate CA in XCA you need to make sure you have the <code>My Private Root CA</code> certificate selected in the <code>Certificates</code> tab, and then press the <code>New Certificate</code> button. This presents a dialog with a number of tabs, that should be configured with the following information and settings, after which a new Intermediate CA certificate will be created (as a subordinate of the Root CA) in the XCA database for you.</p><!--kg-card-begin: markdown--><ul>
<li><strong>Type:</strong> <code>x509 Certificate</code></li>
<li><strong>Source:</strong>
<ul>
<li><strong>Signing:</strong> <code>My Private Root CA</code></li>
<li><strong>Signature algorithm:</strong> <code>SHA 512</code></li>
<li><strong>Template for new certificate:</strong> <code>[default] Empty template</code></li>
</ul>
</li>
<li><strong>Subject:</strong>
<ul>
<li><strong>Internal Name:</strong> <code>My Private Intermediate CA</code></li>
<li><strong>Distinguished Name:</strong>
<ul>
<li><strong>countryName:</strong> <code>GB</code></li>
<li><strong>stateOrProvinceName:</strong> <code>My State</code></li>
<li><strong>localityName:</strong> <code>My Locality</code></li>
<li><strong>organizationName:</strong> <code>My Company</code></li>
<li><strong>organizationalUnitName:</strong> <code>DevOps</code></li>
<li><strong>commonName:</strong> <code>intermediate.ca.cert.private.mydomain.com</code></li>
<li><strong>emailAddress:</strong> <code>devops@mydomain.com</code></li>
</ul>
</li>
<li><strong>Private Key:</strong>
<ul>
<li><strong>Name:</strong> <code>My Private Intermediate CA</code></li>
<li><strong>Type:</strong> <code>RSA</code></li>
<li><strong>Size:</strong> <code>4096 bit</code></li>
</ul>
</li>
</ul>
</li>
<li><strong>Extensions:</strong>
<ul>
<li><strong>x509v3 Basic Constraints:</strong>
<ul>
<li><strong>Type:</strong> <code>Certification Authority</code></li>
</ul>
</li>
<li><strong>Key Identifier:</strong>
<ul>
<li><code>x509v3 Subject Key Identifier</code></li>
<li><code>x509v3 Authority Key Identifier</code></li>
</ul>
</li>
<li><strong>Validity:</strong> (5 Years)
<ul>
<li><strong>Not Before:</strong> <code>2023-09-01 00:00 GMT</code></li>
<li><strong>Not After:</strong> <code>2028-08-31 23:59 GMT</code></li>
</ul>
</li>
</ul>
</li>
<li><strong>Key usage</strong>:
<ul>
<li><strong>x509v3 Key Usage:</strong>
<ul>
<li><code>Certificate Sign</code></li>
<li><code>CRL Sign</code></li>
</ul>
</li>
</ul>
</li>
</ul>
<!--kg-card-end: markdown--><!--kg-card-begin: image--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.adamretter.org.uk/content/images/2023/09/Screenshot-2023-09-24-at-19.27.36.png" class="kg-image" alt="AWS Client VPN with Manually Provisioned Certificates using Terraform"><figcaption>Intermediate CA visible in XCA</figcaption></figure><!--kg-card-end: image--><p>If you prefer to generate the Intermediate CA with OpenSSL then you can use the following commands:</p><!--kg-card-begin: code--><figure class="kg-card kg-code-card"><pre><code class="language-bash">openssl genrsa -out intermediate.ca.private.key.pem 4096

openssl req -new -config openssl.conf -section req_ca -sha512 \
    -subj "/C=GB/ST=My State/L=My Locality/O=My Company/OU=DevOps/CN=intermediate.ca.cert.private.mydomain.com/emailAddress=devops@mycompany.com" \
    -key intermediate.ca.private.key.pem -out intermediate.ca.csr.pem

openssl req -x509 -config openssl.conf -section req_ca -sha512 \
    -CA root.ca.crt.pem -CAkey root.ca.private.key.pem \
    -days 1825 -set_serial 0x2 -in intermediate.ca.csr.pem -key intermediate.ca.private.key.pem -out intermediate.ca.crt.pem</code></pre><figcaption>Generating an Intermediate CA with OpenSSL</figcaption></figure><!--kg-card-end: code--><h3 id="cvpn-ca"><strong>CVPN CA</strong></h3><p>To generate the subordinate CVPN CA in XCA you need to make sure you have the <code>My Private Intermediate CA</code> certificate selected in the <code>Certificates</code> tab, and then press the <code>New Certificate</code> button. This presents a dialog with a number of tabs, that should be configured with the following information and settings, after which a new CVPN CA certificate will be created (as a subordinate of the Intermediate CA) in the XCA database for you.</p><!--kg-card-begin: markdown--><ul>
<li><strong>Type:</strong> <code>x509 Certificate</code></li>
<li><strong>Source:</strong>
<ul>
<li><strong>Signing:</strong> <code>My Private Intermediate CA</code></li>
<li><strong>Signature algorithm:</strong> <code>SHA 512</code></li>
<li><strong>Template for new certificate:</strong> <code>[default] Empty template</code></li>
</ul>
</li>
<li><strong>Subject:</strong>
<ul>
<li><strong>Internal Name:</strong> <code>My Private CVPN CA</code></li>
<li><strong>Distinguished Name:</strong>
<ul>
<li><strong>countryName:</strong> <code>GB</code></li>
<li><strong>stateOrProvinceName:</strong> <code>My State</code></li>
<li><strong>localityName:</strong> <code>My Locality</code></li>
<li><strong>organizationName:</strong> <code>My Company</code></li>
<li><strong>organizationalUnitName:</strong> <code>DevOps</code></li>
<li><strong>commonName:</strong> <code>cvpn.ca.cert.private.mydomain.com</code></li>
<li><strong>emailAddress:</strong> <code>devops@mydomain.com</code></li>
</ul>
</li>
<li><strong>Private Key:</strong>
<ul>
<li><strong>Name:</strong> <code>My Private CVPN CA</code></li>
<li><strong>Type:</strong> <code>RSA</code></li>
<li><strong>Size:</strong> <code>2048 bit</code></li>
</ul>
</li>
</ul>
</li>
<li><strong>Extensions:</strong>
<ul>
<li><strong>x509v3 Basic Constraints:</strong>
<ul>
<li><strong>Type:</strong> <code>Certification Authority</code></li>
</ul>
</li>
<li><strong>Key Identifier:</strong>
<ul>
<li><code>x509v3 Subject Key Identifier</code></li>
<li><code>x509v3 Authority Key Identifier</code></li>
</ul>
</li>
<li><strong>Validity:</strong> (3 Years)
<ul>
<li><strong>Not Before:</strong> <code>2023-09-01 00:00 GMT</code></li>
<li><strong>Not After:</strong> <code>2026-08-31 23:59 GMT</code></li>
</ul>
</li>
</ul>
</li>
<li><strong>Key usage</strong>:
<ul>
<li><strong>x509v3 Key Usage:</strong>
<ul>
<li><code>Certificate Sign</code></li>
<li><code>CRL Sign</code></li>
</ul>
</li>
</ul>
</li>
</ul>
<!--kg-card-end: markdown--><p>Note that for the CVPN CA, the Private Key size is only 2048 bits and not 4096 bits (as used by the Root and Intermediate CA); this is due to a limitation with the AWS Client VPN only supporting a maximum key size of 2048 bits.</p><!--kg-card-begin: image--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.adamretter.org.uk/content/images/2023/09/Screenshot-2023-09-24-at-20.49.00.png" class="kg-image" alt="AWS Client VPN with Manually Provisioned Certificates using Terraform"><figcaption>CVPN CA visible in XCA</figcaption></figure><!--kg-card-end: image--><p>If you prefer to generate the CVPN CA with OpenSSL then you can use the following commands:</p><!--kg-card-begin: code--><figure class="kg-card kg-code-card"><pre><code class="language-bash">openssl genrsa -out cvpn.ca.private.key.pem 2048

openssl req -new -config openssl.conf -section req_ca -sha512 \
    -subj "/C=GB/ST=My State/L=My Locality/O=My Company/OU=DevOps/CN=cvpn.ca.cert.private.mydomain.com/emailAddress=devops@mycompany.com" \
    -key cvpn.ca.private.key.pem -out cvpn.ca.csr.pem

openssl req -x509 -config openssl.conf -section req_ca -sha512 \
    -CA intermediate.ca.crt.pem -CAkey intermediate.ca.private.key.pem \
    -days 1095 -set_serial 0x3 -in cvpn.ca.csr.pem -key cvpn.ca.private.key.pem -out cvpn.ca.crt.pem

cat root.ca.crt.pem intermediate.ca.crt.pem &gt; cvpn.ca.crt.chain.pem</code></pre><figcaption>Generating a CVPN CA with OpenSSL</figcaption></figure><!--kg-card-end: code--><h3 id="cvpn-server-certificate"><strong>CVPN Server Certificate</strong></h3><p>To generate the endpoint CVPN Server certificate in XCA you need to make sure you have the <code>My Private CVPN CA</code> certificate selected in the <code>Certificates</code> tab, and then press the <code>New Certificate</code> button. This presents a dialog with a number of tabs, that should be configured with the following information and settings, after which a new CVPN Server certificate will be created (as a subordinate of the CVPN CA) in the XCA database for you.</p><!--kg-card-begin: markdown--><ul>
<li><strong>Type:</strong> <code>x509 Certificate</code></li>
<li><strong>Source:</strong>
<ul>
<li><strong>Signing:</strong> <code>My Private CVPN CA</code></li>
<li><strong>Signature algorithm:</strong> <code>SHA 512</code></li>
<li><strong>Template for new certificate:</strong> <code>[default] Empty template</code></li>
</ul>
</li>
<li><strong>Subject:</strong>
<ul>
<li><strong>Internal Name:</strong> <code>My Private CVPN - Server</code></li>
<li><strong>Distinguished Name:</strong>
<ul>
<li><strong>countryName:</strong> <code>GB</code></li>
<li><strong>stateOrProvinceName:</strong> <code>My State</code></li>
<li><strong>localityName:</strong> <code>My Locality</code></li>
<li><strong>organizationName:</strong> <code>My Company</code></li>
<li><strong>organizationalUnitName:</strong> <code>DevOps</code></li>
<li><strong>commonName:</strong> <code>cvpn-server.cvpn.cert.private.mydomain.com</code></li>
<li><strong>emailAddress:</strong> <code>devops@mydomain.com</code></li>
</ul>
</li>
<li><strong>Private Key:</strong>
<ul>
<li><strong>Name:</strong> <code>My Private CVPN - Server</code></li>
<li><strong>Type:</strong> <code>RSA</code></li>
<li><strong>Size:</strong> <code>2048 bit</code></li>
</ul>
</li>
</ul>
</li>
<li><strong>Extensions:</strong>
<ul>
<li><strong>x509v3 Basic Constraints:</strong>
<ul>
<li><strong>Type:</strong> <code>End Entity</code></li>
</ul>
</li>
<li><strong>Key Identifier:</strong>
<ul>
<li><code>x509v3 Subject Key Identifier</code></li>
<li><code>x509v3 Authority Key Identifier</code></li>
</ul>
</li>
<li><strong>Validity:</strong> (3 Years)
<ul>
<li><strong>Not Before:</strong> <code>2023-09-01 00:00 GMT</code></li>
<li><strong>Not After:</strong> <code>2026-08-31 23:59 GMT</code></li>
</ul>
</li>
</ul>
</li>
<li><strong>Key usage</strong>:
<ul>
<li><strong>x509v3 Key Usage:</strong>
<ul>
<li><code>Digital Signature</code></li>
</ul>
</li>
<li><strong>x509v3 Extended Key Usage:</strong>
<ul>
<li><code>TLS Web Server Authentication</code></li>
</ul>
</li>
</ul>
</li>
</ul>
<!--kg-card-end: markdown--><!--kg-card-begin: image--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.adamretter.org.uk/content/images/2023/09/Screenshot-2023-09-24-at-21.16.43.png" class="kg-image" alt="AWS Client VPN with Manually Provisioned Certificates using Terraform"><figcaption>CVPN Server certificate visible in XCA</figcaption></figure><!--kg-card-end: image--><p>If you prefer to generate the CVPN Server certificate with OpenSSL then you can use the following commands:</p><!--kg-card-begin: code--><figure class="kg-card kg-code-card"><pre><code class="language-bash">openssl genrsa -out cvpn-server.private.key.pem 2048

openssl req -new -config openssl.conf -section req_tls_server -sha512 \
    -subj "/C=GB/ST=My State/L=My Locality/O=My Company/OU=DevOps/CN=cvpn-server.cvpn.cert.private.mydomain.com/emailAddress=devops@mycompany.com" \
    -key cvpn-server.private.key.pem -out cvpn-server.csr.pem

openssl req -x509 -config openssl.conf -section req_tls_server -sha512 \
    -CA cvpn.ca.crt.pem -CAkey cvpn.ca.private.key.pem \
    -days 1095 -set_serial 0x4 -in cvpn-server.csr.pem -key cvpn-server.private.key.pem -out cvpn-server.crt.pem

cat root.ca.crt.pem intermediate.ca.crt.pem cvpn.ca.crt.pem &gt; cvpn-server.crt.chain.pem</code></pre><figcaption>Generating a CVPN Server certificate with OpenSSL</figcaption></figure><!--kg-card-end: code--><h3 id="cvpn-client-certificate-s-"><strong>CVPN Client Certificate(s)</strong></h3><p>To generate the endpoint CVPN Client certificate(s) that will enable a client (e.g. user) to connect to the CVPN Server, you may use the following as a template and repeat it for as many clients as you need; just replace <code>&lt;&lt;username&gt;&gt;</code> with the username of the user, etc.</p><p>In XCA you need to make sure you have the <code>My Private CVPN CA</code> certificate selected in the <code>Certificates</code> tab, and then press the <code>New Certificate</code> button. This presents a dialog with a number of tabs, that should be configured with the following information and settings, after which a new CVPN Client certificate will be created (as a subordinate of the CVPN CA) in the XCA database for you.</p><!--kg-card-begin: markdown--><ul>
<li><strong>Type:</strong> <code>x509 Certificate</code></li>
<li><strong>Source:</strong>
<ul>
<li><strong>Signing:</strong> <code>My Private CVPN CA</code></li>
<li><strong>Signature algorithm:</strong> <code>SHA 512</code></li>
<li><strong>Template for new certificate:</strong> <code>[default] Empty template</code></li>
</ul>
</li>
<li><strong>Subject:</strong>
<ul>
<li><strong>Internal Name:</strong> <code>My Private CVPN - User - &lt;&lt;username&gt;&gt;</code></li>
<li><strong>Distinguished Name:</strong>
<ul>
<li><strong>countryName:</strong> <code>&lt;&lt;countryCode&gt;&gt;</code></li>
<li><strong>stateOrProvinceName:</strong> <code>&lt;&lt;county&gt;&gt;</code></li>
<li><strong>localityName:</strong> <code>&lt;&lt;city&gt;&gt;</code></li>
<li><strong>organizationName:</strong> <code>&lt;&lt;organisation&gt;&gt;</code></li>
<li><strong>organizationalUnitName:</strong> <code>&lt;&lt;department&gt;&gt;</code></li>
<li><strong>commonName:</strong> <code>&lt;&lt;username&gt;&gt;.cvpn.cert.private.mydomain.com</code></li>
<li><strong>emailAddress:</strong> <code>&lt;&lt;email&gt;&gt;</code></li>
</ul>
</li>
<li><strong>Private Key:</strong>
<ul>
<li><strong>Name:</strong> <code>My Private CVPN - User - &lt;&lt;username&gt;&gt;</code></li>
<li><strong>Type:</strong> <code>RSA</code></li>
<li><strong>Size:</strong> <code>2048 bit</code></li>
</ul>
</li>
</ul>
</li>
<li><strong>Extensions:</strong>
<ul>
<li><strong>x509v3 Basic Constraints:</strong>
<ul>
<li><strong>Type:</strong> <code>End Entity</code></li>
</ul>
</li>
<li><strong>Key Identifier:</strong>
<ul>
<li><code>x509v3 Subject Key Identifier</code></li>
<li><code>x509v3 Authority Key Identifier</code></li>
</ul>
</li>
<li><strong>Validity:</strong> (1 Year)
<ul>
<li><strong>Not Before:</strong> <code>2023-09-01 00:00 GMT</code></li>
<li><strong>Not After:</strong> <code>2024-08-31 23:59 GMT</code></li>
</ul>
</li>
</ul>
</li>
<li><strong>Key usage</strong>:
<ul>
<li><strong>x509v3 Key Usage:</strong>
<ul>
<li><code>Digital Signature</code></li>
</ul>
</li>
<li><strong>x509v3 Extended Key Usage:</strong>
<ul>
<li><code>TLS Web Client Authentication</code></li>
</ul>
</li>
</ul>
</li>
</ul>
<!--kg-card-end: markdown--><!--kg-card-begin: image--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.adamretter.org.uk/content/images/2023/09/Screenshot-2023-09-24-at-21.25.37.png" class="kg-image" alt="AWS Client VPN with Manually Provisioned Certificates using Terraform"><figcaption>A CVPN client certificate for the user <code>aretter</code> visible in XCA</figcaption></figure><!--kg-card-end: image--><p>If you prefer to generate the CVPN Client certificate for the user with OpenSSL then you can use the following commands:</p><!--kg-card-begin: code--><figure class="kg-card kg-code-card"><pre><code class="language-bash">export CVPN_USERNAME=aretter

openssl genrsa -out $CVPN_USERNAME.private.key.pem 2048

openssl req -new -config openssl.conf -section req_tls_client -sha512 \
    -subj "/C=GB/ST=My State/L=My Locality/O=My Company/OU=DevOps/CN=$CVPN_USERNAME.cvpn.cert.private.mydomain.com/emailAddress=$CVPN_USERNAME@mycompany.com" \
    -key $CVPN_USERNAME.private.key.pem -out $CVPN_USERNAME.csr.pem

openssl req -x509 -config openssl.conf -section req_tls_client -sha512 \
    -CA cvpn.ca.crt.pem -CAkey cvpn.ca.private.key.pem \
    -days 365 -set_serial 0x5 -in $CVPN_USERNAME.csr.pem -key $CVPN_USERNAME.private.key.pem -out $CVPN_USERNAME.crt.pem</code></pre><figcaption>Generating a CVPN Server certificate with OpenSSL</figcaption></figure><!--kg-card-end: code--><h2 id="deploying-the-certificates-in-terraform">Deploying the Certificates in Terraform</h2><p>Now that we have generated CA certificates and endpoint certificates, we can now deploy them to ACM (AWS Certificate Manager) by using Terraform.</p><!--kg-card-begin: code--><figure class="kg-card kg-code-card"><pre><code class="language-hcl"># Deploy the Client VPN CA
data "local_sensitive_file" "my_private_cvpn_ca_private_key" {
  filename = ("cvpn.ca.private.key.pem")
}

data "tls_certificate" "my_private_cvpn_ca_certificate" {
  content = file("cvpn.ca.private.crt.pem")
}

data "tls_certificate" "my_private_cvpn_ca_chain_certificate" {
  content = file("cvpn.ca.private.crt.chain.pem")
}

resource "aws_acm_certificate" "my_private_cvpn_ca_certificate" {
  private_key       = data.local_sensitive_file.my_private_cvpn_ca_private_key.content
  certificate_body  = data.tls_certificate.my_private_cvpn_ca_certificate.content
  certificate_chain = data.tls_certificate.my_private_cvpn_ca_chain_certificate.content

  lifecycle {
    create_before_destroy = true
  }

  tags = {
    Environment = "cvpn"
  }
}


# Deploy the Client VPN Server certificate
data "local_sensitive_file" "my_private_cvpn_server_private_key" {
  filename = ("cvpn-server.private.key.pem")
}

data "tls_certificate" "my_private_cvpn_server_certificate" {
  content = file("cvpn-server.crt.pem")
}

data "tls_certificate" "my_private_cvpn_server_chain_certificate" {
  content = file("cvpn-server.crt.chain.pem")
}

resource "aws_acm_certificate" "my_private_cvpn_server_certificate" {
  private_key       = data.local_sensitive_file.my_private_cvpn_server_private_key.content
  certificate_body  = data.tls_certificate.my_private_cvpn_server_certificate.content
  certificate_chain = data.tls_certificate.my_private_cvpn_server_chain_certificate.content

  lifecycle {
    create_before_destroy = true
  }

  tags = {
    Environment = "cvpn"
  }
}</code></pre><figcaption>Terraform code to deploy the Client VPN Certificates to AWS Certificate Manager</figcaption></figure><!--kg-card-end: code--><p>Note that as the client (e.g. user) certificates are signed by the CVPN CA (i.e. the same CA as used by the Client VPN and the Client VPN Server certificate), they do not need to be uploaded into AWS ACM.</p><h2 id="creating-the-client-vpn-in-terraform">Creating the Client VPN in Terraform</h2><p>Now that we have the CA certificates and endpoint certificates in place, we can create and configure the AWS Client VPN by using Terraform.</p><!--kg-card-begin: code--><figure class="kg-card kg-code-card"><pre><code class="language-hcl">resource "aws_ec2_client_vpn_endpoint" "cvpn" {
  description = "Client VPN"

  vpc_id = &lt;YOUR VPC ID&gt;

  client_cidr_block = "192.168.68.0/22"
  split_tunnel      = true

  server_certificate_arn = aws_acm_certificate.my_private_cvpn_server_certificate.arn

  authentication_options {
    type                       = "certificate-authentication"
    root_certificate_chain_arn = aws_acm_certificate.my_private_cvpn_ca_certificate.arn
  }

  self_service_portal = "disabled"

  security_group_ids = [
    module.cvpn_access_security_group.security_group_id
  ]

  tags = {
    Name        = "cvpn_endpoint"
    Environment = "cvpn"
  }
}

resource "aws_ec2_client_vpn_network_association" "cvpn" {
  count = 1

  client_vpn_endpoint_id = aws_ec2_client_vpn_endpoint.cvpn.id
  subnet_id              = &lt;ID OF YOUR IPv4 SUBNET WHERE THE CLIENT VPN WILL TERMINATE&gt;

  lifecycle {
    // The issue why we are ignoring changes is that on every change
    // terraform screws up most of the cvpn assosciations
    // see: https://github.com/hashicorp/terraform-provider-aws/issues/14717
    ignore_changes = [subnet_id]
  }
}

resource "aws_ec2_client_vpn_authorization_rule" "cvpn_auth" {
  client_vpn_endpoint_id = aws_ec2_client_vpn_endpoint.cvpn.id
  target_network_cidr    = &lt;YOUR IPv4 SUBNET WHERE THE CLIENT VPN WILL TERMINATE&gt;
  authorize_all_groups   = true
}


module "cvpn_access_security_group" {
  source  = "terraform-aws-modules/security-group/aws"
  version = "4.17.2"

  name        = "cvpn_access_security_group"
  description = "Security group for CVPN Access"

  vpc_id = &lt;YOUR VPC ID&gt;

  computed_ingress_with_cidr_blocks = [
    {
      description = "VPN TLS"
      from_port   = 443
      to_port     = 443
      protocol    = "udp"
      cidr_blocks = &lt;YOUR IPv4 SUBNET WHERE THE CLIENT VPN WILL TERMINATE&gt;
    }
  ]
  number_of_computed_ingress_with_cidr_blocks = 1

  computed_ingress_with_ipv6_cidr_blocks = [
    {
      description      = "VPN TLS (IPv6)"
      from_port        = 443
      to_port          = 443
      protocol         = "udp"
      ipv6_cidr_blocks = &lt;YOUR IPv6 SUBNET WHERE THE CLIENT VPN WILL TERMINATE&gt;
    }
  ]
  number_of_computed_ingress_with_ipv6_cidr_blocks = 1

  egress_with_cidr_blocks = [
    {
      description = "All"
      from_port   = -1
      to_port     = -1
      protocol    = -1
      cidr_blocks = "0.0.0.0/0"
    }
  ]

  egress_with_ipv6_cidr_blocks = [
    {
      description = "All (IPv6)"
      from_port   = -1
      to_port     = -1
      protocol    = -1
      cidr_blocks = "2001:db8::/64"
    }
  ]

  tags = {
    Name        = "sg_cvpn"
    Type        = "security_group"
    Environment = "cvpn"
  }
}</code></pre><figcaption>Terraform code for a Client VPN</figcaption></figure><!--kg-card-end: code--><p>The following placeholders in the code above need to be filled out with your own Terraform AWS configuration values:</p><ul><li><code>&lt;YOUR VPC ID&gt;</code> - the AWS ID of your VPC in which you wish to deploy the Client VPN.</li><li><code>&lt;ID OF YOUR IPv4 SUBNET WHERE THE CLIENT VPN WILL TERMINATE&gt;</code> - the AWS ID of the IPv4 Subnet within your VPC where you wish VPN traffic to arrive to.</li><li><code>&lt;YOUR IPv4 SUBNET WHERE THE CLIENT VPN WILL TERMINATE&gt;</code> -  the IPv4 Subnet CIDR address within your VPC where you wish VPN traffic to arrive to.</li><li><code>&lt;YOUR IPv6 SUBNET WHERE THE CLIENT VPN WILL TERMINATE&gt;</code> - the IPv6 Subnet CIDR address within your VPC where you wish VPN traffic to arrive to.</li></ul><h2 id="connecting-to-the-client-vpn"><strong>Connecting to the Client VPN</strong></h2><p>Once you have the above setup you can download a skeleton OpenVPN configuration file from the AWS Dashboard.</p><!--kg-card-begin: image--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.adamretter.org.uk/content/images/2023/09/Screenshot-2023-09-11-at-13.34.28-1.png" class="kg-image" alt="AWS Client VPN with Manually Provisioned Certificates using Terraform"><figcaption>Download OpenVPN Configuration file from AWS</figcaption></figure><!--kg-card-end: image--><p>The OpenVPN configuration file provided by AWS will be incomplete, and will something look like this:</p><!--kg-card-begin: code--><figure class="kg-card kg-code-card"><pre><code class="language-ovpn">client
dev tun
proto udp
remote cvpn-endpoint-006d2181ae8616b54.prod.clientvpn.eu-west-2.amazonaws.com 443
remote-random-hostname
resolv-retry infinite
nobind
remote-cert-tls server
cipher AES-256-GCM
verb 3
&lt;ca&gt;
-----BEGIN CERTIFICATE-----
SECRET STUFF HERE ;-)
-----END CERTIFICATE-----

&lt;/ca&gt;


reneg-sec 0

verify-x509-name cvpn-server.cvpn.cert.private.mydomain.com name</code></pre><figcaption>AWS provided OpenVPN configuration file</figcaption></figure><!--kg-card-end: code--><p>You will need to modify it to add:</p><ol><li>A <code>&lt;cert&gt;</code> section containing the certificate for one of the CVPN Client certificate(s) that you generated above with XCA or OpenSSL.</li><li>A <code>&lt;key&gt;</code> section containing the private key for the CVPN Client certificate that you are using.</li></ol><p>You should then have a complete OpenVPN configuration file that looks similar to this:</p><!--kg-card-begin: code--><figure class="kg-card kg-code-card"><pre><code class="language-ovpn">client
dev tun
proto udp
remote cvpn-endpoint-006d2181ae8616b54.prod.clientvpn.eu-west-2.amazonaws.com 443
remote-random-hostname
resolv-retry infinite
nobind
remote-cert-tls server
cipher AES-256-GCM
verb 3
&lt;ca&gt;
-----BEGIN CERTIFICATE-----
SECRET STUFF HERE ;-)
-----END CERTIFICATE-----

&lt;/ca&gt;
&lt;cert&gt;
-----BEGIN CERTIFICATE-----
SECRET STUFF HERE ;-)
-----END CERTIFICATE-----

&lt;/cert&gt;
&lt;key&gt;
-----BEGIN RSA PRIVATE KEY-----
SECRET STUFF HERE ;-)
-----END RSA PRIVATE KEY-----

&lt;/key&gt;

reneg-sec 0

verify-x509-name cvpn-server.cvpn.cert.private.mydomain.com name</code></pre><figcaption>Complete OpenVPN configuration file</figcaption></figure><!--kg-card-end: code--><p>You may now use your favourite OpenVPN client tool, with the above OpenVPN configuration file to connect to your new Client VPN; I personally use <a href="https://tunnelblick.net/">Tunnelblick</a>. Enjoy!</p>]]></content:encoded></item><item><title><![CDATA[AWS Client VPN with Managed Certificates using Terraform]]></title><description><![CDATA[<p>This is a brief explanation of how to use Terraform to setup an AWS CVPN (Client VPN) where the certificates (for VPN authentication) are entirely generated and managed by AWS.</p><p>The advantage of using a managed certificates approach is that you need not generate or directly manage any certificates or</p>]]></description><link>https://blog.adamretter.org.uk/terraform-aws-client-vpn-with-managed-certificates/</link><guid isPermaLink="false">64fede79312ce60ea93d6eb0</guid><category><![CDATA[Terraform]]></category><category><![CDATA[AWS]]></category><category><![CDATA[VPN]]></category><dc:creator><![CDATA[Adam Retter]]></dc:creator><pubDate>Mon, 11 Sep 2023 12:58:48 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1605003823507-22247a88bf4d?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDg0fHxjZXJ0aWZpY2F0ZXxlbnwwfHx8fDE2OTQ0MzcwNzd8MA&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=1080" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1605003823507-22247a88bf4d?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wxMTc3M3wwfDF8c2VhcmNofDg0fHxjZXJ0aWZpY2F0ZXxlbnwwfHx8fDE2OTQ0MzcwNzd8MA&ixlib=rb-4.0.3&q=80&w=1080" alt="AWS Client VPN with Managed Certificates using Terraform"><p>This is a brief explanation of how to use Terraform to setup an AWS CVPN (Client VPN) where the certificates (for VPN authentication) are entirely generated and managed by AWS.</p><p>The advantage of using a managed certificates approach is that you need not generate or directly manage any certificates or private keys manually. To avoid such manual processes, this approach makes use of AWS Private CA (Certificate Authority) and AWS Certificate Manager. The main disadvantage of this approach is that AWS Private CA is an expensive proposition, at the time of writing, it is priced at $400 / Month / CA Certificate; we will use two CA Certificates (although you could just use one if you wish), at a total cost of $800 / Month.</p><p>If you want to avoid the cost of AWS Private CA, you can take an alternative approach where you manually provision and manage the certificates. You can find details of how to achieve that in my blog article - <a href="https://blog.adamretter.org.uk/terraform-aws-client-vpn-with-openssl-certificates/">AWS Client VPN with Manually Provisioned Certificates using Terraform</a><strong>.</strong></p><h2 id="chain-of-trust">Chain of Trust</h2><p>We will create a number of certificates that will form a chain of trust. Each subordinate certificate is signed by its parent certificate. Our chain of trust will look like:</p><ol><li>Root CA Certificate.</li><li>CVPN Server Certificate - this endpoint certificate is for the CVPN server itself in AWS. This certificate is signed by the Root CA Certificate.</li><li>CVPN Client CA Certificate - this CA certificate is used for issuing and signing certificates that are used by clients (e.g. users) to connect to the CVPN Server. This certificate is also signed by the Root CA Certificate.</li><li>CVPN Root Client Certificate - this endpoint certificate is for configuring the CVPN server with a client certificate. This certificate is signed by the CVPN Client CA Certificate. This certificate is needed due to a peculiarity with how AWS Client VPN is configured in AWS; their CVPN Server configuration requires a client certificate so that it can access the chain-of-trust for the client certificates (e.g. Client CA Certificate and Root CA certificate)! No client (e.g. user) will actually ever use this certificate directly.</li><li>CVPN Client Certificate - we create one of these endpoint certificates for each client (e.g. user) that wishes to connect to the CPVN Server. These certificates will also be signed by the CVPN Client CA Certificate.</li></ol><!--kg-card-begin: image--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.adamretter.org.uk/content/images/2023/09/AWS-Client-VPN-Managed-Certificates--1-.png" class="kg-image" alt="AWS Client VPN with Managed Certificates using Terraform"><figcaption>Chain of Trust diagram</figcaption></figure><!--kg-card-end: image--><p>The purpose of having a separate Root CA and CVPN Client CA is that we have delegated trust from the Root to the CVPN. We can therefore easily isolate the CVPN if needed and manage CVPN certificate issuance and revocation completely separately to the Root. In future it may be desirable to create further delegated CAs from the Root CA for other purposes.</p><h2 id="creating-the-certificates-in-terraform">Creating the Certificates in Terraform</h2><p>We can create all of the CA certificates and endpoint certificates, and store them in AWS Private CA and AWS Certificate Manager by using Terraform.</p><h3 id="terraform-root-ca">Terraform Root CA</h3><p>The Terraform code below will create a Root CA and certificate:</p><!--kg-card-begin: code--><figure class="kg-card kg-code-card"><pre><code class="language-hcl">resource "aws_acmpca_certificate_authority" "root_ca" {
  type = "ROOT"

  certificate_authority_configuration {
    key_algorithm     = "RSA_4096"
    signing_algorithm = "SHA512WITHRSA"

    subject {
      common_name         = "root.ca.cert.private.mydomain.com"
      organizational_unit = "DevOps"
      organization        = "My Company"
      locality            = "My City"
      state               = "My State"
      country             = "GB"
    }
  }

  tags = {
    Name        = "certificate_authority"
    Environment = "cvpn"
  }
}

resource "aws_acmpca_certificate" "root_ca_certificate" {
  certificate_authority_arn   = aws_acmpca_certificate_authority.root_ca.arn
  certificate_signing_request = aws_acmpca_certificate_authority.root_ca.certificate_signing_request
  signing_algorithm           = "SHA512WITHRSA"

  template_arn = "arn:${data.aws_partition.current.partition}:acm-pca:::template/RootCACertificate/V1"

  validity {
    type  = "YEARS"
    value = 5
  }
}

resource "aws_acmpca_certificate_authority_certificate" "root_ca_certificate_association" {
  certificate_authority_arn = aws_acmpca_certificate_authority.root_ca.arn

  certificate       = aws_acmpca_certificate.root_ca_certificate.certificate
  certificate_chain = aws_acmpca_certificate.root_ca_certificate.certificate_chain
}</code></pre><figcaption>Terraform code for a Root CA with AWS Private CA</figcaption></figure><!--kg-card-end: code--><p>Note that the <code>type</code> of the <code>aws_acmpca_certificate_authority</code> resource is set to <code>ROOT</code>.</p><h3 id="terraform-cvpn-server-certificate">Terraform CVPN Server Certificate</h3><p>The Terraform code below will create a certificate for use as the CVPN Server certificate:</p><!--kg-card-begin: code--><figure class="kg-card kg-code-card"><pre><code class="language-hcl">resource "tls_private_key" "cvpn_server_certificate_private_key" {
  algorithm = "RSA"
  rsa_bits  = "2048"
}

resource "tls_cert_request" "cvpn_server_certificate_signing_request" {
  private_key_pem = tls_private_key.cvpn_server_certificate_private_key.private_key_pem

  subject {
    common_name         = "cvpn-server.cvpn.cert.private.mydomain.com"
    organizational_unit = "DevOps"
    organization        = "My Company"
    street_address      = ["My Street"]
    locality            = "My City"
    state               = "My State"
    country             = "GB"
    postal_code         = "XX1 2XX"
  }
}

resource "aws_acmpca_certificate" "cvpn_server_certificate" {
  certificate_authority_arn   = aws_acmpca_certificate_authority.root_ca.arn
  certificate_signing_request = tls_cert_request.cvpn_server_certificate_signing_request.cert_request_pem
  signing_algorithm           = "SHA512WITHRSA"
  validity {
    type  = "YEARS"
    value = 3
  }
}

resource "aws_acm_certificate" "cvpn_server_certificate" {
  private_key       = tls_private_key.cvpn_server_certificate_private_key.private_key_pem
  certificate_body  = aws_acmpca_certificate.cvpn_server_certificate.certificate
  certificate_chain = aws_acmpca_certificate.cvpn_server_certificate.certificate_chain

  lifecycle {
    create_before_destroy = true
  }

  tags = {
    Name        = "certificate"
    Scope       = "cvpn_server"
    Environment = "cvpn"
  }
}</code></pre><figcaption>Terraform code for a CVPN Server certificate</figcaption></figure><!--kg-card-end: code--><p>Note that the <code>certificate_authority_arn</code> of the <code>aws_acmpca_certificate</code> resource is set to the ARN (Amazon Resource Name) of our previously created Root CA, i.e.: <code>aws_acmpca_certificate_authority.root_ca.arn</code>.</p><h3 id="terraform-cvpn-client-ca">Terraform CVPN Client CA</h3><p>The Terraform code below will create a CVPN Client CA and certificate delegated from the Root CA:</p><!--kg-card-begin: code--><figure class="kg-card kg-code-card"><pre><code class="language-hcl">resource "aws_acmpca_certificate_authority" "cvpn_client_ca" {
  type = "SUBORDINATE"

  certificate_authority_configuration {
    key_algorithm     = "RSA_4096"
    signing_algorithm = "SHA512WITHRSA"

    subject {
      common_name         = "cvpn-client.ca.cert.private.mydomain.com"
      organizational_unit = "DevOps"
      organization        = "My Company"
      locality            = "My City"
      state               = "My State"
      country             = "GB"
    }
  }

  tags = {
    Name        = "certificate_authority"
    Environment = "cvpn"
  }
}

resource "aws_acmpca_certificate" "cvpn_client_ca_certificate" {
  certificate_authority_arn   = aws_acmpca_certificate_authority.root_ca.arn
  certificate_signing_request = aws_acmpca_certificate_authority.cvpn_client_ca.certificate_signing_request
  signing_algorithm           = "SHA512WITHRSA"

  template_arn = "arn:${data.aws_partition.current.partition}:acm-pca:::template/SubordinateCACertificate_PathLen0/V1"

  validity {
    type  = "YEARS"
    value = 3
  }
}

resource "aws_acmpca_certificate_authority_certificate" "cvpn_client_ca_certificate_association" {
  certificate_authority_arn = aws_acmpca_certificate_authority.cvpn_client_ca.arn

  certificate       = aws_acmpca_certificate.cvpn_client_ca_certificate.certificate
  certificate_chain = aws_acmpca_certificate.cvpn_client_ca_certificate.certificate_chain
}</code></pre><figcaption>Terraform code for a subordinate CVPN Client CA with AWS Private CA</figcaption></figure><!--kg-card-end: code--><p>Note that the <code>type</code> of the <code>aws_acmpca_certificate_authority</code> resource is set to <code>SUBORDINATE</code>, and that the <code>certificate_authority_arn</code> of the <code>aws_acmpca_certificate</code> resource is set to the ARN (Amazon Resource Name) of our previously created Root CA, i.e.: <code>aws_acmpca_certificate_authority.root_ca.arn</code>.</p><h3 id="terraform-cvpn-root-client-certificate">Terraform CVPN Root Client Certificate</h3><p>The Terraform code below will create a CVPN Root Client Certificate. It will only be used as part of the CVPN Server configuration in AWS.</p><!--kg-card-begin: code--><figure class="kg-card kg-code-card"><pre><code class="language-hcl">resource "tls_private_key" "root_user_cvpn_client_certificate_private_key" {
  algorithm = "RSA"
  rsa_bits  = "2048"
}

resource "tls_cert_request" "root_user_cvpn_client_certificate_signing_request" {
  private_key_pem = tls_private_key.root_user_cvpn_client_certificate_private_key.private_key_pem

  subject {
    common_name         = "root-user.cvpn.cert.private.mydomain.com"
    organizational_unit = "DevOps"
    organization        = "My Company"
    street_address      = ["My Street"]
    locality            = "My City"
    state               = "My State"
    country             = "GB"
    postal_code         = "XX1 2XX"
  }
}

resource "aws_acmpca_certificate" "root_user_cvpn_client_certificate" {
  certificate_authority_arn   = aws_acmpca_certificate_authority.cvpn_client_ca.arn
  certificate_signing_request = tls_cert_request.root_user_cvpn_client_certificate_signing_request.cert_request_pem
  signing_algorithm           = "SHA512WITHRSA"
  validity {
    type  = "YEARS"
    value = 1
  }
}

resource "aws_acm_certificate" "root_user_cvpn_client_certificate" {
  private_key       = tls_private_key.root_user_cvpn_client_certificate_private_key.private_key_pem
  certificate_body  = aws_acmpca_certificate.root_user_cvpn_client_certificate.certificate
  certificate_chain = aws_acmpca_certificate.root_user_cvpn_client_certificate.certificate_chain

  lifecycle {
    create_before_destroy = true
  }

  tags = {
    Name        = "certificate"
    Scope       = "cvpn_server"
    Environment = "cvpn"
  }
}</code></pre><figcaption>Terraform code for a CVPN Root Client Certificate</figcaption></figure><!--kg-card-end: code--><p>Note that the <code>certificate_authority_arn</code> of the <code>aws_acmpca_certificate</code> resource is set to the ARN (Amazon Resource Name) of our previously created CVPN Client CA, i.e.: <code>aws_acmpca_certificate_authority.cvpn_client_ca.arn</code>.</p><h3 id="terraform-cvpn-client-certificate-s-">Terraform CVPN Client Certificate(s)</h3><p>The Terraform code below will create a CVPN Client Certificate that will enable a client (e.g. user) to connect to the CVPN Server. You may use this as a template and repeat it for as many clients as you need.</p><!--kg-card-begin: code--><figure class="kg-card kg-code-card"><pre><code class="language-hcl">resource "tls_private_key" "user_1_cvpn_client_certificate_private_key" {
  algorithm = "RSA"
  rsa_bits  = "2048"
}

resource "tls_cert_request" "user_1_cvpn_client_certificate_signing_request" {
  private_key_pem = tls_private_key.user_1_cvpn_client_certificate_private_key.private_key_pem

  subject {
    common_name         = "user-1.cvpn.cert.private.mydomain.com"
    organizational_unit = "DevOps"
    organization        = "My Company"
    street_address      = ["My Street"]
    locality            = "My City"
    state               = "My State"
    country             = "GB"
    postal_code         = "XX1 2XX"
  }
}

resource "aws_acmpca_certificate" "user_1_cvpn_client_certificate" {
  certificate_authority_arn   = aws_acmpca_certificate_authority.cvpn_client_ca.arn
  certificate_signing_request = tls_cert_request.user_1_cvpn_client_certificate_signing_request.cert_request_pem
  signing_algorithm           = "SHA512WITHRSA"
  validity {
    type  = "YEARS"
    value = 1
  }
}

resource "aws_acm_certificate" "user_1_cvpn_client_certificate" {
  private_key       = tls_private_key.user_1_cvpn_client_certificate_private_key.private_key_pem
  certificate_body  = aws_acmpca_certificate.user_1_cvpn_client_certificate.certificate
  certificate_chain = aws_acmpca_certificate.user_1_cvpn_client_certificate.certificate_chain

  lifecycle {
    create_before_destroy = true
  }

  tags = {
    Name        = "certificate"
    Scope       = "cvpn_client"
    Environment = "cvpn"
  }
}</code></pre><figcaption>Terraform code to produce a CVPN Client (e.g. User) certificate</figcaption></figure><!--kg-card-end: code--><p>Note that the <code>certificate_authority_arn</code> of the <code>aws_acmpca_certificate</code> resource is set to the ARN (Amazon Resource Name) of our previously created CVPN Client CA, i.e.: <code>aws_acmpca_certificate_authority.cvpn_client_ca.arn</code>.</p><h2 id="creating-the-client-vpn-in-terraform">Creating the Client VPN in Terraform</h2><p>Now that we have the CA certificates and endpoint certificates in place, we can create and configure the AWS Client VPN by using Terraform.</p><!--kg-card-begin: code--><figure class="kg-card kg-code-card"><pre><code class="language-hcl">
resource "aws_ec2_client_vpn_endpoint" "cvpn" {
  description = "Client VPN"

  vpc_id = &lt;YOUR VPC ID&gt;

  client_cidr_block = "192.168.68.0/22"
  split_tunnel      = true

  server_certificate_arn = aws_acm_certificate.cvpn_server_certificate.arn

  authentication_options {
    type                       = "certificate-authentication"
    root_certificate_chain_arn = aws_acm_certificate.root_user_cvpn_client_certificate.arn
  }

  self_service_portal = "disabled"

  security_group_ids = [
    module.cvpn_access_security_group.security_group_id
  ]

  tags = {
    Name        = "cvpn_endpoint"
    Environment = "cvpn"
  }
}

resource "aws_ec2_client_vpn_network_association" "cvpn" {
  count = 1

  client_vpn_endpoint_id = aws_ec2_client_vpn_endpoint.cvpn.id
  subnet_id              = &lt;ID OF YOUR IPv4 SUBNET WHERE THE CLIENT VPN WILL TERMINATE&gt;

  lifecycle {
    // The issue why we are ignoring changes is that on every change
    // terraform screws up most of the cvpn assosciations
    // see: https://github.com/hashicorp/terraform-provider-aws/issues/14717
    ignore_changes = [subnet_id]
  }
}

resource "aws_ec2_client_vpn_authorization_rule" "cvpn_auth" {
  client_vpn_endpoint_id = aws_ec2_client_vpn_endpoint.cvpn.id
  target_network_cidr    = &lt;YOUR IPv4 SUBNET WHERE THE CLIENT VPN WILL TERMINATE&gt;
  authorize_all_groups   = true
}


module "cvpn_access_security_group" {
  source  = "terraform-aws-modules/security-group/aws"
  version = "4.17.2"

  name        = "cvpn_access_security_group"
  description = "Security group for CVPN Access"

  vpc_id = &lt;YOUR VPC ID&gt;

  computed_ingress_with_cidr_blocks = [
    {
      description = "VPN TLS"
      from_port   = 443
      to_port     = 443
      protocol    = "udp"
      cidr_blocks = &lt;YOUR IPv4 SUBNET WHERE THE CLIENT VPN WILL TERMINATE&gt;
    }
  ]
  number_of_computed_ingress_with_cidr_blocks = 1

  computed_ingress_with_ipv6_cidr_blocks = [
    {
      description      = "VPN TLS (IPv6)"
      from_port        = 443
      to_port          = 443
      protocol         = "udp"
      ipv6_cidr_blocks = &lt;YOUR IPv6 SUBNET WHERE THE CLIENT VPN WILL TERMINATE&gt;
    }
  ]
  number_of_computed_ingress_with_ipv6_cidr_blocks = 1

  egress_with_cidr_blocks = [
    {
      description = "All"
      from_port   = -1
      to_port     = -1
      protocol    = -1
      cidr_blocks = "0.0.0.0/0"
    }
  ]

  egress_with_ipv6_cidr_blocks = [
    {
      description = "All (IPv6)"
      from_port   = -1
      to_port     = -1
      protocol    = -1
      cidr_blocks = "2001:db8::/64"
    }
  ]

  tags = {
    Name        = "sg_cvpn"
    Type        = "security_group"
    Environment = "cvpn"
  }
}</code></pre><figcaption>Terraform code for a Client VPN</figcaption></figure><!--kg-card-end: code--><p>The following placeholders in the code above need to be filled out with your own Terraform AWS configuration values:</p><ul><li><code>&lt;YOUR VPC ID&gt;</code> - the AWS ID of your VPC in which you wish to deploy the Client VPN.</li><li><code>&lt;ID OF YOUR IPv4 SUBNET WHERE THE CLIENT VPN WILL TERMINATE&gt;</code> - the AWS ID of the IPv4 Subnet within your VPC where you wish VPN traffic to arrive to.</li><li><code>&lt;YOUR IPv4 SUBNET WHERE THE CLIENT VPN WILL TERMINATE&gt;</code> -  the IPv4 Subnet CIDR address within your VPC where you wish VPN traffic to arrive to.</li><li><code>&lt;YOUR IPv6 SUBNET WHERE THE CLIENT VPN WILL TERMINATE&gt;</code> - the IPv6 Subnet CIDR address within your VPC where you wish VPN traffic to arrive to.</li></ul><h2 id="connecting-to-the-client-vpn">Connecting to the Client VPN</h2><p>Once you have the above setup you can download a skeleton OpenVPN configuration file from the AWS Dashboard.</p><!--kg-card-begin: image--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.adamretter.org.uk/content/images/2023/09/Screenshot-2023-09-11-at-13.34.28.png" class="kg-image" alt="AWS Client VPN with Managed Certificates using Terraform"><figcaption>Download OpenVPN Configuration file from AWS</figcaption></figure><!--kg-card-end: image--><p>The OpenVPN configuration file provided by AWS will be incomplete, and will something look like this:</p><!--kg-card-begin: code--><figure class="kg-card kg-code-card"><pre><code class="language-ovpn">client
dev tun
proto udp
remote cvpn-endpoint-006d2181ae8616b54.prod.clientvpn.eu-west-2.amazonaws.com 443
remote-random-hostname
resolv-retry infinite
nobind
remote-cert-tls server
cipher AES-256-GCM
verb 3
&lt;ca&gt;
-----BEGIN CERTIFICATE-----
SECRET STUFF HERE ;-)
-----END CERTIFICATE-----

&lt;/ca&gt;


reneg-sec 0

verify-x509-name cvpn-server.cvpn.cert.private.mydomain.com name</code></pre><figcaption>AWS provided OpenVPN configuration file</figcaption></figure><!--kg-card-end: code--><p><br>You will need to modify it to add:</p><ol><li>A <code>&lt;cert&gt;</code> section containing the certificate for one of the CVPN Client Certificate(s) that you generated above.</li><li>A <code>&lt;key&gt;</code> section containing the private key for the CVPN Client Certificate that you are using.</li></ol><p>Note that the values needed for the <code>&lt;cert&gt;</code> and <code>&lt;key&gt;</code> can be found in your <code>terraform.tfstate</code> file. You should then have a complete OpenVPN configuration file that looks similar to this:</p><!--kg-card-begin: code--><figure class="kg-card kg-code-card"><pre><code class="language-ovpn">client
dev tun
proto udp
remote cvpn-endpoint-006d2181ae8616b54.prod.clientvpn.eu-west-2.amazonaws.com 443
remote-random-hostname
resolv-retry infinite
nobind
remote-cert-tls server
cipher AES-256-GCM
verb 3
&lt;ca&gt;
-----BEGIN CERTIFICATE-----
SECRET STUFF HERE ;-)
-----END CERTIFICATE-----

&lt;/ca&gt;
&lt;cert&gt;
-----BEGIN CERTIFICATE-----
SECRET STUFF HERE ;-)
-----END CERTIFICATE-----

&lt;/cert&gt;
&lt;key&gt;
-----BEGIN RSA PRIVATE KEY-----
SECRET STUFF HERE ;-)
-----END RSA PRIVATE KEY-----

&lt;/key&gt;

reneg-sec 0

verify-x509-name cvpn-server.cvpn.cert.private.mydomain.com name</code></pre><figcaption>Complete OpenVPN configuration file</figcaption></figure><!--kg-card-end: code--><p>You may now use your favourite OpenVPN client tool, with the above OpenVPN configuration file to connect to your new Client VPN; I personally use <a href="https://tunnelblick.net/">Tunnelblick</a>. Enjoy!</p>]]></content:encoded></item><item><title><![CDATA[Processing Historical Dates]]></title><description><![CDATA[In Project Omega at The National Archives processing of Historical Dates is complicated if the context of the dates is unknown. Software Engineers must consider, Calendars, Time-zones, and Geography. This is further compounded when tools (e.g. Java) take different/unexpected approaches to dates.]]></description><link>https://blog.adamretter.org.uk/processing-historical-dates/</link><guid isPermaLink="false">60461bff55477602318d66ae</guid><category><![CDATA[Java]]></category><category><![CDATA[RDF]]></category><category><![CDATA[dates]]></category><category><![CDATA[calendars]]></category><category><![CDATA[catalogue]]></category><category><![CDATA[The National Archives]]></category><dc:creator><![CDATA[Adam Retter]]></dc:creator><pubDate>Fri, 04 Jun 2021 10:41:00 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1582757557019-d878177b9e1b?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDF8fG9ycmVyeSx8ZW58MHx8fHwxNjE1MjQzMDM2&amp;ixlib=rb-1.2.1&amp;q=80&amp;w=1080" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1582757557019-d878177b9e1b?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMTc3M3wwfDF8c2VhcmNofDF8fG9ycmVyeSx8ZW58MHx8fHwxNjE1MjQzMDM2&ixlib=rb-1.2.1&q=80&w=1080" alt="Processing Historical Dates"><p>Phase 2 of <a href="https://www.nationalarchives.gov.uk/about/our-role/plans-policies-performance-and-projects/our-plans/our-digital-cataloguing-practices/project-omega/">Project Omega</a> at TNA (<a href="https://www.nationalarchives.gov.uk">The National Archives</a>) commenced at the end of January 2021, and our first goal was to perform a large export of their Catalogue Records from their previous system, ILDB (Microsoft SQL Server), into our new pan-archival RDF data model. I <a href="https://blog.adamretter.org.uk/rdf-plugins-for-pentaho-kettle/">previously discussed</a> the tooling that we were building to enable such ETL (Extract Transform Load) processes.</p><p>Whilst improving the ETL pipeline I experienced some interesting problems when trying to parse and process what I believed to be already computed dates from ILDB. These values that looked like dates suitable for computation were expressed as serial numeric values, for example in that scheme the 17th March 2021, would be expressed as <code>20210317</code>.<br><br>I later found out that even before the year 2000, The National Archives had already encountered this problem. Difficulties that arose previously from attempting to store historical dates as computable dates, gave rise to the decision at that time to store them as text and to convert regnal years (e.g. 3 Eliz I) into serial numeric values rather than computable date values.</p><p>The existing data in the ILDB system should adhere to TNA-CS13 (<br>The National Archives - Cataloguing Standards, June 2013) specification, which itself is an extension of <a href="https://www.ica.org/en/isadg-general-international-standard-archival-description-second-edition">ISAD(G)</a> (General International Standard Archival Description). </p><h3 id="covering-dates">Covering Dates</h3><p>The date Data Elements that I was trying to process are known in TNA-CS13 as the <code>Covering Dates</code>, and are described as:</p><blockquote>Identifies and records the date(s) of creation of the records being described.</blockquote><p>As a non-archivist, I personally find the term Covering Dates non-intuitive! The first time I encountered it some years ago I incorrectly assumed that it was the period of time discussed in the records, as opposed to the creation date(s) of the records themselves.</p><p>The expert archivist however exactly understands the concept of Covering Dates, and I am told that:</p><blockquote>An archival catalogue describes records/documents not historical events. The catalogue is agnostic to historical events and so is the archivist. All intrinsic metadata in the catalogue refers to the record. There may be extrinsic metadata providing contextual information (generally added later as a result of enrichment) but the dates refer to the records.</blockquote><p>Further explanation of how to complete the Covering Dates is also provided:</p><blockquote>"Give the covering dates of the creation of the records within the unit of description as a single calendar date or range of dates as appropriate."</blockquote><p>Within the database behind ILDB, the Covering Dates are stored using 3 fields:</p><ol><li><code>date_text</code> This is a textual description of the dates as they appear in the document or file (or the metadata for a born-digital folder file or folder). It outlines the date or range of dates when the document or file was created or accumulated.</li><li><code>first_date</code> This is the earliest creation date of any record within the unit of description. It is stored as a serial numeric value of the form <code>yyyyMMdd</code>.</li><li><code>last_date</code> If the unit of description encompasses more than one record and those records have different creating dates, then this is the latest creation date of any record within that unit. If there is only a single record, or all creation dates are the same within the unit, then this duplicates the <code>first_date</code>. It is stored as a serial numeric value of the form <code>yyyyMMdd</code>.</li></ol><!--kg-card-begin: image--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.adamretter.org.uk/content/images/2021/03/ildb-covering-dates-fields.png" class="kg-image" alt="Processing Historical Dates"><figcaption>ILDB Item Level Fields (Covering Date fields highlighted)</figcaption></figure><!--kg-card-end: image--><p>The system attempts to infer the <code>first_date</code> and <code>last_date</code> fields from the <code>date_text</code> as explained in TNA-CS13, however ultimately the archivist can override this behaviour manually:</p><blockquote>Dates must be entered in a particular format because the covering date format automatically generates numeric start and end dates in the catalogue in order to enable date searching.</blockquote><p>Data for archival catalogues was often generated through the conversion of paper lists into digital form. In the days before the internet, archivists typed the calendar year before the month and the day as this helped readers to rule out or identify relevant information faster. If you are familiar with <a href="https://en.wikipedia.org/wiki/ISO_8601">ISO 8601</a> style dates, then this will be somewhat familiar to you already.</p><p>Some example Covering Dates:</p><!--kg-card-begin: image--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.adamretter.org.uk/content/images/2021/03/Example-covering-dates.png" class="kg-image" alt="Processing Historical Dates"><figcaption>Example Covering Dates stored in ILDB</figcaption></figure><!--kg-card-end: image--><h3 id="problems-processing-covering-dates">Problems Processing Covering Dates</h3><p>For our RDF data model, we wanted to express the Covering Dates using <a href="https://www.w3.org/TR/owl-time/">W3C OWL-Time</a>. We had decided upon using either an <a href="https://www.w3.org/TR/owl-time/#time:Instant">Instant</a> for a single covering date where the first and last dates are the same, or a <a href="https://www.w3.org/TR/owl-time/#time:ProperInterval">ProperInterval </a>for a covering date where the first and last dates differ.</p><p>During our ETL process we have a step which parses the numeric <code>first_date</code> and <code>last_date</code> fields into <a href="https://docs.oracle.com/javase/8/docs/api/java/util/Date.html">Java Date</a> objects, and later a subsequent step that adds these to our RDF Model as <a href="https://www.w3.org/TR/xmlschema-2/#date">xsd:date </a>literals.</p><p>During execution of the ETL which was in total processing ~8.2 million records at item level, we would occasionally see a perplexing error related to <code>first_date</code>:</p><!--kg-card-begin: code--><pre><code class="language-java">covering_date_start String(9) : couldn't convert string [15821011] to a date using format [yyyyMMdd] on offset location 8</code></pre><!--kg-card-end: code--><!--kg-card-begin: image--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.adamretter.org.uk/content/images/2021/03/convert-covering-dates-composite-error.png" class="kg-image" alt="Processing Historical Dates"><figcaption>Pentaho - Date Conversion Error</figcaption></figure><!--kg-card-end: image--><p>To the uninitiated (i.e. my past self), the string <code>15821011</code> looks like it should be parsable using the pattern <code>yyyyMMdd</code>; I can see the year is 1582, the month is October, and the day of the month is the 11th. So what's wrong with this?</p><h3 id="reproducing-the-issue">Reproducing the Issue</h3><p>As Pentaho is written in Java, my first thought was to try and reproduce the issue in a couple of lines of Java code, so that I can either rule in or out an issue with Pentaho. So I wrote the following idiomatic Java code:</p><!--kg-card-begin: code--><figure class="kg-card kg-code-card"><pre><code class="language-java">import java.text.ParseException;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;

public class DateTest {

    public static void main(final String args[]) throws ParseException {
        final String input = "15821011";
        final SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");

        final ParsePosition pp = new ParsePosition(0);
        sdf.parse(input, pp);

        if (pp.getErrorIndex() &gt;= 0) {
            // error occurred
            throw new ParseException("Unable to parse: '" + input + "'", pp.getErrorIndex());
        }
    }
}</code></pre><figcaption>Parsing a yyyyMMdd date in Java</figcaption></figure><!--kg-card-end: code--><p>The above code did not throw a <code>ParseException</code>, thus meaning that it was able to parse the date just fine. This made me suspect that there must be some difference between the above and how Pentaho is parsing the date itself.</p><p>To confirm this, I started Pentaho Spoon with a Java debugging agent configured for remote access, connected to it from IntelliJ IDEA and set some break-points in the Select Values step class (<code>org.pentaho.di.trans.steps.selectvalues.SelectValues</code>) that is used for parsing the date into a Java Date object. Through running the ETL and stepping through the executing code using the Java Debugger I was able to ascertain, that by default the step was calling <code><a href="https://docs.oracle.com/javase/8/docs/api/java/text/DateFormat.html#setLenient-boolean-">SimpleDateFormat#setLenient(boolean)</a></code> with the argument <code>false</code> before parsing the date.</p><p>Whether lenient parsing is enabled by default for SimpleDateFormat depends on the Calendar that backs it. This in itself depends on your JDK Platform and likely your locale. On my <em>en_GB.UTF-8 </em>system with OpenJDK 8, the lenient setting is inherited from <code>java.util.Calendar</code> where it is enabled by default.</p><p>Modifying our reproducible Java code to disable lenient parsing yields:</p><!--kg-card-begin: code--><figure class="kg-card kg-code-card"><pre><code class="language-java">import java.text.ParseException;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;

public class DateTest {

    public static void main(final String args[]) throws ParseException {
        final String input = "15821011";
        final SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");

        sdf.setLenient(false);  // disable lenient parsing mode

        final ParsePosition pp = new ParsePosition(0);
        sdf.parse(input, pp);

        if (pp.getErrorIndex() &gt;= 0) {
            // error occurred
            throw new ParseException("Unable to parse: '" + input + "'", pp.getErrorIndex());
        }
    }
}</code></pre><figcaption>Strict parsing of a yyyyMMdd date in Java</figcaption></figure><!--kg-card-end: code--><p>The above modified code does indeed now throw a <code>ParseException</code> as a result of trying to parse the date <code>15821011</code>. So far so good, we have isolated and reproduced the issue!</p><h3 id="it-s-all-about-the-calendar-">It's all about the Calendar!</h3><p>So, <code>15821011</code> looks like a valid date... so why does it not parse when lenient parsing is disabled?</p><p>Our first hint is from examining the result of <code>sdf.parse</code> (as a String) when lenient parsing is enabled. The result is:</p><!--kg-card-begin: code--><pre><code class="language-java">Thu Oct 21 00:00:00 CET 1582</code></pre><!--kg-card-end: code--><p>Interesting! I was expecting the 11th October 1582, but we are told that we have the 21st October 1582.</p><p>I previously had a basic understanding that there was a Julian Calendar and that this pre-dated the creation and use of the Gregorian Calendar, and that the switch-over happened in October 1582. That switch-over happened such that, Thursday 4th October 1582 (Julian Calendar) was followed by Friday 15th October 1582 (Gregorian Calendar). I can only imagine that this must have confused a few people who woke up on that Friday morning ;-)</p><p>As the calendar switched from Julian to Gregorian in October 1582 we can see that according to the Julian-Gregorian hybrid calendar (which is what Java uses by default on my platform) there was no 11th October 1582, and therefore <code>15821011</code> is not a valid date (for that Calendar).</p><p>So what's up with lenient parsing? Basically <code>SimpleDateFormat</code> makes a best effort attempt to interpret the supplied date. As 11th October 1582 falls in the period between the Julian-Gregorian switch-over, Java adds 10 days (the difference at that time between the calendars), to yield the 21st October 1582 in the Gregorian Calendar. However, that's not really what we want! We want non-lenient parsing as we may have other dates in the source data that are actually incorrect and we don't want them slipping through undetected.</p><p>...and if that was the end of it, it wouldn't be too bad as we could just correct an invalid date in the source data. However, that date is perfectly valid... keep reading!</p><h3 id="julian-gregorian-switch-over-wasn-t-universal-">Julian-Gregorian Switch-Over Wasn't Universal!</h3><p>Simply put, whilst most of Roman Catholic Europe switched from Julian to Gregorian calendars in October 1582, other countries followed later. The countries now making up the UK and Ireland are important in this story because this date comes from The Catalogue of The National Archives, and they didn't in fact switch-over until almost 200 years later - 14th September 1752.</p><p>When given a date, I think you actually have to know two things to be able to parse it, 1) the date itself, and 2) to which Calendar the date is in reference to. Our date 11th October 1582 (<code>15821011</code>) is perfectly valid in the UK <em>at that time</em> (according to the Julian Calendar), as the UK had not yet switched over to the Gregorian calendar.</p><p>When parsing <code>15821011</code> in Java, we need to instruct Java to use the correct Calendar configuration. Initially I (incorrectly) assumed that Java would know the switch-over dates on a per-country basis and adjust its Julian-Gregorian calendar appropriately, as such I tried setting both the Locale and Time Zone to UK:</p><!--kg-card-begin: code--><figure class="kg-card kg-code-card"><pre><code class="language-java">import java.text.ParseException;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Locale;
import java.util.TimeZone;

public class DateTest {

    public static void main(final String args[]) throws ParseException {
        final String input = "15821011";

        final Locale ukLocale = Locale.UK;
        final SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd", ukLocale);

        final TimeZone ukTimeZone = TimeZone.getTimeZone("Europe/London");
        final GregorianCalendar ukCalendar = new GregorianCalendar(ukTimeZone, ukLocale);
        sdf.setCalendar(ukCalendar);

        sdf.setLenient(false);  // disable lenient parsing mode

        final ParsePosition pp = new ParsePosition(0);
        final Date result = sdf.parse(input, pp);

        if (pp.getErrorIndex() &gt;= 0) {
            // error occurred
            throw new ParseException("Unable to parse: '" + input + "'", pp.getErrorIndex());
        }

        System.out.println("Result: " + result.toString());
    }
}</code></pre><figcaption>Incorrect way to configure UK Julian-Gregorian switch-over in Java</figcaption></figure><!--kg-card-end: code--><p>Unfortunately, as mentioned above, my assumption that Java would configure the Julian-Gregorian switch-over date automatically once it knew the Locale and Time Zone was incorrect; the above code still throws a <code>ParseException</code>.</p><p>Once you know where to look, this is documented and expected behaviour. Java's <a href="https://docs.oracle.com/javase/8/docs/api/java/util/GregorianCalendar.html">GregorianCalendar </a>class documentation states:</p><blockquote><code>GregorianCalendar</code> is a hybrid calendar that supports both the Julian and Gregorian calendar systems with the support of a single discontinuity, which corresponds by default to the Gregorian date when the Gregorian calendar was instituted (October 15, 1582 in some countries, later in others). The cutover date may be changed by the caller by calling <a href="https://docs.oracle.com/javase/8/docs/api/java/util/GregorianCalendar.html#setGregorianChange-java.util.Date-"><code>setGregorianChange()</code></a>.</blockquote><blockquote>Historically, in those countries which adopted the Gregorian calendar first, October 4, 1582 (Julian) was thus followed by October 15, 1582 (Gregorian). This calendar models this correctly. Before the Gregorian cutover, <code>GregorianCalendar</code> implements the Julian calendar.</blockquote><p>Therefore, to construct a calendar that respects the UK Julian-Gregorian calendar switch-over we can call <code>setGregorianChange</code> with an argument of 14th September 1752. For example:</p><!--kg-card-begin: code--><figure class="kg-card kg-code-card"><pre><code class="language-java">import java.text.ParseException;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Locale;
import java.util.TimeZone;

public class DateTest {

    public static void main(final String args[]) throws ParseException {
        final String input = "15821011";

        final Locale locale = Locale.UK;
        final TimeZone timeZone = TimeZone.getTimeZone("Europe/London");

        // setup a UK Julian-Gregorian Calendar with the correct switch-over date for the UK
        final GregorianCalendar ukJulianGregorianCalendar = new GregorianCalendar(timeZone, locale);
        final GregorianCalendar ukGregorianCalendarCutoverDate = (GregorianCalendar) ukJulianGregorianCalendar.clone();
        ukGregorianCalendarCutoverDate.set(1752, Calendar.September, 14);
        ukJulianGregorianCalendar.setGregorianChange(ukGregorianCalendarCutoverDate.getTime());

        final SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd", locale);
        sdf.setCalendar(ukJulianGregorianCalendar);

        sdf.setLenient(false);  // disable lenient parsing mode

        final ParsePosition pp = new ParsePosition( 0 );
        final Date result = sdf.parse(input, pp);

        if ( pp.getErrorIndex() &gt;= 0 ) {
            // error occurred
            throw new ParseException( input, pp.getErrorIndex());
        }

        System.out.println("parsed='" + result.toString() + "'");
        System.out.println("serialized='" + sdf.format(result) + "'");
    }
}
</code></pre><figcaption>Strict parsing of a yyyyMMdd date in Java with correct UK Julian-Gregorian switch-over</figcaption></figure><!--kg-card-end: code--><p><strong>NOTE:</strong> In the above code the call to <code><a href="https://docs.oracle.com/javase/7/docs/api/java/util/Calendar.html#set(int,%20int,%20int)">Calendar#set(int, int, int)</a></code> takes arguments for a <em>year</em>, a <em>month</em>, and a <em>day</em>, but... the <em>month</em> argument is zero-based and not one-based! So for September you must enter <code>8</code> and not <code>9</code>! Alternatively you can use the constants from the Calendar API, e.g.:  <code>set(1752, Calendar.September, 14)</code>. This small difference confused me for some time :-(<br></p><p>The most important thing to note in the code above, is that we have still explicitly disabled lenient parsing, but because we have correctly set the UK Julian-Gregorian switch-over date, we can  now parse our date 11th October 1582 (<code>15821011</code>) without any errors. The result of the above code is:</p><!--kg-card-begin: code--><pre><code class="language-java">parsed='Thu Oct 21 01:00:00 CET 1582'
serialized='15821011'</code></pre><!--kg-card-end: code--><p>Now you might be thinking... Hang-on one hot-moment, the <code>parsed</code> line still says "21st October 2021"!<br>Indeed it does! However, that is correct because as explained in Java's <a href="https://docs.oracle.com/javase/8/docs/api/java/util/GregorianCalendar.html">GregorianCalendar </a>class documentation:</p><blockquote><code>GregorianCalendar</code> implements <em>proleptic</em> Gregorian and Julian calendars. That is, dates are computed by extrapolating the current rules indefinitely far backward and forward in time. As a result, <code>GregorianCalendar</code> may be used for all years to generate meaningful and consistent results.</blockquote><p>The important term above is "<em>proleptic</em>", and I will leave that for the reader to look up. I am simplifying but you can expect the Java Date class to always represent dates internally according to the Gregorian Calendar. That's not a problem because we can convert forward and backward as needed between Calendar representations. This is demonstrated by the <code>serialized</code> output line in the results above.</p><p>Now that our Calendar is correctly configured (for the UK) in Java we have no problem parsing our Covering Dates. Unfortunately I have been unable to find any options for configuring this in Pentaho for our ETL, and as such we have 2 options:</p><ol><li>As Pentaho is Open Source, we could improve Pentaho's Select Value step to offer suitable configuration.</li><li>Create a custom Pentaho Plugin step which handles such date parsing.<br>I may cover such topics in a future post, but we won't consider Pentaho any further today.</li></ol><h3 id="what-about-that-rdf">What about that RDF?</h3><p>As briefly mention earlier we want to output our Covering Dates into our RDF using a <a href="https://www.w3.org/TR/owl-time/">W3C OWL-Time</a> format. We need to incorporate the 3 fields: <code>date_text</code>, <code>first_date</code>, and the optional <code>last_date</code>. The <code>first_date</code> and <code>last_date</code> fields are now valid Java Date objects according to our calendar (as discussed above), whilst the <code>date_text</code> remains a string value.</p><p>An example, of a Covering Date indicating a single point in time for a unit of description, may have the <code>date_text</code> with a value of <code>1859 Aug 30</code>, and only a <code>first_date</code> with a value of <code>18590830</code>. This can be expressed in our RDF model for Project Omega as: </p><!--kg-card-begin: code--><figure class="kg-card kg-code-card"><pre><code class="language-turtle">omg:created [
    a time:Instant ;
    dct:description "1859 Aug 30" ;
    time:inXSDDate "1859-08-30Z"^^xsd:date
] .</code></pre><figcaption>Project Omega - Example Record Covering Date for a Point in Time</figcaption></figure><!--kg-card-end: code--><p>Another example, of a Covering Date indicating a period of time for a unit of description,  may have the <code>date_text</code> with a value of <code>11 Oct 1582 - 29 Nov 1582</code>, the <code>first_date</code> with a value of <code>15821011</code>, and <code>last_date</code> with a value of <code>15821129</code>. This can be expressed in our RDF model for Project Omega as:</p><!--kg-card-begin: code--><figure class="kg-card kg-code-card"><pre><code class="language-turtle">omg:created [
    a time:ProperInterval ;
    dct:description "11 Oct 1582 - 29 Nov 1582" ;
    time:hasBeginning [
        a time:Instant ;
        time:inXSDDate "1582-10-21Z"^^xsd:date
    ] ;
    time:hasEnd [
        a time:Instant ;
        time:inXSDDate "1582-12-09Z"^^xsd:date
    ]
] .</code></pre><figcaption>Project Omega - Example Record Covering Date for a Period of Time</figcaption></figure><!--kg-card-end: code--><p>Now, if you have been paying close attention so far, you will have noticed that the literal values of the <code>time:inXSDDate</code> properties don't look like the <code>first_date</code> and <code>last_date</code> values!</p><p><br>However, if I told you that these dates in RDF are stored according to the <a href="https://www.w3.org/TR/xmlschema-2/#date">xsd:date</a> (W3C XML Schema Date) data-type, and that that specifies a <em>proleptic </em>Gregorian Calendar, then perhaps you might have an "<em>Ah ha!</em>" moment.</p><p><br>If not, then let me explain that the dates in the RDF are the Gregorian equivalent of the Julian dates that were provided as input. No information has been lost as you can convert back and forward between these with relative ease.</p><h3 id="finally-the-archivist-vs-the-software-engineer">Finally, The Archivist vs. The Software Engineer</h3><p>The Digital Humanities require a fine and pragmatic balance between Human and Technical factors.</p><p><strong>Software Engineering - Technical Factors</strong></p><p>In the example of storing these Covering Dates in RDF the Software Engineer has proposed storing them as <code>xsd:date</code> typed values. The software Engineer recalling that TNA-CS13 states:</p><blockquote>the covering date format automatically generates numeric start and end dates in the catalogue in order to enable date searching.</blockquote><p>believes that the <code>date_text</code> is the important property from an archival descriptive perspective, and that the <code>first_date</code> and <code>last_date</code> are really just present to enable the access function of searching records by dates. The Software Engineer also understands by assumption that the <code>first_date</code> and <code>last_date</code> should be in synchronisation with the <code>date_text</code> and therefore be a faithful representation of that period.</p><p>For the software engineer, how the <code>first_date</code> and <code>last_date</code> are stored is a technical consideration that centers around arguments of accuracy, performant indexing, and range searches over dates. The Software Engineer believes that dates for dates to be processed consistently in isolation, they must be expressed according to a Calendar and Time Zone. In effect 3 facts are required to process a date, the date itself, the calendar for which the date is expressed, and any Time Zone information for how that date is recorded.</p><p>Ultimately, should there be a presentation requirement, the Software Engineer knows that they can convert the <code>first_date</code> and <code>last_date</code> and present/search it in any format requested by the user. The Software Engineer is confident that no information has been lost or destroyed.</p><p><strong>Archival - Preservation Factors</strong></p><p>The archivist is concerned that at present the <code>first_date</code> and <code>last_date</code> are recorded as simple sequential numbers. The archivist understands the context of the record, and is happy to glance at the serial date and interpret it within its historical context. The archivist believes that all 3 properties (<code>first_date</code>, <code>last_date</code>, and <code>date_text</code>) are equally important from a records keeping perspective and should be preserved as is.</p><p>The archivist worries that the raw expression of an <code>xsd:date</code> e.g. <code>1859-08-30Z</code> may be harder to interpret that the previous serial format: <code>18590830</code>; worse yet, dates that were written for the Julian Calendar (as that was the Calendar in use at that date) e.g. <code>15821129</code> might now be expressed as <code>1582-12-09Z</code> for the Gregorian Calendar. Without careful explanation to the user, the new Gregorian form of the Julian date is confusing, and use of the original serial format of date perhaps makes more sense.</p><p><strong>In Conclusion - Human and Technical Together</strong><br><br>Ultimately, all of these dates regardless of their formatting for expression relative to a particular calendar are stored as electro-magnetic <code>1</code>s and <code>0</code>s on a disk. The current serial formatted first and last dates stored in ILDB are SQL Integers laid down inside a complex proprietary MS SQL Server database format, that likely few could hope to decipher!</p><p>Ignoring for the moment the field of Digital Preservation, we can have the best of both worlds. The Software Engineer can design correct and performant software, which accurately records the dates, and that the archivists and the users can be presented and allowed to search those dates in whichever format is most desirable. Such presentation could include displaying the calendar or adjusting the display dates to the historically relevant calendar for the record.</p><p>This Omega Catalogue system is designed to be an online system, for the purposes of Digital Preservation, one could imagine preserving frequent exports of our RDF data as perhaps Turtle or similar (UTF-8 encoded text).  As a Software Engineer and potential Digital Archeologist I would argue that (given pre-knowledge of the spec), it is much easier to interpret and process <code>1582-12-09Z</code> over <code>15821129</code>, as the first form can only be according to the Gregorian Calendar (as per the W3C specifications for the XML Schema Date Type) and also indicates the time-zone (the <code>Z</code> character denotes UTC). I therefore have everything I need within the date string itself. I don't need to research further into the history of Julian to Gregorian Calendar switch-overs, which I would otherwise have to do with the second form! </p>]]></content:encoded></item><item><title><![CDATA[Reusing Standard RDF Vocabularies - Part 2]]></title><description><![CDATA[Following on from Part 1, we look at a further example of trade-offs and compromise that I made with regards to reusing existing RDF vocabularies in Project Omega at The National Archives; thus reinforcing our earlier position on reuse through a different example.]]></description><link>https://blog.adamretter.org.uk/vocabulary-reuse-part2/</link><guid isPermaLink="false">605076c755477602318d6f75</guid><category><![CDATA[RDF]]></category><category><![CDATA[catalogue]]></category><category><![CDATA[The National Archives]]></category><category><![CDATA[Vocabulary]]></category><dc:creator><![CDATA[Adam Retter]]></dc:creator><pubDate>Wed, 24 Mar 2021 09:49:45 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1485550409059-9afb054cada4?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDN8fGlkZW50aXR5fGVufDB8fHx8MTYxNTkyNTU3Nw&amp;ixlib=rb-1.2.1&amp;q=80&amp;w=1080" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1485550409059-9afb054cada4?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMTc3M3wwfDF8c2VhcmNofDN8fGlkZW50aXR5fGVufDB8fHx8MTYxNTkyNTU3Nw&ixlib=rb-1.2.1&q=80&w=1080" alt="Reusing Standard RDF Vocabularies - Part 2"><p>In <a href="https://blog.adamretter.org.uk/vocabulary-reuse-part1/">Part 1</a> we looked at the challenges of strictly sticking to a policy of reusing existing vocabularies within <a href="https://www.nationalarchives.gov.uk/about/our-role/plans-policies-performance-and-projects/our-plans/our-digital-cataloguing-practices/project-omega/">Project Omega</a> at TNA (<a href="https://www.nationalarchives.gov.uk">The National Archives</a>) and why you may occasionally have to make concessions to correctly express your data.</p><p>The example discussed in <a href="https://blog.adamretter.org.uk/vocabulary-reuse-part1/">Part 1</a> was concerned with the scenario where one cannot from a suitable property from an existing popular standardised vocabulary. In this shorter article, we will look at a second scenario where there may be a reusable property available, but its implementation is problematic.</p><h3 id="primary-and-secondary-identifiers">Primary and Secondary Identifiers</h3><p>For this  example let me explain another use-case that we recently had to solve for Project Omega. TNA's current Catalogue contains multiple identifiers, for each Unit of Description (a single document of folder):</p><ol><li>Database Table Primary Key, e.g. <code>tbl_item.-4653191</code></li><li>CCR (Classic Catalogue Reference), e.g. <code>AIR 79/1064/118667</code></li><li><em>Optional </em>- The Former Reference - Creating Department, e.g. <code>R515333</code></li><li><em>Optional </em>- The Former Reference - PRO (Public Records Office), e.g. <code>E 315/509/Fo. 11</code></li></ol><p>In addition, in the new Omega Catalogue, every Resource has:</p><ol><li>An OCI (Omega Catalogue Identifier), e.g. <code>FO.2020.3J.P.1</code></li><li><em>Optional</em> - A related identifier from the <a href="https://discovery.nationalarchives.gov.uk">Discovery</a> system called an IAID (Information Asset Identifier), e.g. <code>01d43d64-d7a6-4250-a2f2-4153a606a948</code>.</li></ol><p>The Primary Identifier in Omega is the OCI, and we can happily reuse the <a href="https://www.dublincore.org/specifications/dublin-core/dcmi-terms/">Dublin Core Terms</a>' <code>dc:identifier</code> property for this. For example:</p><!--kg-card-begin: code--><figure class="kg-card kg-code-card"><pre><code class="language-turtle">@prefix tna:     &lt;http://www.nationalarchives.gov.uk/&gt; .
@prefix dct:     &lt;http://purl.org/dc/terms/&gt; .

tna:res.FO.2020.3J.P.1
    dct:identifier "FO.2020.3J.P.1" ;
    .</code></pre><figcaption>A Record with a Primary Identifier</figcaption></figure><!--kg-card-end: code--><p>This leads us to the question of - <em>What is the best way to express our Secondary Identifiers?</em></p><h3 id="expressing-secondary-identifiers">Expressing Secondary Identifiers</h3><p>If we were to express our Secondary Identifiers also using <code>dct:identifier</code>, it becomes difficult, impossible even, to differentiate the scheme to which identifier belongs as the <code>dct:identifier</code> is a Data Type Property and only permits a single literal value. Consider for example:</p><!--kg-card-begin: code--><figure class="kg-card kg-code-card"><pre><code class="language-turtle">@prefix tna:     &lt;http://www.nationalarchives.gov.uk/&gt; .
@prefix dct:     &lt;http://purl.org/dc/terms/&gt; .

tna:res.FO.2020.3J.P.1
    dct:identifier "FO.2020.3J.P.1" ;
    dct:identifier "01d43d64-d7a6-4250-a2f2-4153a606a948" ;
    dct:identifier "tbl_item.-4653191" ;
    dct:identifier "AIR 79/1064/118667" ;
    dct:identifier "R515333" ;
    dct:identifier "E 315/509/Fo. 11" ;
    .</code></pre><figcaption>A Record with Many Identifiers; problematic?</figcaption></figure><!--kg-card-end: code--><p>The difficulty in working with the above data is that it raises questions such as:</p><ol><li>Why are there so many identifiers?</li><li>To which schemes do these identifiers belong, and where can I find more information about those?</li><li>Which identifier should I use?</li><li>If I perform a query involving <code>dct:identifier</code>, then I am querying across identifier schemes, but am I guaranteed that there are no duplicate or conflicting identifiers across those schemes?</li><li>As a maintainer of the data, are all of the identifiers that are needed present?</li></ol><p>Ideally we want instead a mechanism for Secondary Identifiers that not only expresses the identifier itself, but also the scheme which defines the use and syntax of that identifier.</p><p>After looking through several popular and standardised vocabularies, <a href="https://schema.org">Schema.org</a>'s identifier property appears to suit our needs - <code><a href="https://schema.org/identifier">schema:identifier</a></code>.</p><!--kg-card-begin: code--><figure class="kg-card kg-code-card"><pre><code class="language-turtle">@prefix tna:     &lt;http://www.nationalarchives.gov.uk/&gt; .
@prefix dct:     &lt;http://purl.org/dc/terms/&gt; .
@prefix schema:  &lt;https://schema.org/&gt; .

tna:res.FO.2020.3J.P.1
    dct:identifier "FO.2020.3J.P.1" ;
    schema:identifier [
        a schema:PropertyValue ;
        schema:name "CCR" ;
        schema:description "The Classic Catalogue Reference" ;
        schema:value "FO 12/34/56"
    ] ;
    schema:identifier [
        a schema:PropertyValue ;
        schema:name "FRCD" ;
        schema:description "The Former Reference - Creating Department" ;
        schema:value "R123456"
    ]
    .</code></pre><figcaption>A Record with Primary and Secondary Identifier - Literals</figcaption></figure><!--kg-card-end: code--><p>This is an improvement over the sole and repeated use of <code>dct:identifier</code> as it allows us to reserve <code>dct:identifier</code> to indicate our Primary Identifier, and our secondary identifiers are now easily located via <code>schema:identifier</code>. In addition, each Secondary Identifier carries information explaining its purpose.</p><h3 id="further-concessions-against-reuse">Further Concessions Against Reuse</h3><p>We could refactor this to eliminate duplication and make it easier to query against specific secondary identifier(s). Thus yielding:</p><!--kg-card-begin: code--><figure class="kg-card kg-code-card"><pre><code class="language-turtle">@prefix tna:     &lt;http://www.nationalarchives.gov.uk/&gt; .
@prefix dct:     &lt;http://purl.org/dc/terms/&gt; .
@prefix schema:  &lt;https://schema.org/&gt; .

tna:res.FO.2020.3J.P.1
    dct:identifier "FO.2020.3J.P.1" ;
    schema:identifier [
        a schema:PropertyValue ;
        schema:propertyID tna:ccr ;
        schema:value "FO 12/34/56"
    ] ;
    schema:identifier [
        a schema:PropertyValue ;
        schema:propertyID tna:frcd ;
        schema:value "R123456"
    ]
    .</code></pre><figcaption>A Record with Primary and Secondary Identifier - URIs</figcaption></figure><!--kg-card-end: code--><p>The above involves trading-off the reuse of existing vocabulary properties for further precision of meaning.</p><p>We gain:</p><ol><li>A reduction in duplicated strings, e.g. the <code>schema:name</code> and <code>schema:description</code> being placed on each secondary identifier.</li><li>The ability to easily and confidently search the data, we can match on <code>? schema:propertyID tna:ccr</code> instead of <code>? schema:name "CCR"</code>. This becomes even more important where data may have been mis-keyed.</li></ol><p>We lose:</p><ol><li>The ability for strangers to interpret our data easily by glancing at a Secondary Identifier (<code>schema:identifier</code>) and immediately know what it is by reading its inline <code>schema:name</code> and <code>schema:description</code>.</li><li>The ability to express our data without needing to define our own vocabulary. </li></ol><p>These trade-offs are quite severe and I think we lose too much for the (maybe as yet unknown) humans who want to work with our data. Of course we gain for the machines, but if we were only concerned about machines we would just use the most efficient binary encoding possible and this article would be redundant.</p><p>To complete the example above, we should also define <code>tna:ccr</code> and <code>tna:frcd</code> in a new vocabulary of our own:</p><!--kg-card-begin: code--><figure class="kg-card kg-code-card"><pre><code class="language-turtle">@prefix owl      &lt;http://www.w3.org/2002/07/owl#&gt; .
@prefix rdfs     &lt;http://www.w3.org/2000/01/rdf-schema#&gt; .
@prefix tna:     &lt;http://www.nationalarchives.gov.uk/&gt; .
@prefix dct:     &lt;http://purl.org/dc/terms/&gt; .
@prefix schema:  &lt;https://schema.org/&gt; .

tna:ccr
    a owl:Class ;
    rdfs:label "Classic Catalogue Reference" ;
    rdfs:comment "The CCR (Classic Catalogue Reference) is a secondary
                  identifier for a Unit of Description. It reflects the
                  historic ISAD(G) like archival arrangement of the unit, i.e
                  Department, Series, Piece, and Item. It has been in use
                  at The National Archives since the 19th century and is
                  aligned to the ISAD(G) standard. It is defined on page 13
                  of the document: TNA-CS13 (Cataloguing Standards - Part A
                  Data Elements, June 2013)."@en ;
    rdfs:seeAlso tna:cs13-a, schema:identifier, dct:identifier
    .
  
 tna:frcd
    a owl:Class ;
    rdfs:label "Former Reference - Creating Department" ;
    rdfs:comment "The FRCD (Former Reference - Creating Department) is
                  a secondary identifier for a Unit of Description. It holds
                  the unique identifier given to the material by the
                  originating creator. It is defined on page 17 of the
                  document: TNA-CS13 (Cataloguing Standards - Part A Data
                  Elements, June 2013)."@en ;
    rdfs:seeAlso tna:cs13-a, schema:identifier, dct:identifier
    .
    
tna:cs13-a
    a owl:Class ;
    rdfs:label "Cataloguing Standards - Part A Data Elements, June 2013" ;
    rdfs:comment "Describes the various Catalogue Elements derived from
                  ISAD(G) to manage descriptive data which is available
                  in PROCAT Editorial."@en
    .</code></pre><figcaption>Describing the Schemes of our Secondary Identifiers</figcaption></figure><!--kg-card-end: code--><h3 id="further-compromise-for-human-use">Further Compromise for Human Use</h3><p>We had chosen to reuse <code>schema:identifier</code> for our secondary identifiers because we had reserved <code>dct:identifier</code> for our primary identifier, and we wanted to follow our guiding rule of reusing common vocabularies wherever possible. </p><p>However, I feel that we have not yet arrived at a good solution. Perhaps, there is a different approach that we might take that would yield a more favourable balance between human understandability, precision or our data, and computability by machines?</p><p>What about if we took a similar approach to that which we ultimately proposed in <a href="https://blog.adamretter.org.uk/vocabulary-reuse-part1/">Part 1</a>? That is to say, that we could derive our own property(s) for secondary identifiers from an existing one, thus reusing the common definition yet adding further meaning. Of course we must still be very considerate of human users, and wisely choose straight-forward or obvious generic names for such properties so as to help them infer their purpose.</p><p>Whilst we can't use <code>dct:identifier</code> directly for our secondary identifiers, there is nothing to stop us deriving our own properties for secondary identifiers from it!</p><!--kg-card-begin: code--><figure class="kg-card kg-code-card"><pre><code class="language-turtle">@prefix owl      &lt;http://www.w3.org/2002/07/owl#&gt; .
@prefix rdfs     &lt;http://www.w3.org/2000/01/rdf-schema#&gt; .
@prefix xsd:     &lt;http://www.w3.org/2001/XMLSchema#&gt; .
@prefix tna:     &lt;http://www.nationalarchives.gov.uk/&gt; .
@prefix dct:     &lt;http://purl.org/dc/terms/&gt; .

tna:classicCatalogueReference
    a owl:DatatypeProperty ;
    rdfs:subPropertyOf dct:identifier
    rdfs:range xsd:string ;
    rdfs:label "Classic Catalogue Reference" ;
    rdfs:comment "The Classic Catalogue Reference is a secondary
                  identifier for a Unit of Description. It reflects the
                  historic ISAD(G) like archival arrangement of the unit, i.e
                  Department, Series, Piece, and Item. It has been in use
                  at The National Archives since the 19th century and is
                  aligned to the ISAD(G) standard. It is defined on page 13
                  of the document: TNA-CS13 (Cataloguing Standards - Part A
                  Data Elements, June 2013)."@en ;
    rdfs:seeAlso tna:cs13-a, schema:identifier, dct:identifier
    .

tna:formerReferenceFromDepartment
    a owl:DatatypeProperty ;
    rdfs:range xsd:string ;
    rdfs:subPropertyOf dct:identifier ;
    rdfs:label "Former Reference - Creating Department" ;
    rdfs:comment "The 'Former Reference - Creating Department' is a secondary
                  identifier for a Unit of Description. It holds the unique
                  identifier given to the material by the originating
                  creator. It is defined on page 17 of the document:
                  TNA-CS13 (Cataloguing Standards - Part A Data Elements,
                  June 2013)."@en ;
    rdfs:seeAlso tna:cs13-a
    .</code></pre><figcaption>Definition of our derived properties for two of our Secondary Identifiers</figcaption></figure><!--kg-card-end: code--><p>Using our own derived properties would then yield an expression for our Record that looks something like:</p><!--kg-card-begin: code--><figure class="kg-card kg-code-card"><pre><code class="language-turtle">@prefix tna:     &lt;http://www.nationalarchives.gov.uk/&gt; .
@prefix dct:     &lt;http://purl.org/dc/terms/&gt; .
@prefix schema:  &lt;https://schema.org/&gt; .

tna:res.FO.2020.3J.P.1
    dct:identifier "FO.2020.3J.P.1" ;
    tna:classicCatalogueReference "FO 12/34/56" ;
    tna:formerReferenceFromDepartment "R123456"
    .</code></pre><figcaption>A Record with Primary and Secondary Identifier - Bespoke Vocabulary</figcaption></figure><!--kg-card-end: code--><p>I believe that this final approach strikes a good compromise. Whilst we are not directly reusing an existing common vocabulary here for our secondary identifiers, we have a good reason, which is that <code>schema:identifier</code> is not a good fit for use considering our use-case. However, all is not lost, whilst we have had to create our own properties, they themselves are derived from a property (<code>dct:identifier</code>) from an existing common vocabulary (Dublin Core Terms). Additionally, and very subjectively, I would argue that it is much easier for humans to understand a single line which says <code>tna:formerReferenceFromDepartment</code> than looking within data or object properties of <code>schema:identifier</code>.</p><h3 id="conclusion">Conclusion</h3><p>Although we started out with a different problem, and tried different approaches along the way, we ultimately ended up with an approach that looks remarkably similar to that in <a href="https://blog.adamretter.org.uk/vocabulary-reuse-part1/">Part 1</a>.</p><p>Hopefully this has re-enforced the idea that when attempting to solely reuse existing popular vocabularies, if you falter due a lack of suitable available classes and properties, there are options available, but there are trade-offs that have to be made between reuse and precision.</p>]]></content:encoded></item><item><title><![CDATA[Reusing Standard RDF Vocabularies - Part 1]]></title><description><![CDATA[For Project Omega at The National Archives, we are reusing existing RDF vocabularies for our new Catalogue. I examine what happens when a suitable property or class cannot be found within an existing vocabulary, and weigh-up the different approaches and compromises that may need to be made.]]></description><link>https://blog.adamretter.org.uk/vocabulary-reuse-part1/</link><guid isPermaLink="false">604f517f55477602318d6b9f</guid><category><![CDATA[RDF]]></category><category><![CDATA[catalogue]]></category><category><![CDATA[The National Archives]]></category><category><![CDATA[Vocabulary]]></category><category><![CDATA[Matterhorn]]></category><category><![CDATA[RiC]]></category><dc:creator><![CDATA[Adam Retter]]></dc:creator><pubDate>Wed, 17 Mar 2021 17:05:09 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1518792528501-352f829886dc?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDMwfHxyZWN5Y2xlfGVufDB8fHx8MTYxNTg0ODE4NQ&amp;ixlib=rb-1.2.1&amp;q=80&amp;w=1080" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1518792528501-352f829886dc?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMTc3M3wwfDF8c2VhcmNofDMwfHxyZWN5Y2xlfGVufDB8fHx8MTYxNTg0ODE4NQ&ixlib=rb-1.2.1&q=80&w=1080" alt="Reusing Standard RDF Vocabularies - Part 1"><p>In Phase 1 of Project Omega at TNA (<a href="https://www.nationalarchives.gov.uk">The National Archives</a>) we evaluated several different existing models and vocabularies/ontologies to ascertain their suitability for expressing the data of TNA's new Pan-Archival Catalogue. We published a fairly comprehensive report of our findings and proposed a way forward: <a href="https://www.nationalarchives.gov.uk/documents/omega-catalogue-model-proposal.pdf">Catalogue Model Proposal</a>.</p><p>In summary, we felt that none of the existing models were perfect. We recognised that ICA's RiC (International Council on Archives' Record in Contexts) was very promising but currently under-developed for TNA's needs. Ultimately, we felt that the approach taken in developing <a href="https://ipres2019.org/static/proceedings/iPRES2019.pdf#page=271">The Matterhorn RDF Data Model</a> had a lot of strengths and that we would take a similar path.</p><p>We decided that the new Data Model for Project Omega would:</p><ol><li> attempt to adhere to the broader principles of <a href="https://www.ica.org/en/egad-ric-conceptual-model">RiC's Conceptual Model</a>, but discard <a href="https://www.ica.org/en/records-in-contexts-ontology">RiC's Ontology</a>.</li><li>follow the approach of The Matterhorn RDF Data Model, i.e. reuse existing vocabularies and NOT create our own.</li></ol><p>We started with the model specified in Matterhorn and added additional properties and classes from other shared and standardised vocabularies as we needed. The (work-in-progress) documentation of our data model: <a href="https://www.nationalarchives.gov.uk/documents/omega-catalogue-data-model.pdf">Omega Catalogue Data Model</a>. </p><p>Now that we are in Phase 2 of Project Omega and exporting data into this data model in the form of Turtle RDF, we are starting to revisit some of our initial assumptions about reuse.</p><h3 id="shared-language-vs-precision">Shared Language vs. Precision</h3><p>The beauty of reusing existing vocabularies (assuming that you choose popular and standardised ones), is that any developer, data scientist, or user who has worked with RDF before can likely already understand and work with our data. For example let's consider a simplified description of a Record:</p><!--kg-card-begin: code--><figure class="kg-card kg-code-card"><pre><code class="language-turtle">@prefix tna:     &lt;http://www.nationalarchives.gov.uk/&gt; .
@prefix dct:     &lt;http://purl.org/dc/terms/&gt; .

tna:res.FO.2020.3J.P.1
    dct:identifier "FO.2020.3J.P.1" ;
    dct:description "This is a Foreign Office record about..." ;
    .</code></pre><figcaption>Record with an Identifier and Description that reuse common vocabularies</figcaption></figure><!--kg-card-end: code--><p>DCT (Dublin Core Terms) is a vocabulary that has been around since 2008 and its use is ubiquitous. Even if somehow the user was not aware of Dublin Core, the naming of the terms is straight-forward. As a human I can likely infer the meaning of <code>dct:identifier</code> as holding an identifier for the resource, and <code>dct:description</code> as holding a description of the resource. If you felt the need to, you could confirm your suspicions by checking the Dublin Core standard document itself, however, the point here is that you didn't have to, the meaning is already known or at least almost-obvious. This is a major benefit of reusing popular vocabularies as it both reduces the cognitive load for those working with the data, and enables us to form and use a shared language even when working with vastly different datasets.</p><p>On the flip-side, the disadvantage of reusing popular shared vocabularies is that they are often, by design, quite generic in their definitions. This is of course by necessity, common terms acceptable to a wide-audience need to be agreeable by that audience, and so generic and/or vaguely defined terms are more palatable.</p><p>Defining your own vocabulary has the absolute advantage of allowing you to precisely define your world-view and exactly what you mean. That's powerful stuff!</p><p>By way of contrast consider the same record expressed in a (fictional) bespoke vocabulary:</p><!--kg-card-begin: code--><figure class="kg-card kg-code-card"><pre><code class="language-turtle">@prefix tna:     &lt;http://www.nationalarchives.gov.uk/&gt; .

tna:res.FO.2020.3J.P.1
    tna:oci "FO.2020.3J.P.1" ;
    tna:scope-content "This is a Foreign Office record about..." ;
    .</code></pre><figcaption>Record with an Identifier and Description that use bespoke vocabulary</figcaption></figure><!--kg-card-end: code--><p>If you work in the Archives sector you might well guess that <code>tna:scope-content</code> holds the <em>Scope and Content</em> of the record... but how many people outside of the Archives sector know exactly what is meant by the "Scope and Content" of a record? Even then, you likely wouldn't know the meaning of <code>tna:oci</code>! It's the <em>Omega Catalogue Identifier</em>, and awareness of that is not even organisation-wide throughout TNA yet.<br>We would of course write OWL and documentation to define exactly what <code>tna:oci</code> and <code>tna:scope-content</code> mean, but the user has to go and read those before they can work with the data.</p><p><strong>The trade-off is ultimately: </strong>Ease of consumption through reuse of Shared Vocabularies vs. Precisely/Correctly expressing your domain and data.</p><h3 id="when-and-how-to-trade-off">When and how to trade-off?</h3><p>In Omega our underlying principle is to always attempt reuse first. We are discovering that sometimes however there just isn't an appropriate Property or Class that can be reused from a popular standardised vocabulary.</p><p>By way of an example let me explain a use-case that we recently had to solve for Project Omega. TNA's Catalogue currently contains <code>Covering Dates</code> for each Unit of Description (a single document of folder). These <em>covering dates</em> are the period-of-time during which the record(s) being described were created. They are expressed using between 1 and 3 values: The <em>Date Text</em> (as it appears on the unit of description), the <em>First Date</em> (the start of the period), and the <em>Last Date</em> (the end of the period).</p><p>Originally we had decided to use a property from a common vocabulary to express these, Dublin Core Terms (perhaps you know it!). The property we initially selected was <code>dct:temporal</code>. As I interpret the DCT (Dublin Core Terms) standard, it appears to me that <code>dct:temporal</code> is intended to describe the <em>temporal coverage</em> of the resource, i.e. the time period discussed/indicated within the resource as opposed to the date that the resource was created. So after further consideration, we decided to use something else instead of <code>dct:temporal</code>, and this is where we had to start making trade-offs.</p><p>The options we considered:</p><ol><li>Use <code>dct:created</code> instead.<br>Unfortunately <code>dct:created</code> is a Data Type Property and so requires a literal value, yet we need to store 3 literal values (<em>Date Text</em>,<em> First Date</em>, and <em>Last Date</em>). To achieve this we could either:<br><br>a) Encode the 3 literal values into 1 literal value using <a href="https://www.iso.org/iso-8601-date-and-time-format.html">ISO 8601-1</a>, <a href="https://www.w3.org/TR/NOTE-datetime">W3CDTF</a>, <a href="http://www.loc.gov/standards/datetime/">EDTF</a>, or <a href="https://www.dublincore.org/specifications/dublin-core/dcmi-period/">DCMI Period</a>. This has the downside that querying this with SPARQL becomes complex and requires various string split operations. For example, encoding using DCMI Period might produce the single literal string value:<br><code>name=1941-1951; scheme=W3C-DTF; start=1941-01-01Z; end=1951-01-01Z</code>.<br><br>b) Ignore Dublin Core specifics here, and use an Object Property. We could somewhat enforce this approach with SHACL and documentation. However, those that are used to Dublin Core may be surprised; SPARQL queries for <code>dct:created</code> would be different in our system than other systems. This negates the advantage of using a property from a shared standardised vocabulary!<br></li><li>Use <code>time:hasTime</code> instead.<br>This is a generic property from the <a href="https://www.w3.org/TR/owl-time/">W3C Time Ontology in OWL</a>. This is an Object Property that allows us to express our covering dates exactly as we would need. Unfortunately <code>tme:hasTime</code> only tells us that there is a time, not what that time represents. It is too generic and fails to adequately describe that these are the created dates of the records; <code>dct:created</code> would have been much more precise!<br></li><li>Create our own vocabulary property.<br>We have two main options of how to approach this:<br><br>a) Define our own standalone property in our own vocabulary.<br><br>b) If there is a property from a common vocabulary that is close to what we need, we can define our own property which is derived from that.</li></ol><p>Whilst <code>dct:created</code> infers (to a human) the meaning that we are looking for it, doesn't allow us to store the information we need. The <code>time:hasTime</code> property is the opposite, it lacks a sufficient precise meaning, but allows great flexibility in how we store our covering dates. Therefore, as there is no readily suitable property from a common vocabulary we have little choice but to create our own!</p><p>As the property <code>time:hasTime</code> allows us to store the data we need, but is lacking in sufficient descriptive power, rather than defining our own standalone property, we can instead derive our property from <code>time:hasTime</code> and add further descriptive information. Our new derived property will be <code>tna:created</code> and could look something like this:</p><!--kg-card-begin: code--><figure class="kg-card kg-code-card"><pre><code class="language-turtle">@prefix owl      &lt;http://www.w3.org/2002/07/owl#&gt; .
@prefix rdfs     &lt;http://www.w3.org/2000/01/rdf-schema#&gt; .
@prefix tna:     &lt;http://www.nationalarchives.gov.uk/&gt; .
@prefix dct:     &lt;http://purl.org/dc/terms/&gt; .
@prefix time:    &lt;http://www.w3.org/2006/time#&gt; .
@prefix rdae:    &lt;http://rdaregistry.info/Elements/e/&gt; .

tna:created
    a owl:ObjectProperty ;
    rdfs:subPropertyOf time:hasTime ;
    rdfs:label "Created date" ;
    rdfs:comment "The date that the resource was created, or the date-period
                  during which the resource was created. Historically at The
                  National Archives, this has also been known as the
                  'Covering Dates' (of the Unit of Description)."@en ;
    rdfs:seeAlso dct:created, rdae:P20214
  .</code></pre><figcaption>Definition of our derived property for Covering Dates</figcaption></figure><!--kg-card-end: code--><p>The above definition of <code>tna:created</code> declares it as a sub-property of <code>time:hasTime</code> but gives further information about its use, and also informs us that additional information can be found by looking at <code>dct:created</code> and <code>rdae:P20214</code>.</p><p>In practical use our earlier RDF augmented with our Covering Dates now finally looks something like:</p><!--kg-card-begin: code--><figure class="kg-card kg-code-card"><pre><code class="language-turtle">@prefix tna:     &lt;http://www.nationalarchives.gov.uk/&gt; .
@prefix dct:     &lt;http://purl.org/dc/terms/&gt; .
@prefix time:    &lt;http://www.w3.org/2006/time#&gt; .
@prefix xsd:     &lt;http://www.w3.org/2001/XMLSchema#&gt; .

tna:res.FO.2020.3J.P.1
    dct:identifier "FO.2020.3J.P.1" ;
    dct:description "This is a Foreign Office record about..." ;
    tna:created [
        a       time:ProperInterval ;
        dct:description "1941-1951" ;
        time:hasBeginning [
            a       time:Instant ;
            time:inXSDDate "1941-01-01Z"^^xsd:date
        ] ;
        time:hasEnd [
            a       time:Instant ;
            time:inXSDDate "1951-12-31Z"^^xsd:date
        ]
    ]
    .</code></pre><figcaption>Record with an Identifier, Description, and Created Dates</figcaption></figure><!--kg-card-end: code--><p>We still utilise <code>dct:identifier</code> and <code>dct:description</code> above, because they are a good fit for our reuse. Whilst the new <code>tna:created</code> demonstrates our trade-off perfectly! </p><p>Earlier, I explained that we had wanted to reuse <code>dct:created</code> because it is a property that is widely used and understood, but that it was unsuitable for storing our covering dates (as it is specified as a Data Type Property).<br>As we could not find a suitable property from an existing popular vocabulary that we could reuse, we were forced to create our own. This property, <code>tna:created</code>, has two important design aspects:</p><ol><li>It's name is straight forward. Even someone from outside of the Archival sector can likely guess it's meaning and purpose. It's unlikely that someone would have to look at our OWL definition or documentation to be able to <em>start </em>working with it. It's very much intentional that at a glance it looks a lot like <code>dct:created</code>.</li><li>Whilst this property is TNA specific and explicates a precise meaning, it does not standalone. Instead, it reuses the W3C Time Ontology in OWL (a popular vocabulary itself) by virtue of being derived from the <code>time:hasTime</code> property.</li></ol><h3 id="conclusion">Conclusion</h3><p>For Project Omega - we still prefer reuse wherever possible as it enables easier consumption by others. Creating a new Property or Class (even if derived from a common vocabulary) is sometimes unavoidable, but should be considered as an absolute last resort and undertaken only when no common property exists, or said property fails to adequately describe the data.</p><p>Hopefully this article has provided you with some insight into the challenges that arise when strictly trying to reuse existing vocabularies, and the trade-offs that may have to be made.</p><!--kg-card-begin: hr--><hr><!--kg-card-end: hr--><p>In <a href="https://blog.adamretter.org.uk/vocabulary-reuse-part2/">Part 2</a>, I work through a second use-case from Project Omega, and show a further example of where vocabulary reuse can be challenging.</p>]]></content:encoded></item><item><title><![CDATA[Extreme Identifiers (for use in URIs)]]></title><description><![CDATA[<p>Recently I have been thinking about how Identifiers for things should be constructed. More specifically, as part of Project Omega for TNA (<a href="https://www.nationalarchives.gov.uk/">The National Archives</a>), I have been thinking about identifiers for resources in RDF. This blog post continues on from my previous posts: <a href="https://blog.adamretter.org.uk/archival-identifiers-for-digital-files/">Archival Identifiers for Digital Files</a>, and</p>]]></description><link>https://blog.adamretter.org.uk/extreme-identifiers-for-uri/</link><guid isPermaLink="false">5ebc082c076ded022a3cafcb</guid><category><![CDATA[Archive]]></category><category><![CDATA[RDF]]></category><category><![CDATA[URI]]></category><category><![CDATA[catalogue]]></category><category><![CDATA[nationalarchives]]></category><dc:creator><![CDATA[Adam Retter]]></dc:creator><pubDate>Wed, 17 Jun 2020 09:38:38 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1521673252667-e05da380b252?ixlib=rb-1.2.1&amp;q=80&amp;fm=jpg&amp;crop=entropy&amp;cs=tinysrgb&amp;w=1080&amp;fit=max&amp;ixid=eyJhcHBfaWQiOjExNzczfQ" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1521673252667-e05da380b252?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjExNzczfQ" alt="Extreme Identifiers (for use in URIs)"><p>Recently I have been thinking about how Identifiers for things should be constructed. More specifically, as part of Project Omega for TNA (<a href="https://www.nationalarchives.gov.uk/">The National Archives</a>), I have been thinking about identifiers for resources in RDF. This blog post continues on from my previous posts: <a href="https://blog.adamretter.org.uk/archival-identifiers-for-digital-files/">Archival Identifiers for Digital Files</a>, and <a href="https://blog.adamretter.org.uk/archival-catalog-identifiers/">Archival Catalogue Record Identifiers</a>.</p><p>Whilst this article frames the content in the context of RDF and Archives, the principles are much more widely applicable. This blog post shows how to efficiently encode any positive numeric integer into a URI.</p><p>A resource in RDF is identified by a URI and that URI often consists of two major components, first a <em>base URI</em>, and then some sort of (domain specific) <em>local</em> <em>identifier</em>. For example:</p><!--kg-card-begin: markdown--><ul>
<li>
<p>A Base URI: <code>http://cat.nationalarchives.gov.uk</code></p>
</li>
<li>
<p>A local identifier: <code>Record12345</code></p>
</li>
<li>
<p>The final URI: <code>http://cat.nationalarchives.gov.uk/Record12345</code></p>
</li>
</ul>
<!--kg-card-end: markdown--><p></p><p>One of the types of resource that we need to describe in RDF is that of a <em>Digital File,</em> you can think of it simply as a file on your computer. It is the identifiers of those digital files that we will concern ourselves with in this article.</p><p>In my recent article - <a href="https://blog.adamretter.org.uk/archival-identifiers-for-digital-files">Archival Identifiers for Digital Files</a>, I covered two different identifier schemes for Digital File: <a href="https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_4_(random)">UUID</a> (Universally Unique Identifier), and ACID (Archival Content Identifier). One thing that bothered me was the length of the presentation representation (i.e. <a href="https://en.wikipedia.org/wiki/Hexadecimal">hexadecimal</a> encoding) of those identifiers.</p><p>If for a moment we ignore the <em>Hash Function Type</em> prefix of the ACID scheme, we can recognise that each scheme is really just generating a large positive integer! As hexadecimal has a numeric base of 16, i.e. there are only 16 symbols in its alphabet, we might likewise recognise that it is perhaps not the most efficient presentation representation.</p><p>Our Digital File identifier will ultimately form the local identifier component of our RDF resource URI. What would be the most efficient way to encode that Digital File identifier into a URI?</p><h3 id="encoding-a-number-into-a-uri-path">Encoding a Number into a URI Path</h3><p>If we wanted to encode any positive integer (i.e. <a href="https://en.wikipedia.org/wiki/Decimal">Base10</a>) into a URI path as efficiently as possible, we first need to create an alphabet that utilizes every possible character that can be legally expressed within the path of a URI. Once we have that alphabet we can encode a Base10 number into a Base<em>N</em> number, where <em>N</em> is the number of characters in our new alphabet.</p><p>I took the following steps to create such an alphabet:</p><!--kg-card-begin: markdown--><ol>
<li>
<p>Start with all possible path characters for use in a URI from <a href="https://www.ietf.org/rfc/rfc2396.txt">RFC 2396</a>. These are defined as <code>pchar</code> in section 3.3 of the RFC.</p>
</li>
<li>
<p>Eliminate escaped characters by removing the <code>%</code> character. An escaped character is modelled in a URI by using the <code>%</code> character and then two hexadecimal characters. We are interested in using the least characters possible, so using 3 characters here to represent 1 character is the opposite of what we want to do!</p>
</li>
<li>
<p>(optional) Eliminate the English vowels - <code>A</code>, <code>E</code>, <code>I</code>, <code>O</code>, and <code>U</code>, and <code>a</code>, <code>e</code>, <code>i</code>, <code>o</code>, and <code>u</code>. We don't want to incidentally create meaningful words! This is a requirement for TNA; if they were to publish their RDF as Linked Data, then such encoded numbers would become publicly visible.</p>
</li>
<li>
<p>Sort the remaining characters byte-wise by their UTF-8 numeric value.</p>
</li>
</ol>
<!--kg-card-end: markdown--><p>Following these steps yields an alphabet of 78 characters, or if you perform the optional Step 3, then 68 characters.</p><!--kg-card-begin: markdown--><table>
<thead>
<tr>
<th>Numeric Value</th>
<th>Encoded Symbol</th>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td>!</td>
</tr>
<tr>
<td>1</td>
<td>$</td>
</tr>
<tr>
<td>2</td>
<td>&amp;</td>
</tr>
<tr>
<td>3</td>
<td>'</td>
</tr>
<tr>
<td>4</td>
<td>(</td>
</tr>
<tr>
<td>5</td>
<td>)</td>
</tr>
<tr>
<td>6</td>
<td>*</td>
</tr>
<tr>
<td>7</td>
<td>+</td>
</tr>
<tr>
<td>8</td>
<td>,</td>
</tr>
<tr>
<td>9</td>
<td>-</td>
</tr>
<tr>
<td>10</td>
<td>.</td>
</tr>
<tr>
<td>11</td>
<td>0</td>
</tr>
<tr>
<td>12</td>
<td>1</td>
</tr>
<tr>
<td>13</td>
<td>2</td>
</tr>
<tr>
<td>14</td>
<td>3</td>
</tr>
<tr>
<td>15</td>
<td>4</td>
</tr>
<tr>
<td>16</td>
<td>5</td>
</tr>
<tr>
<td>17</td>
<td>6</td>
</tr>
<tr>
<td>18</td>
<td>7</td>
</tr>
<tr>
<td>19</td>
<td>8</td>
</tr>
<tr>
<td>20</td>
<td>9</td>
</tr>
<tr>
<td>21</td>
<td>:</td>
</tr>
<tr>
<td>22</td>
<td>=</td>
</tr>
<tr>
<td>23</td>
<td>@</td>
</tr>
<tr>
<td>24</td>
<td>B</td>
</tr>
<tr>
<td>25</td>
<td>C</td>
</tr>
<tr>
<td>26</td>
<td>D</td>
</tr>
<tr>
<td>27</td>
<td>F</td>
</tr>
<tr>
<td>28</td>
<td>G</td>
</tr>
<tr>
<td>29</td>
<td>H</td>
</tr>
<tr>
<td>30</td>
<td>J</td>
</tr>
<tr>
<td>31</td>
<td>K</td>
</tr>
<tr>
<td>32</td>
<td>L</td>
</tr>
<tr>
<td>33</td>
<td>M</td>
</tr>
<tr>
<td>34</td>
<td>N</td>
</tr>
<tr>
<td>35</td>
<td>P</td>
</tr>
<tr>
<td>36</td>
<td>Q</td>
</tr>
<tr>
<td>37</td>
<td>R</td>
</tr>
<tr>
<td>38</td>
<td>S</td>
</tr>
<tr>
<td>39</td>
<td>T</td>
</tr>
<tr>
<td>40</td>
<td>V</td>
</tr>
<tr>
<td>41</td>
<td>W</td>
</tr>
<tr>
<td>42</td>
<td>X</td>
</tr>
<tr>
<td>43</td>
<td>Y</td>
</tr>
<tr>
<td>44</td>
<td>Z</td>
</tr>
<tr>
<td>45</td>
<td>_</td>
</tr>
<tr>
<td>46</td>
<td>b</td>
</tr>
<tr>
<td>47</td>
<td>c</td>
</tr>
<tr>
<td>48</td>
<td>d</td>
</tr>
<tr>
<td>49</td>
<td>f</td>
</tr>
<tr>
<td>50</td>
<td>g</td>
</tr>
<tr>
<td>51</td>
<td>h</td>
</tr>
<tr>
<td>52</td>
<td>j</td>
</tr>
<tr>
<td>53</td>
<td>k</td>
</tr>
<tr>
<td>54</td>
<td>l</td>
</tr>
<tr>
<td>55</td>
<td>m</td>
</tr>
<tr>
<td>56</td>
<td>n</td>
</tr>
<tr>
<td>57</td>
<td>p</td>
</tr>
<tr>
<td>58</td>
<td>q</td>
</tr>
<tr>
<td>59</td>
<td>r</td>
</tr>
<tr>
<td>60</td>
<td>s</td>
</tr>
<tr>
<td>61</td>
<td>t</td>
</tr>
<tr>
<td>62</td>
<td>v</td>
</tr>
<tr>
<td>63</td>
<td>w</td>
</tr>
<tr>
<td>64</td>
<td>x</td>
</tr>
<tr>
<td>65</td>
<td>y</td>
</tr>
<tr>
<td>66</td>
<td>z</td>
</tr>
<tr>
<td>67</td>
<td>~</td>
</tr>
</tbody>
</table>
<!--kg-card-end: markdown--><p>Our 68 characters can yield a Base68 representation, which is much more compact than a Base16 (hexadecimal) representation.</p><p>Encoding from Base10 to any other base is performed by a common recursive mathematical function. How this is achieved is not particularly relevant, but for both completeness and those readers that are interested, I have written an expression of it for encoding to Base68 using the Scala programming language:</p><!--kg-card-begin: code--><pre><code class="language-scala">/**
 * Encodes a Base10 positive integer to a Base68 String.
 *
 * @param value a positive integer
 * @return the encoded representation
 */
def encode(value: BigInt): String = {

  val b68Alphabet = Seq(
    '!', '$', '&amp;', ''', '(', ')', '*', '+', ',', '-', '.',
    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':',
    '=', '@', 'B', 'C', 'D', 'F', 'G', 'H', 'J', 'K', 'L',
    'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'V', 'W', 'X', 'Y',
    'Z', '_', 'b', 'c', 'd', 'f', 'g', 'h', 'j', 'k', 'l',
    'm', 'n', 'p', 'q', 'r', 's', 't', 'v', 'w', 'x', 'y',
    'z', '~'
  )
  val len = b68Alphabet.length

  @tailrec
  def encode(v: BigInt, accum: List[Char]): String = {
    if(v == 0 &amp;&amp; accum.nonEmpty) {
      accum.mkString
    } else if(v &lt;= 1) {
      (b68Alphabet(v.toInt) :: accum).mkString
    } else {
      val div = v / len
      val mod = v % len
      encode(div, b68Alphabet(mod.toInt) :: accum)
    }
  }

  encode(value, List.empty[Char])
}</code></pre><!--kg-card-end: code--><p><br><strong>Examples</strong></p><p>Let's look at some examples of what these encoded identifiers look like.<br></p><ol><li>A Version 4 UUID in its default hexadecimal representation: <code>d879f8b2-5f67-495d-8796-5ce5b06ba238</code>, consists of 36 characters and is equivalent to the Base10 number: <code>287746559179145117594110380901673968184</code>.<br><br>If we instead encode the Base10 number into our Base68 alphabet this yields the string: <code>xDZTz4*0-L0+S5V@4wFZB</code>, which is just 21 characters in length. Compared to the default hexadecimal representation, this is a saving of 15 characters, i.e. ~42%.<br></li><li>An ACID in its default hexadecimal representation: <code>!3cbae8f16217ad44981e5843100092cd582202e69d452eb094480f2d24abdb49</code>, after dropping the <code>!</code> character (for the <em>Hash Function Type</em> prefix), consists of 64 characters and is equivalent to the Base10 number: <code>27469012181874709647382529974656231352255408628267256258746051793118097562441</code>.<br><br>If we instead encode the Base10 number into our Base68 alphabet this yields the string: <code>94TTsZ-tsvNkZzcM2jWXYCy,ym4d1XZ8N7).8:N9v6</code>, which is just 42 characters in length. Compared to the default hexadecimal representation, this is a saving of 22 characters, i.e. ~34%.<br></li></ol><p>Our Digital File resource URI would now look like:</p><!--kg-card-begin: markdown--><ol>
<li>A UUID based identifier for a Digital File using Base68:</li>
</ol>
<pre><code>http://cat.nationalarchives.gov.uk/xDZTz4*0-L0+S5V@4wFZB
</code></pre>
<ol start="2">
<li>An ACID based identifier for a Digital File using Base68 (with the <em>Hash Function Type</em> prefix reinstated):</li>
</ol>
<pre><code>http://cat.nationalarchives.gov.uk/!94TTsZ-tsvNkZzcM2jWXYCy,ym4d1XZ8N7).8:N9v6
</code></pre>
<!--kg-card-end: markdown--><p><br>Do these Base68 encoded identifiers look ugly to the human eye? Absolutely!<br>But... I did not design them for human-use or even to be humane! ;-) Instead, this encoding scheme is designed to fit the identifier for a Digital File (a number), into a URI as succinctly as possible, whilst still yielding a valid URI. I expect these URIs to be used by machines and not humans!</p><p>Is this a good solution? It certainly meets the requirements I set out, and it feels computationally neat. In practice, it likely needs further thought and experimentation. Will these identifies prove too cumbersome for developers writing SPAQL queries against our data? Possibly!</p><p><br><strong>Decoding from Base68</strong></p><p>Finally, decoding from Base<em>N</em> to Base10 is performed by a very simple table lookup against our alphabet. Like encoding, how this is achieved is not particularly relevant, but again for both completeness and those readers that are interested, I have included a decoder from Base68 using the Scala programming language:</p><!--kg-card-begin: code--><pre><code class="language-scala">/**
 * Decodes a Base68 string to a Base10 positive integer.
 *
 * @param str the encoded representation
 * @return a positive integer
 */
def decode(str: String): BigInt = {

  val b68Alphabet = Seq(
    '!', '$', '&amp;', ''', '(', ')', '*', '+', ',', '-', '.',
    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':',
    '=', '@', 'B', 'C', 'D', 'F', 'G', 'H', 'J', 'K', 'L',
    'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'V', 'W', 'X', 'Y',
    'Z', '_', 'b', 'c', 'd', 'f', 'g', 'h', 'j', 'k', 'l',
    'm', 'n', 'p', 'q', 'r', 's', 't', 'v', 'w', 'x', 'y',
    'z', '~'
  )
  val len = b68Alphabet.length

  val indicies = str.map(b68Alphabet.indexOf(_))
  val vs: Seq[BigInt] = for(i &lt;- 0 to indicies.length - 1) yield {
    val exp = (indicies.length - i) - 1
    indicies(i) * BigInt(len).pow(exp)
  }
  vs.reduceLeft(_ + _)
}</code></pre><!--kg-card-end: code--><p></p><p><strong>Open Source</strong></p><p>More complete encoders and decoders can be found on GitHub implemented in both Scala: <a href="https://github.com/nationalarchives/oci-tools-scala">oci-tools-scala</a>, and TypeScript: <a href="https://github.com/nationalarchives/oci-tools-ts">oci-tools-typescript</a>.</p>]]></content:encoded></item><item><title><![CDATA[Archival Identifiers for Digital Files]]></title><description><![CDATA[When considering development of a new archival catalogue that can describe both physical, digitised, and born digital records, we quickly realised that unlike its predecessors this catalogue will also need to describe digital files. This blog post looks at using Content Identifiers for Digital File.]]></description><link>https://blog.adamretter.org.uk/archival-identifiers-for-digital-files/</link><guid isPermaLink="false">5ebd05cc076ded022a3cb096</guid><category><![CDATA[nationalarchives]]></category><category><![CDATA[catalogue]]></category><category><![CDATA[digital preservation]]></category><category><![CDATA[Persistent Identifiers]]></category><category><![CDATA[Records Management]]></category><dc:creator><![CDATA[Adam Retter]]></dc:creator><pubDate>Tue, 09 Jun 2020 18:36:16 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1550221927-f7e52256370b?ixlib=rb-1.2.1&amp;q=80&amp;fm=jpg&amp;crop=entropy&amp;cs=tinysrgb&amp;w=1080&amp;fit=max&amp;ixid=eyJhcHBfaWQiOjExNzczfQ" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1550221927-f7e52256370b?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjExNzczfQ" alt="Archival Identifiers for Digital Files"><p>As part of Project Omega for TNA (<a href="https://www.nationalarchives.gov.uk/">The National Archives</a>), I have been thinking about how identifiers for Digital Files should be constructed. This blog entry continues on from my previous entry: <a href="https://blog.adamretter.org.uk/archival-catalog-identifiers/">Archival Catalogue Record Identifiers</a>.<strong> </strong></p><p>When considering development of a new archival catalogue that can describe both physical, digitised, and born digital records, we quickly realised that unlike its predecessors this catalogue will also need to describe digital files.</p><p>At this point you might think that I am mixing current concerns between what archives' have often thought of as two separate systems, 1) their Archival catalogue, and 2) their Digital Preservation system. Yes, I am, and intentionally so! However, I would argue that this soup has been cooking for some time; I have seen that until now digital preservation systems have had to include some aspect of cataloguing (for their digital records) as the traditional archival catalogues, that were already in-place, were ill-equipped to describe the new digital world. I believe that a clean and mutually-beneficial separation between cataloguing and (digital) preservation activities can be established, but that as practitioners we are still very much writing the book on digital preservation.</p><p>Anyway, I digress! The archival concept of a Digital File is a complex one, as archivists we have to ask difficult questions like:</p><!--kg-card-begin: markdown--><ul>
<li>What is a digital file?</li>
<li>How do I describe a digital file?</li>
<li>Is a copy of a file the same digital file?</li>
<li>If I change the name of the file, is it still the same digital file?</li>
</ul>
<!--kg-card-end: markdown--><p>All of these things have to be considered when designing a scheme for local identifiers of Digital File. Without writing an extended article on various principles of digital preservation, it is perhaps enough to say that the file's path and/or name are not suitable for use as an identifier; in no small part due to both their transient nature, and inability to be combined with files from other systems which may cause rise to naming conflicts.</p><h3 id="the-current-approach">The Current Approach</h3><p>To date the predominant approach in digital preservation for generating identifiers for digital files has been to simply assign them a <a href="https://en.wikipedia.org/wiki/Universally_unique_identifier">UUID</a> (Universally Unique Identifier); more specifically a Version 4 UUID. This approach has several nice properties:</p><!--kg-card-begin: markdown--><ul>
<li>
<p>These can be generated independently of each other.</p>
<p>You can just <em>magic</em> a UUID into existence without concern for other UUIDs that have gone before or come after it.</p>
<p>The chance of a collision is <a href="https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_4_(random)#Collisions">incredibly small</a> - &quot;<em>the probability to find a duplicate within 103 trillion version-4 UUIDs is one in a billion</em>&quot;.</p>
</li>
<li>
<p>They are relatively compact and presentable.</p>
<p>A UUID is just a 128-bit positive integer. This is typically formatted for presentation as a hexadecimal string of five components, totaling 36 printable characters, albeit they are not very human friendly.</p>
</li>
<li>
<p>They are cheap to compute.</p>
<p>On a modern laptop we can easily generate over 500,000 every second.</p>
</li>
</ul>
<!--kg-card-end: markdown--><h3 id="a-new-approach-content-identifiers"><br>A New Approach - Content Identifiers</h3><p>As an alternative to UUIDs, I am proposing a new approach for generating an identifier for Digital File which is computed from the content of the file itself.</p><p>I should be clear that this is not some stroke of genius on my part, similar approaches are already widely used in other domains. For example, the <a href="https://git-scm.com/">Git</a> SCM (Source Code Management) uses SHA1 digests to identify files and changes. Likewise, the <a href="https://en.wikipedia.org/wiki/InterPlanetary_File_System">IPFS</a> (InterPlanetary File System) uses <a href="https://docs.ipfs.io/guides/concepts/cid/">its definition of a CID</a> (Content Identifier), which is a hash function's digest of a file's content to address that file.</p><p>To avoid any confusion between IPFS CID's and our "Content Identifiers", I will herein use the abbreviation ACID (Archival Content Identifier) to refer to my proposal for identifiers.</p><p>The main part of an ACID is generated by computing the digest of the byte-stream (i.e. content) of the digital file via a hash function. This raises the question, of which hash function should be used? There is a wealth of <a href="https://en.wikipedia.org/wiki/List_of_hash_functions">different hash algorithms</a> available with various properties and different trade-offs. That being said, I am going to suggest that we use a <a href="https://blake2.net/">BLAKE2b</a>-256 hash for the following reasons:</p><!--kg-card-begin: markdown--><ul>
<li>Recognised and verified by NIST.</li>
<li>Likelihood of collision is incredibly small.</li>
<li>Much faster to generate than equivalents such as SHA-256.</li>
<li>At least as secure as SHA-3.</li>
</ul>
<!--kg-card-end: markdown--><p>For example, if we wanted to generate a BLAKE2b-256 hash digest for the Apache 2.0 License file, we could run:</p><!--kg-card-begin: code--><pre><code class="language-bash">curl https://www.apache.org/licenses/LICENSE-2.0.txt | b2sum --length 256 --binary</code></pre><!--kg-card-end: code--><p>This yields a 256-bit number encoded into a hexadecimal string totaling 64 printable characters:</p><!--kg-card-begin: code--><pre><code>3cbae8f16217ad44981e5843100092cd582202e69d452eb094480f2d24abdb49</code></pre><!--kg-card-end: code--><p>This hexadecimal string has some interesting properties:</p><!--kg-card-begin: markdown--><ul>
<li>
<p>It can be used an an identifier for the Digital File.</p>
</li>
<li>
<p>Verifiable Descriptions.</p>
<p>Provoided with both, 1) the description and identifier of a digital file and, 2) the file itself, we can verify that the description is indeed about the file by re-computing the hash digest of the file and comparing the result with the digital file identifier.</p>
</li>
<li>
<p>Verifiable Preservation.</p>
<p>Similarly to above, if the hash digest of the file changes over time, then we can assert that there has been an issue with its preservation, e.g. <a href="https://en.wikipedia.org/wiki/Data_degradation">data-rot</a>.</p>
</li>
</ul>
<!--kg-card-end: markdown--><p>There are some down-sides to using a hash digest as opposed to a UUID:</p><!--kg-card-begin: markdown--><ul>
<li>
<p>More expensive to compute.</p>
<p>A hash digest is much more expensive to compute than a UUID, and the larger the file being digested the more expensive it becomes.</p>
</li>
<li>
<p>Less compact.</p>
<p>Our 256-bit hash generates a result which is twice as long as a UUID.</p>
</li>
</ul>
<!--kg-card-end: markdown--><p>I believe that the down-sides of a hash digest are outweighed by its advantage of offering verifiability.</p><p><strong>Which Hash Function was it?</strong></p><p>For the purposes of preservation and interoperability, one thing that we have not yet considered is how one determines which hash function was used to generate an identifier. Sure, I said we would use BLAKE2b-256, but what if you want to use a different hash function? Also, from an digital archaeological perspective, given an identifier like:</p><!--kg-card-begin: code--><pre><code>3cbae8f16217ad44981e5843100092cd582202e69d452eb094480f2d24abdb49</code></pre><!--kg-card-end: code--><p>You might be able to infer that it is a hash digest, and the selection of characters used and the number of them would indicate that it could be a 256-bit hash... but which hash function was used?</p><p>Ideally, we need a mechanism to also communicate the hash function that was used. In fact IPFS already thought about this, and they use an encoding called <a href="https://multiformats.io/multihash/">Multihash</a> which prefixes their CIDs with a code indicating the hash function used. Whilst we could adopt Multihash here, it's much more complex than we need (famous last words?!?). Instead, I propose that our ACID's have a single ASCII character at the start that indicates the hash function that was used. A single ASCII character has the advantage of a fixed-length numeric encoding, and it makes the number of characters in the hexadecimal string representation an odd number, thus providing a hint to a digital archeologist that perhaps this ACID is similar to a digest but with an extra character. I will go one step further and say that this character should be outside of the hexadecimal alphabet (and ignorant of case-sensitivity), this should make it glaringly obvious to such a digital archeologist that the prefix character has a meaning which is distinct from the rest of the string.</p><p>An ACID is then formatted from a template like this:</p><!--kg-card-begin: code--><pre><code>{Hash Function Type}{Hash Digest}</code></pre><!--kg-card-end: code--><p>For the Hash Function Type, I am going to reserve the <code>!</code> character to indicate BLAKE2b-256. Why? Because, I think it looks cool! This would mean that our earlier digital file identifier now simply becomes:</p><!--kg-card-begin: code--><pre><code>!3cbae8f16217ad44981e5843100092cd582202e69d452eb094480f2d24abdb49</code></pre><!--kg-card-end: code--><p><br><strong>What about Collisions?</strong></p><p>Sure, generating a digital file identifier with BLAKE2b-256 has a very small chance of generating a collision (i.e. two different files with the same identifier), but what if...?</p><p>If you detect a collision, I will build you a new digital archive system for free... Nope! Just joking! We actually already have a mechanism for coping with this, the Hash Function Type; for the new file which creates the collision you could switch to a different hash function, perhaps a 512-bit one! This would at least give you a different unique identifier. But... what to do about the original file which is on the other side of the collision, it's probably deeply embedded in your archive by now! You could re-catalogue it, but maybe you don't even need to???<br></p><p><br>I have in mind the idea to write another article about further encoding such ACID's for compact machine use. Okay… that’s enough for today!</p>]]></content:encoded></item><item><title><![CDATA[Archival Catalogue Record Identifiers]]></title><description><![CDATA[For The National Archives, we examine existing schemes for identifying Archival Catalogue Records. We then propose a new modern scheme suitable for use by both humans and URI for Linked Data: Omega Catalogue Identifiers.]]></description><link>https://blog.adamretter.org.uk/archival-catalog-identifiers/</link><guid isPermaLink="false">5e99be634e02800232715fe2</guid><category><![CDATA[The National Archives]]></category><category><![CDATA[Persistent Identifiers]]></category><category><![CDATA[URI]]></category><category><![CDATA[RDF]]></category><category><![CDATA[Records Management]]></category><category><![CDATA[Archive]]></category><category><![CDATA[nationalarchives]]></category><category><![CDATA[OCI]]></category><dc:creator><![CDATA[Adam Retter]]></dc:creator><pubDate>Wed, 03 Jun 2020 10:48:08 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1516496636080-14fb876e029d?ixlib=rb-1.2.1&amp;q=80&amp;fm=jpg&amp;crop=entropy&amp;cs=tinysrgb&amp;w=1080&amp;fit=max&amp;ixid=eyJhcHBfaWQiOjExNzczfQ" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1516496636080-14fb876e029d?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjExNzczfQ" alt="Archival Catalogue Record Identifiers"><p>As <a href="https://blog.adamretter.org.uk/rdf-plugins-for-pentaho-kettle/">previously described</a>, in Project Omega at TNA (<a href="https://www.nationalarchives.gov.uk">The National Archives</a>), we will be using a Graph Model to hold all of the catalogue information, specifically an RDF model.</p><p>One of the key things in RDF is that every resource has a URI. In the Omega Catalogue we will have a plethora of different types of resources - (Archival Records, Locations, People, Organisations, etc.). Every one of these resources will need a URI. For our purposes these URI are composed of two parts, a <em>base</em> and an <em>identifier</em>.</p><p>In Omega we will be using a flat addressing scheme (i.e. no sub-folders/paths in the URI), and so our <em>base</em> is fixed and could be something like either: <br><code>http://cat.nationalarchives.gov.uk/</code></p><p>or </p><p><code>http://cat.nationalarchives.gov.uk#</code>.</p><p>Determining which is best to use depends on which approach of the <a href="https://www.w3.org/wiki/HashVsSlash">HashVsSlash</a> schemes is most advantageous to your application. I have decided that in Omega we will be using the Slash scheme, i.e. <code>http://cat.nationalarchives.gov.uk/</code>. The Slash scheme has the advantage that there are multiple documents. As our catalogue will be very large, the single document approach as used by the Hash scheme would be unwieldy.</p><p>Now, the interesting part is the <em>identifier</em>! Every resource in our RDF graph needs a URI and therefore an identifier, in this article I will focus solely on identifiers for Archival Records.</p><h2 id="requirements-for-a-good-identifier">Requirements for a Good Identifier</h2><p>When choosing identifiers for our resources, there are some properties that they <em>must</em>/<em>should</em>/<em>count</em> exhibit:</p><!--kg-card-begin: markdown--><ol>
<li>
<p><em>Must</em> be Unique (within our <em>domain</em>).<br>
We can't have one identifier identify more than one resource without breaking our RDF model.</p>
</li>
<li>
<p><em>Must</em> be Persistent.<br>
We don't want our resources disappearing and/or reappearing with different URI over time, otherwise we end up with broken links. Therefore the identifier must be immutable, thus meaning that we must exclude any changeable properties of a resource from use within its identifier. This is also for archival purposes, as ideally we don't want to have to retrieve records from potentially distant locations or media to modify their identifiers.</p>
</li>
<li>
<p><em>Must</em> be Computable.<br>
Whatever form the identifier takes it must be computationally valid for use within a URI. Ideally it should be possible to generate such an identifier computationally without requiring a manual (human) registration/validation process.</p>
</li>
<li>
<p><em>Must</em> be Uniform.<br>
By ensuring that every identifier follows a prescribed format and length, it is easy to validate what is an identifier and what is not; that is not to say that an identifier leads to a resource.</p>
</li>
<li>
<p><em>Should</em> be Humane.<br>
The <em>identifier</em> when considered as part of a larger URI is often used by humans, perhaps within SPARQL statements that they construct to query the data, or by de-referencing such URI via the Web.<br>
Additionally the <em>identifier</em> as a stand-alone element may have value in itself and could conceivable be used by humans to communicate about resources, for example a visitor to TNA could imaginably ask to see the record with identifier X.</p>
<p>Consideration should be given to making the identifiers communicable by humans, which implies that there are additional desirable properties, such as:</p>
<ol>
<li>
<p><em>Should</em> be succinct.<br>
Typically humans are better at accurately communicating short sequences of data.</p>
</li>
<li>
<p><em>Should</em> be easy to transcribe.<br>
Human transcription errors can be reduced by using a commonly known alphabet. <a href="https://en.wikipedia.org/wiki/Latin_alphabet">The Latin or Roman alphabet</a> would seem sensible for an archive based in the UK. This would suggest excluding any non-alphanumeric characters from the identifier.</p>
</li>
<li>
<p><em>Should</em> be easy to verbalise.<br>
Records of TNA are often discussed or requested verbally. For example, collaboration between staff members, or by a member of the public telephoning or making an enquiry face-to-face on site.</p>
</li>
</ol>
</li>
<li>
<p><em>Could</em> Convey Knowledge.<br>
If the identifier is able to convey some knowledge about the resource that can be interpreted by machines and/or humans, we gain the advantage of being able to determine certain facts about the resource just from its identifier. This has to be carefully balanced with (2).</p>
</li>
</ol>
<!--kg-card-end: markdown--><p>There are two interesting articles from the W3C about designing URI schemes both for the Web and RDF that may be of further interest to the reader:</p><ul><li><a href="https://www.w3.org/Provider/Style/URI">Cool URIs don't change</a></li><li><a href="https://www.w3.org/TR/cooluris/">Cool URIs for the Semantic Web</a></li></ul><h2 id="existing-identifiers-for-archival-records">Existing Identifiers for Archival Records</h2><p>Those of you readers already familiar with records keeping, may of course be thinking that TNA must have an existing identifier scheme for its records, and you would be right. In fact TNA has several different schemes in-use today for identifying its records. For brevity's sake I will discount those used within various internal systems, and focus on what the general public tend to see.</p><p>The predominant identifier used by TNA for its records that the general public (and most staff) see and work with is simply known as a "<em>Catalogue Reference</em>". In actuality there are two different identifier schemes in use today, and a Catalogue Reference may be expressed in one or the other scheme. The schemes are:</p><!--kg-card-begin: markdown--><ol>
<li>
<p>CCR (Classic Catalogue Reference)<br>
Before the advent of GCRs, this was simply known as &quot;The Catalogue Reference&quot; and was the de-facto identifier for any record catalogued by TNA. It was developed before the advent of digital records.</p>
</li>
<li>
<p>GCR (Generated Catalogue Reference)<br>
I developed this scheme for TNA in 2012 to allow computational generation of identifiers for digital records.</p>
</li>
</ol>
<!--kg-card-end: markdown--><p>At present, TNA uses CCRs for physical (think paper) records, and GCRs for <a href="https://www.nationalarchives.gov.uk/information-management/manage-information/digital-records-transfer/what-are-born-digital-records/">Born-Digital</a> records. CCRs were previously used for Digitised records, but GCRs are now starting to be used for those too.</p><p>We will briefly take a look at each existing identifier scheme, and show that unfortunately both have properties which make them unsuitable for use as identifiers within URI.</p><h3 id="ccr-classic-catalogue-references-">CCR (Classic Catalogue References)</h3><p>To understand CCRs, you need to first understand a little bit about how archival records are arranged. Ultimately it all comes down to the principle of <a href="https://en.wikipedia.org/wiki/Respect_des_fonds"><em>Respect des fonds</em></a>, in the simplest of terms - we must respect the arrangement of the records as defined by their creator. In more concrete terms TNA uses an internal standard known simply as TNA-CS13 (The National Archives - Cataloguing Standards 2013) which is itself derived from <a href="https://www.ica.org/en/isadg-general-international-standard-archival-description-second-edition">ISAD(G)</a> (General International Standard Archival Description).</p><p>TNA-CS13 basically stipulates that each record is arranged according to a mono-hierarchical structure, that structure may have between 3 and 7 levels. These levels are known by the names (from top-to-bottom): Department, Division, Series, Sub-series, Sub-sub-series, Piece, and Item; you can read more about them in the article <a href="https://www.nationalarchives.gov.uk/help-with-your-research/citing-records-national-archives/#section3">Citing records in The National Archives</a>.</p><p>A CCR identifier basically encodes all the references used for 3 or 4 levels of the record's arrangement. The CCR scheme has one of two forms, for records catalogued to Piece level it is:</p><!--kg-card-begin: code--><pre><code>{Department Reference} {Series Reference}/{Piece Reference}
</code></pre><!--kg-card-end: code--><p>For records catalogue to Item level the scheme is:</p><!--kg-card-begin: code--><pre><code>{Department Reference} {Series Reference}/{Piece Reference}/{Item Reference}
</code></pre><!--kg-card-end: code--><p>Here are five examples of valid CCRs that are in use:</p><ol><li><a href="https://discovery.nationalarchives.gov.uk/details/r/C658130"><code>MH 55/2713</code></a></li><li><a href="https://discovery.nationalarchives.gov.uk/details/r/C11978467"><code>AIR 79/1064/118667</code></a></li><li><a href="https://discovery.nationalarchives.gov.uk/details/r/C5005305"><code>E 317/Devon/1</code></a></li><li><a href="https://discovery.nationalarchives.gov.uk/details/r/C4090372"><code>AIR 1/1983/204/273/89-M-N-O-P-R-S</code></a></li><li><a href="https://discovery.nationalarchives.gov.uk/details/r/C7655609"><code>T 1/440/15,65-66107-113,116-142,166,180-189,etc</code></a></li></ol><p>From what I have explained so far, hopefully you have recognised that Example (1) is a CCR for a Piece, whereas Example (2) is a CCR for an Item.</p><p><br>Now, I would not blame you for thinking that Example (3) is also an Item, however you would be mistaken! Unfortunately whilst the <code>/</code> character is used as a separator between the Series, Piece, and Item references, at some point in the past it was also introduced as a valid character within the Piece and Item references themselves. We will cover the reason for that decision shortly, but for now we can safely assert that it causes problems: As a human I can no longer visually determine whether the CCR refers to a Piece or an Item, and perhaps worse yet, if I try and parse the identifier using a software program I get an ambiguous result. Sadly the intention for a CCR to carry information that is meant to be helpful to understanding the record has not held up well, instead many CCRs are ambiguous which may lead to confusion, and ultimately the fact that the arrangement of the record cannot be known without going into the physical archival stacks and retrieving it.<br>I chose Example (4) and Example (5) to further illustrate the non-uniformity of CCRs, they refer to a Piece and an Item respectively.</p><p>We can identify several issues that make CCRs unsuitable for our identifier needs in Omega:</p><!--kg-card-begin: markdown--><ol>
<li>
<p>A CCR may be ambiguous and therefore does not meet our requirement for unique identifiers.</p>
</li>
<li>
<p>A CCR encodes the arrangement of the record, whilst one would hope the arrangement is fixed at the time of accession, the reality is that mistakes can be made and from there the record may need to be re-catalogued which could also involve a change to its CCR. Therefore, CCRs do not meet our requirement for persistent identifiers.</p>
</li>
<li>
<p>Each CCR is allocated and registered manually by an archivist whereas we would need to be able to compute such identifiers within Omega. Additionally their ambiguity and non-uniformity means that they cannot be computationally validated.</p>
</li>
<li>
<p>For CCRs with Piece and Item identifiers containing non-alphanumeric characters (e.g. <code>/</code>, or <code>,</code>), such characters would require <a href="https://en.wikipedia.org/wiki/Percent-encoding">URI Encoding</a> to be able to use the CCR as part of a URI. Unfortunately URI Encoding is non-intuitive to humans.</p>
</li>
<li>
<p>The '/' character was introduced into the Piece and Item references within a CCR to allow the archivist to <em>hint</em> at further levels of arrangement which were prohibited by TNA-CS13. A goal of Project Omega is to provide a catalogue that works for any record regardless of its medium (e.g. physical or digital), one known axiom of digital records preservation is that such records have far more complex arrangements than their paper counterparts, often requiring arbitrarily deep levels of hierarchy or poly-hierarchical arrangement. For this reason the encoding of level identifiers into CCRs will not scale for digital records, and was in fact one of the drivers for creating the GCR scheme.</p>
</li>
</ol>
<!--kg-card-end: markdown--><p>Whilst CCRs may not be perfect, it should be recognised they have until now largely been successful in providing an identifier for the retrieval of a record, thus demonstrated by the fact that they are still used daily to access millions of archival records.</p><h3 id="gcr-generated-catalogue-references-">GCR (Generated Catalogue References)</h3><p>I designed the GCR scheme for TNA back in 2012 when I was leading the design and implementation of their DRI (Digital Repository Infrastructure) project. The goal of that 3 year project was to design and implement a new Digital Archive for preserving digital records.</p><p>DRI needed to be able to accession Born Digital records. The practice of archiving and cataloguing physical records is rather well established and understood. At that time the practice of archiving and cataloguing digital records was still in its infancy with much international discussion, and arguably best practice is still being refined. In particular, Born Digital records have several aspects that make them much more complex to handle than physical records, if we are to apply principle of <em>Respect des fonds</em> then we must preserve the creator's arrangement of the digital files comprising the records. Generally digital files are organised according to a mono-hierarchical file-system or file-plan, however such a hierarchy may be of an arbitrarily deep number of levels and operate without any global constraints on the naming of each level. In addition there are some systems (e.g. Content/Document Management Systems and/or Cloud Office Suites) which offer label based arrangements of documents, thus resulting in arbitrarily deep poly-hierarchical structures and again without restrictions on the naming of labels (i.e. levels).</p><p>By recognising that CCRs reflected an arrangement of 3 or 4 levels, and that Born Digital files could have many more levels of arrangement, we realised that adding additional level identifiers to CCRs would not scale; as we could end up with very long CCRs which are encoding file-system paths with each component of arbitrary length. In addition whilst TNA may receive a large collection of paper records and these can be catalogued and accessioned by humans, the volume of digital files for Born Digital records is much much higher, to the extent that cataloguing such records manually becomes impossible with the resources available.</p><p>To solve this problem I developed GCRs, with the goals of:</p><!--kg-card-begin: markdown--><ol>
<li>
<p>Eliminating the encoding of multiple levels of arrangement into the Catalogue Reference.</p>
</li>
<li>
<p>Computing Catalogue References automatically during the automated accessioning process for a collection of digital records.</p>
</li>
<li>
<p>Creating Catalogue References that are unambiguous, uniform, and can be easily validated.</p>
</li>
<li>
<p>Ensuring that the GCR scheme is still easily communicable by humans by both written and verbal mechanisms.</p>
</li>
</ol>
<!--kg-card-end: markdown--><p>A GCR starts just like a CCR by encoding the Department and Series References, however from there it deviates, instead of encoding further levels, it instead uses a sequentially allocated Record Number, and finally an optional Revision Number. The GCR scheme for most records, i.e. those with a single manifestation, looks like:</p><!--kg-card-begin: code--><pre><code>{Department Reference} {Series Reference} {Record Number} Z
</code></pre><!--kg-card-end: code--><p>For records with more than one manifestation, the additional manifestations can be identified by the GCR scheme:</p><!--kg-card-begin: code--><pre><code>{Department Reference} {Series Reference} {Record Number} Z{Revision Number}
</code></pre><!--kg-card-end: code--><p>Each record number is monotonically increasing per Department and Series pair. To ensure that the GCR remains succinct even when there are many records, I then encoded the record number using a custom Base25 alphabet. This encoding results in a significant compression of the number of characters needed to express the record number. The Base25 alphabet was carefully chosen to eliminate characters which could be confused when communicated by humans, for example <code>0</code> (the digit) and <code>O</code> (the letter), in addition I removed vowels so that we were not incidentally generating recognisable words. The <code>Z</code> character, which I also removed from the alphabet, is carefully placed to enable a GCR to be easily distinguishable from a CCR.</p><p><strong>GCR Base25 Alphabet</strong></p><!--kg-card-begin: markdown--><table>
<thead>
<tr>
<th>Numeric Value</th>
<th>Encoded Symbol</th>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td>B</td>
</tr>
<tr>
<td>1</td>
<td>C</td>
</tr>
<tr>
<td>2</td>
<td>D</td>
</tr>
<tr>
<td>3</td>
<td>F</td>
</tr>
<tr>
<td>4</td>
<td>G</td>
</tr>
<tr>
<td>5</td>
<td>H</td>
</tr>
<tr>
<td>6</td>
<td>J</td>
</tr>
<tr>
<td>7</td>
<td>K</td>
</tr>
<tr>
<td>8</td>
<td>L</td>
</tr>
<tr>
<td>9</td>
<td>M</td>
</tr>
<tr>
<td>10</td>
<td>N</td>
</tr>
<tr>
<td>11</td>
<td>P</td>
</tr>
<tr>
<td>12</td>
<td>Q</td>
</tr>
<tr>
<td>13</td>
<td>R</td>
</tr>
<tr>
<td>14</td>
<td>S</td>
</tr>
<tr>
<td>15</td>
<td>T</td>
</tr>
<tr>
<td>16</td>
<td>V</td>
</tr>
<tr>
<td>17</td>
<td>W</td>
</tr>
<tr>
<td>18</td>
<td>X</td>
</tr>
<tr>
<td>19</td>
<td>2</td>
</tr>
<tr>
<td>20</td>
<td>3</td>
</tr>
<tr>
<td>21</td>
<td>4</td>
</tr>
<tr>
<td>22</td>
<td>5</td>
</tr>
<tr>
<td>23</td>
<td>6</td>
</tr>
<tr>
<td>24</td>
<td>7</td>
</tr>
</tbody>
</table>
<!--kg-card-end: markdown--><p>Examples of valid GCRs:</p><!--kg-card-begin: markdown--><ol>
<li><a href="https://discovery.nationalarchives.gov.uk/details/r/02a88459172048279324d7bbd5777059"><code>LOC 5 CWG Z</code></a></li>
<li><a href="https://discovery.nationalarchives.gov.uk/details/r/56b36e67c0f64058b3399982cd0a64a5"><code>LOC 5/FPF/Z</code></a></li>
<li><code>LOC 5 CWG Z3</code></li>
</ol>
<!--kg-card-end: markdown--><p>Example (1) and Example(2) are both valid GCRs. The GCR scheme does not actually stipulate that there should be a <code>/</code> used between the Series, Record, and <code>Z</code> components, however, for visual continuity TNA have elected to use this when presenting them. Example (3) shows the Revision number component, which serves to allow multiple <em>manifestations</em> of a record to be addressed by a GCR, for example you may have a Microsoft Word 2000 Document original, and a migrated PDF manifestation.</p><p>TNA have now been using GCRs for digital records for a few years. Retrospectively looking back at the design of GCRs, I have to admit that I am still quite happy with them, my younger self must have been having a particularly good day when he sat down to design the GCR scheme! It is certainly humbling to think that these innocuous little identifiers are forming a small part of the UKs permanent history, and that I had a hand in defining them.</p><p>For the purpose of considering them for use as the identifier scheme in Omega, GCRs have many of the properties that we require in a good identifier - they are unique, they are persistent (for the vast majority of records), they are computable, they are uniform, and they are humane (in many ways more so that CCRs, although perhaps not as memorable).</p><p>Indeed, we could casually adopt GCRs for use in Omega as our identifier scheme. Yet with further thought there are a couple of minor issues with that, and as we have the opportunity, I could perhaps even improve on GCRs yet. The issues that I perceive with adopting GCRs for Omega are:</p><!--kg-card-begin: markdown--><ol>
<li>
<p>In Omega we want our identifiers to be persistent. If the records need to be re-arranged, whist it is extremely unlikely that the Department reference would change, it is possible that the Series reference could. With a GCR, the Series reference is encoded in the identifier, which means the re-arrangement would unfortunately result in a change to the identifier.</p>
</li>
<li>
<p>To support our goal of building an immutable history of our records in Omega, we have a very clear distinction between, the enduring form of the record (i.e. the concept of the record), and our temporal understanding of the record (i.e. descriptions of the record). For this end, we need identifiers that can indicate both the concept of the record, and its descriptions as our understanding of the record accumulates and evolves through time. Whilst a GCR has a revision number to identify manifestations of the record, and one might consider re-purposing that for indicating revisions of description, that would then fall-short as we also have the concept of manifestations in Omega.</p>
</li>
<li>
<p>Adopting GCRs as identifiers for all records would mean that physical records would also gain a GCR alongside their CCR, born-digital records already have GCRs. There is in fact precedent for this, TNA-CS13 allows records to have Former References alongside their Catalogue Reference, one such Former Reference is the PRO (Public Records Office) reference; The PRO is of course the predecessor of TNA. The issue I perceive is one of mindset, staff know that GCR are used only for digital records, so when they see the <code>Z</code> character in a GCR they infer that it refers to a digital record. This is perhaps unfortunate, whilst it was never previously envisaged that the GCR scheme would be used for physical records, it didn't impose any such limitation, its specification in-fact states: &quot;not solely limited to Born Digital Records&quot; and that the <code>Z</code> character (the Generated Catalogue Reference Indicator) is for the purpose of &quot;allow[ing] users to visually differentiate a GCR from a CCR easily&quot;.</p>
</li>
</ol>
<!--kg-card-end: markdown--><p>Additionally, one place where I think I could improve upon the GCR scheme is where CCRs have the advantage of being able to convey more information directly to the user about the record, thus reducing the need for the user to actually retrieve the record. Sometimes this information may be ambiguous and/or confusing, but more often than not, it is helpful to the user. GCRs removed a lot of that information to meet its goals, and ultimately ended up with a much more persistent identifier scheme which is a good thing. In Omega we have a clear split and definition of information that we believe changes over time, and information which we believe is enduring. If there is enduring information that is useful to the user, and assuming it is sensible for use in an identifier and URI, we can place that into the identifier without compromising on our requirement for persistent identifiers.</p><h3 id="oci-omega-catalogue-identifier-">OCI (Omega Catalogue Identifier)</h3><p>From what I have learnt about CCRs and how TNA use GCRs, in consultation with TNA's Catalogue Team I have developed a new identifier scheme which rather unimaginatively I am simply calling OCI (Omega Catalogue Identifier).</p><p>Let me be clear, my driver for this is solely the requirements of Project Omega. I believe that these identifiers will work well for the URI of our catalogue resources in our RDF graph, and would equally work well within a Linked Data context.</p><p>OCIs can be used for any type of record held by TNA. I might be suggesting, but I am NOT proposing, that the canonical Catalogue Reference of a record catalogued by TNA change from the existing CCR or GCR scheme. At this stage, I see OCIs as complementing CCRs and GCRs, whereby in some applications, such as Omega, the OCI is the primary identifier of the record. Regardless, OCIs will be generated for all existing catalogue records at TNA when they are imported into the Project Omega system. Could TNA start using OCIs instead of CCRs and GCRs for all new records that it accessions? Yes, of course! Will TNA do that? I am the wrong person to ask... that level of decision making is high above my position!</p><p>The basic OCI scheme for records has the following components:</p><!--kg-card-begin: code--><pre><code>{Creator Reference}.{Accession Year}.{Record Number}.{Accession Format}</code></pre><!--kg-card-end: code--><!--kg-card-begin: markdown--><ul>
<li>
<p>Creator Reference<br>
This is some identifier that uniquely identifies the organisation, group, or individual that created the records. Historically at TNA this is most often the Government Department, known as &quot;Department reference&quot; in CCR terms.</p>
</li>
<li>
<p>Accession Year<br>
The year in which the record was accessioned into the archive.</p>
</li>
<li>
<p>Record Number<br>
A monotonically increasing number, initialised per Creator Reference and Accession Year pair. This number is encoded using a special purpose alphabet. This is not the same Base25 alphabet used in GCRs for some important reasons covered below.</p>
</li>
<li>
<p>Accession Format<br>
A single character to indicate the format of the accessioned record. Currently limited to <code>P</code> for physical records, or <code>D</code> for digital records. Note that accession format is useful to indicate the format of the public record that was initially accessioned, but it should be remembered that there might also be additional manifestations of the record available in complementary formats, e.g. a digitisation of a physical record.</p>
</li>
</ul>
<!--kg-card-end: markdown--><p>As already mentioned, Omega separates the concept (or enduring form) of a record from TNA's descriptions' of the record which evolve through time. The OCI scheme illustrated above allows one to identify a record, but how are we to identify the descriptions of the record? To identify a specific description of a record, we simply number them, and add an additional component to indicate the description:</p><!--kg-card-begin: code--><pre><code>{Creator}.{Accession Year}.{Record Number}.{Accession Format}.{Description Number}
</code></pre><!--kg-card-end: code--><!--kg-card-begin: markdown--><ul>
<li>Description Number<br>
A monotonically increasing number, initialised per record concept. This is not encoded. Comparing these numbers only infers an ordering through time of descriptions, it does not infer the correct description as there may be multiple competing descriptions from different sources.</li>
</ul>
<!--kg-card-end: markdown--><p>It is perhaps worth pointing out that within the RDF graph for Omega there are explicit relationships that link a record with all of its descriptions, and also indicate the latest description. From a Linked Data perspective, if a user wished to resolve the record using the web of data, we would provide just the data about the concept and links to its descriptions. However, if another user was to resolve the record via a Web Browser, then we would likely redirect them to an HTML page of the latest description of the record.</p><p>Similarly to how we have multiple descriptions of a record, Omega also offers multiple manifestations of a record. A manifestation of a record can take many different forms, but there is always the original manifestation of the record as accessioned by TNA, for example, a parchment or digital file. There may also be additional manifestations of the record created for preservation or presentation purposes, for example, copies, digitisation, thumbnails, language translation, transcription, redaction, or file-format migration. The OCI scheme adds a component for manifestation, which is numbered in a similar manner to descriptions.</p><!--kg-card-begin: code--><pre><code>{Creator}.{Accession Year}.{Record Number}.{Accession Format}.{M{Manifestation Number}}
</code></pre><!--kg-card-end: code--><!--kg-card-begin: markdown--><ul>
<li>Manifestation Number<br>
Prefixed by an <code>M</code> character, this is a monotonically increasing number, initialised per record concept. This is not encoded. Comparing these numbers does not infer an ordering of manifestations.</li>
</ul>
<!--kg-card-end: markdown--><p>Note, it is important to realise that descriptions and manifestations are both numbered per record concept. Descriptions and manifestations do not have a hierarchical relationship, instead they are orthogonal to each other.</p><p>Here are some examples of (fictional) OCIs:</p><!--kg-card-begin: markdown--><ol>
<li>
<p><code>MSW.1970.7GH.P</code><br>
This is the OCI for a physical record numbered 7GH which was created by MSW (<a href="https://en.wikipedia.org/wiki/The_Ministry_of_Silly_Walks">The Ministry of Silly Walks</a>) and accessioned by TNA in 1970.</p>
</li>
<li>
<p><code>MSW.2014.L4F.D</code><br>
This is the OCI for a digital record numbered L4F which was created by MSW and accessioned by TNA in 2014.</p>
</li>
<li>
<p><code>MSW.1981.HGF.P.1</code><br>
This is the OCI for the 1st description of, the physical record numbered HGF which was created by MSW and accessioned by TNA in 1981.</p>
</li>
<li>
<p><code>MSW.1981.HGF.P.5</code><br>
This is the OCI for the 5th description of, the physical record numbered HGF which was created by MSW and accessioned by TNA in 1981.</p>
</li>
<li>
<p><code>MSW.1999.TSF.P.M1</code><br>
This is the OCI for the 1st manifestation of, the physical record numbered HGF which was created by MSW and accessioned by TNA in 1999.</p>
</li>
<li>
<p><code>MSW.1999.TSF.P.M5</code><br>
This is the OCI for the 5th manifestation of, the physical record numbered HGF which was created by MSW and accessioned by TNA in 1999.</p>
</li>
</ol>
<!--kg-card-end: markdown--><p>Astute readers may have noticed that we could potentially remove the <code>.</code> character between the components in an OCI without loosing precision or introducing ambiguity. This is an interesting idea, and one that I discussed with the Catalogue Team, whilst it would make no difference to a machine, the majority felt that it was clearer for human use if they remained.</p><p>The alphabet for encoding record numbers in OCIs was created by:</p><ol><li>Starting with the <a href="https://tools.ietf.org/html/rfc4648#page-10">Base32 alphabet from RFC 4648</a>.</li><li>Eliminating the English vowels - <code>A</code>, <code>E</code>, <code>I</code>, <code>O</code>, and <code>U</code>. We don't want to incidentally create meaningful words!</li><li>Removing the characters <code>P</code>, <code>D</code>, and <code>M</code>, as they are reserved to signify Physical, Digital, and Manifestation.</li><li>Removing the digit <code>0</code> (zero) as it could be misconstrued as numeric padding.</li><li>Removing the character <code>B</code> as it could be confused with the digit <code>8</code> (eight) when read or written by humans.</li><li>Adding the characters <code>W</code>, <code>X</code>, and <code>Y</code>. We opted not to add <code>Z</code> so as to avoid any confusion with GCRs.</li></ol><p><strong>OCI Base25 Alphabet</strong></p><!--kg-card-begin: markdown--><table>
<thead>
<tr>
<th>Numeric Value</th>
<th>Encoded Symbol</th>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td>1</td>
</tr>
<tr>
<td>1</td>
<td>2</td>
</tr>
<tr>
<td>2</td>
<td>3</td>
</tr>
<tr>
<td>3</td>
<td>4</td>
</tr>
<tr>
<td>4</td>
<td>5</td>
</tr>
<tr>
<td>5</td>
<td>6</td>
</tr>
<tr>
<td>6</td>
<td>7</td>
</tr>
<tr>
<td>7</td>
<td>8</td>
</tr>
<tr>
<td>8</td>
<td>9</td>
</tr>
<tr>
<td>9</td>
<td>C</td>
</tr>
<tr>
<td>10</td>
<td>F</td>
</tr>
<tr>
<td>11</td>
<td>G</td>
</tr>
<tr>
<td>12</td>
<td>H</td>
</tr>
<tr>
<td>13</td>
<td>J</td>
</tr>
<tr>
<td>14</td>
<td>K</td>
</tr>
<tr>
<td>15</td>
<td>L</td>
</tr>
<tr>
<td>16</td>
<td>N</td>
</tr>
<tr>
<td>17</td>
<td>Q</td>
</tr>
<tr>
<td>18</td>
<td>R</td>
</tr>
<tr>
<td>19</td>
<td>S</td>
</tr>
<tr>
<td>20</td>
<td>T</td>
</tr>
<tr>
<td>21</td>
<td>V</td>
</tr>
<tr>
<td>22</td>
<td>W</td>
</tr>
<tr>
<td>23</td>
<td>X</td>
</tr>
<tr>
<td>24</td>
<td>Y</td>
</tr>
</tbody>
</table>
<!--kg-card-end: markdown--><p>The Base25 alphabet as used in OCIs has a small advantage over that used in GCRs - encoded data maintains its sort order when it is compared bit-wise.</p><p>I believe that the OCI scheme has two key advantages over GCRs:</p><!--kg-card-begin: markdown--><ol>
<li>
<p>100% Persistent.<br>
A URI using an OCI will never change, even if the description or arrangement of the record changes. Once an OCI is created in Omega it lives forever.</p>
</li>
<li>
<p>Conveys Knowledge.<br>
Like a CCR an OCI confers some information about the record that it identifies, however unlike a CCR this is done without compromising on persistence of the identifier.</p>
</li>
</ol>
<!--kg-card-end: markdown--><p>The OCI scheme <strong>should certainly be considered as a draft</strong> at the moment, and I am looking forward to both experimenting with it and receiving further feedback.</p><p>We have placed the source code for two software tools for encoding/decoding OCI Base25 (and also GCR Base25) onto GitHub: <a href="https://github.com/nationalarchives/oci-tools-scala">OCI Tools (Scala)</a> and <a href="https://github.com/nationalarchives/oci-tools-ts">OCI Tools (TypeScript)</a>.</p><h2 id="full-circle-back-to-uris">Full Circle back to URIs</h2><p>As discussed at the start of this article... for Project Omega I defined a static base URI for expressing TNAs resources in RDF, and I have now also defined a suitable identifier scheme - OCI.</p><p>The URI for our <em>data</em> now look like this for a record (concept):</p><!--kg-card-begin: code--><pre><code>http://cat.nationalarchives.gov.uk/MSW.1970.7GH.P
</code></pre><!--kg-card-end: code--><p>and this for a record's description:</p><!--kg-card-begin: code--><pre><code>http://cat.nationalarchives.gov.uk/MSW.1970.7GH.P.2
</code></pre><!--kg-card-end: code--><p>Just above I said: "URI for our <u><em>data</em></u>", ideally such URI <em>should</em> be resolvable via the Web with various content negotiation options. At present alongside Project Omega, TNA is also operating Project Alpha. Alpha is focused upon the User Experience around the discoverability of records through TNAs website. There has already been some collaboration and information sharing between the two projects around URI. Yet, it is important to kept in mind that URI for addressing records (Omega), are not necessarily the same as URI for finding records (Alpha).</p><p>I expect that there will be further collaboration in the near-future between the Alpha and Omega projects to ensure that TNA can benefit from the exciting Linked Data on the Web applications that Omega is unlocking! :-)</p>]]></content:encoded></item><item><title><![CDATA[Business Source License Adoption]]></title><description><![CDATA[<p>I have been following the re-licensing upheaval which has been happening with Open Source database software over the last few years. Last year, I gave a couple of small talks on the subject at both, the <a href="https://www.meetup.com/London-Open-Source-Database-Meetup/">London Open Source Databases Meetup</a> (slides: <a href="https://slides.com/adamretter/database-licensing-chaos-losdbm19/">Database Licensing Chaos</a>), and at <a href="https://homepages.cwi.nl/~steven/declarative/">Declarative Amsterdam</a> (slides:</p>]]></description><link>https://blog.adamretter.org.uk/business-source-license-adoption/</link><guid isPermaLink="false">5e7c7428f3f56d02318b6e6e</guid><category><![CDATA[Open Source]]></category><category><![CDATA[Source Available]]></category><category><![CDATA[BSL]]></category><category><![CDATA[Licensing]]></category><dc:creator><![CDATA[Adam Retter]]></dc:creator><pubDate>Thu, 26 Mar 2020 16:59:31 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1447023029226-ef8f6b52e3ea?ixlib=rb-1.2.1&amp;q=80&amp;fm=jpg&amp;crop=entropy&amp;cs=tinysrgb&amp;w=1080&amp;fit=max&amp;ixid=eyJhcHBfaWQiOjExNzczfQ" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1447023029226-ef8f6b52e3ea?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjExNzczfQ" alt="Business Source License Adoption"><p>I have been following the re-licensing upheaval which has been happening with Open Source database software over the last few years. Last year, I gave a couple of small talks on the subject at both, the <a href="https://www.meetup.com/London-Open-Source-Database-Meetup/">London Open Source Databases Meetup</a> (slides: <a href="https://slides.com/adamretter/database-licensing-chaos-losdbm19/">Database Licensing Chaos</a>), and at <a href="https://homepages.cwi.nl/~steven/declarative/">Declarative Amsterdam</a> (slides: <a href="https://slides.com/adamretter/are-we-still-open-source/">Are we still Open Source?</a>).</p><p>The summary is that, many well-known databases that were previously Open Source have either moved to an <a href="https://en.wikipedia.org/wiki/Open-core_model">Open-Core Model</a> and/or changed to <a href="https://en.wikipedia.org/wiki/Source-available_software">Source Available</a> licensing. There are various reasons behind this including refinement of business models and protecting investment in intellectual property. I won't debate the motivations or merits of such approaches in this article, there are already many other articles out there which do! Instead I will look briefly at one such Source Available license, the Business Source License, and whom has adopted it and how.</p><h3 id="the-business-source-license">The Business Source License</h3><p>The<a href="https://mariadb.com/bsl11/"> Business Source License</a> 1.1 (BSL) is a <a href="https://en.wikipedia.org/wiki/Source-available_software">Source Available</a> software license, which guarantees that the software's source code will become <a href="https://opensource.org/osd">Open Source</a> after a period of time (up to a maximum of 4 years).</p><p>The BSL was created by Michael Widenius and David Axmark and developed with Linus Nyman back in 2013 (see:<a href="https://timreview.ca/article/691"> Introducing “Business Source”: The Future of Corporate Open Source Licensing?</a>). As you likely know, Michael and David have a long history and involvement in MySQL and MariaDB. Previously they had taken a <a href="https://en.wikipedia.org/wiki/Multi-licensing">Dual-Licensing</a> approach with MySQL, however they felt that did not work well due to how customers wanted to use and deploy the software. As a response they developed the BSL for use with MariaDB products.</p><!--kg-card-begin: markdown--><blockquote>
<p>Finally, I hope that BSL will pave the way for a new business model that sustains software development without relying primarily on support.</p>
</blockquote>
<p><a href="http://monty-says.blogspot.com/2016/08/applying-business-source-licensing-bsl.html">Michael Widenius, 2016</a></p>
<!--kg-card-end: markdown--><p>The Business Source License was revised in <a href="https://perens.com/2017/02/14/bsl-1-1/">version 1.1 with the help of Bruce Perens</a> (a co-founder of the <a href="https://opensource.org/">OSI</a>, and creator of the <a href="https://opensource.org/osd">OSD</a>); Adjustments were made from BSL 1.0 to make it clearer to users what they get and when, to impose constraints on the licensor as to both, the period of the Change Date (a maximum of 4 years) and the choice of Change License (must be GPL 2 or later compatible).</p><!--kg-card-begin: markdown--><p>BSL is a parameterised license, which allows the copyright holder some flexibility in how they apply it to their work. The three parameters are:</p>
<ol>
<li>
<p>Change License</p>
<p>A license which is compatible with GPL version 2.0 or later, which the <em>work</em> becomes licensed under after the &quot;Change Date&quot;.</p>
</li>
<li>
<p>Change Date</p>
<p>The date at which the <em>work</em> ceases to be licensed under BSL and instead becomes licensed under the &quot;Change License&quot;.</p>
</li>
<li>
<p>Additional Use Grant (Optional)</p>
<p>The BSL by default prohibits <em>production</em> use of the software. This parameter can optionally be used to grant additional rights to the licensee by the licensor. For example, so that it may be used with various restrictions in some form of production environment. It cannot be used to limit the other rights granted by the license.</p>
</li>
</ol>
<!--kg-card-end: markdown--><h3 id="who-is-using-the-bsl">Who is using the BSL?</h3><p>The obvious adopter of BSL is MariaDB, who are <a href="https://mariadb.com/projects-using-bsl-11/">using it for</a> their MaxScale, ColumnStore Backup Restore Tool, ColumnStore MaxScale CDC Data Adapter, and ColumnStore Kafka Data Adapter software products.</p><p>In addition to MariaDB, finding other software products that have adopted BSL has not been easy. Through Google, I was able to only identify four more products: <a href="https://github.com/cockroachdb/cockroach">CockroachDB</a>, <a href="https://github.com/getsentry/sentry">Sentry.io</a>, <a href="https://github.com/MaterializeInc/materialize/">Materialize</a>, and <a href="https://github.com/zerotier/ZeroTierOne">ZeroTier</a>. There are likely others, but even with relatively little effort I was surprised to find so few!</p><h3 id="how-is-the-bsl-being-used">How is the BSL being Used?</h3><p>I did a brief survey of how each of the adopters that I found are parameterizing the BSL for their needs...</p><p><strong>Change License</strong></p><!--kg-card-begin: markdown--><ol>
<li>MaxScale: GPL Version 2 or later</li>
<li>CockroachDB: Apache 2.0</li>
<li>Sentry.io: Apache 2.0</li>
<li>Materialize: Apache 2.0</li>
<li>ZeroTier: Apache 2.0</li>
</ol>
<!--kg-card-end: markdown--><p>The majority of those appear to be using Apache 2.0 as their Change License. BSL 1.1 states the following about the choice of Change License:</p><blockquote>a license that is compatible with GPL Version 2.0 or a later version, where “compatible” means that software provided under the Change License can be included in a program with software provided under GPL Version 2.0 or a later version.</blockquote><p>This is interesting because actually, the Apache License, Version 2.0 is not compatible with GPL Version 2. However, it is compatible with GPL Version 3 (see: <a href="https://www.apache.org/licenses/GPL-compatibility.html">Apache's GPL Compatibility</a>), so I guess that the "<em>or later version</em>" text allows this to work.</p><p><strong>(Period of) Change Date</strong></p><!--kg-card-begin: markdown--><ol>
<li>MaxScale: 4 Years</li>
<li>CockroachDB: 3 Years</li>
<li>Sentry.io: 3 Years</li>
<li>Materialize: 4 Years</li>
<li>ZeroTier: 3 Years, 4 Months *</li>
</ol>
<p>* <em>This seems an unusual period; it was calculated based on their commits and stated Change Date)</em>.</p>
<!--kg-card-end: markdown--><p><strong>Additional Use Grant</strong></p><!--kg-card-begin: markdown--><ol>
<li>
<p>MaxScale</p>
<p>Allows your application to use MaxScale up to and including two server instances for any purpose (e.g. production).</p>
</li>
<li>
<p>CockroachDB</p>
<p>Allows you to use CockroachDB for any purpose (e.g. production) as long as you are not offering it as a commercial <a href="https://en.wikipedia.org/wiki/Cloud_database">DBaaS (Database as a Service)</a>. It seems likely that they are trying to head off large Cloud Providers (e.g. Amazon AWS) from monetizing their work for free.</p>
</li>
<li>
<p>Sentry.io</p>
<p>Their wording is almost identical to that used for CockroachDB. You can use Sentry for any purpose (e.g. production) as long as you are not offering is as a commercial <a href="https://en.wikipedia.org/wiki/Software_as_a_service">SaaS (Software as a Service)</a>. Likewise, I suspect they also wanted to inhibit Cloud Providers from monetizing their work for free.</p>
</li>
<li>
<p>Materialize</p>
<p>Allows you to use as many non-clustered isolated server instances of Materialize as you want for any purpose (e.g. production) as long as you are not offering it as a commercial DBaaS.</p>
</li>
<li>
<p>ZeroTier</p>
<p>Their Additional Use Grant is the most complicated of the adopters that I looked at. In summary I interpret it as allowing you to use ZeroTier (e.g. in production), providing you are not:<br>
1. offering a commercial service e.g. ZeroTier SaaS<br>
2. creating a non-open source commercial derivative work<br>
3. using it within Government, unless for physical or mental health care, family and social services, social welfare, senior care, child care, and the care of persons with disabilities.</p>
</li>
</ol>
<!--kg-card-end: markdown--><h3 id="applying-the-bsl-to-software">Applying the BSL to Software</h3><p>MariaDB provide guidance on adopting the BSL in the form of an <a href="https://mariadb.com/bsl-faq-adopting">FAQ</a>, however the details of how to correctly apply the BSL to your software are not crystal clear. In some places MariaDB also appear to offer contradictory advice. </p><p>Consider the following statement from <a href="https://mariadb.com/bsl-faq-adopting/#future">https://mariadb.com/bsl-faq-adopting/#future</a>:</p><blockquote><em><em>Q: How far in the future is the recommended Change Date?</em></em></blockquote><blockquote>A: At most five years from the initial alpha release of your BSL software. Picking the Change Date depends on how rapidly the software is changing. For most software, the recommendation is four years.</blockquote><p>This seems to directly conflict with the following statement from the BSL 1.1 itself:</p><blockquote>Effective on the Change Date, or the fourth anniversary of the first publicly available distribution of a specific version of the Licensed Work under this License, whichever comes first</blockquote><p>If the maximum period is the Change Date or the fourth anniversary, then what would be the purpose of the Change Date being "<em>At most five years</em>". The period of limitation is presumably 4 years maximum?</p><p>Likewise, consider the following statement from the <a href="https://mariadb.com/bsl-faq-adopting/#changedate">BSL Adopting FAQ</a> on Change Date expiry: </p><blockquote>All source files under BSL have a Change Date and the name of an Open Source license in the header.</blockquote><p>This advice seems to vary from <a href="https://mariadb.com/bsl-faq-mariadb/">https://mariadb.com/bsl-faq-mariadb/</a>, which by my interpretation, seems to imply that the Change Date need only appear in the BSL license file:</p><blockquote>To convert your software to BSL, you have to add the BSL header to all your software files and include the BSL license file in your software distribution. In addition, you have to add the usage limits and Change Date that suits your software in the header of the BSL license file.</blockquote><p>I have yet to find an explicit definition of what the "BSL header" is. In my opinion, understanding clearly how to apply the BSL to your software is at the moment difficult. Conversely, most Open Source licenses make this easy by having an explicit section with instructions on how to apply the license to your software, and often include a template header which can be copied and pasted into each source file. Similar explicit and precise instructions for BSL would be welcome.</p><p><strong>How has BSL been applied so far?</strong></p><p>Looking at each of the adopters that I previously identified, between them there seem to have been two distinct approaches taken to applying the BSL to their software:</p><ol><li>Maintaining the Change Date in both, a central license file, and also within a license header at the top of each source code file.<br>This is the approach taken by both, MaxScale (<a href="https://github.com/mariadb-corporation/MaxScale/blob/2.4/LICENSE24.TXT">LICENSE.TXT</a> / <a href="https://github.com/mariadb-corporation/MaxScale/blob/2.4/server/core/server.cc#L7">server.cc</a>), and ZeroTier (<a href="https://github.com/zerotier/ZeroTierOne/blob/1.4.6/LICENSE.txt">LICENSE.txt</a> / <a href="https://github.com/zerotier/ZeroTierOne/blob/master/node/Node.cpp#L7">Node.cc</a>).</li><li>Maintaining the Change Date only in a central license file, the license header at the top of each source code file then references the central license file. This is the approach taken by both: CockroachDB (<a href="https://github.com/cockroachdb/cockroach/blob/v19.2.5/licenses/BSL.txt#L18">BSL.txt</a> / <a href="https://github.com/cockroachdb/cockroach/blob/v19.2.5/pkg/server/server.go#L4">server.go</a>), and Materialize (<a href="https://github.com/MaterializeInc/materialize/blob/v0.1.3/LICENSE#L32">LICENSE</a> / <a href="https://github.com/MaterializeInc/materialize/blob/v0.1.3/src/dataflow/server.rs#L4">server.rs</a>). Sentry.io also takes a similar approach (<a href="https://github.com/getsentry/sentry/blob/master/LICENSE#L19">LICENSE</a>), but unusually (at least in my experience) does not include any license header or copyright notice at the top of their source files.</li></ol><p>The advantage of approach (2) for the developer/publisher is that they only have to update their licenses Change Date in a single place. This removes any opportunity for inconsistency across multiple files.</p><p>However, from a user/developer (or even archivist) perspective I think there are advantages to approach (1), in so far as, if I am viewing the source code files individually, perhaps because they had been distributed individually or separately to the main body of work, then the Change Date is immediately apparent.</p><p>Which approach is correct? I don't know! It could be one or the other, both, or neither.</p><p>Regardless, at present there is no evidence of a consistent approach. I believe it is likely that this may have been caused by a lack of explicit and precise guidance on how to apply the BSL to your software.</p><p>Personally, I rather like the BSL for the purpose that it serves, and hope for clarification on how it should be applied in the near-future.</p>]]></content:encoded></item><item><title><![CDATA[RDF Plugins for Pentaho KETTLE]]></title><description><![CDATA[For The National Archives we developed and open sourced custom workflow step plugins for Pentaho Kettle which use Apache Jena to allow you to generate RDF output.]]></description><link>https://blog.adamretter.org.uk/rdf-plugins-for-pentaho-kettle/</link><guid isPermaLink="false">5e5a7537a21a820224fea129</guid><category><![CDATA[ETL]]></category><category><![CDATA[Pentaho]]></category><category><![CDATA[KETTLE]]></category><category><![CDATA[RDF]]></category><category><![CDATA[Jena]]></category><category><![CDATA[SQL]]></category><dc:creator><![CDATA[Adam Retter]]></dc:creator><pubDate>Mon, 02 Mar 2020 13:58:25 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1582978851931-4697a7c95ff5?ixlib=rb-1.2.1&amp;q=80&amp;fm=jpg&amp;crop=entropy&amp;cs=tinysrgb&amp;w=1080&amp;fit=max&amp;ixid=eyJhcHBfaWQiOjExNzczfQ" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1582978851931-4697a7c95ff5?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjExNzczfQ" alt="RDF Plugins for Pentaho KETTLE"><p>;<strong><em>tldr</em></strong>: <a href="https://github.com/nationalarchives/kettle-jena-plugins ">Jena Plugins for Pentaho Kettle (GitHub)</a>, and <a href="https://www.youtube.com/watch?v=2uqG_z2Qy9g">Demo of building a SQL to RDF Workflow (YouTube)</a>.</p><h3 id="background">Background</h3><p>At the end of 2019 TNA (<a href="https://nationalarchives.gov.uk">The National Archives</a>) launched a small Proof-of-Concept project called Project OMEGA. The goal of Project OMEGA was to investigate and prototype a potential replacement for their Catalogue. Initially the scope of the project was limited to PROCat and ILDB, the GUI and database respectively, that forms their existing Catalogue system for physical (e.g. paper) records.</p><p>After much research into TNAs business of cataloguing, and at our suggestion, the project has been expanded. The goal of the project is now to build a new singular "Pan-Archival Catalogue" system, which will replace several existing systems, and be able to describe all types of records held by TNA, i.e. physical, born-digital, and digital surrogates.</p><p>One of the obvious complexities for a Pan-Archival Catalogue is bringing together data about records from multiple sources, each of which has a different logical model. We opted to first identify a new suitable data model for TNAs future Pan-Archival Catalogue, after evaluating the major models in use, and the latest research for archival records modelling, we settled upon a Graph based model. The Graph based model allows us to describe complex relationships between records, and to easily add new facts and relationships to the graph in future as TNAs knowledge and interpretation of its records is enriched.</p><p>In a previous post entitled - "<em><strong><a href="https://blog.adamretter.org.uk/pentaho-kettle-and-sql-server/">Pentaho KETTLE and SQL Server</a></strong></em>", I explained that we were using <a href="https://sourceforge.net/projects/pentaho/">Pentaho Kettle</a> to work with our initial data from ILDB which is a SQL Server database. Having chosen a Graph based data model, and specifically <a href="https://www.w3.org/RDF/">RDF</a> (Resource Description Framework), we needed to have our data transformation workflows in Kettle produce RDF output for us. Unfortunately, out-of-the box Kettle doesn't include any workflow steps for producing RDF, and we could not find any 3rd-party plugins to do this. So we built and Open Sourced our own...</p><h3 id="integrating-kettle-and-jena">Integrating Kettle and Jena</h3><p>Fortunately we were able to develop custom workflow steps to generate RDF in Java for Pentaho Kettle's <a href="https://help.pentaho.com/Documentation/8.3/Developer_center/Create_step_plugins">plugin API</a>. For the heavy RDF lifting, our custom workflow steps make use of <a href="https://jena.apache.org/">Apache Jena</a>, which is a Java framework for building <a href="https://www.w3.org/standards/semanticweb/">Semantic Web</a> and <a href="https://www.w3.org/standards/semanticweb/data">Linked Data</a> applications. Our workflow steps provide the UI dialogs for configuration in Kettle, and act as the glue between Kettle and Jena by mapping row fields from Kettle to RDF Resources and Properties in Jena.</p><p>We developed two custom workflow steps:</p><ol><li>Create Jena Model<br>For each input row provided by Kettle, a Jena Model is created and stored as a <em>target field</em> in the output row.  The step allows the user to configure a mapping of fields from the input row to RDF Resources and Properties in the Jena Model of the output row.</li><li>Serialize Jena Model<br>This step is designed to receive input rows from Kettle which contain a Jena Model as one of their fields (created via a <em>Create Jena Model</em> step). The step allows the user to configure a file path and RDF serialization type, for a file that will be serialized to disk from the Jena Model.</li></ol><p>For anyone else who wants to create RDF with Pentaho Kettle, TNA have kindly agreed to release these custom workflow step plugins as Open Source under the <a href="https://opensource.org/licenses/MIT">MIT license</a>. If you are interested, you can read more about <a href="https://www.nationalarchives.gov.uk/information-management/re-using-public-sector-information/uk-government-licensing-framework/open-government-licence/open-software-licences/">TNA's Open Source licensing policy</a>. The plugins were developed in Java 8 and tested with Pentaho Kettle 8.3.0.9-719 and Apache Jena 3.14.0. You can find them on their GitHub: <a href="https://github.com/nationalarchives/kettle-jena-plugins">https://github.com/nationalarchives/kettle-jena-plugins</a> </p><!--kg-card-begin: image--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.adamretter.org.uk/content/images/2020/02/demo-model-screenshot.png" class="kg-image" alt="RDF Plugins for Pentaho KETTLE"><figcaption>Example Kettle workflow with Jena RDF creation</figcaption></figure><!--kg-card-end: image--><h3 id="configuring-the-create-jena-model-step">Configuring the Create Jena Model step</h3><p>The Create Jena Model step is concerned with mapping fields from the input row to an RDF Resource and Properties in the output row. The step's configuration dialog is shown below.</p><!--kg-card-begin: image--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.adamretter.org.uk/content/images/2020/02/Screenshot-2020-02-29-at-17.13.44-1.png" class="kg-image" alt="RDF Plugins for Pentaho KETTLE"><figcaption>Create Jena Model step configuration dialog</figcaption></figure><!--kg-card-end: image--><!--kg-card-begin: markdown--><ul>
<li>Target Field Name<br>
This is the name of the field in the output row which will hold the Jena Model. You can call this anything you want, e.g. <code>my_jena_model</code>.</li>
<li>Remove Selected Fields?<br>
When thus is selected, then any fields added in the <em>Fields to RDF Properties</em> table, will no longer be available in the output row.</li>
<li>Namespace Prefix / Namespace URI<br>
This table holds the namespace mappings. You must add entries in here for any prefixes which you use in the <em>Resource rdf:type</em> field, or <em>Fields to RDF Properties</em> table.</li>
<li>Resource rdf:type<br>
This is the name of the RDF class that your resource instantiates.</li>
<li>Resource URI (field)<br>
This is the field from the input row which contains the URI of your resource.</li>
<li>Fields to RDF Properties<br>
This table maps input row fields to RDF Properties. If you leave the <em>RDF Property type</em> empty, then <code>xsd:String</code> is assumed. Rather than properties, you can also map to other resources by setting the <em>RDF Resource type</em> to <code>Resource</code> and making sure that your field contains a URI or QName.</li>
</ul>
<!--kg-card-end: markdown--><h3 id="configuring-the-serialize-jena-model-step">Configuring the Serialize Jena Model step</h3><p>The Serialize Jena Model step is concerned with serializing a previously created Jena Model which is present in a field of the input row. Typically a Serialize Jena Model step follows a Create Jena Model step. The step's configuration dialog is shown below.</p><!--kg-card-begin: image--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.adamretter.org.uk/content/images/2020/02/Screenshot-2020-02-29-at-17.14.26.png" class="kg-image" alt="RDF Plugins for Pentaho KETTLE"><figcaption>Serialize Jena Model step configuration dialog</figcaption></figure><!--kg-card-end: image--><!--kg-card-begin: markdown--><ul>
<li>Field (Jena Model)<br>
This is the name of the field in the input row which contains the Jena Model. This should be the same as the <em>Target Field Name</em> from the corresponding Create Jena Model step.</li>
<li>Serialization Format<br>
The format of the RDF file that you wish to create. e.g. <code>RDF/XML</code>, <code>RDF/XML-Abbrev</code>, <code>N3</code>, <code>Turtle</code>, or <code>N-Tripples</code>.</li>
<li>Filename<br>
This is the path and name of the file on disk that you wish to create.</li>
<li>Create Parent Folder<br>
When checked, the parent folder of the output file will be created if it does not already exist.</li>
<li>Include step number in Filename<br>
When checked, the Kettle step number will be appended into the output filename. This can help to uniquely identify the output file in complex workflows.</li>
<li>Include partition number in Filename<br>
When checked, the Kettle partition number will be appended into the output filename. This can help to uniquely identify the output file in complex workflows.</li>
<li>Include date in Filename<br>
When checked, the date will be appended into the output filename. This can help to uniquely identify the output file in complex workflows.</li>
<li>Include time in Filename<br>
When checked, the step number will be appended into the output filename. This can help to uniquely identify the output file in complex workflows.</li>
</ul>
<!--kg-card-end: markdown--><h3 id="demo-of-building-a-sql-to-rdf-workflow">Demo of building a SQL to RDF Workflow</h3><p>We have also produced a simple screencast which demonstrates using our plugins to create RDF from a SQL database.</p><!--kg-card-begin: embed--><figure class="kg-card kg-embed-card"><iframe width="480" height="270" src="https://www.youtube.com/embed/2uqG_z2Qy9g?feature=oembed" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></figure><!--kg-card-end: embed-->]]></content:encoded></item><item><title><![CDATA[The Shell on my Mac]]></title><description><![CDATA[<p>On my previous Mac I used <a href="https://github.com/Powerlevel9k/powerlevel9k">powerlevel9k</a> to both give my shell a nice look, and to add some extra metadata to my prompt when working with git repositories (which for me is most of the time). I recently got a new Mac and somewhere between following the install instructions</p>]]></description><link>https://blog.adamretter.org.uk/the-shell-on-my-mac/</link><guid isPermaLink="false">5de10247be54db021b3a9fd3</guid><category><![CDATA[powerlevel10k]]></category><category><![CDATA[oh-my-zsh]]></category><category><![CDATA[zsh]]></category><category><![CDATA[iterm2]]></category><category><![CDATA[mac]]></category><dc:creator><![CDATA[Adam Retter]]></dc:creator><pubDate>Fri, 29 Nov 2019 12:34:33 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1563699182-58375278b2b9?ixlib=rb-1.2.1&amp;q=80&amp;fm=jpg&amp;crop=entropy&amp;cs=tinysrgb&amp;w=1080&amp;fit=max&amp;ixid=eyJhcHBfaWQiOjExNzczfQ" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1563699182-58375278b2b9?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjExNzczfQ" alt="The Shell on my Mac"><p>On my previous Mac I used <a href="https://github.com/Powerlevel9k/powerlevel9k">powerlevel9k</a> to both give my shell a nice look, and to add some extra metadata to my prompt when working with git repositories (which for me is most of the time). I recently got a new Mac and somewhere between following the install instructions and importing my old <code>.zshrc</code>, I ended up with a corrupted looking shell prompt, which I could not seem to fix.</p><p>So, I decided to reinstall all the shell stuff I use from scratch, along the way I discovered <a href="https://github.com/romkatv/powerlevel10k">powerlevel10k</a> which claims to be much faster than powerlevel9k.</p><p>This blog article is really for my own reference on how to setup a nice shell on a Mac, should I ever encounter such problems again. It might also be useful for anyone else who wants a fancy shell on their Mac.</p><p>The installation consists of <a href="https://iterm2.com/">iTerm2</a>, <a href="https://en.wikipedia.org/wiki/Z_shell">Zsh</a>, <a href="https://ohmyz.sh/">Oh My Zsh!</a>, and <a href="https://github.com/romkatv/powerlevel10k">powerlevel10k</a>, and it assumes that you have <a href="https://brew.sh/">Homebrew</a> already installed.</p><h2 id="install-iterm2">Install iTerm2</h2><p>From the Mac Terminal.app:</p><!--kg-card-begin: code--><pre><code class="language-bash">$ brew cask install iterm2
</code></pre><!--kg-card-end: code--><p>Then close Terminal.app and launch a new iTerm2 terminal and follow the instructions below in iTerm2.</p><h2 id="install-z-shell">Install Z shell</h2><!--kg-card-begin: code--><pre><code class="language-bash">$ brew install zsh
</code></pre><!--kg-card-end: code--><h3 id="z-shell-configuration">Z Shell Configuration</h3><p>My basic <code>~/.zshrc</code> reflects the fact that I have several tools for Java, Rust and Node installed, and that I have a little script that generates a nice custom MOTD (Message of the Day) for me; anyone else can likely ignore this configuration.</p><!--kg-card-begin: code--><pre><code class="language-zsh">fpath=(/usr/local/share/zsh-completions $fpath)

export PATH="/usr/local/bin:$PATH"
export PATH="$HOME/.cargo/bin:$PATH"
export PATH="/opt/local/bin:/opt/local/sbin:$PATH"
export PATH="/usr/local/maven/bin:$PATH"

export JAVA_HOME="/Library/Java/JavaVirtualMachines/zulu8"

export EDITOR=vim

export NVM_DIR="$HOME/.nvm"
source /usr/local/opt/nvm/nvm.sh

# My custom MOTD
$HOME/random-cowsay-fortune.sh
</code></pre><!--kg-card-end: code--><h2 id="install-oh-my-zsh-">Install Oh My Zsh!</h2><!--kg-card-begin: code--><pre><code class="language-zsh">sh -c "$(curl -fsSL https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"</code></pre><!--kg-card-end: code--><h3 id="updated-z-shell-configuration">Updated Z Shell configuration</h3><p>Installing Oh My Zsh will make changes to your Z Shell configuration file <code>~/.zshrc</code>, you can then further modify or customise it, mine looks like:</p><!--kg-card-begin: code--><pre><code class="language-zsh">export ZSH="$HOME/.oh-my-zsh"

ZSH_THEME="robbyrussell"
CASE_SENSITIVE="true"
HYPHEN_INSENSITIVE="false"
HIST_STAMPS="dd/mm/yyyy"

plugins=(
        brew
        colored-man-pages
        docker
        git
        mosh
        mvn
        osx
        ripgrep
        rust
        sbt
        scala
        sublime
        vscode
        xcode
)

source $ZSH/oh-my-zsh.sh


# User configuration

export PATH="/usr/local/bin:$PATH"
export PATH="$HOME/.cargo/bin:$PATH"
export PATH="/opt/local/bin:/opt/local/sbin:$PATH"
export PATH="/usr/local/maven/bin:$PATH"

export JAVA_HOME="/Library/Java/JavaVirtualMachines/zulu8"

export EDITOR=vim

export NVM_DIR="$HOME/.nvm"
source /usr/local/opt/nvm/nvm.sh

# My custom MOTD
$HOME/random-cowsay-fortune.sh</code></pre><!--kg-card-end: code--><h2 id="install-powerlevel10k">Install powerlevel10k</h2><!--kg-card-begin: code--><pre><code class="language-zsh">$ git clone --depth=1 https://github.com/romkatv/powerlevel10k.git $ZSH_CUSTOM/themes/powerlevel10k
</code></pre><!--kg-card-end: code--><h3 id="update-z-shell-configuration-for-powerlevel10k">Update Z Shell configuration for powerlevel10k</h3><p>You need to modify the <code>ZSH_THEME</code> variable in your Z Shell configuration to use the powerlevel10k theme. After which the start of my <code>~/.zshrc</code> file looks like:</p><!--kg-card-begin: code--><pre><code class="language-zsh">export ZSH="$HOME/.oh-my-zsh"

ZSH_THEME=powerlevel10k/powerlevel10k

...</code></pre><!--kg-card-end: code--><h3 id="configure-powerlevel10k">Configure powerlevel10k</h3><p>When you next open a terminal window, the powerlevel10k configuration script will run. If it prompts you to install a font and/or restart iTerm2, then do so. It will then prompt you with a number of questions about how you want the shell's prompt to visually appear. After which it will append the line <code>[[ ! -f ~/.p10k.zsh ]] || source ~/.p10k.zsh</code> to your <code>~./zshrc</code> for you.</p><p>Just for my own future reference, the answers I gave to the questions of the configuration script are recorded in the top of my <code>~/.p10k.zsh</code> file:</p><!--kg-card-begin: code--><pre><code class="language-zsh"># Generated by Powerlevel10k configuration wizard on 2019-11-29 at 12:18 CET.
# Based on romkatv/powerlevel10k/config/p10k-rainbow.zsh, checksum 20931.
# Wizard options: nerdfont-complete + powerline, small icons, rainbow, round separators,
# sharp heads, flat tails, 1 line, sparse, many icons, concise, transient_prompt.

...
</code></pre><!--kg-card-end: code--><h2 id="conclusion">Conclusion</h2><p>Based on the settings I chose, my iTerm2 terminal prompt now looks rather nice IMHO.</p><!--kg-card-begin: image--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.adamretter.org.uk/content/images/2019/11/Screenshot-2019-11-29-at-12.32.53.png" class="kg-image" alt="The Shell on my Mac"><figcaption>A new Terminal shell</figcaption></figure><!--kg-card-end: image--><!--kg-card-begin: image--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.adamretter.org.uk/content/images/2019/11/Screenshot-2019-11-29-at-12.33.37.png" class="kg-image" alt="The Shell on my Mac"><figcaption>Listing directory contents of a Git repository; prompt shows the git branch and status.</figcaption></figure><!--kg-card-end: image--><h2 id="my-custom-motd-script">My custom MOTD script</h2><p>In case anyone is interested, I have included my MOTD script <code>random-cowsay-fortune.sh</code> below. It requires you to first install cowsay, fortune, and lolcat via Homebrew.</p><!--kg-card-begin: code--><figure class="kg-card kg-code-card"><pre><code class="language-bash">#!/usr/bin/env bash

set -e

file=$( ls /usr/local/Cellar/cowsay/3.04/share/cows/*.cow | sort -R | tail -1 )

/usr/local/Cellar/fortune/9708/bin/fortune  | cowsay -f "$file" | lolcat</code></pre><figcaption>random-cowsay-fortune.sh</figcaption></figure><!--kg-card-end: code-->]]></content:encoded></item></channel></rss>