Skip to content

Commit 669b40c

Browse files
authored
feat: RDF IRI using column identifier in key pairs (#4987)
* moved PrimaryKey creation from RdfService to PrimaryKey * use identifier instead of name * fixed test * Updated tests + relevant changes to PrimaryKey * added exception * adjusted test to reflect real use case more correctly * correctly deal with composite keys * simplified some code * made tests more robust * added exception * fixed typo * extra validation for mismatching table + row/primarykey string * reverted unused change * auto-formatting * fixed a test * removed check due to MG_TABLECLASS not always being present (null check for values in constructor should cover invalid cases as well) * fixed rowIRI always using root table identifier * rowIRI always uses root schema as well * minor string adjustment * adjusted error message * updating iri generator test * general improvements (extra tests, typo's in javadoc, etc.) * re-added old test * reverted test name
1 parent e8a1b48 commit 669b40c

File tree

6 files changed

+301
-92
lines changed

6 files changed

+301
-92
lines changed

backend/molgenis-emx2-rdf/src/main/java/org/molgenis/emx2/rdf/ColumnTypeRdfMapper.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,7 @@ private static Set<Value> retrieveReferenceValues(
309309
final Row row,
310310
final Column tableColumn,
311311
final Map<String, String> colNameToRefTableColName) {
312-
final Map<Integer, Map<String, String>> items = new HashMap<>();
312+
final Map<Integer, SortedMap<String, String>> items = new HashMap<>();
313313
for (final String colName : colNameToRefTableColName.keySet()) {
314314
final String[] values =
315315
(tableColumn.isArray()
@@ -319,14 +319,14 @@ private static Set<Value> retrieveReferenceValues(
319319
if (values == null) continue;
320320

321321
for (int i = 0; i < values.length; i++) {
322-
Map<String, String> keyValuePairs = items.getOrDefault(i, new LinkedHashMap<>());
322+
SortedMap<String, String> keyValuePairs = items.getOrDefault(i, new TreeMap<>());
323323
keyValuePairs.put(colNameToRefTableColName.get(colName), values[i]);
324324
items.put(i, keyValuePairs);
325325
}
326326
}
327327

328328
final Set<Value> values = new HashSet<>();
329-
for (final Map<String, String> item : items.values()) {
329+
for (final SortedMap<String, String> item : items.values()) {
330330
values.add(
331331
rowIRI(
332332
rdfMapData.getBaseURL(),

backend/molgenis-emx2-rdf/src/main/java/org/molgenis/emx2/rdf/IriGenerator.java

+11-3
Original file line numberDiff line numberDiff line change
@@ -68,18 +68,26 @@ static IRI rowIRI(String baseURL, TableMetadata table, PrimaryKey primaryKey) {
6868
return Values.iri(
6969
baseURL
7070
+ "/"
71-
+ escaper.escape(table.getSchemaName())
71+
+ escaper.escape(table.getRootTable().getSchemaName())
7272
+ API_RDF
7373
+ "/"
74-
+ escaper.escape(table.getIdentifier())
74+
+ escaper.escape(table.getRootTable().getIdentifier())
7575
+ "/"
76-
+ primaryKey.getEncodedValue());
76+
+ primaryKey.getEncodedString());
7777
}
7878

7979
static IRI rowIRI(String baseURL, Table table, PrimaryKey primaryKey) {
8080
return rowIRI(baseURL, table.getMetadata(), primaryKey);
8181
}
8282

83+
static IRI rowIRI(String baseURL, TableMetadata table, Row row) {
84+
return rowIRI(baseURL, table, PrimaryKey.fromRow(table, row));
85+
}
86+
87+
static IRI rowIRI(String baseURL, Table table, Row row) {
88+
return rowIRI(baseURL, table.getMetadata(), row);
89+
}
90+
8391
static IRI fileIRI(String baseURL, Row row, Column column) {
8492
return Values.iri(
8593
baseURL
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,22 @@
11
package org.molgenis.emx2.rdf;
22

3+
import static java.util.stream.Collectors.toCollection;
34
import static org.molgenis.emx2.FilterBean.and;
45
import static org.molgenis.emx2.FilterBean.f;
56
import static org.molgenis.emx2.Operator.EQUALS;
67
import static org.molgenis.emx2.rdf.IriGenerator.escaper;
8+
import static org.molgenis.emx2.utils.TypeUtils.convertToCamelCase;
79

810
import java.net.URLDecoder;
911
import java.nio.charset.StandardCharsets;
1012
import java.util.*;
13+
import org.molgenis.emx2.Column;
1114
import org.molgenis.emx2.Filter;
1215
import org.molgenis.emx2.MolgenisException;
16+
import org.molgenis.emx2.Reference;
17+
import org.molgenis.emx2.Row;
18+
import org.molgenis.emx2.Table;
19+
import org.molgenis.emx2.TableMetadata;
1320

1421
/**
1522
* Does not support "{@code _ARRAY}" {@link org.molgenis.emx2.ColumnType}'s
@@ -20,45 +27,109 @@
2027
class PrimaryKey {
2128
public static final String NAME_VALUE_SEPARATOR = "=";
2229
public static final String KEY_PARTS_SEPARATOR = "&";
23-
private final Map<String, String> keys;
30+
// "column name", "column value" (non-escaped due to getFilter() functionality)
31+
private final SortedMap<String, String> keys;
2432

25-
// use map instead of list<NameValuePair> to prevent duplicate entries
26-
// some foreign key have overlapping relationships which resulted in a bug
33+
/**
34+
* A table should always have a key=1 column (and it must have a value), so validating on empty
35+
* values should not be needed.
36+
*/
37+
static PrimaryKey fromRow(TableMetadata table, Row row) {
38+
final SortedMap<String, String> keyParts = new TreeMap<>();
39+
for (final Column column : table.getPrimaryKeyColumns()) {
40+
if (column.isReference()) {
41+
for (final Reference reference : column.getReferences()) {
42+
final String[] values = row.getStringArray(reference.getName());
43+
for (final String value : values) {
44+
keyParts.put(reference.getName(), value);
45+
}
46+
}
47+
} else {
48+
keyParts.put(column.getName(), row.getString(column.getName()));
49+
}
50+
}
51+
return new PrimaryKey(keyParts);
52+
}
2753

28-
static PrimaryKey makePrimaryKeyFromEncodedKey(String encodedValue) {
29-
String[] encodedPairs = encodedValue.split(KEY_PARTS_SEPARATOR);
54+
static PrimaryKey fromRow(Table table, Row row) {
55+
return fromRow(table.getMetadata(), row);
56+
}
57+
58+
/**
59+
* Uses map instead of {@code list<NameValuePair>} to prevent duplicate entries as some foreign
60+
* key have overlapping relationships which resulted in a bug.
61+
*
62+
* @throws IllegalArgumentException if encodedString contains 0 pairs, encodedString is unsorted
63+
*/
64+
static PrimaryKey fromEncodedString(TableMetadata table, String encodedString) {
65+
String[] encodedPairs = encodedString.split(KEY_PARTS_SEPARATOR);
3066
if (encodedPairs.length == 0) {
3167
throw new IllegalArgumentException("There must be at least one key.");
68+
} else if (encodedPairs.length > 1
69+
&& !Arrays.equals(Arrays.stream(encodedPairs).sorted().toArray(), encodedPairs)) {
70+
throw new IllegalArgumentException("The encoded String does not contain sorted values");
3271
} else {
33-
Map<String, String> pairs = new LinkedHashMap<>();
72+
SortedMap<String, String> pairs = new TreeMap<>();
3473
for (var pair : encodedPairs) {
3574
var parts = pair.split(NAME_VALUE_SEPARATOR);
3675
if (parts.length != 2) {
3776
throw new IllegalArgumentException(
3877
"Can't decode the key, name value pair is incomplete.");
3978
}
40-
var name = URLDecoder.decode(parts[0], StandardCharsets.UTF_8);
41-
var value = URLDecoder.decode(parts[1], StandardCharsets.UTF_8);
79+
String identifier = URLDecoder.decode(parts[0], StandardCharsets.UTF_8);
80+
String name =
81+
recursiveIdentifierToName(
82+
table,
83+
Arrays.stream(identifier.split("\\.")).collect(toCollection(ArrayList::new)));
84+
String value = URLDecoder.decode(parts[1], StandardCharsets.UTF_8);
4285
pairs.put(name, value);
4386
}
4487
return new PrimaryKey(pairs);
4588
}
4689
}
4790

48-
PrimaryKey(Map<String, String> keys) {
91+
private static String recursiveIdentifierToName(TableMetadata table, List<String> remaining) {
92+
String currentIdentifier = remaining.remove(0);
93+
Column currentColumn = table.getColumnByIdentifier(currentIdentifier);
94+
if (currentColumn == null) {
95+
throw new IllegalArgumentException(
96+
"Could not find (inherited) column for identifier \""
97+
+ currentIdentifier
98+
+ "\" in table \""
99+
+ table.getTableName()
100+
+ "\"");
101+
}
102+
103+
if (!remaining.isEmpty()) {
104+
return currentColumn.getName()
105+
+ "."
106+
+ recursiveIdentifierToName(currentColumn.getRefTable(), remaining);
107+
}
108+
return currentColumn.getName();
109+
}
110+
111+
static PrimaryKey fromEncodedString(Table table, String encodedValue) {
112+
return fromEncodedString(table.getMetadata(), encodedValue);
113+
}
114+
115+
PrimaryKey(SortedMap<String, String> keys) {
49116
if (keys.isEmpty()) {
50117
throw new IllegalArgumentException("There must be at least one key.");
118+
} else if (keys.containsValue(null)) {
119+
throw new IllegalArgumentException("Values are not allowed to be null.");
51120
}
52121
this.keys = keys;
53122
}
54123

55-
String getEncodedValue() {
124+
PrimaryKey(Map<String, String> keys) {
125+
this(new TreeMap<>(keys));
126+
}
127+
128+
String getEncodedString() {
56129
try {
57130
List<String> encodedPairs = new ArrayList<>();
58-
// Sort the list to have a stable order
59-
var sortedMap = new TreeMap<>(this.keys);
60-
for (var pair : sortedMap.entrySet()) {
61-
var name = escaper.escape(pair.getKey());
131+
for (var pair : keys.entrySet()) {
132+
var name = escaper.escape(convertToCamelCase(pair.getKey()));
62133
var value = escaper.escape(pair.getValue());
63134
encodedPairs.add(name + NAME_VALUE_SEPARATOR + value);
64135
}
@@ -74,7 +145,7 @@ Filter getFilter() {
74145
return and(filters);
75146
}
76147

77-
Map<String, String> getKeys() {
148+
SortedMap<String, String> getKeys() {
78149
return keys;
79150
}
80151
}

backend/molgenis-emx2-rdf/src/main/java/org/molgenis/emx2/rdf/RDFService.java

+7-37
Original file line numberDiff line numberDiff line change
@@ -398,25 +398,16 @@ public void rowsToRdf(
398398
final IRI tableIRI = tableIRI(baseURL, table);
399399
final List<Row> rows = getRows(table, rowId);
400400
switch (table.getMetadata().getTableType()) {
401-
case ONTOLOGIES ->
402-
rows.forEach(
403-
row -> ontologyRowToRdf(builder, rdfMapData, table, row, getIriForRow(row, table)));
404-
case DATA ->
405-
rows.forEach(
406-
row ->
407-
dataRowToRdf(
408-
builder, rdfMapData, namespaces, table, row, getIriForRow(row, table)));
401+
case ONTOLOGIES -> rows.forEach(row -> ontologyRowToRdf(builder, rdfMapData, table, row));
402+
case DATA -> rows.forEach(row -> dataRowToRdf(builder, rdfMapData, namespaces, table, row));
409403
default -> throw new MolgenisException("Cannot convert unsupported TableType to RDF");
410404
}
411405
}
412406

413407
private void ontologyRowToRdf(
414-
final ModelBuilder builder,
415-
final RdfMapData rdfMapData,
416-
final Table table,
417-
final Row row,
418-
final IRI subject) {
408+
final ModelBuilder builder, final RdfMapData rdfMapData, final Table table, final Row row) {
419409
final IRI tableIRI = tableIRI(baseURL, table);
410+
final IRI subject = rowIRI(rdfMapData.getBaseURL(), table, row);
420411

421412
builder.add(subject, RDF.TYPE, BasicIRI.NCIT_CODED_VALUE_DATA_TYPE);
422413
builder.add(subject, RDF.TYPE, OWL.CLASS);
@@ -459,9 +450,9 @@ private void dataRowToRdf(
459450
final RdfMapData rdfMapData,
460451
final Map<String, Map<String, Namespace>> namespaces,
461452
final Table table,
462-
final Row row,
463-
final IRI subject) {
453+
final Row row) {
464454
final IRI tableIRI = tableIRI(baseURL, table);
455+
final IRI subject = rowIRI(rdfMapData.getBaseURL(), table, row);
465456

466457
builder.add(subject, RDF.TYPE, tableIRI);
467458
builder.add(subject, RDF.TYPE, BasicIRI.LD_OBSERVATION);
@@ -547,7 +538,7 @@ private List<Row> getRows(Table table, final String rowId) {
547538
Query query = table.query();
548539

549540
if (rowId != null) {
550-
PrimaryKey key = PrimaryKey.makePrimaryKeyFromEncodedKey(rowId);
541+
PrimaryKey key = PrimaryKey.fromEncodedString(table, rowId);
551542
query.where(key.getFilter());
552543
}
553544

@@ -557,25 +548,4 @@ private List<Row> getRows(Table table, final String rowId) {
557548
}
558549
return query.retrieveRows();
559550
}
560-
561-
private IRI getIriForRow(final Row row, final Table table) {
562-
return getIriForRow(row, table.getMetadata());
563-
}
564-
565-
private IRI getIriForRow(final Row row, final TableMetadata metadata) {
566-
final Map<String, String> keyParts = new LinkedHashMap<>();
567-
for (final Column column : metadata.getPrimaryKeyColumns()) {
568-
if (column.isReference()) {
569-
for (final Reference reference : column.getReferences()) {
570-
final String[] values = row.getStringArray(reference.getName());
571-
for (final String value : values) {
572-
keyParts.put(reference.getName(), value);
573-
}
574-
}
575-
} else {
576-
keyParts.put(column.getName(), row.get(column).toString());
577-
}
578-
}
579-
return rowIRI(baseURL, metadata.getRootTable(), new PrimaryKey(keyParts));
580-
}
581551
}

backend/molgenis-emx2-rdf/src/test/java/org/molgenis/emx2/rdf/IriGeneratorTest.java

+34-2
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,15 @@ class IriGeneratorTest {
2424
private static final String COLUMN_ID = "myColumn";
2525
private static final String ENCODED_PRIMARYKEY = "lastName=van%20de%20achternaam";
2626

27+
private static final String CHILD_SCHEMA_NAME = "my Child Schema";
28+
private static final String CHILD_TABLE_NAME = "my Child Table";
29+
private static final String CHILD_TABLE_ID = "MyChildTable";
30+
private static final String CHILD_COLUMN_NAME = "my Child Column";
31+
private static final String CHILD_COLUMN_ID = "myChildColumn";
32+
2733
@Test
2834
void testIRIGenerator() {
35+
// Primary test data
2936
SchemaMetadata schemaMetadata = mock(SchemaMetadata.class);
3037
when(schemaMetadata.getName()).thenReturn(SCHEMA_NAME);
3138

@@ -35,6 +42,7 @@ void testIRIGenerator() {
3542
TableMetadata tableMetadata = mock(TableMetadata.class);
3643
when(tableMetadata.getSchemaName()).thenReturn(SCHEMA_NAME);
3744
when(tableMetadata.getIdentifier()).thenReturn(TABLE_ID);
45+
when(tableMetadata.getRootTable()).thenReturn(tableMetadata);
3846

3947
Table table = mock(Table.class);
4048
when(table.getMetadata()).thenReturn(tableMetadata);
@@ -45,7 +53,27 @@ void testIRIGenerator() {
4553
when(column.getIdentifier()).thenReturn(COLUMN_ID);
4654

4755
PrimaryKey primaryKey = mock(PrimaryKey.class);
48-
when(primaryKey.getEncodedValue()).thenReturn(ENCODED_PRIMARYKEY);
56+
when(primaryKey.getEncodedString()).thenReturn(ENCODED_PRIMARYKEY);
57+
58+
// Test data for checking table inheritance
59+
SchemaMetadata childSchemaMetadata = mock(SchemaMetadata.class);
60+
when(childSchemaMetadata.getName()).thenReturn(CHILD_SCHEMA_NAME);
61+
62+
Schema childSchema = mock(Schema.class);
63+
when(childSchema.getMetadata()).thenReturn(childSchemaMetadata);
64+
65+
TableMetadata childTableMetadata = mock(TableMetadata.class);
66+
when(childTableMetadata.getSchemaName()).thenReturn(CHILD_SCHEMA_NAME);
67+
when(childTableMetadata.getIdentifier()).thenReturn(CHILD_TABLE_ID);
68+
when(childTableMetadata.getRootTable()).thenReturn(tableMetadata); // return parent!
69+
70+
Table childTable = mock(Table.class);
71+
when(childTable.getMetadata()).thenReturn(childTableMetadata);
72+
73+
Column childColumn = mock(Column.class);
74+
when(childColumn.getSchemaName()).thenReturn(CHILD_SCHEMA_NAME);
75+
when(childColumn.getTable()).thenReturn(childTableMetadata);
76+
when(childColumn.getIdentifier()).thenReturn(CHILD_COLUMN_ID);
4977

5078
assertAll(
5179
() ->
@@ -62,6 +90,10 @@ void testIRIGenerator() {
6290
() ->
6391
assertEquals(
6492
"http://example.com/my%20Schema/api/rdf/MyTable/lastName=van%20de%20achternaam",
65-
rowIRI(baseURL, table, primaryKey).toString()));
93+
rowIRI(baseURL, table, primaryKey).toString()),
94+
() -> // PrimaryKey is only part that can differ due to root table overriding current table!
95+
assertEquals(
96+
"http://example.com/my%20Schema/api/rdf/MyTable/lastName=van%20de%20achternaam",
97+
rowIRI(baseURL, childTable, primaryKey).toString()));
6698
}
6799
}

0 commit comments

Comments
 (0)