├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── graph ├── bimap.go ├── bimap_test.go ├── bitmapRDF.go ├── bitmapRDF_test.go ├── graph.go ├── listGraph.go ├── listGraph_test.go ├── treeGraph.go └── treeGraph_test.go ├── joseki.go ├── joseki_test.go ├── parser ├── NTParser.go ├── NTParser_test.go ├── TurtleParser.go ├── TurtleParser_test.go ├── baseTokens.go ├── baseTokens_test.go ├── datas │ ├── test.nt │ └── test.ttl ├── parser.go ├── rdfToken.go ├── separatorTokens.go ├── separatorTokens_test.go ├── stack.go ├── turtleTokens.go └── turtleTokens_test.go ├── rdf ├── binding.go ├── nodes.go ├── nodes_test.go ├── triple.go └── triple_test.go └── travis_test.sh /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | watDiv*.nt 27 | watdiv*.nt 28 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - tip 4 | notifications: 5 | email: false 6 | before_script: 7 | - wget http://dsg.uwaterloo.ca/watdiv/watdiv.10M.tar.bz2 && tar jxvf watdiv.10M.tar.bz2 8 | - head -30000 watdiv.10M.nt > $HOME/gopath/src/github.com/Callidon/joseki/graph/watdiv.30k.nt 9 | - go get github.com/mattn/goveralls 10 | - go get golang.org/x/tools/cmd/cover 11 | script: 12 | - ./travis_test.sh 13 | - goveralls -coverprofile=coverage.out -service=travis-ci 14 | env: 15 | global: 16 | secure: nT5EKMXY/6xKDWZ6MpL9JRzaVbn5v35E8kflXLfsSboX1ALkDlVbnNw1Ll3ilfZ3yHYJ6rMa+fI2nnvDIyuMfJAdCUKoNrIvAhOkpHDBfA0zKTxK8vPLZDmuChUMW361w/2KGsy4CVy+5bqqy5TB6UzLfJhJHQn6nQJyjGznYpPNZhRW284kvZ++2199aLhhgT7tXQvpAnDg8m0e5ViY+xFUFlRxYPHGg+nxhJol2Rt7HIsYJmaXa/pnHpzmYCTqgJH91QqupR5gf4xlXNlwnGNThI+0aMXaGL+ii7/2xDCnr5gUaNJ25rp4Z1WQxd3HZT0bQdZvJu7poVFj0j96OTJMRaIN9RiqXzh+aSMyBUefemAqNoxca0CEGSTBlMvQV5q+XcESF+NtY7dZleMkbO/11UOz4B5Eh+vKxxR/tgAkQN96C2Hh4ykyancS9MaIkYwdx8+w3J4l/1yHCSUah0mSF1H1kFpr58oqgj2E3GJSyJvp75+htFLL0HIflscdWYb32P4lLGa1rTdW7a1W2vSz+19hp0nhRI1SUIxp58/CdfOxBQ446z+zUqKZl/AYSK0GG0O9VPNXZTFaQX+SO36LBDMCSAx/x5sLMjZfQFcio+VK3vyusEydJZ5aubyG34ep2t59qmugABeXqyodR41UBmEXT68AhvuRuhAqFvs= 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Thomas Minier 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # joseki [![license](https://img.shields.io/github/license/mashape/apistatus.svg?maxAge=2592000)]() [![GoDoc](https://godoc.org/github.com/Callidon/joseki/rdf?status.svg)](https://godoc.org/github.com/Callidon/joseki/) [![Build Status](https://travis-ci.org/Callidon/joseki.svg?branch=master)](https://travis-ci.org/Callidon/joseki) [![Coverage Status](https://coveralls.io/repos/github/Callidon/joseki/badge.svg?branch=master&dummy=0)](https://coveralls.io/github/Callidon/joseki?branch=master) 2 | 3 | Joseki is a pure Go library for working with RDF, a powerful framework for representing informations as graphs. 4 | 5 | For more informations about RDF itself, please see [https://www.w3.org/TR/rdf11-concepts](https://www.w3.org/TR/rdf11-concepts) 6 | 7 | ## Features 8 | Joseki provides the following features to work with RDF : 9 | * Structures to represent and manipulate the RDF model (URIs, Literals, Blank Nodes, Triples, etc) 10 | * RDF Graphs to store data, with several implentations provided. 11 | * A Low level API to query data stored in graphs. 12 | * A High level API to query data using the [SPARQL 1.1 query language](https://www.w3.org/TR/sparql11-overview/). (WIP - Unstable) 13 | * Query processing using modern techniques such as join ordering or optimized query execution plans. 14 | * Load RDF data stored in files in various formats (N-Triples, Turtle, etc) into any graph. 15 | 16 | ## Getting Started 17 | This package aims to work with RDF graphs, which are composed of RDF Triple {Subject Object Predicate}. 18 | Using joseki, you can represent an RDF Triple as followed : 19 | ```go 20 | import ( 21 | "github.com/Callidon/joseki/rdf" 22 | "fmt" 23 | ) 24 | subject := rdf.NewURI("http://example.org/book/book1") 25 | predicate := rdf.NewURI("http://purl.org/dc/terms/title") 26 | object := rdf.NewLiteral("Harry Potter and the Order of the Phoenix") 27 | triple := rdf.NewTriple(subject, predicate, object) 28 | fmt.Println(triple) 29 | // Output : { "Harry Potter and the Order of the Phoenix"} 30 | ``` 31 | You can also store your RDF Triples in a RDF Graph, using various type of graphs. 32 | Here, we use a Tree Graph to store our triple : 33 | ```go 34 | import ( 35 | "github.com/Callidon/joseki/rdf" 36 | "github.com/Callidon/joseki/graph" 37 | ) 38 | subject := rdf.NewURI("http://example.org/book/book1") 39 | predicate := rdf.NewURI("http://purl.org/dc/terms/title") 40 | object := rdf.NewLiteral("Harry Potter and the Order of the Phoenix") 41 | graph := graph.NewTreeGraph() 42 | graph.Add(rdf.NewTriple(subject, predicate, object)) 43 | ``` 44 | You can also query any triple from a RDF Graph, using a low level API or a SPARQL query. 45 | ```go 46 | import ( 47 | "github.com/Callidon/joseki/rdf" 48 | "github.com/Callidon/joseki/graph" 49 | "fmt" 50 | ) 51 | graph := graph.NewTreeGraph() 52 | // Datas stored in a file can be easily loaded into a graph 53 | graph.LoadFromFile("datas/awesome-books.ttl", "turtle") 54 | // Let's fetch the titles of all the books in our graph ! 55 | subject := rdf.NewVariable("title") 56 | predicate := rdf.NewURI("http://www.w3.org/1999/02/22-rdf-syntax-ns#type") 57 | object := rdf.NewURI("https://schema.org/Book") 58 | for bindings := range graph.Filter(subject, predicate, object) { 59 | fmt.Println(bindings) 60 | } 61 | ``` 62 | For more informations about specific features, see the [documentation](https://godoc.org/github.com/Callidon/joseki/) 63 | -------------------------------------------------------------------------------- /graph/bimap.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Thomas Minier. All rights reserved. 2 | // Use of this source code is governed by a MIT License 3 | // license that can be found in the LICENSE file. 4 | 5 | package graph 6 | 7 | import "github.com/Callidon/joseki/rdf" 8 | 9 | // Data structure that represents bidirectional relations between elements of two collections. 10 | // Used as a dictionnary in the HDT-MR Graph implementation 11 | type bimap struct { 12 | keyToValue map[int]rdf.Node 13 | valueToKey map[rdf.Node]int 14 | } 15 | 16 | // Return a new empty Bimap. 17 | func newBimap() *bimap { 18 | return &bimap{make(map[int]rdf.Node), make(map[rdf.Node]int)} 19 | } 20 | 21 | // Add a (key, value) to the Bimap. 22 | // If a key or a value already exist in the bimap, erase the associate relation. 23 | func (b *bimap) push(key int, value rdf.Node) { 24 | // insert value in map, but check if it's already present before 25 | previousValue, inMap := b.keyToValue[key] 26 | if inMap { 27 | b.keyToValue[key] = value 28 | // remove association in other map before updating it 29 | delete(b.valueToKey, previousValue) 30 | } else { 31 | b.keyToValue[key] = value 32 | } 33 | // same thing for the key 34 | previousKey, inMap := b.valueToKey[value] 35 | if inMap { 36 | b.valueToKey[value] = key 37 | // remove association in other map before updating it 38 | delete(b.keyToValue, previousKey) 39 | } else { 40 | b.valueToKey[value] = key 41 | } 42 | } 43 | 44 | // Return the key associated to a value in the Bimap. 45 | func (b *bimap) locate(value rdf.Node) (key int, inMap bool) { 46 | key, inMap = b.valueToKey[value] 47 | return 48 | } 49 | 50 | // Return the value associated to a key in the Bimap. 51 | func (b *bimap) extract(key int) (value rdf.Node, inMap bool) { 52 | value, inMap = b.keyToValue[key] 53 | return 54 | } 55 | -------------------------------------------------------------------------------- /graph/bimap_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Thomas Minier. All rights reserved. 2 | // Use of this source code is governed by a MIT License 3 | // license that can be found in the LICENSE file. 4 | 5 | package graph 6 | 7 | import ( 8 | "github.com/Callidon/joseki/rdf" 9 | "testing" 10 | ) 11 | 12 | func TestBimap(t *testing.T) { 13 | var value rdf.Node 14 | bimap := newBimap() 15 | nodeA := rdf.NewLiteral("foo") 16 | nodeB := rdf.NewLiteral("bar") 17 | nodeC := rdf.NewLiteral("test") 18 | 19 | // test with classic insertion 20 | bimap.push(1, nodeA) 21 | bimap.push(2, nodeB) 22 | 23 | if key, _ := bimap.locate(nodeA); key != 1 { 24 | t.Error("expected key = 1 but got", key) 25 | } 26 | value, _ = bimap.extract(1) 27 | if test, _ := value.Equals(nodeA); !test { 28 | t.Error("expected value = 'foo' but got", value) 29 | } 30 | if key, _ := bimap.locate(nodeB); key != 2 { 31 | t.Error("expected key = 2 but got", key) 32 | } 33 | value, _ = bimap.extract(2) 34 | if test, _ := value.Equals(nodeB); !test { 35 | t.Error("expected value = 'bar' but got", value) 36 | } 37 | 38 | // test with non-existent key/value 39 | if _, test := bimap.locate(nodeC); test { 40 | t.Error("cannot return true when locating a non-existent value") 41 | } 42 | if _, test := bimap.extract(5); test { 43 | t.Error("cannot return true when extracting a non-existent key") 44 | } 45 | 46 | // test with value override 47 | bimap.push(1, nodeB) 48 | if key, _ := bimap.locate(nodeB); key != 1 { 49 | t.Error("expected key = 1 but got", key) 50 | } 51 | value, _ = bimap.extract(1) 52 | if test, _ := value.Equals(nodeB); !test { 53 | t.Error("expected value = 'bar' but got", value) 54 | } 55 | if _, test := bimap.locate(nodeA); test { 56 | t.Error("cannot return true when locating a non-existent value") 57 | } 58 | if _, test := bimap.extract(2); test { 59 | t.Error("cannot return true when extracting a non-existent key") 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /graph/bitmapRDF.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Thomas Minier. All rights reserved. 2 | // Use of this source code is governed by a MIT License 3 | // license that can be found in the LICENSE file. 4 | 5 | package graph 6 | 7 | import ( 8 | "errors" 9 | "github.com/Callidon/joseki/rdf" 10 | "sync" 11 | ) 12 | 13 | // Node represented in the Bitmap standard, following the HDT-MR model. 14 | type bitmapNode struct { 15 | id int 16 | sons map[int]*bitmapNode 17 | } 18 | 19 | // Triple represented in the Bitmap standard, following the HDT-MR model. 20 | type bitmapTriple struct { 21 | subjectID int 22 | predicateID int 23 | objectID int 24 | } 25 | 26 | // newBitmapNode creates a new Bitmap Node without any son. 27 | func newBitmapNode(id int) *bitmapNode { 28 | return &bitmapNode{id, make(map[int]*bitmapNode)} 29 | } 30 | 31 | // depth calculates the number of nodes in the tree, starting from this node. 32 | func (n *bitmapNode) length() int { 33 | res := 0 34 | res += len(n.sons) 35 | for _, son := range n.sons { 36 | res += son.length() 37 | } 38 | return res 39 | } 40 | 41 | // updateCounter update a Wait Group counter for a node & his sons recursively. 42 | func (n *bitmapNode) updateCounter(wg *sync.WaitGroup) { 43 | wg.Done() 44 | for _, son := range n.sons { 45 | son.updateCounter(wg) 46 | } 47 | } 48 | 49 | // Recursively remove the sons of a Bitmap Node 50 | func (n *bitmapNode) removeSons() { 51 | for key, son := range n.sons { 52 | son.removeSons() 53 | delete(n.sons, key) 54 | } 55 | } 56 | 57 | // newBitmapTriple creates a New Bitmap Triple. 58 | func newBitmapTriple(subj, pred, obj int) bitmapTriple { 59 | return bitmapTriple{subj, pred, obj} 60 | } 61 | 62 | // Equals returns True if two Bitmap Triple are equals, False otherwise 63 | func (b *bitmapTriple) Equals(other bitmapTriple) bool { 64 | subjEq := b.subjectID == other.subjectID 65 | predEq := b.predicateID == other.predicateID 66 | objEq := b.objectID == other.objectID 67 | if b.subjectID < 0 || other.subjectID < 0 { 68 | subjEq = true 69 | } 70 | if b.predicateID < 0 || other.predicateID < 0 { 71 | predEq = true 72 | } 73 | if b.objectID < 0 || other.objectID < 0 { 74 | objEq = true 75 | } 76 | return subjEq && predEq && objEq 77 | } 78 | 79 | // Convert a BitMap Triple to a RDF Triple. 80 | func (b *bitmapTriple) Triple(dict *bimap) (rdf.Triple, error) { 81 | var triple rdf.Triple 82 | subj, foundSubj := dict.extract(b.subjectID) 83 | if !foundSubj { 84 | return triple, errors.New("Error : cannot found the subject id in the dictionnary") 85 | } 86 | pred, foundPred := dict.extract(b.predicateID) 87 | if !foundPred { 88 | return triple, errors.New("Error : cannot found the predicate id in the dictionnary") 89 | } 90 | obj, foundObj := dict.extract(b.objectID) 91 | if !foundObj { 92 | return triple, errors.New("Error : cannot found the object id in the dictionnary") 93 | } 94 | triple = rdf.NewTriple(subj, pred, obj) 95 | return triple, nil 96 | } 97 | -------------------------------------------------------------------------------- /graph/bitmapRDF_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Thomas Minier. All rights reserved. 2 | // Use of this source code is governed by a MIT License 3 | // license that can be found in the LICENSE file. 4 | 5 | package graph 6 | 7 | import ( 8 | "github.com/Callidon/joseki/rdf" 9 | "testing" 10 | ) 11 | 12 | // Test the Equals operator for bitmapTriple struct 13 | func TestBitmapTripleEquals(t *testing.T) { 14 | tripleA := newBitmapTriple(1, 2, 3) 15 | tripleB := newBitmapTriple(2, 1, 3) 16 | tripleC := newBitmapTriple(-1, 2, 3) 17 | 18 | if test := tripleA.Equals(tripleA); !test { 19 | t.Error("a triple should be equals to itself") 20 | } 21 | if test := tripleA.Equals(tripleB); test { 22 | t.Error(tripleA, "cannot be equals to", tripleB) 23 | } 24 | if test := tripleA.Equals(tripleC); !test { 25 | t.Error(tripleA, "should be equals to", tripleC) 26 | } 27 | } 28 | 29 | // Test the Triple method for bitmapTriple struct 30 | func TestBitmapTripleCast(t *testing.T) { 31 | subject := rdf.NewURI("http://example.org#subject") 32 | predicate := rdf.NewURI("http://example.org#predicate") 33 | object := rdf.NewURI("http://example.org#object") 34 | refTriple := rdf.NewTriple(subject, predicate, object) 35 | 36 | bimap := newBimap() 37 | bimap.push(1, subject) 38 | bimap.push(2, predicate) 39 | bimap.push(3, object) 40 | 41 | tripleA := newBitmapTriple(1, 2, 3) 42 | tripleB := newBitmapTriple(4, 2, 3) 43 | tripleC := newBitmapTriple(1, 4, 3) 44 | tripleD := newBitmapTriple(1, 2, 4) 45 | 46 | if _, err := tripleA.Triple(bimap); err != nil { 47 | t.Error(tripleA, "shouldn't throw an error when cast to rdf.Triple") 48 | } else { 49 | triple, _ := tripleA.Triple(bimap) 50 | if test, err := triple.Equals(refTriple); !test && err != nil { 51 | t.Error(triple, "should be equals to", refTriple) 52 | } 53 | } 54 | 55 | if _, err := tripleB.Triple(bimap); err == nil { 56 | t.Error(tripleB, "should throw an error when casted to rdf.triple") 57 | } 58 | 59 | if _, err := tripleC.Triple(bimap); err == nil { 60 | t.Error(tripleC, "should throw an error when casted to rdf.triple") 61 | } 62 | 63 | if _, err := tripleD.Triple(bimap); err == nil { 64 | t.Error(tripleD, "should throw an error when casted to rdf.triple") 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /graph/graph.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Thomas Minier. All rights reserved. 2 | // Use of this source code is governed by a MIT License 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package graph provides various implementation for a RDF Graph 6 | package graph 7 | 8 | import ( 9 | "errors" 10 | "github.com/Callidon/joseki/parser" 11 | "github.com/Callidon/joseki/rdf" 12 | "strings" 13 | ) 14 | 15 | const ( 16 | // Max size for the buffer of this package 17 | bufferSize = 100 18 | ) 19 | 20 | // Graph represents a generic RDF Graph 21 | // 22 | // Package graph provides several implementations for this interface. 23 | // RDF Graph reference : https://www.w3.org/TR/rdf11-concepts/#section-rdf-graph 24 | type Graph interface { 25 | // Add a new Triple pattern to the graph. 26 | Add(triple rdf.Triple) 27 | // Delete triples from the graph that match a BGP given in parameters. 28 | Delete(subject, predicate, object rdf.Node) 29 | // Fetch triples form the graph that match a BGP given in parameters. 30 | Filter(subject, predicate, object rdf.Node) <-chan rdf.Triple 31 | // Same as Filter, but with a Limit and an Offset 32 | FilterSubset(subject rdf.Node, predicate rdf.Node, object rdf.Node, limit int, offset int) <-chan rdf.Triple 33 | } 34 | 35 | // rdfReader represents a reader capable of reading RDF data encoded in various format. 36 | // 37 | // This structure is designed to be embedded into types which implement the Graph interface 38 | type rdfReader struct { 39 | graph Graph 40 | // list of prefixes used in some RDF formats (Turtle, JSON-LD, ...) 41 | prefixes map[string]string 42 | } 43 | 44 | // newRDFReader creates a new rdfReader 45 | func newRDFReader() *rdfReader { 46 | return &rdfReader{nil, nil} 47 | } 48 | 49 | // LoadFromFile loads triples from a file into a graph, with a given format 50 | // In the desired format isn't supported or doesn't exist, no new triples will 51 | // be inserted into the graph and an error will be returned. 52 | func (r *rdfReader) LoadFromFile(filename string, format string) error { 53 | var p parser.Parser 54 | hasPrefixes := false 55 | // determine which parser to use depending on the format 56 | switch strings.ToLower(format) { 57 | case "nt", "n-triples", "ntriples": 58 | p = parser.NewNTParser() 59 | case "ttl", "turtle": 60 | p = parser.NewTurtleParser() 61 | hasPrefixes = true 62 | default: 63 | return errors.New("Error : " + format + " is not a supported format." + 64 | "Please see the documentation at https://godoc.org/github.com/Callidon/joseki/parser to see the available parsers.") 65 | } 66 | // read triples from file, then load prefixes if necessary 67 | for triple := range p.Read(filename) { 68 | r.graph.Add(triple) 69 | } 70 | if hasPrefixes { 71 | r.prefixes = p.Prefixes() 72 | } 73 | return nil 74 | } 75 | 76 | // Utility function for checking errors 77 | func check(err error) { 78 | if err != nil { 79 | panic(err) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /graph/listGraph.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Thomas Minier. All rights reserved. 2 | // Use of this source code is governed by a MIT License 3 | // license that can be found in the LICENSE file. 4 | 5 | package graph 6 | 7 | import ( 8 | "github.com/Callidon/joseki/rdf" 9 | "sync" 10 | ) 11 | 12 | // ListGraph is implementation of a RDF Graph, using a slice to store RDF Triples. 13 | type ListGraph struct { 14 | dictionnary *bimap 15 | triples []bitmapTriple 16 | nextID int 17 | *sync.RWMutex 18 | *rdfReader 19 | } 20 | 21 | // NewListGraph creates a new List Graph. 22 | func NewListGraph() *ListGraph { 23 | reader := newRDFReader() 24 | g := &ListGraph{newBimap(), make([]bitmapTriple, 0), 0, &sync.RWMutex{}, reader} 25 | reader.graph = g 26 | return g 27 | } 28 | 29 | // Register a new Node in the graph dictionnary, then return its unique ID. 30 | func (g *ListGraph) registerNode(node rdf.Node) int { 31 | // insert the node in dictionnary only if it's not in 32 | key, inDict := g.dictionnary.locate(node) 33 | if !inDict { 34 | g.dictionnary.push(g.nextID, node) 35 | g.nextID++ 36 | return g.nextID - 1 37 | } 38 | return key 39 | } 40 | 41 | // identifyNode returns the id of a node in the graph dictionnary, and a boolean to 42 | // indicate if the node is known by the dictionnary 43 | func (g *ListGraph) identifyNode(node rdf.Node) (int, bool) { 44 | if _, isVar := node.(rdf.Variable); isVar { 45 | return -1, true 46 | } 47 | return g.dictionnary.locate(node) 48 | } 49 | 50 | // Add a new Triple pattern to the graph. 51 | func (g *ListGraph) Add(triple rdf.Triple) { 52 | g.Lock() 53 | defer g.Unlock() 54 | // add each node of the triple to the dictionnary & then update the slice 55 | subjID, predID, objID := g.registerNode(triple.Subject), g.registerNode(triple.Predicate), g.registerNode(triple.Object) 56 | g.triples = append(g.triples, newBitmapTriple(subjID, predID, objID)) 57 | } 58 | 59 | // Delete triples from the graph that match a BGP given in parameters. 60 | func (g *ListGraph) Delete(subject, predicate, object rdf.Node) { 61 | g.Lock() 62 | defer g.Unlock() 63 | newTriples := make([]bitmapTriple, 0, len(g.triples)) 64 | subjID, subjKnown := g.identifyNode(subject) 65 | predID, predKnown := g.identifyNode(predicate) 66 | objID, objKnown := g.identifyNode(object) 67 | // continue onyl if we know all elements of the pattern 68 | if subjKnown && predKnown && objKnown { 69 | refTriple := newBitmapTriple(subjID, predID, objID) 70 | // resinsert into the graph the elements we doesn't want to delete 71 | for _, triple := range g.triples { 72 | if test := triple.Equals(refTriple); !test { 73 | newTriples = append(newTriples, triple) 74 | } 75 | } 76 | // update the slice 77 | g.triples = make([]bitmapTriple, len(newTriples)) 78 | copy(g.triples, newTriples) 79 | } 80 | } 81 | 82 | // FilterSubset fetch triples form the graph that match a BGP given in parameters. 83 | // It impose a Limit(the max number of results to be send in the output channel) 84 | // and an Offset (the number of results to skip before sending them in the output channel) to the nodes requested. 85 | func (g *ListGraph) FilterSubset(subject rdf.Node, predicate rdf.Node, object rdf.Node, limit int, offset int) <-chan rdf.Triple { 86 | results := make(chan rdf.Triple, bufferSize) 87 | // search for matching triple pattern in graph 88 | go func() { 89 | g.RLock() 90 | defer g.RUnlock() 91 | defer close(results) 92 | subjID, subjKnown := g.identifyNode(subject) 93 | predID, predKnown := g.identifyNode(predicate) 94 | objID, objKnown := g.identifyNode(object) 95 | // continue onyl if we know all elements of the pattern 96 | if subjKnown && predKnown && objKnown { 97 | refTriple := newBitmapTriple(subjID, predID, objID) 98 | cpt := 0 99 | for _, triple := range g.triples { 100 | if test := refTriple.Equals(triple); test { 101 | // send the result only if the offset has been reached 102 | if cpt >= offset { 103 | value, err := triple.Triple(g.dictionnary) 104 | check(err) 105 | results <- value 106 | } 107 | cpt++ 108 | } 109 | // terminate the loop when the limit has been reached 110 | if (limit != -1) && (cpt-offset > limit) { 111 | break 112 | } 113 | } 114 | } 115 | }() 116 | return results 117 | } 118 | 119 | // Filter fetch triples form the graph that match a BGP given in parameters. 120 | func (g *ListGraph) Filter(subject, predicate, object rdf.Node) <-chan rdf.Triple { 121 | return g.FilterSubset(subject, predicate, object, -1, 0) 122 | } 123 | -------------------------------------------------------------------------------- /graph/listGraph_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Thomas Minier. All rights reserved. 2 | // Use of this source code is governed by a MIT License 3 | // license that can be found in the LICENSE file. 4 | 5 | package graph 6 | 7 | import ( 8 | "github.com/Callidon/joseki/rdf" 9 | "math/rand" 10 | "testing" 11 | ) 12 | 13 | func TestAddListGraph(t *testing.T) { 14 | graph := NewListGraph() 15 | subj := rdf.NewURI("http://dbpl.org#Thomas") 16 | pred := rdf.NewURI("http://foaf.com/age") 17 | obj := rdf.NewLiteral("22") 18 | triple := rdf.NewTriple(subj, pred, obj) 19 | graph.Add(triple) 20 | graphTriple, _ := graph.triples[0].Triple(graph.dictionnary) 21 | if test, err := graphTriple.Equals(triple); !test && (err != nil) { 22 | t.Error(triple, "hasn't been inserted into the graph") 23 | } 24 | } 25 | 26 | func TestFilterListGraph(t *testing.T) { 27 | skipTest("./watdiv.30k.nt", t) 28 | graph := NewListGraph() 29 | graph.LoadFromFile("./watdiv.30k.nt", "nt") 30 | subj := rdf.NewURI("http://db.uwaterloo.ca/~galuc/wsdbm/Offer10001") 31 | pred := rdf.NewURI("http://schema.org/eligibleRegion") 32 | obj := rdf.NewURI("http://db.uwaterloo.ca/~galuc/wsdbm/Country0") 33 | triple := rdf.NewTriple(subj, pred, obj) 34 | cpt := 0 35 | 36 | // select one triple specific triple pattern 37 | for result := range graph.Filter(subj, pred, obj) { 38 | if test, err := result.Equals(triple); !test || (err != nil) { 39 | t.Error("expected", triple, "but instead got", result) 40 | } 41 | cpt++ 42 | } 43 | 44 | if cpt != 1 { 45 | t.Error("expected 1 result but instead got", cpt, "results") 46 | } 47 | 48 | // select all triples 49 | cpt = 0 50 | for _ = range graph.Filter(rdf.NewVariable("v"), rdf.NewVariable("w"), rdf.NewVariable("z")) { 51 | cpt++ 52 | } 53 | if cpt != 30000 { 54 | t.Error("expected 30000 results but instead got", cpt, "results") 55 | } 56 | 57 | // select multiple triples with the same subject 58 | cpt = 0 59 | for _ = range graph.Filter(rdf.NewURI("http://db.uwaterloo.ca/~galuc/wsdbm/Offer1375"), rdf.NewVariable("v"), rdf.NewVariable("w")) { 60 | cpt++ 61 | } 62 | if cpt != 9 { 63 | t.Error("expected 9 results but instead got", cpt, "results") 64 | } 65 | 66 | // select multiple triples with the same predicate 67 | cpt = 0 68 | for _ = range graph.Filter(rdf.NewVariable("v"), rdf.NewURI("http://www.geonames.org/ontology#parentCountry"), rdf.NewVariable("w")) { 69 | cpt++ 70 | } 71 | if cpt != 240 { 72 | t.Error("expected 240 results but instead got", cpt, "results") 73 | } 74 | 75 | // select multiple triples with the same object 76 | cpt = 0 77 | for _ = range graph.Filter(rdf.NewVariable("v"), rdf.NewVariable("w"), rdf.NewLiteral("673")) { 78 | cpt++ 79 | } 80 | if cpt != 6 { 81 | t.Error("expected 6 results but instead got", cpt, "results") 82 | } 83 | 84 | // select a triple that doesn't exist in the graph 85 | cpt = 0 86 | for _ = range graph.Filter(rdf.NewURI("http://example.org"), rdf.NewVariable("v1"), rdf.NewVariable("v2")) { 87 | cpt++ 88 | } 89 | 90 | if cpt > 0 { 91 | t.Error("expected no result but instead found", cpt, "results") 92 | } 93 | } 94 | 95 | func TestFilterSubsetListGraph(t *testing.T) { 96 | skipTest("./watdiv.30k.nt", t) 97 | graph := NewListGraph() 98 | graph.LoadFromFile("./watdiv.30k.nt", "nt") 99 | nbDatas, limit, offset := 30000, 600, 800 100 | cpt := 0 101 | 102 | // test a FilterSubset with a simple Limit 103 | for _ = range graph.FilterSubset(rdf.NewVariable("x"), rdf.NewVariable("v"), rdf.NewVariable("w"), limit, -1) { 104 | cpt++ 105 | } 106 | 107 | if cpt != limit { 108 | t.Error("expected ", limit, "results but instead found ", cpt, "results") 109 | } 110 | 111 | // test a FilterSubset with a simple offset 112 | cpt = 0 113 | for _ = range graph.FilterSubset(rdf.NewVariable("x"), rdf.NewVariable("v"), rdf.NewVariable("w"), -1, offset) { 114 | cpt++ 115 | } 116 | 117 | if cpt != nbDatas-offset { 118 | t.Error("expected ", nbDatas-offset, "results but instead found ", cpt, "results") 119 | } 120 | 121 | // test with a offset than doesn't allow enough results to reach the limit 122 | cpt = 0 123 | offset = nbDatas - 10 124 | for _ = range graph.FilterSubset(rdf.NewVariable("x"), rdf.NewVariable("v"), rdf.NewVariable("w"), limit, offset) { 125 | cpt++ 126 | } 127 | 128 | if cpt != nbDatas-offset { 129 | t.Error("expected ", nbDatas-offset, "results but instead found ", cpt, "results") 130 | } 131 | } 132 | 133 | func TestDeleteListGraph(t *testing.T) { 134 | var triple rdf.Triple 135 | graph := NewListGraph() 136 | nbDatas := 1000 137 | cpt := 0 138 | subj := rdf.NewURI("http://dblp.com#foo") 139 | 140 | // insert random triples in the graph 141 | for i := 0; i < nbDatas; i++ { 142 | triple = rdf.NewTriple(subj, rdf.NewURI(string(rand.Intn(nbDatas))), rdf.NewLiteral(string(rand.Intn(nbDatas)))) 143 | graph.Add(triple) 144 | } 145 | 146 | // remove the last triple pattern inserted 147 | graph.Delete(triple.Subject, triple.Predicate, triple.Object) 148 | for _ = range graph.Filter(triple.Subject, triple.Predicate, triple.Object) { 149 | cpt++ 150 | } 151 | 152 | if cpt > 0 { 153 | t.Error("the graph shouldn't contains the triple", triple) 154 | } 155 | 156 | // remove all triple with a given subject 157 | graph.Delete(subj, rdf.NewVariable("v"), rdf.NewVariable("w")) 158 | 159 | // select all triple of the graph 160 | cpt = 0 161 | for _ = range graph.Filter(subj, rdf.NewVariable("v"), rdf.NewVariable("w")) { 162 | cpt++ 163 | } 164 | 165 | if cpt > 0 { 166 | t.Error("the graph should be empty") 167 | } 168 | } 169 | 170 | func TestLoadFromFileListGraph(t *testing.T) { 171 | graph := NewListGraph() 172 | cpt := 0 173 | graph.LoadFromFile("../parser/datas/test.nt", "nt") 174 | 175 | // select all triple of the graph 176 | for _ = range graph.Filter(rdf.NewVariable("y"), rdf.NewVariable("v"), rdf.NewVariable("w")) { 177 | cpt++ 178 | } 179 | 180 | if cpt != 5 { 181 | t.Error("the graph should contains 5 triples, but it contains", cpt, "triples") 182 | } 183 | } 184 | 185 | // Benchmarking with WatDiv 1K 186 | 187 | func BenchmarkAddListGraph(b *testing.B) { 188 | b.Skip("skipped because it's currently not accurate") 189 | graph := NewListGraph() 190 | graph.LoadFromFile("./watdiv.30k.nt", "nt") 191 | triple := rdf.NewTriple(rdf.NewURI("http://example.org/subject"), rdf.NewURI("http://example.org/predicate"), rdf.NewURI("http://example.org/object")) 192 | b.ResetTimer() 193 | for i := 0; i < b.N; i++ { 194 | graph.Add(triple) 195 | } 196 | } 197 | 198 | func BenchmarkLoadFromFileListGraph(b *testing.B) { 199 | for i := 0; i < b.N; i++ { 200 | graph := NewListGraph() 201 | graph.LoadFromFile("./watdiv.30k.nt", "nt") 202 | } 203 | } 204 | 205 | func BenchmarkAllFilterListGraph(b *testing.B) { 206 | graph := NewListGraph() 207 | graph.LoadFromFile("./watdiv.30k.nt", "nt") 208 | cpt := 0 209 | b.ResetTimer() 210 | 211 | for i := 0; i < b.N; i++ { 212 | // select all triple of the graph 213 | for _ = range graph.Filter(rdf.NewVariable("v"), rdf.NewVariable("w"), rdf.NewVariable("z")) { 214 | cpt++ 215 | } 216 | } 217 | } 218 | 219 | func BenchmarkSpecificFilterListGraph(b *testing.B) { 220 | graph := NewListGraph() 221 | graph.LoadFromFile("./watdiv.30k.nt", "nt") 222 | subj := rdf.NewURI("http://db.uwaterloo.ca/~galuc/wsdbm/Offer10001") 223 | pred := rdf.NewURI("http://schema.org/eligibleRegion") 224 | obj := rdf.NewURI("http://db.uwaterloo.ca/~galuc/wsdbm/Country0") 225 | cpt := 0 226 | b.ResetTimer() 227 | 228 | for i := 0; i < b.N; i++ { 229 | // fetch the last inserted triple into the graph 230 | for _ = range graph.Filter(subj, pred, obj) { 231 | cpt++ 232 | } 233 | } 234 | } 235 | 236 | func BenchmarkAllFilterSubsetListGraph(b *testing.B) { 237 | graph := NewListGraph() 238 | graph.LoadFromFile("./watdiv.30k.nt", "nt") 239 | limit, offset := 600, 200 240 | cpt := 0 241 | b.ResetTimer() 242 | 243 | for i := 0; i < b.N; i++ { 244 | // select all triple of the graph 245 | for _ = range graph.FilterSubset(rdf.NewVariable("v"), rdf.NewVariable("w"), rdf.NewVariable("z"), limit, offset) { 246 | cpt++ 247 | } 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /graph/treeGraph.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Thomas Minier. All rights reserved. 2 | // Use of this source code is governed by a MIT License 3 | // license that can be found in the LICENSE file. 4 | 5 | package graph 6 | 7 | import ( 8 | "github.com/Callidon/joseki/rdf" 9 | "sync" 10 | ) 11 | 12 | // TreeGraph is a implementation of a RDF Graph based on the HDT-MR model proposed by Giménez-García et al. 13 | // 14 | // For more details, see http://dataweb.infor.uva.es/projects/hdt-mr/ 15 | type TreeGraph struct { 16 | dictionnary *bimap 17 | root *bitmapNode 18 | nextID int 19 | triples map[string][]rdf.Triple 20 | *sync.RWMutex 21 | *rdfReader 22 | } 23 | 24 | // NewTreeGraph creates a new empty Tree Graph. 25 | func NewTreeGraph() *TreeGraph { 26 | reader := newRDFReader() 27 | g := &TreeGraph{newBimap(), newBitmapNode(-1), 0, make(map[string][]rdf.Triple), &sync.RWMutex{}, reader} 28 | reader.graph = g 29 | return g 30 | } 31 | 32 | // Register a new Node in the graph dictionnary, then return its unique ID. 33 | func (g *TreeGraph) registerNode(node rdf.Node) int { 34 | // insert the node in dictionnary only if it's not in 35 | key, inDict := g.dictionnary.locate(node) 36 | if !inDict { 37 | g.dictionnary.push(g.nextID, node) 38 | g.nextID++ 39 | return g.nextID - 1 40 | } 41 | return key 42 | } 43 | 44 | // Recursively remove nodes that match criteria 45 | func (g *TreeGraph) removeNodes(root, previous *bitmapNode, datas []*rdf.Node) { 46 | if root != nil { 47 | node := (*datas[0]) 48 | _, isVar := node.(rdf.Variable) 49 | id, inDict := g.dictionnary.locate(node) 50 | // delegate operation to root's sons if it's a Variable or if the root match the current citeria 51 | if isVar || (inDict && root.id == id) { 52 | for _, son := range root.sons { 53 | g.removeNodes(son, root, datas[1:]) 54 | } 55 | // if root doesn't have any sons after the operation, delete it 56 | if len(root.sons) == 0 { 57 | delete(previous.sons, root.id) 58 | } 59 | } 60 | } 61 | } 62 | 63 | // sendTriples sends triples collected by another process, and respect the limit & offset of a query 64 | func sendTriple(input <-chan bitmapTriple, out chan<- rdf.Triple, dict *bimap, limit, offset int) { 65 | defer close(out) 66 | cpt := 0 67 | for bTriple := range input { 68 | // send the triple if the offset threshold has been reached but not the limit threashold 69 | if cpt >= offset && (cpt-offset <= limit || limit == -1) { 70 | triple, err := bTriple.Triple(dict) 71 | if err != nil { 72 | panic(err) 73 | } 74 | out <- triple 75 | } 76 | cpt++ 77 | } 78 | } 79 | 80 | // Recursively collect data from the graph in order to form triple pattern matching criterias. 81 | // The graph can be query with a Limit (the max number of rsults to send in the output channel) 82 | // and an Offset (the number of results to skip before sending them in the output channel). 83 | // These two parameters can be set to -1 to be ignored. 84 | func (g *TreeGraph) findNodes(root *bitmapNode, datas []*rdf.Node, triple []int, out chan<- bitmapTriple, wg *sync.WaitGroup) { 85 | defer wg.Done() 86 | // utilitary function to update WaitGroup counter when skipping sons 87 | skipSons := func(wg *sync.WaitGroup) { 88 | for _, son := range root.sons { 89 | son.updateCounter(wg) 90 | } 91 | } 92 | 93 | // skip the node if the limit have a default value or has been reached 94 | node := (*datas[0]) 95 | _, isVar := node.(rdf.Variable) 96 | id, inDict := g.dictionnary.locate(node) 97 | // when the root is a variable or the value we need, save it & delegate the operation to its sons 98 | if isVar || (inDict && root.id == id) { 99 | if len(root.sons) == 0 { 100 | out <- newBitmapTriple(triple[0], triple[1], root.id) 101 | //sendValue(append(triple, root.id), out, g.dictionnary, limit, offset) 102 | } else { 103 | for _, son := range root.sons { 104 | go g.findNodes(son, datas[1:], append(triple, root.id), out, wg) 105 | } 106 | } 107 | } else { 108 | // the node doesn't match our query, so there's no need to visit its sons 109 | skipSons(wg) 110 | } 111 | } 112 | 113 | // Add a new Triple pattern to the graph. 114 | func (g *TreeGraph) Add(triple rdf.Triple) { 115 | defer g.Unlock() 116 | // add each node of the triple to the dictionnary & then update the graph 117 | subjID, predID, objID := g.registerNode(triple.Subject), g.registerNode(triple.Predicate), g.registerNode(triple.Object) 118 | datas := []int{subjID, predID, objID} 119 | currentNode := g.root 120 | g.Lock() 121 | // insert each data in the graph 122 | for _, nodeID := range datas { 123 | node, inSons := currentNode.sons[nodeID] 124 | if inSons { 125 | // skip to next node if the current data is the same as the current node 126 | currentNode = node 127 | } else { 128 | // add the new node, then use it for the next data ton insert 129 | currentNode.sons[nodeID] = newBitmapNode(nodeID) 130 | currentNode = currentNode.sons[nodeID] 131 | } 132 | } 133 | } 134 | 135 | // Delete triples from the graph that match a BGP given in parameters. 136 | func (g *TreeGraph) Delete(subject, predicate, object rdf.Node) { 137 | g.Lock() 138 | defer g.Unlock() 139 | for _, son := range g.root.sons { 140 | g.removeNodes(son, g.root, []*rdf.Node{&subject, &predicate, &object}) 141 | } 142 | } 143 | 144 | // FilterSubset fetch triples form the graph that match a BGP given in parameters. 145 | // It impose a Limit(the max number of results to be send in the output channel) 146 | // and an Offset (the number of results to skip before sending them in the output channel) to the nodes requested. 147 | func (g *TreeGraph) FilterSubset(subject rdf.Node, predicate rdf.Node, object rdf.Node, limit int, offset int) <-chan rdf.Triple { 148 | var wg sync.WaitGroup 149 | bitmapResults := make(chan bitmapTriple, bufferSize) 150 | results := make(chan rdf.Triple, bufferSize) 151 | 152 | // fetch data in the tree & wait for the operation to be complete before closing the pipeline 153 | g.RLock() 154 | go sendTriple(bitmapResults, results, g.dictionnary, limit, offset) 155 | for _, son := range g.root.sons { 156 | wg.Add(son.length() + 1) 157 | go g.findNodes(son, []*rdf.Node{&subject, &predicate, &object}, make([]int, 0, 3), bitmapResults, &wg) 158 | } 159 | // use a daemon to wait for the end of all related goroutines before closing the channel 160 | go func() { 161 | defer close(bitmapResults) 162 | defer g.RUnlock() 163 | wg.Wait() 164 | }() 165 | return results 166 | } 167 | 168 | // Filter fetch triples form the graph that match a BGP given in parameters. 169 | func (g *TreeGraph) Filter(subject, predicate, object rdf.Node) <-chan rdf.Triple { 170 | return g.FilterSubset(subject, predicate, object, -1, 0) 171 | } 172 | -------------------------------------------------------------------------------- /graph/treeGraph_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Thomas Minier. All rights reserved. 2 | // Use of this source code is governed by a MIT License 3 | // license that can be found in the LICENSE file. 4 | 5 | package graph 6 | 7 | import ( 8 | "github.com/Callidon/joseki/rdf" 9 | "math/rand" 10 | "os" 11 | "testing" 12 | ) 13 | 14 | // skip a test if a file does'nt exist 15 | func skipTest(file string, t *testing.T) { 16 | if _, err := os.Stat(file); os.IsNotExist(err) { 17 | t.Skip(file, "doesn't exist, skipping test") 18 | } 19 | } 20 | 21 | func TestAddTreeGraph(t *testing.T) { 22 | var node rdf.Node 23 | graph := NewTreeGraph() 24 | 25 | subj := rdf.NewURI("http://dbpl.org#Thomas") 26 | predA := rdf.NewURI("http://foaf.com/age") 27 | predB := rdf.NewURI("http://Schema.org#livesIn") 28 | objA := rdf.NewLiteral("22") 29 | objB := rdf.NewLiteral("Nantes") 30 | tripleA := rdf.NewTriple(subj, predA, objA) 31 | tripleB := rdf.NewTriple(subj, predB, objB) 32 | graph.Add(tripleA) 33 | graph.Add(tripleB) 34 | 35 | // check for the structure of the tree (repartition of nodes & number of levels) 36 | if len(graph.root.sons) != 1 { 37 | t.Error("doesn't found exactly one subject after inserting two triples with the same subject") 38 | } 39 | if len(graph.root.sons[0].sons) != 2 { 40 | t.Error("doesn't found exactly two predicates after inserting two triples with different predicates") 41 | } 42 | if len(graph.root.sons[0].sons[1].sons) != 1 { 43 | t.Error("doesn't found exactly one subject") 44 | } 45 | if len(graph.root.sons[0].sons[1].sons[2].sons) > 0 { 46 | t.Error("the tree has 4 levels instead of only three (excluding the root level)") 47 | } 48 | 49 | // check for the values in the nodes 50 | node, _ = graph.dictionnary.extract(graph.root.sons[0].id) 51 | if test, _ := node.Equals(subj); !test { 52 | t.Error("expected to be the only subject node but found", node) 53 | } 54 | node, _ = graph.dictionnary.extract(graph.root.sons[0].sons[1].id) 55 | if test, _ := node.Equals(predA); !test { 56 | t.Error("expected to be the first predicate of but found", node) 57 | } 58 | node, _ = graph.dictionnary.extract(graph.root.sons[0].sons[3].id) 59 | if test, _ := node.Equals(predB); !test { 60 | t.Error("expected to be the second predicate of but found", node) 61 | } 62 | node, _ = graph.dictionnary.extract(graph.root.sons[0].sons[1].sons[2].id) 63 | if test, _ := node.Equals(objA); !test { 64 | t.Error("expected \"20\" to be the object of but found", node) 65 | } 66 | node, _ = graph.dictionnary.extract(graph.root.sons[0].sons[3].sons[4].id) 67 | if test, _ := node.Equals(objB); !test { 68 | t.Error("expected \"Nantes\" to be the object of but found", node) 69 | } 70 | } 71 | 72 | func TestFilterTreeGraph(t *testing.T) { 73 | skipTest("./watdiv.30k.nt", t) 74 | graph := NewTreeGraph() 75 | graph.LoadFromFile("./watdiv.30k.nt", "nt") 76 | subj := rdf.NewURI("http://db.uwaterloo.ca/~galuc/wsdbm/Offer10001") 77 | pred := rdf.NewURI("http://schema.org/eligibleRegion") 78 | obj := rdf.NewURI("http://db.uwaterloo.ca/~galuc/wsdbm/Country0") 79 | triple := rdf.NewTriple(subj, pred, obj) 80 | cpt := 0 81 | 82 | // select one triple specific triple pattern 83 | for result := range graph.Filter(subj, pred, obj) { 84 | if test, err := result.Equals(triple); !test || (err != nil) { 85 | t.Error("expected", triple, "but instead got", result) 86 | } 87 | cpt++ 88 | } 89 | 90 | if cpt != 1 { 91 | t.Error("expected 1 result but instead got", cpt, "results") 92 | } 93 | 94 | // select all triples 95 | cpt = 0 96 | for _ = range graph.Filter(rdf.NewVariable("v"), rdf.NewVariable("w"), rdf.NewVariable("z")) { 97 | cpt++ 98 | } 99 | if cpt != 30000 { 100 | t.Error("expected 30000 results but instead got", cpt, "results") 101 | } 102 | 103 | // select multiple triples with the same subject 104 | cpt = 0 105 | for _ = range graph.Filter(rdf.NewURI("http://db.uwaterloo.ca/~galuc/wsdbm/Offer1375"), rdf.NewVariable("v"), rdf.NewVariable("w")) { 106 | cpt++ 107 | } 108 | if cpt != 9 { 109 | t.Error("expected 9 results but instead got", cpt, "results") 110 | } 111 | 112 | // select multiple triples with the same predicate 113 | cpt = 0 114 | for _ = range graph.Filter(rdf.NewVariable("v"), rdf.NewURI("http://www.geonames.org/ontology#parentCountry"), rdf.NewVariable("w")) { 115 | cpt++ 116 | } 117 | if cpt != 240 { 118 | t.Error("expected 240 results but instead got", cpt, "results") 119 | } 120 | 121 | // select multiple triples with the same object 122 | cpt = 0 123 | for _ = range graph.Filter(rdf.NewVariable("v"), rdf.NewVariable("w"), rdf.NewLiteral("673")) { 124 | cpt++ 125 | } 126 | if cpt != 6 { 127 | t.Error("expected 6 results but instead got", cpt, "results") 128 | } 129 | 130 | // select a triple that doesn't exist in the graph 131 | cpt = 0 132 | for _ = range graph.Filter(rdf.NewURI("http://example.org"), rdf.NewVariable("v1"), rdf.NewVariable("v2")) { 133 | cpt++ 134 | } 135 | 136 | if cpt > 0 { 137 | t.Error("expected no result but instead found", cpt, "results") 138 | } 139 | } 140 | 141 | func TestFilterSubsetTreeGraph(t *testing.T) { 142 | skipTest("./watdiv.30k.nt", t) 143 | graph := NewTreeGraph() 144 | graph.LoadFromFile("./watdiv.30k.nt", "nt") 145 | nbDatas, limit, offset := 30000, 600, 800 146 | cpt := 0 147 | 148 | // test a FilterSubset with a simple Limit 149 | for _ = range graph.FilterSubset(rdf.NewVariable("x"), rdf.NewVariable("v"), rdf.NewVariable("w"), limit, -1) { 150 | cpt++ 151 | } 152 | 153 | if cpt != limit { 154 | t.Error("expected ", limit, "results but instead found ", cpt, "results") 155 | } 156 | 157 | // test a FilterSubset with a simple offset 158 | cpt = 0 159 | for _ = range graph.FilterSubset(rdf.NewVariable("x"), rdf.NewVariable("v"), rdf.NewVariable("w"), -1, offset) { 160 | cpt++ 161 | } 162 | 163 | if cpt != nbDatas-offset { 164 | t.Error("expected ", nbDatas-offset, "results but instead found ", cpt, "results") 165 | } 166 | 167 | // test with a offset than doesn't allow enough results to reach the limit 168 | cpt = 0 169 | offset = nbDatas - 10 170 | for _ = range graph.FilterSubset(rdf.NewVariable("x"), rdf.NewVariable("v"), rdf.NewVariable("w"), limit, offset) { 171 | cpt++ 172 | } 173 | 174 | if cpt != nbDatas-offset { 175 | t.Error("expected ", nbDatas-offset, "results but instead found ", cpt, "results") 176 | } 177 | } 178 | 179 | func TestDeleteTreeGraph(t *testing.T) { 180 | var triple rdf.Triple 181 | graph := NewTreeGraph() 182 | nbDatas := 1000 183 | cpt := 0 184 | subj := rdf.NewURI("http://dblp.org#foo") 185 | 186 | // insert random triples in the graph 187 | for i := 0; i < nbDatas; i++ { 188 | triple = rdf.NewTriple(subj, rdf.NewURI(string(rand.Intn(nbDatas))), rdf.NewLiteral(string(rand.Intn(nbDatas)))) 189 | graph.Add(triple) 190 | } 191 | 192 | // remove the last triple pattern inserted 193 | graph.Delete(triple.Subject, triple.Predicate, triple.Object) 194 | 195 | // check for the absence of the triple 196 | for _ = range graph.Filter(triple.Subject, triple.Predicate, triple.Object) { 197 | cpt++ 198 | } 199 | if cpt > 0 { 200 | t.Error("the graph shouldn't contains the triple", triple) 201 | } 202 | 203 | // check for the size of the graph 204 | cpt = 0 205 | for _ = range graph.Filter(rdf.NewVariable("v"), rdf.NewVariable("w"), rdf.NewVariable("y")) { 206 | cpt++ 207 | } 208 | if cpt != 999 { 209 | t.Error("the graph should contains 999 triples, but instead it contains", cpt, "triples") 210 | } 211 | 212 | // remove all triple with a given subject 213 | graph.Delete(subj, rdf.NewVariable("v"), rdf.NewVariable("w")) 214 | 215 | // select all triple of the graph 216 | cpt = 0 217 | for _ = range graph.Filter(subj, rdf.NewVariable("v"), rdf.NewVariable("w")) { 218 | cpt++ 219 | } 220 | 221 | if cpt > 0 { 222 | t.Error("Error : the graph should be empty") 223 | } 224 | } 225 | 226 | func TestLoadFromFileTreeGraph(t *testing.T) { 227 | graph := NewTreeGraph() 228 | cpt := 0 229 | graph.LoadFromFile("../parser/datas/test.nt", "nt") 230 | 231 | // select all triple of the graph 232 | for _ = range graph.Filter(rdf.NewVariable("y"), rdf.NewVariable("v"), rdf.NewVariable("w")) { 233 | cpt++ 234 | } 235 | 236 | if cpt != 4 { 237 | t.Error("the graph should contains 4 triples, but it contains", cpt, "triples") 238 | } 239 | } 240 | 241 | // Benchmarking 242 | 243 | func BenchmarkAddTreeGraph(b *testing.B) { 244 | b.Skip("skipped because it's currently not accurate") 245 | graph := NewTreeGraph() 246 | graph.LoadFromFile("./watdiv.30k.nt", "nt") 247 | triple := rdf.NewTriple(rdf.NewURI("http://example.org/subject"), rdf.NewURI("http://example.org/predicate"), rdf.NewURI("http://example.org/object")) 248 | b.ResetTimer() 249 | for i := 0; i < b.N; i++ { 250 | graph.Add(triple) 251 | } 252 | } 253 | 254 | func BenchmarkLoadFromFileTreeGraph(b *testing.B) { 255 | for i := 0; i < b.N; i++ { 256 | graph := NewTreeGraph() 257 | graph.LoadFromFile("./watdiv.30k.nt", "nt") 258 | } 259 | } 260 | 261 | func BenchmarkAllFilterTreeGraph(b *testing.B) { 262 | graph := NewTreeGraph() 263 | graph.LoadFromFile("./watdiv.30k.nt", "nt") 264 | cpt := 0 265 | b.ResetTimer() 266 | 267 | for i := 0; i < b.N; i++ { 268 | // select all triple of the graph 269 | for _ = range graph.Filter(rdf.NewVariable("v"), rdf.NewVariable("w"), rdf.NewVariable("z")) { 270 | cpt++ 271 | } 272 | } 273 | } 274 | 275 | func BenchmarkSpecificFilterTreeGraph(b *testing.B) { 276 | graph := NewTreeGraph() 277 | graph.LoadFromFile("./watdiv.30k.nt", "nt") 278 | subj := rdf.NewURI("http://db.uwaterloo.ca/~galuc/wsdbm/Offer10001") 279 | pred := rdf.NewURI("http://schema.org/eligibleRegion") 280 | obj := rdf.NewURI("http://db.uwaterloo.ca/~galuc/wsdbm/Country0") 281 | cpt := 0 282 | b.ResetTimer() 283 | 284 | for i := 0; i < b.N; i++ { 285 | // fetch the last inserted triple into the graph 286 | for _ = range graph.Filter(subj, pred, obj) { 287 | cpt++ 288 | } 289 | } 290 | } 291 | 292 | func BenchmarkAllFilterSubsetTreeGraph(b *testing.B) { 293 | graph := NewTreeGraph() 294 | graph.LoadFromFile("./watdiv.30k.nt", "nt") 295 | limit, offset := 600, 200 296 | cpt := 0 297 | b.ResetTimer() 298 | 299 | for i := 0; i < b.N; i++ { 300 | // select all triple of the graph 301 | for _ = range graph.FilterSubset(rdf.NewVariable("v"), rdf.NewVariable("w"), rdf.NewVariable("z"), limit, offset) { 302 | cpt++ 303 | } 304 | } 305 | } 306 | -------------------------------------------------------------------------------- /joseki.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Thomas Minier. All rights reserved. 2 | // Use of this source code is governed by a MIT License 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package joseki is a pure Go library for working with RDF, a powerful framework for representing informations as graphs. 6 | // For more informations about RDF itself, please see https://www.w3.org/TR/rdf11-concepts 7 | // 8 | // Features 9 | // 10 | // Joseki provides the following features to work with RDF : 11 | // 12 | // * Structures to represent and manipulate the RDF model (URIs, Literals, Blank Nodes, Triples, etc). 13 | // 14 | // * RDF Graphs to store data, with several implentations provided. 15 | // 16 | // * A Low level API to query data stored in graphs. 17 | // 18 | // * A High level API to query data using the SPARQL 1.1 query language. 19 | // 20 | // * Query processing using modern techniques such as join ordering or optimized query execution plans. 21 | // 22 | // * Load RDF data stored in files in various formats (N-Triples, Turtle, etc) into any graph. 23 | // 24 | // * Serialize a RDF Graph into various formats. 25 | // 26 | // Getting Started 27 | // 28 | // This package aims to work with RDF graphs, which are composed of RDF Triple {Subject Object Predicate}. 29 | // Using joseki, you can represent an RDF Triple as followed : 30 | // 31 | // import ( 32 | // "github.com/Callidon/joseki/rdf" 33 | // "fmt" 34 | // ) 35 | // subject := rdf.NewURI("http://example.org/book/book1") 36 | // predicate := rdf.NewURI("http://purl.org/dc/terms/title") 37 | // object := rdf.NewLiteral("Harry Potter and the Order of the Phoenix") 38 | // triple := rdf.NewTriple(subject, predicate, object) 39 | // fmt.Println(triple) 40 | // // Output : { "Harry Potter and the Order of the Phoenix"} 41 | // 42 | // You can also store your RDF Triples in a RDF Graph, using various type of graphs. 43 | // Here, we use a Tree Graph to store our triple : 44 | // 45 | // import ( 46 | // "github.com/Callidon/joseki/rdf" 47 | // "github.com/Callidon/joseki/graph" 48 | // ) 49 | // subject := rdf.NewURI("http://example.org/book/book1") 50 | // predicate := rdf.NewURI("http://purl.org/dc/terms/title") 51 | // object := rdf.NewLiteral("Harry Potter and the Order of the Phoenix") 52 | // graph := graph.NewTreeGraph() 53 | // graph.Add(rdf.NewTriple(subject, predicate, object)) 54 | // 55 | // You can also query any triple from a RDF Graph, using a low level API or a SPARQL query. 56 | // 57 | // import ( 58 | // "github.com/Callidon/joseki/rdf" 59 | // "github.com/Callidon/joseki/graph" 60 | // "fmt" 61 | // ) 62 | // graph := graph.NewTreeGraph() 63 | // // Datas stored in a file can be easily loaded into a graph 64 | // graph.LoadFromFile("datas/awesome-books.ttl", "turtle") 65 | // // Let's fetch the titles of all the books in our graph ! 66 | // subject := rdf.NewVariable("title") 67 | // predicate := rdf.NewURI("http://www.w3.org/1999/02/22-rdf-syntax-ns#type") 68 | // object := rdf.NewURI("https://schema.org/Book") 69 | // for bindings := range graph.Filter(subject, predicate, object) { 70 | // fmt.Println(bindings) 71 | // } 72 | // 73 | // For more informations about specific features, see the documentation of each subpackage. 74 | // 75 | // Author : Thomas Minier 76 | package joseki 77 | 78 | import "fmt" 79 | 80 | func ComingSoon() { 81 | fmt.Println("More to come ;)") 82 | } 83 | -------------------------------------------------------------------------------- /joseki_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Thomas Minier. All rights reserved. 2 | // Use of this source code is governed by a MIT License 3 | // license that can be found in the LICENSE file. 4 | 5 | package joseki 6 | 7 | import ( 8 | "fmt" 9 | "testing" 10 | ) 11 | 12 | func TestStuff(t *testing.T) { 13 | fmt.Println("Not implemented yet") 14 | } 15 | -------------------------------------------------------------------------------- /parser/NTParser.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Thomas Minier. All rights reserved. 2 | // Use of this source code is governed by a MIT License 3 | // license that can be found in the LICENSE file. 4 | 5 | package parser 6 | 7 | import ( 8 | "bufio" 9 | "github.com/Callidon/joseki/rdf" 10 | "io" 11 | "os" 12 | ) 13 | 14 | // NTParser is a parser for reading & loading triples in N-Triples format. 15 | // 16 | // N-Triples reference : https://www.w3.org/2011/rdf-wg/wiki/N-Triples-Format 17 | type NTParser struct { 18 | cutter *lineCutter 19 | } 20 | 21 | // scanNtriples read a file in N-Triples format, identify and extract token with their values. 22 | // 23 | // The results are sent through a channel, which is closed when the scan of the file has been completed. 24 | func scanNtriples(reader io.Reader, out chan<- rdfToken, l *lineCutter) { 25 | // walk through the file using a goroutine 26 | go func() { 27 | defer close(out) 28 | 29 | scanner := bufio.NewScanner(reader) 30 | lineNumber := 1 31 | for scanner.Scan() { 32 | line := l.extractSegments(scanner.Text()) 33 | rowNumber := 1 34 | // skip blank lines & comments 35 | if len(line) == 0 || line[0] == "#" { 36 | lineNumber++ 37 | continue 38 | } 39 | // scan elements of the line 40 | for _, elt := range line { 41 | // skip to next line when a comment is detect 42 | if string(elt[0]) == "#" { 43 | break 44 | } 45 | switch { 46 | case elt == ".": 47 | out <- newTokenEnd(lineNumber, rowNumber) 48 | case string(elt[0]) == "<" && string(elt[len(elt)-1]) == ">": 49 | out <- newTokenURI(elt[1 : len(elt)-1]) 50 | case string(elt[0]) == "_" && string(elt[1]) == ":": 51 | out <- newTokenBlankNode(elt[2:]) 52 | case string(elt[0]) == "\"" && string(elt[len(elt)-1]) == "\"", string(elt[0]) == "'" && string(elt[len(elt)-1]) == "'": 53 | out <- newTokenLiteral(elt[1 : len(elt)-1]) 54 | case len(elt) >= 2 && elt[0:2] == "^^": 55 | out <- newTokenType(elt[2:], lineNumber, rowNumber) 56 | case string(elt[0]) == "@": 57 | out <- newTokenLang(elt[1:], lineNumber, rowNumber) 58 | default: 59 | out <- newTokenIllegal("Unexpected token when scanning '"+elt+"'", lineNumber, rowNumber) 60 | } 61 | rowNumber += len(elt) + 1 62 | } 63 | lineNumber++ 64 | } 65 | }() 66 | } 67 | 68 | // NewNTParser creates a new NTParser 69 | func NewNTParser() *NTParser { 70 | return &NTParser{newLineCutter(wordRegexp)} 71 | } 72 | 73 | // Prefixes returns the prefixes read by the parser during the last parsing. 74 | // Since N-Triples format doesn't use prefixes, this function always return nil. 75 | func (p NTParser) Prefixes() map[string]string { 76 | return nil 77 | } 78 | 79 | // Read a file containg RDF triples in N-Triples format & convert them in triples. 80 | // 81 | // Triples generated are send through a channel, which is closed when the parsing of the file has been completed. 82 | func (p NTParser) Read(filename string) chan rdf.Triple { 83 | tokenPipe := make(chan rdfToken, bufferSize) 84 | out := make(chan rdf.Triple, bufferSize) 85 | stack := newStack() 86 | 87 | // scan the file & analyse the tokens using a goroutine 88 | go func() { 89 | defer close(out) 90 | f, err := os.Open(filename) 91 | check(err) 92 | defer f.Close() 93 | // launch the scan, then interpret each token produced 94 | go scanNtriples(bufio.NewReader(f), tokenPipe, p.cutter) 95 | for token := range tokenPipe { 96 | err = token.Interpret(stack, nil, out) 97 | check(err) 98 | } 99 | }() 100 | return out 101 | } 102 | -------------------------------------------------------------------------------- /parser/NTParser_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Thomas Minier. All rights reserved. 2 | // Use of this source code is governed by a MIT License 3 | // license that can be found in the LICENSE file. 4 | 5 | package parser 6 | 7 | import ( 8 | "github.com/Callidon/joseki/rdf" 9 | "strings" 10 | "testing" 11 | ) 12 | 13 | func TestReadNTParser(t *testing.T) { 14 | parser := NewNTParser() 15 | cpt := 0 16 | datas := []rdf.Triple{ 17 | rdf.NewTriple(rdf.NewURI("http://www.w3.org/2001/sw/RDFCore/ntriples"), 18 | rdf.NewURI("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"), 19 | rdf.NewURI("http://xmlns.com/foaf/0.1/Document")), 20 | rdf.NewTriple(rdf.NewURI("http://www.w3.org/2001/sw/RDFCore/ntriples"), 21 | rdf.NewURI("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"), 22 | rdf.NewURI("http://xmlns.com/foaf/0.1/Document")), 23 | rdf.NewTriple(rdf.NewURI("http://www.w3.org/2001/sw/RDFCore/ntriples"), 24 | rdf.NewURI("http://purl.org/dc/terms/title"), 25 | rdf.NewLangLiteral("N-Triples", "en")), 26 | rdf.NewTriple(rdf.NewURI("http://www.w3.org/2001/sw/RDFCore/ntriples"), 27 | rdf.NewURI("http://purl.org/dc/terms/title"), 28 | rdf.NewTypedLiteral("My Typed Literal", "")), 29 | rdf.NewTriple(rdf.NewURI("http://www.w3.org/2001/sw/RDFCore/ntriples"), 30 | rdf.NewURI("http://xmlns.com/foaf/0.1/maker"), 31 | rdf.NewBlankNode("art")), 32 | } 33 | 34 | for elt := range parser.Read("datas/test.nt") { 35 | if test, err := elt.Equals(datas[cpt]); !test || (err != nil) { 36 | t.Error(datas[cpt], "should be equal to", elt) 37 | } 38 | cpt++ 39 | } 40 | 41 | if cpt != len(datas) { 42 | t.Error("read", cpt, "nodes of the file instead of", len(datas)) 43 | } 44 | } 45 | 46 | func TestIllegalTokenNTParser(t *testing.T) { 47 | input := "illegal_token" 48 | expectedMsg := "Unexpected token when scanning 'illegal_token' at line : 1 row : 1" 49 | out := make(chan rdfToken, bufferSize) 50 | scanNtriples(strings.NewReader(input), out, newLineCutter(wordRegexp)) 51 | 52 | token := <-out 53 | tokenErr := token.Interpret(nil, nil, nil).Error() 54 | if tokenErr != expectedMsg { 55 | t.Error("expected illegal token", expectedMsg, "but instead got", tokenErr) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /parser/TurtleParser.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Thomas Minier. All rights reserved. 2 | // Use of this source code is governed by a MIT License 3 | // license that can be found in the LICENSE file. 4 | 5 | package parser 6 | 7 | import ( 8 | "bufio" 9 | "github.com/Callidon/joseki/rdf" 10 | "io" 11 | "os" 12 | "strings" 13 | ) 14 | 15 | // TurtleParser is a parser for reading & loading triples in Turtle format. 16 | // 17 | // Turtle reference : https://www.w3.org/TR/turtle/ 18 | type TurtleParser struct { 19 | prefixes map[string]string 20 | cutter *lineCutter 21 | } 22 | 23 | // scanTurtle read a file in Turtle format, identify and extract token with their values. 24 | // 25 | // The results are sent through a channel, which is closed when the scan of the file has been completed. 26 | func scanTurtle(reader io.Reader, out chan<- rdfToken, l *lineCutter) { 27 | // walk through the file using a goroutine 28 | go func() { 29 | defer close(out) 30 | var prefixName, prefixValue string 31 | var scanPrefixesDone bool 32 | 33 | scanner := bufio.NewScanner(reader) 34 | lineNumber := 1 35 | for scanner.Scan() { 36 | line := l.extractSegments(scanner.Text()) 37 | rowNumber := 1 38 | // skip blank lines & comments 39 | if (len(line) == 0) || (line[0] == "#") { 40 | lineNumber++ 41 | continue 42 | } 43 | scanPrefixesDone = (line[0] != "@prefix") 44 | // scan elements of the line 45 | for _, elt := range line { 46 | // skip comments 47 | if string(elt[0]) == "#" { 48 | break 49 | } 50 | if !scanPrefixesDone { 51 | switch { 52 | case elt == "@prefix" || elt == ":": 53 | continue 54 | case elt == ".": 55 | out <- newTokenPrefix(prefixName, prefixValue) 56 | prefixName, prefixValue = "", "" 57 | case prefixName == "": 58 | if string(elt[len(elt)-1]) != ":" { 59 | out <- newTokenIllegal("Unexpected token : "+elt, lineNumber, rowNumber) 60 | return 61 | } 62 | prefixName = elt[0 : len(elt)-1] 63 | case prefixValue == "": 64 | if string(elt[0]) != "<" && string(elt[len(elt)-1]) != ">" { 65 | out <- newTokenIllegal("Unexpected token : "+elt, lineNumber, rowNumber) 66 | return 67 | } 68 | prefixValue = elt[1 : len(elt)-1] 69 | default: 70 | out <- newTokenIllegal("Unexpected token when scanning '"+elt+"', expected a prefix definition", lineNumber, rowNumber) 71 | } 72 | } else { 73 | switch { 74 | case elt == ".", elt == "]": 75 | out <- newTokenEnd(lineNumber, rowNumber) 76 | case elt == ";", elt == ",", elt == "[": 77 | out <- newTokenSep(elt, lineNumber, rowNumber) 78 | case string(elt[0]) == "<" && string(elt[len(elt)-1]) == ">": 79 | out <- newTokenURI(elt[1 : len(elt)-1]) 80 | case string(elt[0]) == "\"" && string(elt[len(elt)-1]) == "\"", string(elt[0]) == "'" && string(elt[len(elt)-1]) == "'": 81 | out <- newTokenLiteral(elt[1 : len(elt)-1]) 82 | case len(elt) >= 2 && elt[0:2] == "^^": 83 | out <- newTokenType(elt[2:], lineNumber, rowNumber) 84 | case string(elt[0]) == "@": 85 | out <- newTokenLang(elt[1:], lineNumber, rowNumber) 86 | case string(elt[0]) == "_" && string(elt[1]) == ":": 87 | out <- newTokenBlankNode(elt[2:]) 88 | case string(elt[0]) == "?": 89 | out <- newTokenBlankNode(elt[1:]) 90 | case strings.Index(elt, ":") > -1: 91 | out <- newTokenPrefixedURI(elt, lineNumber, rowNumber) 92 | default: 93 | out <- newTokenIllegal("Unexpected token when scanning '"+elt+"'", lineNumber, rowNumber) 94 | } 95 | } 96 | rowNumber += len(elt) + 1 97 | } 98 | lineNumber++ 99 | } 100 | }() 101 | } 102 | 103 | // NewTurtleParser creates a new TurtleParser 104 | func NewTurtleParser() *TurtleParser { 105 | return &TurtleParser{make(map[string]string), newLineCutter(wordRegexp)} 106 | } 107 | 108 | // Prefixes returns the prefixes read by the parser during the last parsing. 109 | func (p TurtleParser) Prefixes() map[string]string { 110 | return p.prefixes 111 | } 112 | 113 | // Read a file containg RDF triples in Turtle format & convert them in triples. 114 | // 115 | // Triples generated are send throught a channel, which is closed when the parsing of the file has been completed. 116 | func (p *TurtleParser) Read(filename string) chan rdf.Triple { 117 | tokenPipe := make(chan rdfToken, bufferSize) 118 | out := make(chan rdf.Triple, bufferSize) 119 | stack := newStack() 120 | 121 | // scan the file & analyse the tokens using a goroutine 122 | go func() { 123 | defer close(out) 124 | f, err := os.Open(filename) 125 | check(err) 126 | defer f.Close() 127 | // launch the scan, then interpret each token produced 128 | go scanTurtle(bufio.NewReader(f), tokenPipe, p.cutter) 129 | for token := range tokenPipe { 130 | err = token.Interpret(stack, &p.prefixes, out) 131 | check(err) 132 | } 133 | }() 134 | return out 135 | } 136 | -------------------------------------------------------------------------------- /parser/TurtleParser_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Thomas Minier. All rights reserved. 2 | // Use of this source code is governed by a MIT License 3 | // license that can be found in the LICENSE file. 4 | 5 | package parser 6 | 7 | import ( 8 | "github.com/Callidon/joseki/rdf" 9 | "strings" 10 | "testing" 11 | ) 12 | 13 | func TestReadTurtleParser(t *testing.T) { 14 | parser := NewTurtleParser() 15 | cpt := 0 16 | prefixes := [][]string{ 17 | []string{"sw", "http://www.w3.org/2001/sw/RDFCore/"}, 18 | []string{"foaf", "http://xmlns.com/foaf/0.1/"}, 19 | } 20 | datas := []rdf.Triple{ 21 | rdf.NewTriple(rdf.NewURI("http://www.w3.org/2001/sw/RDFCore/ntriples"), 22 | rdf.NewURI("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"), 23 | rdf.NewURI("http://xmlns.com/foaf/0.1/Document")), 24 | rdf.NewTriple(rdf.NewURI("http://www.w3.org/2001/sw/RDFCore/ntriples"), 25 | rdf.NewURI("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"), 26 | rdf.NewURI("http://xmlns.com/foaf/0.1/Document")), 27 | rdf.NewTriple(rdf.NewURI("http://www.w3.org/2001/sw/RDFCore/ntriples"), 28 | rdf.NewURI("http://purl.org/dc/terms/title"), 29 | rdf.NewLangLiteral("N-Triples", "en")), 30 | rdf.NewTriple(rdf.NewURI("http://www.w3.org/2001/sw/RDFCore/ntriples"), 31 | rdf.NewURI("http://purl.org/dc/terms/title"), 32 | rdf.NewTypedLiteral("Turtle", "")), 33 | rdf.NewTriple(rdf.NewURI("http://www.w3.org/2001/sw/RDFCore/ntriples"), 34 | rdf.NewURI("http://purl.org/dc/terms/title"), 35 | rdf.NewBlankNode("a")), 36 | rdf.NewTriple(rdf.NewURI("http://www.w3.org/2001/sw/RDFCore/ntriples"), 37 | rdf.NewURI("http://xmlns.com/foaf/0.1/maker"), 38 | rdf.NewVariable("v0")), 39 | rdf.NewTriple(rdf.NewVariable("v0"), 40 | rdf.NewURI("http://purl.org/dc/terms/title"), 41 | rdf.NewLiteral("My Title")), 42 | } 43 | 44 | // check for triples 45 | for elt := range parser.Read("datas/test.ttl") { 46 | if test, err := elt.Equals(datas[cpt]); !test || (err != nil) { 47 | t.Error(elt, "should be equal to", datas[cpt]) 48 | } 49 | cpt++ 50 | } 51 | if cpt != len(datas) { 52 | t.Error("read", cpt, "nodes of the file instead of", len(datas)) 53 | } 54 | 55 | // check for prefixes 56 | parserPrefixes := parser.Prefixes() 57 | for _, prefix := range prefixes { 58 | value, inPrefixes := parserPrefixes[prefix[0]] 59 | if !inPrefixes { 60 | t.Error("the prefix", prefix[0], "hasn't been read by the parser") 61 | } 62 | if value != prefix[1] { 63 | t.Error("for key", prefix[0], "expected value", prefix[1], "but got", value) 64 | } 65 | } 66 | } 67 | 68 | func TestIllegalTokenTurtleParser(t *testing.T) { 69 | inputs := []string{ 70 | "@prefix incorrect_uri", 71 | "@prefix :", 72 | "@prefix : illegal_value", 73 | "illegal_token", 74 | } 75 | expectedMsg := []string{ 76 | "Unexpected token : incorrect_uri at line : 1 row : 1", 77 | "Unexpected token : at line : 1 row : 1", 78 | "Unexpected token : at line : 1 row : 1", 79 | "Unexpected token when scanning 'illegal_token' at line : 1 row : 1", 80 | } 81 | cpt := 0 82 | 83 | for _, input := range inputs { 84 | out := make(chan rdfToken, bufferSize) 85 | scanTurtle(strings.NewReader(input), out, newLineCutter(wordRegexp)) 86 | token := <-out 87 | tokenErr := token.Interpret(nil, nil, nil).Error() 88 | if tokenErr != expectedMsg[cpt] { 89 | t.Error("expected illegal token", expectedMsg[cpt], "but instead got", tokenErr) 90 | } 91 | cpt++ 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /parser/baseTokens.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Thomas Minier. All rights reserved. 2 | // Use of this source code is governed by a MIT License 3 | // license that can be found in the LICENSE file. 4 | 5 | package parser 6 | 7 | import ( 8 | "errors" 9 | "github.com/Callidon/joseki/rdf" 10 | ) 11 | 12 | // tokenURI represent a RDF URI 13 | type tokenURI struct { 14 | value string 15 | } 16 | 17 | // newTokenURI creates a new tokenURI 18 | func newTokenURI(value string) *tokenURI { 19 | return &tokenURI{value} 20 | } 21 | 22 | // Interpret evaluate the token & produce an action. 23 | // In the case of a tokenURI, it push a URI on top of the stack 24 | func (t tokenURI) Interpret(nodeStack *stack, prefixes *map[string]string, out chan rdf.Triple) error { 25 | nodeStack.Push(rdf.NewURI(t.value)) 26 | return nil 27 | } 28 | 29 | // tokenLiteral represent a RDF Literal 30 | type tokenLiteral struct { 31 | value string 32 | } 33 | 34 | // newTokenLiteral creates a new tokenLiteral 35 | func newTokenLiteral(value string) *tokenLiteral { 36 | return &tokenLiteral{value} 37 | } 38 | 39 | // Interpret evaluate the token & produce an action. 40 | // In the case of a tokenLiteral, it push a Literal on top of the stack 41 | func (t tokenLiteral) Interpret(nodeStack *stack, prefixes *map[string]string, out chan rdf.Triple) error { 42 | nodeStack.Push(rdf.NewLiteral(t.value)) 43 | return nil 44 | } 45 | 46 | // tokenType represent a type for a RDF Literal 47 | type tokenType struct { 48 | value string 49 | *tokenPosition 50 | } 51 | 52 | // newTokenType creates a new tokenType. 53 | // Since this token can produce an error, its position is needed for a better error handling 54 | func newTokenType(value string, line int, row int) *tokenType { 55 | return &tokenType{value, newTokenPosition(line, row)} 56 | } 57 | 58 | // Interpret evaluate the token & produce an action. 59 | // In the case of a tokenType, it push a typed Literal on top of the stack 60 | func (t tokenType) Interpret(nodeStack *stack, prefixes *map[string]string, out chan rdf.Triple) error { 61 | if nodeStack.Len() < 1 { 62 | return errors.New("encountered a malformed literal at " + t.position()) 63 | } 64 | literal, isLiteral := nodeStack.Pop().(rdf.Literal) 65 | if !isLiteral { 66 | return errors.New("A XML type can only be associated with a RDF Literal, at " + t.position()) 67 | } 68 | nodeStack.Push(rdf.NewTypedLiteral(literal.Value, t.value)) 69 | return nil 70 | } 71 | 72 | // tokenLang represent a localization information about a RDF Literal 73 | type tokenLang struct { 74 | value string 75 | *tokenPosition 76 | } 77 | 78 | // newTokenLang creates a new tokenLang. 79 | // Since this token can produce an error, its position is needed for a better error handling 80 | func newTokenLang(value string, line int, row int) *tokenLang { 81 | return &tokenLang{value, newTokenPosition(line, row)} 82 | } 83 | 84 | // Interpret evaluate the token & produce an action. 85 | // In the case of a tokenLang, it push a Literal with its associated language on top of the stack 86 | func (t tokenLang) Interpret(nodeStack *stack, prefixes *map[string]string, out chan rdf.Triple) error { 87 | if nodeStack.Len() < 1 { 88 | return errors.New("encountered a malformed literal at " + t.position()) 89 | } 90 | literal, isLiteral := nodeStack.Pop().(rdf.Literal) 91 | if !isLiteral { 92 | return errors.New("A localization information can only be associated with a RDF Literal, at " + t.position()) 93 | } 94 | nodeStack.Push(rdf.NewLangLiteral(literal.Value, t.value)) 95 | return nil 96 | } 97 | 98 | // tokenBlankNode represent a RDF Blank Node 99 | type tokenBlankNode struct { 100 | value string 101 | } 102 | 103 | // newTokenBlankNode creates a new tokenBlankNode 104 | func newTokenBlankNode(value string) *tokenBlankNode { 105 | return &tokenBlankNode{value} 106 | } 107 | 108 | // Interpret evaluate the token & produce an action. 109 | // In the case of a tokenBlankNode, it push a Blank Node on top of the stack 110 | func (t tokenBlankNode) Interpret(nodeStack *stack, prefixes *map[string]string, out chan rdf.Triple) error { 111 | nodeStack.Push(rdf.NewBlankNode(t.value)) 112 | return nil 113 | } 114 | -------------------------------------------------------------------------------- /parser/baseTokens_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Thomas Minier. All rights reserved. 2 | // Use of this source code is governed by a MIT License 3 | // license that can be found in the LICENSE file. 4 | 5 | package parser 6 | 7 | import ( 8 | "github.com/Callidon/joseki/rdf" 9 | "testing" 10 | ) 11 | 12 | func TestInterpretTokenURI(t *testing.T) { 13 | token := newTokenURI("http://example.org/subject") 14 | stack := newStack() 15 | expectedNode := rdf.NewURI("http://example.org/subject") 16 | 17 | // Test for correct interpretation of the token 18 | if err := token.Interpret(stack, nil, nil); err != nil { 19 | t.Error("interpretation of a correct tokenURI shouldn't produce the error :", err) 20 | } 21 | node, _ := stack.Pop().(rdf.Node) 22 | if test, err := expectedNode.Equals(node); !test || err != nil { 23 | t.Error(expectedNode, "produced by tokenURI.Interpret should be equals to", node) 24 | } 25 | if stack.Len()+1 != 1 { 26 | t.Error("after an interpretation of the token, the stack's size should be exactly equals to 1'") 27 | } 28 | } 29 | 30 | func TestInterpretTokenLiteral(t *testing.T) { 31 | token := newTokenLiteral("Harry Potter") 32 | stack := newStack() 33 | expectedNode := rdf.NewLiteral("Harry Potter") 34 | 35 | // Test for correct interpretation of the token 36 | if err := token.Interpret(stack, nil, nil); err != nil { 37 | t.Error("interpretation of a correct tokenLiteral shouldn't produce the error :", err) 38 | } 39 | node, _ := stack.Pop().(rdf.Node) 40 | if test, err := expectedNode.Equals(node); !test || err != nil { 41 | t.Error(expectedNode, "produced by tokenLiteral.Interpret should be equals to", node) 42 | } 43 | if stack.Len()+1 != 1 { 44 | t.Error("after an interpretation of the token, the stack's size should be exactly equals to 1'") 45 | } 46 | } 47 | 48 | func TestInterpretTokenType(t *testing.T) { 49 | token := newTokenType("http://www.w3.org/2001/XMLSchema#string", 1, 1) 50 | stack := newStack() 51 | expectedNode := rdf.NewTypedLiteral("Harry Potter", "http://www.w3.org/2001/XMLSchema#string") 52 | 53 | // Test for correct interpretation of the token 54 | stack.Push(rdf.NewLiteral("Harry Potter")) 55 | if err := token.Interpret(stack, nil, nil); err != nil { 56 | t.Error("interpretation of a correct tokenType shouldn't produce the error :", err) 57 | } 58 | node, _ := stack.Pop().(rdf.Node) 59 | if test, err := expectedNode.Equals(node); !test || err != nil { 60 | t.Error(expectedNode, "produced by tokenType.Interpret should be equals to", node) 61 | } 62 | if stack.Len()+1 != 1 { 63 | t.Error("after an interpretation of the token, the stack's size should be exactly equals to 1'") 64 | } 65 | } 66 | 67 | func TestInterpretErrorsTokenType(t *testing.T) { 68 | token := newTokenType("http://www.w3.org/2001/XMLSchema#string", 1, 1) 69 | stack := newStack() 70 | 71 | // Test for incorrect interpretation of the token 72 | if err := token.Interpret(stack, nil, nil); err == nil { 73 | t.Error("interpretation of a tokenType when they aren't anough nodes in the stack should produce an error") 74 | } 75 | 76 | stack.Push(rdf.NewURI("http://example.org/subject")) 77 | if err := token.Interpret(stack, nil, nil); err == nil { 78 | t.Error("interpretation of a tokenType when the top of the stack is a non-Literal node should produce an error") 79 | } 80 | } 81 | 82 | func TestInterpretTokenLang(t *testing.T) { 83 | token := newTokenType("en", 1, 1) 84 | stack := newStack() 85 | expectedNode := rdf.NewTypedLiteral("Harry Potter", "en") 86 | 87 | // Test for correct interpretation of the token 88 | stack.Push(rdf.NewLiteral("Harry Potter")) 89 | if err := token.Interpret(stack, nil, nil); err != nil { 90 | t.Error("interpretation of a correct tokenLang shouldn't produce the error :", err) 91 | } 92 | node, _ := stack.Pop().(rdf.Node) 93 | if test, err := expectedNode.Equals(node); !test || err != nil { 94 | t.Error(expectedNode, "produced by tokenLang.Interpret should be equals to", node) 95 | } 96 | if stack.Len()+1 != 1 { 97 | t.Error("after an interpretation of the token, the stack's size should be exactly equals to 1'") 98 | } 99 | } 100 | 101 | func TestInterpretErrorsTokenLang(t *testing.T) { 102 | token := newTokenLang("en", 1, 1) 103 | stack := newStack() 104 | 105 | // Test for incorrect interpretation of the token 106 | if err := token.Interpret(stack, nil, nil); err == nil { 107 | t.Error("interpretation of a tokenLang when they aren't anough nodes in the stack should produce an error") 108 | } 109 | 110 | stack.Push(rdf.NewURI("http://example.org/subject")) 111 | if err := token.Interpret(stack, nil, nil); err == nil { 112 | t.Error("interpretation of a tokenLang when the top of the stack is a non-Literal node should produce an error") 113 | } 114 | } 115 | 116 | func TestInterpretTokenBlankNode(t *testing.T) { 117 | token := newTokenBlankNode("v") 118 | stack := newStack() 119 | expectedNode := rdf.NewBlankNode("v") 120 | 121 | // Test for correct interpretation of the token 122 | if err := token.Interpret(stack, nil, nil); err != nil { 123 | t.Error("interpretation of a correct tokenBlankNode shouldn't produce the error :", err) 124 | } 125 | node, _ := stack.Pop().(rdf.Node) 126 | if test, err := expectedNode.Equals(node); !test || err != nil { 127 | t.Error(expectedNode, "produced by tokenBlankNode.Interpret should be equals to", node) 128 | } 129 | if stack.Len()+1 != 1 { 130 | t.Error("after an interpretation of the token, the stack's size should be exactly equals to 1'") 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /parser/datas/test.nt: -------------------------------------------------------------------------------- 1 | . 2 | 3 | # a filthy comment 4 | 5 | # a sneaky comment 6 | 7 | . 8 | "N-Triples"@en . 9 | "My Typed Literal"^^ . 10 | _:art . 11 | -------------------------------------------------------------------------------- /parser/datas/test.ttl: -------------------------------------------------------------------------------- 1 | @prefix sw: . 2 | @prefix foaf: . # a sneaky comment 3 | @prefix rdf: . 4 | @prefix dc: . 5 | 6 | sw:ntriples rdf:type foaf:Document ; 7 | rdf:type foaf:Document . 8 | # a filthy comment 9 | sw:ntriples dc:title "N-Triples"@en , "Turtle"^^ , _:a . 10 | foaf:maker [ dc:title "My Title" ] # another sneaky comment 11 | -------------------------------------------------------------------------------- /parser/parser.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Thomas Minier. All rights reserved. 2 | // Use of this source code is governed by a MIT License 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package parser provides parser to work with several RDF formats (N-Triples, Turtles, JSON-LD, ...) 6 | package parser 7 | 8 | import ( 9 | "github.com/Callidon/joseki/rdf" 10 | "regexp" 11 | ) 12 | 13 | const ( 14 | // Max size for the buffer of this package 15 | bufferSize = 100 16 | // Regexp used to isolate triples and their elements 17 | wordRegexp = "'.*?'|\".*?\"|\\S+" 18 | ) 19 | 20 | // Parser represent a generic interface for parsing every RDF format. 21 | // 22 | // Package parser provides several implementations for this interface. 23 | type Parser interface { 24 | Read(filename string) chan rdf.Triple 25 | Prefixes() map[string]string 26 | } 27 | 28 | // lineCutter wraps up the regexp used isolate triples and their elements in the RDF standard 29 | // It's main purpose is to ensure that the regexp is compiled only once, since it's a high cost operation. 30 | type lineCutter struct { 31 | *regexp.Regexp 32 | } 33 | 34 | // newLineCutter creates a new lineCutter 35 | func newLineCutter(reg string) *lineCutter { 36 | return &lineCutter{regexp.MustCompile(reg)} 37 | } 38 | 39 | // extractSegments parse a string and split the segments into a slice. 40 | // A segment is a string quoted or separated from the other by whitespaces. 41 | func (l lineCutter) extractSegments(line string) []string { 42 | return l.FindAllString(line, -1) 43 | } 44 | 45 | // Utility function for checking errors 46 | func check(err error) { 47 | if err != nil { 48 | panic(err) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /parser/rdfToken.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Thomas Minier. All rights reserved. 2 | // Use of this source code is governed by a MIT License 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package parser provides utilities to work with RDF based languages 6 | package parser 7 | 8 | import ( 9 | "errors" 10 | "github.com/Callidon/joseki/rdf" 11 | "strconv" 12 | ) 13 | 14 | // rdfToken represent a token in a RDF based language 15 | // 16 | // It follows the Interpretor pattern (https://en.wikipedia.org/wiki/Interpreter_pattern) 17 | // and can be used to extract triple pattersn& prefiex when reading a file 18 | type rdfToken interface { 19 | // Interpret evaluate the token & produce an action 20 | Interpret(nodeStack *stack, prefixes *map[string]string, out chan rdf.Triple) error 21 | } 22 | 23 | // tokenPosition represent the position of a token 24 | type tokenPosition struct { 25 | lineNumber int 26 | rowNumber int 27 | } 28 | 29 | // newTokenPosition creates a new tokenPosition 30 | func newTokenPosition(line, row int) *tokenPosition { 31 | return &tokenPosition{line, row} 32 | } 33 | 34 | // position returns a string representation of the token's position 35 | func (t tokenPosition) position() string { 36 | return "line : " + strconv.Itoa(t.lineNumber) + " row : " + strconv.Itoa(t.rowNumber) 37 | } 38 | 39 | // tokenIllegal is an illegal token in the RDF syntax 40 | type tokenIllegal struct { 41 | errMsg string 42 | *tokenPosition 43 | } 44 | 45 | // newTokenIllegal crates a new tokenIllegal 46 | func newTokenIllegal(err string, line int, row int) *tokenIllegal { 47 | return &tokenIllegal{err, newTokenPosition(line, row)} 48 | } 49 | 50 | // Interpret evaluate the token & produce an action. In the case of a tokenIllegal, it causes a panic. 51 | func (t tokenIllegal) Interpret(nodeStack *stack, prefixes *map[string]string, out chan rdf.Triple) error { 52 | return errors.New(t.errMsg + " at " + t.position()) 53 | } 54 | -------------------------------------------------------------------------------- /parser/separatorTokens.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Thomas Minier. All rights reserved. 2 | // Use of this source code is governed by a MIT License 3 | // license that can be found in the LICENSE file. 4 | 5 | package parser 6 | 7 | import ( 8 | "errors" 9 | "github.com/Callidon/joseki/rdf" 10 | "math/rand" 11 | "strconv" 12 | ) 13 | 14 | // tokenEnd represent a RDF URI 15 | type tokenEnd struct { 16 | *tokenPosition 17 | } 18 | 19 | // newTokenEnd creates a new tokenEnd 20 | func newTokenEnd(line, row int) *tokenEnd { 21 | return &tokenEnd{newTokenPosition(line, row)} 22 | } 23 | 24 | // Interpret evaluate the token & produce an action. 25 | // In the case of a tokenEnd, it form a new triple using the nodes in the stack 26 | func (t tokenEnd) Interpret(nodeStack *stack, prefixes *map[string]string, out chan rdf.Triple) error { 27 | if nodeStack.Len() < 3 { 28 | return errors.New("encountered a malformed triple pattern at " + t.position()) 29 | } 30 | object, objIsNode := nodeStack.Pop().(rdf.Node) 31 | predicate, predIsNode := nodeStack.Pop().(rdf.Node) 32 | subject, subjIsNode := nodeStack.Pop().(rdf.Node) 33 | if !objIsNode || !predIsNode || !subjIsNode { 34 | return errors.New("expected a Node in stack but doesn't found it") 35 | } 36 | out <- rdf.NewTriple(subject, predicate, object) 37 | return nil 38 | } 39 | 40 | // tokenSep represent a Turtle separator 41 | type tokenSep struct { 42 | value string 43 | *tokenPosition 44 | } 45 | 46 | // newTokenSep creates a new tokenSep 47 | func newTokenSep(value string, line int, row int) *tokenSep { 48 | return &tokenSep{value, newTokenPosition(line, row)} 49 | } 50 | 51 | // Interpret evaluate the token & produce an action. 52 | // In the case of a tokenSep, it form a new triple based on the separator, using the nodes in the stack 53 | func (t tokenSep) Interpret(nodeStack *stack, prefixes *map[string]string, out chan rdf.Triple) error { 54 | // case of a object separator 55 | if t.value == "[" { 56 | if nodeStack.Len() < 2 { 57 | return errors.New("encountered a malformed triple pattern at " + t.position()) 58 | } 59 | predicate, predIsNode := nodeStack.Pop().(rdf.Node) 60 | subject, subjIsNode := nodeStack.Pop().(rdf.Node) 61 | object := rdf.NewBlankNode("v" + strconv.Itoa(rand.Int())) 62 | if !predIsNode || !subjIsNode { 63 | return errors.New("expected a Node in stack but doesn't found it") 64 | } 65 | out <- rdf.NewTriple(subject, predicate, object) 66 | nodeStack.Push(object) 67 | } else { 68 | if nodeStack.Len() < 3 { 69 | return errors.New("encountered a malformed triple pattern at " + t.position()) 70 | } 71 | object, objIsNode := nodeStack.Pop().(rdf.Node) 72 | predicate, predIsNode := nodeStack.Pop().(rdf.Node) 73 | subject, subjIsNode := nodeStack.Pop().(rdf.Node) 74 | if !objIsNode || !predIsNode || !subjIsNode { 75 | return errors.New("expected a Node in stack but doesn't found it") 76 | } 77 | out <- rdf.NewTriple(subject, predicate, object) 78 | 79 | switch t.value { 80 | case ";": 81 | // push back the subject into the stack 82 | nodeStack.Push(subject) 83 | case ",": 84 | // push back the subject & the predicate into the stack 85 | nodeStack.Push(subject) 86 | nodeStack.Push(predicate) 87 | default: 88 | return errors.New("Unexpected separator token " + t.value + " - at " + t.position()) 89 | } 90 | } 91 | return nil 92 | } 93 | -------------------------------------------------------------------------------- /parser/separatorTokens_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Thomas Minier. All rights reserved. 2 | // Use of this source code is governed by a MIT License 3 | // license that can be found in the LICENSE file. 4 | 5 | package parser 6 | 7 | import ( 8 | "github.com/Callidon/joseki/rdf" 9 | "testing" 10 | ) 11 | 12 | func TestInterpretTokenEnd(t *testing.T) { 13 | token := newTokenEnd(1, 1) 14 | stack := newStack() 15 | out := make(chan rdf.Triple, 1) 16 | subject := rdf.NewURI("http://example.org/subject") 17 | predicate := rdf.NewURI("http://example.org/predicate") 18 | object := rdf.NewURI("http://example.org/object") 19 | expectedTriple := rdf.NewTriple(subject, predicate, object) 20 | 21 | // Test for correct interpretation of the token 22 | stack.Push(subject) 23 | stack.Push(predicate) 24 | stack.Push(object) 25 | 26 | if err := token.Interpret(stack, nil, out); err != nil { 27 | t.Error("interpretation of a correct TokenEnd shouldn't produce the error :", err) 28 | } 29 | triple := <-out 30 | if test, err := expectedTriple.Equals(triple); !test || err != nil { 31 | t.Error(triple, "produced by tokenEnd.Interpret should be equals to", expectedTriple) 32 | } 33 | if stack.Len() > 0 { 34 | t.Error("after an interpretation of the token, this stack should be empty") 35 | } 36 | } 37 | 38 | func TestInterpretErrorsTokenEnd(t *testing.T) { 39 | token := newTokenEnd(1, 1) 40 | stack := newStack() 41 | out := make(chan rdf.Triple, 1) 42 | 43 | // test with not enough nodes in the stack 44 | stack.Push(rdf.NewURI("http://example.org/subject")) 45 | stack.Push(rdf.NewURI("http://example.org/predicate")) 46 | 47 | if err := token.Interpret(stack, nil, out); err == nil { 48 | t.Error("interpretation of a TokenEnd with not enough tokens in the stack should produce an error") 49 | } 50 | 51 | // test with an incorrect element in the stack 52 | stack.Push("incorrect node") 53 | if err := token.Interpret(stack, nil, out); err == nil { 54 | t.Error("interpretation of a TokenEnd with an incorrect element in the stack should produce an error") 55 | } 56 | } 57 | 58 | func TestInterpretTokenSep(t *testing.T) { 59 | var top rdf.Node 60 | var triple rdf.Triple 61 | token := newTokenSep(";", 1, 1) 62 | stack := newStack() 63 | out := make(chan rdf.Triple, 1) 64 | subject := rdf.NewURI("http://example.org/subject") 65 | predicate := rdf.NewURI("http://example.org/predicate") 66 | object := rdf.NewURI("http://example.org/object") 67 | expectedTriple := rdf.NewTriple(subject, predicate, object) 68 | 69 | // Test for a "," separator 70 | stack.Push(subject) 71 | stack.Push(predicate) 72 | stack.Push(object) 73 | 74 | if err := token.Interpret(stack, nil, out); err != nil { 75 | t.Error("interpretation of a correct TokenEnd shouldn't produce the error :", err) 76 | } 77 | 78 | triple = <-out 79 | if test, err := expectedTriple.Equals(triple); !test || err != nil { 80 | t.Error(triple, "produced by tokenSep.Interpret should be equals to", expectedTriple) 81 | } 82 | 83 | top, _ = stack.Pop().(rdf.Node) 84 | if test, err := top.Equals(subject); !test || err != nil { 85 | t.Error("tokenSep.Interpret with the ';' separator value should have put back the subject on top of the stack") 86 | } 87 | if stack.Len()+1 != 1 { 88 | t.Error("after an interpretation of the token, the stack's size should be exactly equals to 1'") 89 | } 90 | 91 | // Test for a "," separator 92 | token.value = "," 93 | stack.Push(subject) 94 | stack.Push(predicate) 95 | stack.Push(object) 96 | 97 | if err := token.Interpret(stack, nil, out); err != nil { 98 | t.Error("interpretation of a correct TokenEnd shouldn't produce the error :", err) 99 | } 100 | 101 | triple = <-out 102 | if test, err := expectedTriple.Equals(triple); !test || err != nil { 103 | t.Error(triple, "produced by tokenSep.Interpret should be equals to", expectedTriple) 104 | } 105 | 106 | top, _ = stack.Pop().(rdf.Node) 107 | if test, err := top.Equals(predicate); !test || err != nil { 108 | t.Error("tokenSep.Interpret with the ',' separator value should have put back the predicate on top of the stack") 109 | } 110 | top, _ = stack.Pop().(rdf.Node) 111 | if test, err := top.Equals(subject); !test || err != nil { 112 | t.Error("tokenSep.Interpret with the ',' separator value should have put back the subject in the 2nd position of the stack") 113 | } 114 | 115 | if stack.Len()+2 != 2 { 116 | t.Error("after an interpretation of the token, the stack's size should be exactly equals to 2'") 117 | } 118 | } 119 | 120 | func TestInterpretErrorsTokenSep(t *testing.T) { 121 | token := newTokenSep("l", 1, 1) 122 | stack := newStack() 123 | out := make(chan rdf.Triple, 1) 124 | 125 | // test with incorrect separator value 126 | if err := token.Interpret(stack, nil, out); err == nil { 127 | t.Error("interpretation of a TokenSep with an incorrect separator value should produce an error") 128 | } 129 | 130 | // test with the separator "[" and not enough nodes in the stack 131 | token.value = "[" 132 | if err := token.Interpret(stack, nil, out); err == nil { 133 | t.Error("interpretation of a TokenSep with the separator '[' & not enough tokens in the stack should produce an error") 134 | } 135 | 136 | // test with the separator "[" and not enough nodes in the stack 137 | stack.Pop() 138 | stack.Push("incorrect node") 139 | if err := token.Interpret(stack, nil, out); err == nil { 140 | t.Error("interpretation of a TokenSep with the separator '[' & an incorrect element in the stack should produce an error") 141 | } 142 | 143 | // test with the separator ";" and not enough nodes in the stack 144 | token.value = "," 145 | stack.Push(rdf.NewURI("http://example.org/subject")) 146 | stack.Push(rdf.NewURI("http://example.org/predicate")) 147 | 148 | if err := token.Interpret(stack, nil, out); err == nil { 149 | t.Error("interpretation of a TokenSep not enough tokens in the stack should produce an error") 150 | } 151 | 152 | // test with the separator ";" and not enough nodes in the stack 153 | stack.Push("incorrect node") 154 | if err := token.Interpret(stack, nil, out); err == nil { 155 | t.Error("interpretation of a TokenSep with an incorrect element in the stack should produce an error") 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /parser/stack.go: -------------------------------------------------------------------------------- 1 | // Based on Douglas Hall stack implementation in Go : https://gist.github.com/bemasher/1777766 2 | // Modified by Thomas Minier to add synchronized access 3 | 4 | package parser 5 | 6 | import "sync" 7 | 8 | // stack is a synchronized LIFO structure implementated as a Linked-List 9 | type stack struct { 10 | top *stackElement 11 | size int 12 | *sync.Mutex 13 | } 14 | 15 | // stackElement is a element in a stack 16 | type stackElement struct { 17 | value interface{} // All types satisfy the empty interface, so we can store anything here. 18 | next *stackElement 19 | } 20 | 21 | // newStack creates a new stack 22 | func newStack() *stack { 23 | return &stack{nil, 0, &sync.Mutex{}} 24 | } 25 | 26 | // Return the stack's length 27 | func (s *stack) Len() int { 28 | s.Lock() 29 | defer s.Unlock() 30 | return s.size 31 | } 32 | 33 | // Push a new element onto the stack 34 | func (s *stack) Push(value interface{}) { 35 | s.Lock() 36 | s.top = &stackElement{value, s.top} 37 | s.size++ 38 | s.Unlock() 39 | } 40 | 41 | // Remove the top element from the stack and return it's value 42 | // If the stack is empty, return nil 43 | func (s *stack) Pop() (value interface{}) { 44 | s.Lock() 45 | defer s.Unlock() 46 | if s.size > 0 { 47 | value, s.top = s.top.value, s.top.next 48 | s.size-- 49 | return 50 | } 51 | return nil 52 | } 53 | -------------------------------------------------------------------------------- /parser/turtleTokens.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Thomas Minier. All rights reserved. 2 | // Use of this source code is governed by a MIT License 3 | // license that can be found in the LICENSE file. 4 | 5 | package parser 6 | 7 | import ( 8 | "errors" 9 | "github.com/Callidon/joseki/rdf" 10 | "strings" 11 | ) 12 | 13 | // tokenPrefixedURI represent a prefixed RDF URI 14 | type tokenPrefixedURI struct { 15 | value string 16 | *tokenPosition 17 | } 18 | 19 | // newTokenPrefixedURI creates a new tokenPrefixedURI 20 | func newTokenPrefixedURI(value string, line int, row int) *tokenPrefixedURI { 21 | return &tokenPrefixedURI{value, newTokenPosition(line, row)} 22 | } 23 | 24 | // Interpret evaluate the token & produce an action. 25 | // In the case of a tokenPrefixedURI, it push a URI to the stack 26 | func (t tokenPrefixedURI) Interpret(nodeStack *stack, prefixes *map[string]string, out chan rdf.Triple) error { 27 | sepIndex := strings.Index(t.value, ":") 28 | prefixValue := string(t.value[0:sepIndex]) 29 | prefixURI, inPrefixes := (*prefixes)[prefixValue] 30 | if !inPrefixes { 31 | return errors.New("unkown prefix " + prefixValue + " at " + t.position()) 32 | } 33 | nodeStack.Push(rdf.NewURI(prefixURI + t.value[sepIndex+1:])) 34 | return nil 35 | } 36 | 37 | // tokenPrefix represent a prefix 38 | type tokenPrefix struct { 39 | name string 40 | value string 41 | } 42 | 43 | // newTokenPrefix creates a new tokenPrefix 44 | func newTokenPrefix(name, value string) *tokenPrefix { 45 | return &tokenPrefix{name, value} 46 | } 47 | 48 | // Interpret evaluate the token & produce an action. 49 | // In the case of a tokenPrefix, it register a new prefix 50 | func (t tokenPrefix) Interpret(nodeStack *stack, prefixes *map[string]string, out chan rdf.Triple) error { 51 | (*prefixes)[t.name] = t.value 52 | return nil 53 | } 54 | -------------------------------------------------------------------------------- /parser/turtleTokens_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Thomas Minier. All rights reserved. 2 | // Use of this source code is governed by a MIT License 3 | // license that can be found in the LICENSE file. 4 | 5 | package parser 6 | 7 | import ( 8 | "github.com/Callidon/joseki/rdf" 9 | "testing" 10 | ) 11 | 12 | func TestInterpretTokenPrefixedURI(t *testing.T) { 13 | token := newTokenPrefixedURI("example:subject", 1, 1) 14 | stack := newStack() 15 | prefixes := make(map[string]string) 16 | prefixes["example"] = "http://example.org/" 17 | expectedNode := rdf.NewURI("http://example.org/subject") 18 | 19 | // Test for correct interpretation of the token 20 | if err := token.Interpret(stack, &prefixes, nil); err != nil { 21 | t.Error("interpretation of a correct tokenPrefixedURI shouldn't produce the error :", err) 22 | } 23 | node, _ := stack.Pop().(rdf.Node) 24 | if test, err := expectedNode.Equals(node); !test || err != nil { 25 | t.Error(expectedNode, "produced by tokenEnd.Interpret should be equals to", node) 26 | } 27 | if stack.Len()+1 != 1 { 28 | t.Error("after an interpretation of the token, the stack's size should be exactly equals to 1'") 29 | } 30 | } 31 | 32 | func TestInterpretErrorsTokenPrefixedURI(t *testing.T) { 33 | token := newTokenPrefixedURI("example:subject", 1, 1) 34 | stack := newStack() 35 | prefixes := make(map[string]string) 36 | 37 | // Test for incorrect interpretation of the token 38 | if err := token.Interpret(stack, &prefixes, nil); err == nil { 39 | t.Error("interpretation of a tokenPrefixedURI with an unknown prefix should produce an error") 40 | } 41 | 42 | if stack.Len() > 0 { 43 | t.Error("after an incorrect interpretation of the token, the stack should be empty'") 44 | } 45 | } 46 | 47 | func TestInterpretTokenPrefix(t *testing.T) { 48 | key, expectedValue := "example", "http://example.org/" 49 | token := newTokenPrefix(key, expectedValue) 50 | prefixes := make(map[string]string) 51 | 52 | // Test for correct interpretation of the token 53 | if err := token.Interpret(nil, &prefixes, nil); err != nil { 54 | t.Error("interpretation of a correct tokenPrefix shouldn't produce the error :", err) 55 | } 56 | if value, inPrefixes := prefixes[key]; !inPrefixes || value != expectedValue { 57 | t.Error("after tokenPrefix.Interpet, the prefix", key, "should exist and have", expectedValue, "as value") 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /rdf/binding.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Thomas Minier. All rights reserved. 2 | // Use of this source code is governed by a MIT License 3 | // license that can be found in the LICENSE file. 4 | 5 | package rdf 6 | 7 | // BindingsGroup represents a group of bindings, a pair (variable, RDF term), as described in SPARQL 1.1 language reference. 8 | // 9 | // SPARQL 1.1 reference : https://www.w3.org/TR/sparql11-query/ 10 | type BindingsGroup struct { 11 | Bindings map[string]Node 12 | } 13 | 14 | // NewBindingsGroup creates a new BindingsGroup. 15 | func NewBindingsGroup() BindingsGroup { 16 | return BindingsGroup{make(map[string]Node)} 17 | } 18 | 19 | // Equals is a function that compare two group of bindings and return True if they are equals, False otherwise. 20 | func (b BindingsGroup) Equals(other BindingsGroup) (bool, error) { 21 | for key, value := range b.Bindings { 22 | if _, inOther := other.Bindings[key]; !inOther { 23 | return false, nil 24 | } 25 | otherValue, _ := other.Bindings[key] 26 | test, err := value.Equals(otherValue) 27 | if !test || (err != nil) { 28 | return false, err 29 | } 30 | } 31 | return true, nil 32 | } 33 | 34 | // Clone creates a duplicate of the group of bindings 35 | func (b BindingsGroup) Clone() BindingsGroup { 36 | newGroup := NewBindingsGroup() 37 | for key, value := range b.Bindings { 38 | newGroup.Bindings[key] = value 39 | } 40 | return newGroup 41 | } 42 | -------------------------------------------------------------------------------- /rdf/nodes.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Thomas Minier. All rights reserved. 2 | // Use of this source code is governed by a MIT License 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package rdf provides primitives to work with RDF 6 | package rdf 7 | 8 | import "errors" 9 | 10 | // Node represents a generic node in a RDF Grapg 11 | // 12 | // RDF Graph reference : https://www.w3.org/TR/rdf11-concepts/#section-rdf-graph 13 | type Node interface { 14 | Equals(n Node) (bool, error) 15 | String() string 16 | } 17 | 18 | // URI represents a URI node in a RDF Graph 19 | // 20 | // RDF URI reference : https://www.w3.org/TR/rdf11-concepts/#section-IRIs 21 | type URI struct { 22 | Value string 23 | } 24 | 25 | // Literal represents a Literal node in a RDF Graph. 26 | // 27 | // RDF Literal reference : https://www.w3.org/TR/rdf11-concepts/#section-Graph-Literal 28 | type Literal struct { 29 | Value string 30 | Type string 31 | Lang string 32 | } 33 | 34 | // BlankNode represents a Blank Node in a RDF Graph. 35 | // 36 | // RDF Blank Node reference : https://www.w3.org/TR/rdf11-concepts/#section-blank-nodes 37 | type BlankNode struct { 38 | Value string 39 | } 40 | 41 | // Variable represents a SPARQL variable used when querying data in a RDF graph 42 | type Variable struct { 43 | Value string 44 | } 45 | 46 | // Equals is a function that compare a URI with another RDF Node and return True if they are equals, False otherwise. 47 | func (u URI) Equals(n Node) (bool, error) { 48 | other, ok := n.(URI) 49 | if ok { 50 | return u.Value == other.Value, nil 51 | } else if _, isVar := n.(Variable); isVar { 52 | return true, nil 53 | } 54 | return false, errors.New("Error : mismatch type, can only compare two URIs") 55 | } 56 | 57 | // Serialize a URI to string and return it. 58 | func (u URI) String() string { 59 | return "<" + u.Value + ">" 60 | } 61 | 62 | // NewURI creates a new URI. 63 | func NewURI(value string) URI { 64 | return URI{value} 65 | } 66 | 67 | // Equals is a function that compare a Literal with another RDF Node and return True if they are equals, False otherwise. 68 | func (l Literal) Equals(n Node) (bool, error) { 69 | other, ok := n.(Literal) 70 | if ok { 71 | return (l.Value == other.Value) && (l.Type == other.Type) && (l.Lang == other.Lang), nil 72 | } else if _, isVar := n.(Variable); isVar { 73 | return true, nil 74 | } 75 | return false, errors.New("Error : mismatch type, can only compare two Literals") 76 | } 77 | 78 | // Serialize a Literal to string and return it. 79 | func (l Literal) String() string { 80 | if l.Type != "" { 81 | return "\"" + l.Value + "\"^^<" + l.Type + ">" 82 | } else if l.Lang != "" { 83 | return "\"" + l.Value + "\"@" + l.Lang 84 | } 85 | return "\"" + l.Value + "\"" 86 | } 87 | 88 | // NewLiteral creates a new Literal. 89 | func NewLiteral(value string) Literal { 90 | return Literal{value, "", ""} 91 | } 92 | 93 | // NewTypedLiteral returns a new Literal with a type. 94 | func NewTypedLiteral(value, xmlType string) Literal { 95 | return Literal{value, xmlType, ""} 96 | } 97 | 98 | // NewLangLiteral returns a new Literal with a language. 99 | func NewLangLiteral(value, lang string) Literal { 100 | return Literal{value, "", lang} 101 | } 102 | 103 | // Equals is a function that compare a Blank Node with another RDF Node and return True if they are equals, False otherwise. 104 | func (b BlankNode) Equals(n Node) (bool, error) { 105 | other, ok := n.(BlankNode) 106 | if ok { 107 | return b.Value == other.Value, nil 108 | } else if _, isVar := n.(Variable); isVar { 109 | return true, nil 110 | } 111 | return false, errors.New("Error : mismatch type, can only compare two Blank Nodes") 112 | } 113 | 114 | // Serialize a Blank Node to string and return it. 115 | func (b BlankNode) String() string { 116 | return "_:" + b.Value 117 | } 118 | 119 | // NewBlankNode creates a new Blank Node. 120 | func NewBlankNode(variable string) BlankNode { 121 | return BlankNode{variable} 122 | } 123 | 124 | // Equals is a function that compare a Variable with another RDF Node and return True if they are equals, False otherwise. 125 | // Two variables are equals if they have the same value, and a variable is always equals to any other RDF node. 126 | func (v Variable) Equals(n Node) (bool, error) { 127 | other, ok := n.(Variable) 128 | if ok { 129 | return v.Value == other.Value, nil 130 | } 131 | return true, nil 132 | } 133 | 134 | // Serialize a Variable to string and return it. 135 | func (v Variable) String() string { 136 | return "?" + v.Value 137 | } 138 | 139 | // NewVariable creates a new Variable. 140 | func NewVariable(value string) Variable { 141 | return Variable{value} 142 | } 143 | -------------------------------------------------------------------------------- /rdf/nodes_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Thomas Minier. All rights reserved. 2 | // Use of this source code is governed by a MIT License 3 | // license that can be found in the LICENSE file. 4 | 5 | package rdf 6 | 7 | import "testing" 8 | 9 | // Test the Equals operator of the URI struct 10 | func TestURIEquals(t *testing.T) { 11 | uri := NewURI("http://dblp.org#foo") 12 | otherURI := NewURI("http://foaf.com/hasFriend") 13 | literal := NewLiteral("Toto") 14 | bnode := NewBlankNode("v") 15 | variable := NewVariable("x") 16 | 17 | if test, err := uri.Equals(uri); !test || (err != nil) { 18 | t.Error("a URI must be equals to itself") 19 | } 20 | 21 | if test, err := uri.Equals(otherURI); test && (err == nil) { 22 | t.Error(uri, "must be different of", otherURI) 23 | } 24 | 25 | if test, err := uri.Equals(literal); test && (err == nil) { 26 | t.Error("a URI and a Literal cannot be equal") 27 | } 28 | 29 | if test, err := uri.Equals(bnode); !test && (err == nil) { 30 | t.Error("a URI and a Blank Node cannot be equal") 31 | } 32 | 33 | if test, err := uri.Equals(variable); !test || (err != nil) { 34 | t.Error("a URI and a Variable are always equals") 35 | } 36 | } 37 | 38 | func TestURIString(t *testing.T) { 39 | uri := NewURI("http://dblp.org#foo") 40 | expected := "" 41 | 42 | if uri.String() != expected { 43 | t.Error(uri.String(), "should be equals to", expected) 44 | } 45 | } 46 | 47 | // Test the Equals operator of the Literal struct 48 | func TestLiteralEquals(t *testing.T) { 49 | uri := NewURI("http://dblp.org#foo") 50 | literal := NewLiteral("Toto") 51 | otherLiteral := NewLiteral("20") 52 | bnode := NewBlankNode("v") 53 | variable := NewVariable("x") 54 | 55 | if test, err := literal.Equals(literal); !test || (err != nil) { 56 | t.Error("a Literal must be equals to itself") 57 | } 58 | 59 | if test, err := literal.Equals(otherLiteral); test && (err == nil) { 60 | t.Error(literal, "must be different of", otherLiteral) 61 | } 62 | 63 | if test, err := literal.Equals(uri); test && (err == nil) { 64 | t.Error("a Literal and a URI cannot be equal") 65 | } 66 | 67 | if test, err := literal.Equals(bnode); test && (err == nil) { 68 | t.Error("a Literal and a Blank Node cannot be equal") 69 | } 70 | 71 | if test, err := literal.Equals(variable); !test || (err != nil) { 72 | t.Error("a Literal and a Variable are always equals") 73 | } 74 | } 75 | 76 | func TestLiteralString(t *testing.T) { 77 | literal := NewLiteral("22") 78 | typedLiteral := NewTypedLiteral("22", "http://www.w3.org/2001/XMLSchema#string") 79 | langLiteral := NewLangLiteral("World of Warcraft", "en") 80 | expectedLiteral := "\"22\"" 81 | expectedTypedLiteral := "\"22\"^^" 82 | expectedLangLiteral := "\"World of Warcraft\"@en" 83 | 84 | if literal.String() != expectedLiteral { 85 | t.Error(literal.String(), "should be equals to", expectedLiteral) 86 | } 87 | 88 | if typedLiteral.String() != expectedTypedLiteral { 89 | t.Error(typedLiteral.String(), "should be equals to", expectedTypedLiteral) 90 | } 91 | 92 | if langLiteral.String() != expectedLangLiteral { 93 | t.Error(langLiteral.String(), "should be equals to", expectedLangLiteral) 94 | } 95 | } 96 | 97 | // Test the Equals operator of the BlankNode struct 98 | func TestBlankNodeEquals(t *testing.T) { 99 | uri := NewURI("http://dblp.org#foo") 100 | literal := NewLiteral("Toto") 101 | bnode := NewBlankNode("v") 102 | otherBnode := NewBlankNode("w") 103 | variable := NewVariable("x") 104 | 105 | if test, err := bnode.Equals(bnode); !test || (err != nil) { 106 | t.Error("a Blank Node must be equals to itself") 107 | } 108 | 109 | if test, err := bnode.Equals(otherBnode); test && (err == nil) { 110 | t.Error(bnode, "must be different of", otherBnode) 111 | } 112 | 113 | if test, err := bnode.Equals(uri); test && (err == nil) { 114 | t.Error("a Blank Node and a URI cannot be equal") 115 | } 116 | 117 | if test, err := bnode.Equals(literal); test && (err == nil) { 118 | t.Error("a Blank Node and a Literal cannot be equal") 119 | } 120 | 121 | if test, err := bnode.Equals(variable); !test || (err != nil) { 122 | t.Error("a Blank Node and a Variable are always equals") 123 | } 124 | } 125 | 126 | func TestBlankNodeString(t *testing.T) { 127 | bnode := NewBlankNode("a") 128 | expected := "_:a" 129 | 130 | if bnode.String() != expected { 131 | t.Error(bnode.String(), "should be equals to", expected) 132 | } 133 | } 134 | 135 | // Test the Equals operator of the Variable struct 136 | func TestVariableEquals(t *testing.T) { 137 | uri := NewURI("http://dblp.org#foo") 138 | literal := NewLiteral("Toto") 139 | bnode := NewBlankNode("v") 140 | variable := NewVariable("x") 141 | otherVariable := NewVariable("w") 142 | 143 | if test, err := variable.Equals(variable); !test || (err != nil) { 144 | t.Error("a Variable must be equals to itself") 145 | } 146 | 147 | if test, err := variable.Equals(otherVariable); test && (err == nil) { 148 | t.Error(variable, "must be different of", otherVariable) 149 | } 150 | 151 | if test, err := variable.Equals(uri); !test || (err != nil) { 152 | t.Error("a Variable and a URI are always equals") 153 | } 154 | 155 | if test, err := variable.Equals(literal); !test || (err != nil) { 156 | t.Error("a Variable and a Literal are always equals") 157 | } 158 | 159 | if test, err := bnode.Equals(bnode); !test || (err != nil) { 160 | t.Error("a Variable and a Blank Node are always equals") 161 | } 162 | } 163 | 164 | func TestVariableString(t *testing.T) { 165 | variable := NewVariable("v") 166 | expected := "?v" 167 | 168 | if variable.String() != expected { 169 | t.Error(variable.String(), "should be equals to", expected) 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /rdf/triple.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Thomas Minier. All rights reserved. 2 | // Use of this source code is governed by a MIT License 3 | // license that can be found in the LICENSE file. 4 | 5 | package rdf 6 | 7 | // Triple represents a RDF Triple 8 | // 9 | // RDF Triple reference : https://www.w3.org/TR/rdf11-concepts/#section-triples 10 | type Triple struct { 11 | Subject Node 12 | Predicate Node 13 | Object Node 14 | } 15 | 16 | // NewTriple creates a new Triple. 17 | func NewTriple(subject, predicate, object Node) Triple { 18 | return (Triple{subject, predicate, object}) 19 | } 20 | 21 | // Equals is a function that compare two Triples and return True if they are equals, False otherwise. 22 | func (t Triple) Equals(other Triple) (bool, error) { 23 | testSubj, err := t.Subject.Equals(other.Subject) 24 | if err != nil { 25 | return false, err 26 | } 27 | testPred, err := t.Predicate.Equals(other.Predicate) 28 | if err != nil { 29 | return false, err 30 | } 31 | testObj, err := t.Object.Equals(other.Object) 32 | if err != nil { 33 | return false, err 34 | } 35 | return testSubj && testPred && testObj, nil 36 | } 37 | 38 | // Complete use a group of bindings to complete the variable in the triple pattern 39 | // and then return a new completed Triple pattern 40 | func (t Triple) Complete(group BindingsGroup) Triple { 41 | newSubj, newPred, newObj := t.Subject, t.Predicate, t.Object 42 | // find the nodes of the triple wich can be completed 43 | subject, freeSubject := t.Subject.(Variable) 44 | predicate, freePredicate := t.Predicate.(Variable) 45 | object, freeObject := t.Object.(Variable) 46 | if !freeSubject { 47 | newSubj = t.Subject 48 | } 49 | if !freePredicate { 50 | newPred = t.Predicate 51 | } 52 | if !freeObject { 53 | newObj = t.Object 54 | } 55 | 56 | for key, binding := range group.Bindings { 57 | // try to complete any node of the triple using the current binding 58 | if freeSubject && subject.Value == key { 59 | newSubj = binding 60 | } 61 | if freePredicate && predicate.Value == key { 62 | newPred = binding 63 | } 64 | if freeObject && object.Value == key { 65 | newObj = binding 66 | } 67 | } 68 | return NewTriple(newSubj, newPred, newObj) 69 | } 70 | -------------------------------------------------------------------------------- /rdf/triple_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Thomas Minier. All rights reserved. 2 | // Use of this source code is governed by a MIT License 3 | // license that can be found in the LICENSE file. 4 | 5 | package rdf 6 | 7 | import "testing" 8 | 9 | // Test the Equals operator for Triple struct 10 | func TestTripleEquals(t *testing.T) { 11 | tripleA := NewTriple(NewURI("foaf:foo"), NewURI("schema:bar"), NewLiteral("22")) 12 | tripleB := NewTriple(NewURI("schema:bar"), NewURI("foaf:foo"), NewLiteral("22")) 13 | tripleC := NewTriple(NewBlankNode("v"), NewURI("schema:bar"), NewLiteral("22")) 14 | 15 | if test, err := tripleA.Equals(tripleA); !test || (err != nil) { 16 | t.Error("a triple should be equals to itself") 17 | } 18 | if test, err := tripleA.Equals(tripleB); (err != nil) && test { 19 | t.Error(tripleA, "cannot be equals to", tripleB) 20 | } 21 | if _, err := tripleA.Equals(tripleC); err == nil { 22 | t.Error("cannot compare two triples with blank nodes in one of them") 23 | } 24 | } 25 | 26 | // Test the Equals operator for Triple struct 27 | func TestTripleComplete(t *testing.T) { 28 | var completed Triple 29 | datas := []Triple{ 30 | NewTriple(NewVariable("x"), NewURI("example.org#pred"), NewLiteral("22")), 31 | NewTriple(NewVariable("x"), NewVariable("y"), NewLiteral("22")), 32 | NewTriple(NewVariable("x"), NewVariable("y"), NewVariable("z")), 33 | NewTriple(NewURI("example.org#subj"), NewVariable("y"), NewLiteral("22")), 34 | NewTriple(NewURI("example.org#subj"), NewVariable("y"), NewVariable("z")), 35 | NewTriple(NewVariable("w"), NewURI("example.org#pred"), NewLiteral("22")), 36 | NewTriple(NewVariable("x"), NewVariable("w"), NewLiteral("22")), 37 | NewTriple(NewVariable("x"), NewVariable("y"), NewVariable("w")), 38 | NewTriple(NewURI("example.org#subj"), NewURI("example.org#pred"), NewLiteral("22")), 39 | } 40 | expected := []Triple{ 41 | NewTriple(NewURI("example.org#subj"), NewURI("example.org#pred"), NewLiteral("22")), 42 | NewTriple(NewURI("example.org#subj"), NewURI("example.org#pred"), NewLiteral("22")), 43 | NewTriple(NewURI("example.org#subj"), NewURI("example.org#pred"), NewLiteral("22")), 44 | NewTriple(NewURI("example.org#subj"), NewURI("example.org#pred"), NewLiteral("22")), 45 | NewTriple(NewURI("example.org#subj"), NewURI("example.org#pred"), NewLiteral("22")), 46 | NewTriple(NewVariable("w"), NewURI("example.org#pred"), NewLiteral("22")), 47 | NewTriple(NewURI("example.org#subj"), NewVariable("w"), NewLiteral("22")), 48 | NewTriple(NewURI("example.org#subj"), NewURI("example.org#pred"), NewVariable("w")), 49 | NewTriple(NewURI("example.org#subj"), NewURI("example.org#pred"), NewLiteral("22")), 50 | } 51 | cpt := 0 52 | 53 | group := NewBindingsGroup() 54 | group.Bindings["x"] = NewURI("example.org#subj") 55 | group.Bindings["y"] = NewURI("example.org#pred") 56 | group.Bindings["z"] = NewLiteral("22") 57 | 58 | for _, data := range datas { 59 | completed = data.Complete(group) 60 | switch { 61 | case completed.Subject == nil: 62 | t.Error("complete", data, "with", group, "shouldn't produce result with nil subject") 63 | case completed.Predicate == nil: 64 | t.Error("complete", data, "with", group, "shouldn't produce result with nil predicate") 65 | case completed.Object == nil: 66 | t.Error("complete", data, "with", group, "shouldn't produce result with nil object") 67 | } 68 | if test, err := expected[cpt].Equals(completed); !test || err != nil { 69 | t.Error(completed, "should be equal to", expected[cpt]) 70 | } 71 | cpt++ 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /travis_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | PACKAGES="graph parser rdf" 3 | for pkg in $PACKAGES; do 4 | go test -coverprofile=$pkg.cover.out -coverpkg=./... ./$pkg 5 | done 6 | echo "mode: set" > coverage.out && cat *.cover.out | grep -v mode: | sort -r | awk '{if($1 != last) {print $0;last=$1}}' >> coverage.out 7 | --------------------------------------------------------------------------------