1
1
// Copyright 2015-2017 Piprate Limited
2
+ // Copyright 2025 Siemens AG
2
3
//
3
4
// Licensed under the Apache License, Version 2.0 (the "License");
4
5
// you may not use this file except in compliance with the License.
@@ -47,6 +48,12 @@ type Context struct {
47
48
inverse map [string ]interface {}
48
49
protected map [string ]bool
49
50
previousContext * Context
51
+ fastCurieMap map [string ]interface {}
52
+ }
53
+
54
+ type FastCurieEntry struct {
55
+ iri string
56
+ terms []string
50
57
}
51
58
52
59
// NewContext creates and returns a new Context object.
@@ -60,6 +67,7 @@ func NewContext(values map[string]interface{}, options *JsonLdOptions) *Context
60
67
options : options ,
61
68
termDefinitions : make (map [string ]interface {}),
62
69
protected : make (map [string ]bool ),
70
+ fastCurieMap : make (map [string ]any ),
63
71
}
64
72
65
73
context .values ["@base" ] = options .Base
@@ -79,6 +87,7 @@ func (c *Context) AsMap() map[string]interface{} {
79
87
"termDefinitions" : c .termDefinitions ,
80
88
"inverse" : c .inverse ,
81
89
"protected" : c .protected ,
90
+ "fastCurieMap" : c .fastCurieMap ,
82
91
}
83
92
if c .previousContext != nil {
84
93
res ["previousContext" ] = c .previousContext .AsMap ()
@@ -628,6 +637,7 @@ func (c *Context) createTermDefinition(context map[string]interface{}, term stri
628
637
termHasColon := colIndex > 0
629
638
630
639
definition ["@reverse" ] = false
640
+ definition ["termHasColon" ] = termHasColon
631
641
632
642
// 11)
633
643
if reverseValue , present := val ["@reverse" ]; present {
@@ -1391,39 +1401,56 @@ func (c *Context) CompactIri(iri string, value interface{}, relativeToVocab bool
1391
1401
compactIRI := ""
1392
1402
1393
1403
// 5)
1394
- for term , termDefinitionVal := range c .termDefinitions {
1395
- if termDefinitionVal == nil {
1396
- continue
1404
+ partialMatches := make ([]FastCurieEntry , 0 )
1405
+ iriMap := c .fastCurieMap
1406
+ // check for partial matches of against `iri`, which means look until
1407
+ // iri.length - 1, not full length
1408
+ maxPartialLength := len (iri ) - 1
1409
+ for i := 0 ; i < maxPartialLength ; i ++ {
1410
+ iriAny , ok := iriMap [string (iri [i ])]
1411
+ if ! ok {
1412
+ break
1397
1413
}
1398
-
1399
- // 5.1)
1400
- if strings . Contains ( term , ":" ) {
1401
- continue
1414
+ iriMap = iriAny .( map [ string ] interface {})
1415
+ if arrAny , ok := iriMap [ "" ]; ok {
1416
+ entry := arrAny .([] FastCurieEntry )[ 0 ]
1417
+ partialMatches = append ( partialMatches , entry )
1402
1418
}
1419
+ }
1420
+ // check partial matches in reverse order to prefer longest ones first
1421
+ for i := len (partialMatches ) - 1 ; i >= 0 ; i -- {
1422
+ entry := partialMatches [i ]
1423
+ for _ , term := range entry .terms {
1424
+ termDefinitionAny , ok := c .termDefinitions [term ]
1425
+ if ! ok {
1426
+ continue
1427
+ }
1428
+ termDef := termDefinitionAny .(map [string ]interface {})
1403
1429
1404
- // 5.2)
1405
- termDefinition := termDefinitionVal .(map [string ]interface {})
1406
- idStr := termDefinition ["@id" ].(string )
1407
- if iri == idStr || ! strings .HasPrefix (iri , idStr ) {
1408
- continue
1409
- }
1430
+ // a CURIE is usable if:
1431
+ // 1. it has no mapping, OR
1432
+ // 2. value is null, which means we're not compacting an @value, AND
1433
+ // the mapping matches the IRI
1434
+ curie := term + ":" + iri [len (entry .iri ):]
1435
+ prefix , hasPrefix := termDef ["_prefix" ]
1436
+ curieMapping , hasCurie := c .termDefinitions [curie ]
1437
+
1438
+ isUsableCurie := hasPrefix && prefix .(bool ) && (! hasCurie || value == nil && curieMapping .(map [string ]any )["@id" ].(string ) == iri )
1410
1439
1411
- // 5.3)
1412
- candidate := term + ":" + iri [len (idStr ):]
1413
- // 5.4)
1414
- candidateVal , containsCandidate := c .termDefinitions [candidate ]
1415
- prefix , hasPrefix := termDefinition ["_prefix" ]
1416
- if (compactIRI == "" || CompareShortestLeast (candidate , compactIRI )) && hasPrefix && prefix .(bool ) &&
1417
- (! containsCandidate ||
1418
- (iri == candidateVal .(map [string ]interface {})["@id" ] && value == nil )) {
1419
- compactIRI = candidate
1440
+ // select curie if it is shorter or the same length but lexicographically
1441
+ // less than the current choice
1442
+ if isUsableCurie && (compactIRI == "" || CompareShortestLeast (curie , compactIRI )) {
1443
+ compactIRI = curie
1444
+ }
1420
1445
}
1421
1446
}
1422
1447
1423
1448
if compactIRI != "" {
1424
1449
return compactIRI , nil
1425
1450
}
1426
1451
1452
+ // If iri could be confused with a compact IRI using a term in this context,
1453
+ // signal an error
1427
1454
for term , td := range c .termDefinitions {
1428
1455
if tdMap , isMap := td .(map [string ]interface {}); isMap {
1429
1456
prefix , hasPrefix := tdMap ["_prefix" ]
@@ -1433,10 +1460,12 @@ func (c *Context) CompactIri(iri string, value interface{}, relativeToVocab bool
1433
1460
}
1434
1461
}
1435
1462
1463
+ // compact IRI relative to base
1436
1464
if ! relativeToVocab {
1437
1465
return RemoveBase (c .values ["@base" ], iri ), nil
1438
1466
}
1439
1467
1468
+ // return IRI as is
1440
1469
return iri , nil
1441
1470
}
1442
1471
@@ -1499,6 +1528,9 @@ func (c *Context) GetInverse() map[string]interface{} {
1499
1528
terms := GetKeys (c .termDefinitions )
1500
1529
sort .Sort (ShortestLeast (terms ))
1501
1530
1531
+ // variables for building fast CURIE map
1532
+ irisToTerms := make (map [string ][]string , 0 )
1533
+
1502
1534
for _ , term := range terms {
1503
1535
definitionVal := c .termDefinitions [term ]
1504
1536
// 3.1)
@@ -1528,11 +1560,31 @@ func (c *Context) GetInverse() map[string]interface{} {
1528
1560
// 3.4 + 3.5)
1529
1561
var containerMap map [string ]interface {}
1530
1562
containerMapVal , present := c .inverse [iri ]
1563
+ isKeyword := IsKeyword (iri )
1564
+ termHasColon := definition ["termHasColon" ].(bool )
1531
1565
if ! present {
1532
1566
containerMap = make (map [string ]interface {})
1533
1567
c .inverse [iri ] = containerMap
1568
+
1569
+ if ! isKeyword && ! termHasColon {
1570
+ // init IRI to term map and fast CURIE map
1571
+ irisToTerms [iri ] = []string {term }
1572
+ entry := FastCurieEntry {iri : iri , terms : irisToTerms [iri ]}
1573
+ letter := string (iri [0 ])
1574
+ if val , ok := c .fastCurieMap [letter ]; ok {
1575
+ arr := val .([]FastCurieEntry )
1576
+ arr = append (arr , entry )
1577
+ c .fastCurieMap [letter ] = arr
1578
+ } else {
1579
+ c .fastCurieMap [letter ] = []FastCurieEntry {entry }
1580
+ }
1581
+ }
1534
1582
} else {
1535
1583
containerMap = containerMapVal .(map [string ]interface {})
1584
+ if ! isKeyword && ! termHasColon {
1585
+ // add IRI to term match
1586
+ irisToTerms [iri ] = append (irisToTerms [iri ], term )
1587
+ }
1536
1588
}
1537
1589
1538
1590
// 3.6 + 3.7)
@@ -1649,10 +1701,48 @@ func (c *Context) GetInverse() map[string]interface{} {
1649
1701
}
1650
1702
}
1651
1703
1704
+ // build fast CURIE map
1705
+ for key := range c .fastCurieMap {
1706
+ buildIriMap (c .fastCurieMap , key , 1 )
1707
+ }
1708
+
1652
1709
// 4)
1653
1710
return c .inverse
1654
1711
}
1655
1712
1713
+ // buildIriMap runs a recursive algorithm to build a lookup map for quickly finding
1714
+ // potential CURIEs.
1715
+ //
1716
+ // iriMap is the map to build.
1717
+ // key is the current key in the map to work on.
1718
+ // idx is the index into the IRI to compare.
1719
+ func buildIriMap (iriMap map [string ]interface {}, key string , idx int ) {
1720
+ entries := iriMap [key ].([]FastCurieEntry )
1721
+ next := make (map [string ]interface {}, 0 )
1722
+ iriMap [key ] = next
1723
+
1724
+ for _ , entry := range entries {
1725
+ letter := ""
1726
+ iri := entry .iri
1727
+ if idx < len (iri ) {
1728
+ letter = string (iri [idx ])
1729
+ }
1730
+ if val , ok := next [letter ]; ok {
1731
+ arr := val .([]FastCurieEntry )
1732
+ arr = append (arr , entry )
1733
+ next [letter ] = arr
1734
+ } else {
1735
+ next [letter ] = []FastCurieEntry {entry }
1736
+ }
1737
+ }
1738
+ for key := range next {
1739
+ if key == "" {
1740
+ continue
1741
+ }
1742
+ buildIriMap (next , key , idx + 1 )
1743
+ }
1744
+ }
1745
+
1656
1746
// SelectTerm picks the preferred compaction term from the inverse context entry.
1657
1747
// See http://www.w3.org/TR/json-ld-api/#term-selection
1658
1748
//
0 commit comments