From 434b07ba0ee6c552e08df6a4a69dd81c99ec445e Mon Sep 17 00:00:00 2001 From: ehennestad Date: Thu, 20 Nov 2025 10:16:13 +0100 Subject: [PATCH 1/4] Enhance node addition logic in Collection class Added 'AbortIfNodeExists' option to node addition methods and improved handling of controlled term instances based on preferences. Subnodes are now added before the main node, and code comments were updated to reflect new functionality. --- code/+openminds/@Collection/Collection.m | 21 ++++++++++++++----- code/+openminds/getpref.m | 1 + .../+openminds/+utility/Preferences.m | 4 ++++ 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/code/+openminds/@Collection/Collection.m b/code/+openminds/@Collection/Collection.m index 4e9a2f25..26a3d925 100644 --- a/code/+openminds/@Collection/Collection.m +++ b/code/+openminds/@Collection/Collection.m @@ -36,6 +36,7 @@ % Todo: Validation. % - Linked subject states should have same subject +% - Add method to update links for specified instances % Need mechanism to check if embedded nodes are added to the collection @@ -190,12 +191,15 @@ function add(obj, instance, options) end arguments options.AddSubNodesOnly = false; + options.AbortIfNodeExists = true; end for i = 1:numel(instance) thisInstance = instance{i}; for j = 1:numel(thisInstance) % If thisInstance is an array - obj.addNode(thisInstance(j), "AddSubNodesOnly", options.AddSubNodesOnly); + obj.addNode(thisInstance(j), ... + "AddSubNodesOnly", options.AddSubNodesOnly, ... + "AbortIfNodeExists", options.AbortIfNodeExists); end end end @@ -498,9 +502,13 @@ function load(obj, loadPath, options) instance.id = obj.getBlankNodeIdentifier(); end - % Do not add openminds controlled term instances - if startsWith(instance.id, "https://openminds.ebrains.eu/instances/") - return + % Do not add openminds controlled term instances if disabled in + % preferences + if startsWith(instance.id, "https://openminds.ebrains.eu/instances/") ... + || startsWith(instance.id, "https://openminds.om-i.org/instances/") + if ~openminds.getpref('AddControlledInstanceToCollection') + return + end end if obj.NumNodes > 0 @@ -511,6 +519,10 @@ function load(obj, loadPath, options) end end end + + % Add subnodes first + obj.addSubNodes(instance) + if ~options.AddSubNodesOnly obj.Nodes(instance.id) = {instance}; @@ -530,7 +542,6 @@ function load(obj, loadPath, options) end end - obj.addSubNodes(instance) if ~nargout clear wasAdded end diff --git a/code/+openminds/getpref.m b/code/+openminds/getpref.m index adf216da..f4152a68 100644 --- a/code/+openminds/getpref.m +++ b/code/+openminds/getpref.m @@ -20,6 +20,7 @@ [ ... "PropertyDisplayMode", ... "DocLinkTarget", ... + "AddControlledInstanceToCollection", ... "" ... ])... } = "" diff --git a/code/internal/+openminds/+utility/Preferences.m b/code/internal/+openminds/+utility/Preferences.m index 8b6ad1c0..968fa0b3 100644 --- a/code/internal/+openminds/+utility/Preferences.m +++ b/code/internal/+openminds/+utility/Preferences.m @@ -10,12 +10,16 @@ % - "help" : MATLAB docstrings % - "online" : Online ReadTheDocs % documentation +% AddControlledInstanceToCollection (logical): Whether to add +% openMINDS controlled instances to a +% collection properties (SetObservable) PropertyDisplayMode (1,1) string ... {mustBeMember(PropertyDisplayMode, ["all", "non-empty"])} = "all" DocLinkTarget (1,1) string ... {mustBeMember(DocLinkTarget, ["help", "online"])} = "online" + AddControlledInstanceToCollection (1,1) logical = true end properties (Constant, Access = private) From 194484f3830ccc8ff4b29c0f9f15683f98a4170a Mon Sep 17 00:00:00 2001 From: ehennestad Date: Thu, 20 Nov 2025 12:50:07 +0100 Subject: [PATCH 2/4] Ensure allInstances is a row vector in addNode Added a check to reshape allInstances to a row vector if it is a column. This prevents issues when iterating over instances and ensures consistent behavior in the addNode method. --- code/+openminds/@Collection/Collection.m | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/code/+openminds/@Collection/Collection.m b/code/+openminds/@Collection/Collection.m index 26a3d925..39d5068a 100644 --- a/code/+openminds/@Collection/Collection.m +++ b/code/+openminds/@Collection/Collection.m @@ -331,6 +331,10 @@ function updateLinks(obj) allInstances = [allInstances{:}]; end + if iscolumn(allInstances) + allInstances = reshape(allInstances, 1, []); + end + for instance = allInstances obj.addNode(instance{1}, ... 'AddSubNodesOnly', true, ... @@ -523,7 +527,6 @@ function load(obj, loadPath, options) % Add subnodes first obj.addSubNodes(instance) - if ~options.AddSubNodesOnly obj.Nodes(instance.id) = {instance}; wasAdded = true; From a428c5cdeb393e58260672fb3a3c6cded3e024cb Mon Sep 17 00:00:00 2001 From: Run tests by ehennestad Date: Thu, 20 Nov 2025 11:54:41 +0000 Subject: [PATCH 3/4] Update code issues and tests badges --- .github/badges/tests.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/badges/tests.svg b/.github/badges/tests.svg index 1a4e5085..4e32f161 100644 --- a/.github/badges/tests.svg +++ b/.github/badges/tests.svg @@ -1 +1 @@ -teststests558 passed558 passed \ No newline at end of file +teststests568 passed568 passed \ No newline at end of file From 39f69844a57df830b6032661ddc5eea1d0e43b90 Mon Sep 17 00:00:00 2001 From: ehennestad Date: Thu, 20 Nov 2025 15:24:29 +0100 Subject: [PATCH 4/4] Clean whitespace --- code/+openminds/@Collection/Collection.m | 70 ++++++++++++------------ code/+openminds/instanceFromIRI.m | 2 +- code/+openminds/setpref.m | 2 +- code/+openminds/startup.m | 4 +- code/+openminds/toolboxversion.m | 2 +- 5 files changed, 40 insertions(+), 40 deletions(-) diff --git a/code/+openminds/@Collection/Collection.m b/code/+openminds/@Collection/Collection.m index 39d5068a..e7d3616a 100644 --- a/code/+openminds/@Collection/Collection.m +++ b/code/+openminds/@Collection/Collection.m @@ -52,7 +52,7 @@ NumNodes NumTypes end - + properties (SetAccess = protected) % Nodes - Dictionary storing instances as values with identifiers % as keys @@ -67,11 +67,11 @@ properties (SetAccess = protected) LinkResolver - + % MetadataStore - Optional metadata store for saving/loading MetadataStore openminds.interface.MetadataStore = openminds.internal.FileMetadataStore.empty end - + methods % Constructor function obj = Collection(instance, options) % Create an instance of an openMINDS collection @@ -98,7 +98,7 @@ % creates a collection with the specified metadata store. If no % instances are provided, the collection will automatically load % instances from the store. - + % collection = openminds.Collection(..., NameA, ValueA, ... ) % also specifies optional name value pairs when creating the % collection. @@ -107,7 +107,7 @@ % - Name : A name for the collection % - Description : A description of the collection % - MetadataStore : A metadata store for saving/loading instances - + arguments (Repeating) instance % openminds.abstract.Schema end @@ -127,13 +127,13 @@ obj.Nodes = containers.Map; obj.TypeMap = containers.Map; end - + obj.initializeFromInstances(instance) obj.Name = options.Name; obj.Description = options.Description; obj.MetadataStore = options.MetadataStore; - + % Auto-load from MetadataStore if provided and no instances given if isempty(instance) && ~isempty(obj.MetadataStore) obj.load(); @@ -149,7 +149,7 @@ numNodes = length(obj.Nodes); end end - + function numTypes = get.NumTypes(obj) if isa(obj.TypeMap, 'dictionary') numTypes = numEntries(obj.TypeMap); @@ -203,7 +203,7 @@ function add(obj, instance, options) end end end - + function tf = contains(obj, instance) % Todo:work for arrays tf = false; @@ -214,10 +214,10 @@ function add(obj, instance, options) end end end - + function remove(obj, instance) % remove - Remove metadata instance from the collection - + if isstring(instance) || ischar(instance) instanceId = instance; elseif openminds.utility.isInstance(instance) @@ -254,11 +254,11 @@ function remove(obj, instance) instance = instance{1}; end end - + function instances = getAll(obj) % getAll - Get all instances of collection instances = obj.Nodes.values(); - + % For older MATLAB releases, the instances might be nested a % cell array, need to unnest if that's the case: if iscell(instances{1}) @@ -273,11 +273,11 @@ function remove(obj, instance) end tf = false; - + if obj.NumNodes == 0 return end - + typeKeys = obj.TypeMap.keys; tf = any( endsWith(typeKeys, "."+type) ); %i.e ".Person" end @@ -297,10 +297,10 @@ function remove(obj, instance) if obj.NumNodes == 0 return end - + instanceKeys = obj.getInstanceKeysForType(type); if isempty(instanceKeys); return; end - + if isa(obj.Nodes, 'dictionary') instances = obj.Nodes(instanceKeys); else @@ -368,14 +368,14 @@ function updateLinks(obj) % ------ % % outputPaths (cell): A list of the file paths created. - + arguments obj openminds.Collection savePath (1,1) string = "" options.MetadataStore openminds.interface.MetadataStore = openminds.internal.FileMetadataStore.empty % options.SaveFormat = "jsonld" Implement if more formats are supported end - + % Update links before saving obj.updateLinks() instances = obj.getAll(); @@ -383,7 +383,7 @@ function updateLinks(obj) if savePath ~= "" tempStore = openminds.internal.store.createTemporaryStore(savePath); outputPaths = tempStore.save(instances); - + elseif ~isempty(options.MetadataStore) outputPaths = obj.MetadataStore.save(instances); @@ -395,7 +395,7 @@ function updateLinks(obj) error('openminds:Collection:NoSavePath', ... 'Either provide savePath or configure a MetadataStore'); end - + if ~nargout clear outputPaths end @@ -444,7 +444,7 @@ function load(obj, loadPath, options) error('openminds:Collection:PathNotFound', 'Path not found: %s', loadPath); end end - + for i = 1:numel(instances) if openminds.utility.isInstance(instances{i}) obj.addNode(instances{i}); @@ -477,13 +477,13 @@ function load(obj, loadPath, options) % -------- % collection : openminds.Collection % A new collection loaded with instances from the store - + arguments metadataStore (1,1) openminds.interface.MetadataStore options.Name (1,1) string = "" options.Description (1,1) string = "" end - + % Create collection with the metadata store collection = openminds.Collection('MetadataStore', metadataStore, ... 'Name', options.Name, 'Description', options.Description); @@ -501,7 +501,7 @@ function load(obj, loadPath, options) end wasAdded = false; - + if isempty(instance.id) instance.id = obj.getBlankNodeIdentifier(); end @@ -544,12 +544,12 @@ function load(obj, loadPath, options) obj.TypeMap(instanceType) = {string(instance.id)}; end end - + if ~nargout clear wasAdded end end - + % Add sub node instances (linked types) to the Node container. function addSubNodes(obj, instance) % Add links. @@ -566,7 +566,7 @@ function addSubNodes(obj, instance) obj.addNode(embeddedInstances{i}, 'AddSubNodesOnly', true); end end - + function identifier = getBlankNodeIdentifier(obj) fmt = '_:%06d'; identifier = length(obj) + 1; @@ -581,19 +581,19 @@ function initializeFromInstances(obj, instance) isFilePath = @(x) (ischar(x) || isstring(x)) && isfile(x); isFolderPath = @(x) (ischar(x) || isstring(x)) && isfolder(x); isMetadata = @(x) openminds.utility.isInstance(x); - + % Initialize from file(s) if all( cellfun(isFilePath, instance) ) obj.load(instance{:}) - + % Initialize from folder elseif all( cellfun(isFolderPath, instance) ) obj.load(instance{:}) - + % Initialize from instance(s) elseif all( cellfun(isMetadata, instance) ) obj.add(instance{:}); - + else ME = MException(... 'OPENMINDS_MATLAB:Collection:InvalidInstanceSpecification', ... @@ -611,7 +611,7 @@ function initializeFromInstances(obj, instance) if obj.NumTypes > 0 typeKeys = obj.TypeMap.keys; - + isMatch = strcmp(typeKeys, instanceType.ClassName); if any(isMatch) if isa(obj.TypeMap, 'dictionary') @@ -629,9 +629,9 @@ function initializeFromInstances(obj, instance) instanceKeys = {}; return end - + existingKeys = obj.Nodes.keys(); - + % Sanity check, make sure all keys exist in Nodes dictionary assert( all( ismember( instanceKeys, existingKeys ) ), ... 'TypeMap has too many keys' ) diff --git a/code/+openminds/instanceFromIRI.m b/code/+openminds/instanceFromIRI.m index 83ef6d99..f2a82985 100644 --- a/code/+openminds/instanceFromIRI.m +++ b/code/+openminds/instanceFromIRI.m @@ -35,7 +35,7 @@ end [typeEnum, instanceName] = openminds.utility.parseInstanceIRI(IRI); - + if contains(typeEnum.ClassName, "controlledterms") instance = feval(typeEnum.ClassName, IRI); else diff --git a/code/+openminds/setpref.m b/code/+openminds/setpref.m index a046de66..da26292d 100644 --- a/code/+openminds/setpref.m +++ b/code/+openminds/setpref.m @@ -18,7 +18,7 @@ end pref = openminds.utility.Preferences.getSingleton; - + prefNames = fieldnames(prefValues); for i = 1:numel(prefNames) preferenceName = prefNames{i}; diff --git a/code/+openminds/startup.m b/code/+openminds/startup.m index 53af3747..8dbc387e 100644 --- a/code/+openminds/startup.m +++ b/code/+openminds/startup.m @@ -3,7 +3,7 @@ function startup(version) % % This function ensures that only one version of openMINDS schema classes % are on MATLAB's search path. - + arguments version (1,1) openminds.internal.utility.VersionNumber ... {openminds.mustBeValidVersion(version)} = "latest" @@ -14,7 +14,7 @@ function startup(version) % NB: Assumes this function is located in code/+openminds: codePath = fileparts( fileparts( mfilename('fullpath') ) ); addpath( fullfile(codePath, 'internal') ) - + % Run internal function that correctly configures the search path openminds.selectOpenMindsVersion(version) fprintf(['Added classes for version "%s" of the openMINDS metadata model ' ... diff --git a/code/+openminds/toolboxversion.m b/code/+openminds/toolboxversion.m index 2471e19f..17dc4647 100644 --- a/code/+openminds/toolboxversion.m +++ b/code/+openminds/toolboxversion.m @@ -5,7 +5,7 @@ contentsFile = fullfile(rootPath, 'Contents.m'); fileStr = fileread(contentsFile); - + % First try to get a version with a sub-patch version number matchedStr = regexp(fileStr, 'Version \d*\.\d*\.\d*.\d*(?= )', 'match');