Adding Ordered Metadata Fields to Samvera Hyrax

One of the first things I did when I joined the University of Michigan’s Library IT department as the lead developer on Deep Blue Data, a Samvera Hyrax-based data repository, was to add user-specified arbitrary ordering for a number of the metadata fields.

Overview

This technique maintains an arbitrary user-specified order for a given metadata field by adding a new text metadata field to store the order encoded in a JSON string, then overriding the reading and writing of the metadata field to make use of this JSON string.

The following steps are detailed below:

Create a Utility Module to Encode/Decode JSON Strings

Isolate the serialization process in a utility module.

  1. module Deepblue
  2.   module OrderedStringHelper
  3.     class DeserializeError < RuntimeError
  4.     end
  5.     #
  6.     # convert a serialized array to a normal array of values
  7.     # assumes values are stored as json converted to strings
  8.     # a failure to deserialize throws a DeserializeError,
  9.     # the exact reason for failure is ignored
  10.     #
  11.     def self.deserialize( serialized_string_containing_an_array )
  12.       if serialized_string_containing_an_array.start_with?('[')
  13.         begin
  14.           arr = ActiveSupport::JSON.decode serialized_string_containing_an_array
  15.           return arr if arr.is_a?( Array )
  16.         rescue ActiveSupport::JSON.parse_error
  17.           # ignore and fall through
  18.         end
  19.       end
  20.       raise OrderedStringHelper::DeserializeError
  21.     end
  22.     #
  23.     # serialize a normal array of values to an array of ordered values
  24.     #
  25.     def self.serialize( arr )
  26.       serialized_string_containing_an_array = ActiveSupport::JSON.encode( arr ).to_s
  27.       return serialized_string_containing_an_array
  28.     end
  29.   end
  30. end

Create a Utility Module for Ordered Values Methods

The ordered and ordered_values methods filter for nil values and catch runtime serialization errors.

  1. module Deepblue
  2.   module MetadataHelper
  3.     def self.ordered( ordered_values: nil, values: nil )
  4.       return nil if values.nil?
  5.       unless ordered_values.nil?
  6.         begin
  7.           values = OrderedStringHelper.deserialize( ordered_values )
  8.         rescue OrderedStringHelper::DeserializeError
  9.           # fallback to original values, which are stored in an unspecified order
  10.           return values
  11.         end
  12.       end
  13.       return values
  14.     end
  15.     def self.ordered_values( values: nil )
  16.       return nil if values.nil?
  17.       OrderedStringHelper.serialize( values )
  18.     end
  19.   end
  20. end

Add Ordered Metadata Field to Work Metadata

The predicate is arbitrary (and could obviously be improved). You’ll need a unique predicate for each ordered metadata field added.

  1. module Deepblue
  2.   module DeepblueWorkMetadata
  3.     extend ActiveSupport::Concern
  4.       # <snip/>
  5.       property :creator_ordered, predicate: ::RDF::URI.new('https://deepblue.lib.umich.edu/data/help#creator_ordered'), multiple: false do |index|
  6.         index.type :text
  7.         index.as :stored_searchable
  8.       end
  9.       # <snip/>
  10.   end
  11. end

Override Accessors in Model

The getter returns the deserialized ordered version of the metadata field if it is populated, else it returns the original unordered version of the metadata field.

The setter stores a serialization of the value being stored to the ordered version of the metadata field, then passes the original version to the overridden store method.

Example model class with ordered creator metadata field:

  1. class ExampleModel < ActiveFedora::Base
  2.   # <snip/>
  3.   def creator
  4.     values = super
  5.     Deepblue::MetadataHelper.ordered( ordered_values: creator_ordered, values: values )
  6.   end
  7.  
  8.   def creator=( values )
  9.     self.creator_ordered = Deepblue::MetadataHelper.ordered_values( values: values )
  10.     super values
  11.   end
  12.   # <snip/>
  13. end

Links to Actual Implementation Code