How to validate instances of a class, based on their usage with shacl? - shacl

In my case there are SingleChoice (SC) and MultipleChoice (MC) questions. SC have a set of answers (as blank nodes) that must contain exectly one "points" and one "text" property. MC have a set of answers that must contain exectly one "points", one "text" and one "pointsNegative" property. Example as turtle:
prefix ex ...
ex:SC a ex:SingleChoice .
ex:hasAnswers [
a ex:Answer .
ex:text "Answer 1" .
ex:points 5 ;
],[ ...sameAsAbove ], ... ;
ex:MC a ex:MultipleChoice .
ex:hasAnswers [
a ex:Answer .
ex:text "Answer 1" .
ex:points 5 .
ex:pointsNegative 1 ;
],[ ...sameAsAbove ], ... ;
I managed to write shacl rules that validate all instances of class ex:Answer. But I can't make a difference to which these instances belong (SC or MC) by validating them with these rules:
ex:AnswerShape
a sh:NodeShape ;
sh:targetClass ex:Answer ;
sh:property [
a sh:PropertyShape ;
sh:path ex:Text ;
sh:minCount 1 ;
sh:maxCount 1 ;
sh:dataType xsd:string .
] .
E.g. if I add another PropertyShape for ex:pointsNegative, the shape will fail for all answers of a SC question (as these don't have ex:pointsNegative). I could omit the minCount restriction, but then answers for MC questions might have no ex:pointsNegative property.
How do I manage to have different rules executed for instances of Class ex:Answer, depending on there links (belong to SC or MC)? Is this even possible with Shacl?

Solution 1 - Create dedicated answer types
An easy solution would be to use different classes for the answers, e.g. ex:SingleChoiceAnswer and ex:MultipleChoiceAnswer. That way you can create dedicated shapes for each answer type.
# answers
prefix ex ...
ex:SC a ex:SingleChoice .
ex:hasAnswers [
a ex:SingleChoiceAnswer .
ex:text "Answer 1" .
ex:points 5 ;
],[ ...sameAsAbove ], ... ;
ex:MC a ex:MultipleChoice .
ex:hasAnswers [
a ex:MultipleChoiceAnswer .
ex:text "Answer 1" .
ex:points 5 .
ex:pointsNegative 1 ;
],[ ...sameAsAbove ], ... ;
# shapes
ex:AnswerShape
a sh:NodeShape ;
sh:targetClass ex:SingleChoiceAnswer ;
sh:property [
a sh:PropertyShape ;
...
] .
ex:AnswerShape
a sh:NodeShape ;
sh:targetClass ex:MultipleChoiceAnswer ;
sh:property [
a sh:PropertyShape ;
...
] .
Solution 2 - SHACL Property Paths
Another solution which works without changing the original schema is to use property paths. That way you could target the choice types and declare dedicated property shapes for them.
# shapes
ex:SingleChoiceShape
a sh:NodeShape ;
sh:targetClass ex:SingleChoice ;
sh:property [
a sh:PropertyShape ;
sh:path (ex:hasAnswers ex:text)
...
] .
ex:MultipleChoice
a sh:NodeShape ;
sh:targetClass ex:MultipleChoice ;
sh:property [
a sh:PropertyShape ;
sh:path (ex:hasAnswer ex:pointsNegative)
...
] .

Related

CSV Document not reading properly

Im trying to add variables to my turtles based on an Excel document. The excel document is just two columns with 19 random numbers. It is saved as a CSV. Im getting an error that says : Expected a literal value. (line number 19, character 2)
error while turtle 17 running FILE-READ
However, all values are numbers in the document. This is the code
extensions [csv]
globals [ turtle-data ]
turtles-own [ awareness income housingtype adopt ]
to setup
clear-all
reset-ticks
create-turtles 20
ask turtles [
set color white
]
ask turtles [
setxy random-xcor random-ycor
]
; load-turtle-data
; setup-turtles
load-data
end
to load-data
file-open "Ejemplocsv.csv"
while [not file-at-end?] [
ask turtles [
set income file-read
set housingtype file-read
set size 1.5]
ask turtles [setxy random-xcor random-ycor]]
file-close
show "file read"
end
I also tried instead a code that another user posted here but got the same error. This part replaces the load-data. It doesn't work either.
to load-turtle-data
ifelse ( file-exists? "Ejemplocsv.csv" ) [
set turtle-data []
file-open "Ejemplocsv.csv"
while [ not file-at-end? ][
set turtle-data sentence turtle-data (list (list file-read file-read))
]
user-message "File loading complete!"
file-close
]
[
user-message "There is no input-data.txt file in current directory!"
]
end
to setup-turtles
foreach turtle-data [ iter ->
crt 1 [
set income item 0 iter
set housingtype item 1 iter
]
]
end
I just want to know the most effective way to load csv data, or excel, to the variable of my turtles.
The problem is likely that your csv file is indeed comma delimited, not space delimited. file-read expects constants to be separated by white space and commas do not count as white space. If you saved your Excel file with spaces as the delimiter, or if you used a text editor to change the commas to spaces in your csv file, your code should work.
But, if you are going to load the csv extension, you might as well use it. The following code uses one command to read the whole csv file into turtle-data, as a list of lists, and setup-turtles then creates the turtles and uses turtle-data to populate their income and housingtype values. csv:from-file is perfectly happy with commas. (Note you don't need file-open or file-close with csv:from-file as it does the opening and closing for you.)
extensions [csv]
globals [ turtle-data ]
turtles-own [ awareness income housingtype adopt ]
to load-turtle-data
ifelse ( file-exists? "Ejemplocsv.csv" ) [
; file-open "Ejemplocsv.csv"
set turtle-data csv:from-file "Ejemplocsv.csv"
user-message "File loading complete!"
; file-close
]
[
user-message "There is no input-data.txt file in current directory!"
]
end
to setup-turtles
foreach turtle-data [ iter ->
crt 1 [
set income item 0 iter
set housingtype item 1 iter
]
]
end

How to create a valid SQL trigger with condition?

I’m stucked with creating SQL trigger, that updates publishingDate when Published field of current row became true. I’m tried multiple variants of declaration, but every time got a syntax error. Googling doesn’t gave the key for current case. Hope, you can help. My code is below. Validator I used: https://ru.rakko.tools/tools/36/
delimiter !
CREATE TRIGGER `vc`.`Articles_Updated_trigger`
BEFORE UPDATE
ON `Articles` FOR EACH ROW
BEGIN
IF new.Published = TRUE
SET new.PublishingdDate = CURRENT_TIMESTAMP
END IF;
END !
If you want to get all rows where the status changed, you need to compare OLD and NEW for that to detect the change
delimiter $$
CREATE TRIGGER `vc`.`Articles_Updated_trigger`
BEFORE UPDATE
ON `Articles` FOR EACH ROW
BEGIN
IF OLD.Published = FALSE AND NEW.Published = TRUE THEN
SET new.PublishingdDate = CURRENT_TIMESTAMP;
END IF;
END$$
DELIMITER ;
CREATE [ OR ALTER ] TRIGGER [ schema_name . ]trigger_name
ON { table | view }
[ WITH <dml_trigger_option> [ ,...n ] ]
{ FOR | AFTER | INSTEAD OF }
{ [ INSERT ] [ , ] [ UPDATE ] [ , ] [ DELETE ] }
[ WITH APPEND ]
[ NOT FOR REPLICATION ]
AS { sql_statement [ ; ] [ ,...n ] | EXTERNAL NAME <method specifier [ ; ] > }
<dml_trigger_option> ::=
[ ENCRYPTION ]
[ EXECUTE AS Clause ]
<method_specifier> ::=
assembly_name.class_name.method_name
Create your triggers using this syntax and try your next aspect.

group by and create a transformed json output

i have data which look like the following
and i need to transform them.
[{"us":{"$event":"5bbf4a4f43d8950b5b0cc6d2"},"org":"TΙ UIH","rc":{"$event":"13"}},
{"us":{"$event":"5bbf4a4f43d8950b5b0cc6d3"},"org":"TΙ UIH","rc":{"$event":"13"}},
{"us":{"$event":"5bbf4a4f43d8950b5b0cc6d4"},"org":"AB KIO","rc":{"$event":"13"}},
{"us":{"$event":"5bbf4a4f43d8950b5b0cc6d5"},"org":"GH SVS","rc":{"$event":"17"}}]
what i use:
`[group_by(.org, .rc."$event")[] | [.[0].rc."$event", .[0].org, length]] |` sort_by(.[0])[]
output i get:
[
"13",
"AB KIO",
1
]
[
"17",
"GH SVS",
1
]
[
"13",
"TΙ UIH",
2
]
How could i get the following format as output?
key1: ["",""]
key2: ["",""]
where each key us the number of the event, and in the array, we have the unique values of each event's org.
output
13: ["AB KIO","TΙ UIH"]
17: ["GH SVS"]
def aggregate_by(f; g):
reduce .[] as $x ({}; .[$x|f] += [$x|g]);
map( {rc: (.rc|.["$event"]), org} )
| aggregate_by(.rc; .org)
| map_values(unique)
With your sample input modified to make it a JSON array, the above jq program produces:
{"13":["AB KIO","TΙ UIH"],"17":["GH SVS"]}
Of course many variations are possible, but there is something to be said for postponing the uniquification.

jq - Map Object values using startswith()

I got two .json files, the first one contains the data:
data.json
[
{"ID_EXT_LARGE":"aaa_1234411","xy":"xyz"},
{"ID_EXT_LARGE":"bbb_1474411","xy":"cfg"},
{"ID_EXT_LARGE":"ccc_8944411","xy":"drt"},
{"ID_EXT_LARGE":"aaa_1234411","xy":"kai"}
]
The other one contains the IDs:
id_array.json
[
{"ID_EXT":"aaa","ID_WEB":30,"ID_ACC":"one"},
{"ID_EXT":"bbb","ID_WEB":40,"ID_ACC":"two"},
{"ID_EXT":"ccc","ID_WEB":50,"ID_ACC":"three"}
]
Now I try to get the "ID_WEB" and "ID_ACC" propertie into the objects of data.json, using the mapping of ID_EXT_LARGE and ID_EXT.
The problem is, that ID_EXT only contains the first characters of ID_EXT_LARGE.
Expected result - (should be the extended data.json file):
data.json
[
{"ID_EXT_LARGE":"aaa_1234411","ID_WEB":30,"ID_ACC":"one","xy":"xyz"},
{"ID_EXT_LARGE":"bbb_1474411","ID_WEB":40,"ID_ACC":"two","xy":"cfg"},
{"ID_EXT_LARGE":"ccc_8944411","ID_WEB":50,"ID_ACC":"three","xy":"drt"},
{"ID_EXT_LARGE":"aaa_1234411","ID_WEB":30,"ID_ACC":"one","xy":"kai"}
]
I tried it for ID_WEB and was thinking of something like this, (the for loop was just an idea):
script.jq
def getIDWEB(id_array);
for i ....
if ."ID_EXT_LARGE"|startswith(id_array[i].ID_EXT) then id_array[i].ID_WEB end
end
;
def setIDWEB(id_array):
.ID_WEB = getIDWEB(id_array)
;
($id_array) as $id_array
| map(setIDWEB($id_array))
Probably I am thinking too complicated and this is actually a one-liner?
Here is an approach which builds a "table" object from id_array.json. This function creates the table:
def maketable:
reduce $id_array[] as $i (
{}
; .[$i.ID_EXT] = ($i | {ID_WEB,ID_ACC})
)
;
With the sample id_array.json in $id_array this returns an object like
{
"aaa": {
"ID_WEB": 30,
"ID_ACC": "one"
},
"bbb": {
"ID_WEB": 40,
"ID_ACC": "two"
},
"ccc": {
"ID_WEB": 50,
"ID_ACC": "three"
}
}
This function takes an object from data.json and returns the corresponding lookup key for the table:
def getkey: .ID_EXT_LARGE | split("_")[0] ;
e.g. given
{"ID_EXT_LARGE":"aaa_1234411","xy":"xyz"}
it returns
"aaa"
With these two functions the result output can be generated with:
maketable as $idtable
| map( . + $idtable[ getkey ] )
Here is a script which puts everything together and uses sponge to update data.json:
#!/bin/bash
jq -M --argfile id_array id_array.json '
def maketable:
reduce $id_array[] as $i (
{}
; .[$i.ID_EXT] = ($i | {ID_WEB,ID_ACC})
)
;
def getkey: .ID_EXT_LARGE | split("_")[0] ;
maketable as $idtable
| map( . + $idtable[ getkey ] )
' data.json | sponge data.json
Here is data.json after a sample run:
[
{
"ID_EXT_LARGE": "aaa_1234411",
"xy": "xyz",
"ID_WEB": 30,
"ID_ACC": "one"
},
{
"ID_EXT_LARGE": "bbb_1474411",
"xy": "cfg",
"ID_WEB": 40,
"ID_ACC": "two"
},
{
"ID_EXT_LARGE": "ccc_8944411",
"xy": "drt",
"ID_WEB": 50,
"ID_ACC": "three"
},
{
"ID_EXT_LARGE": "aaa_1234411",
"xy": "kai",
"ID_WEB": 30,
"ID_ACC": "one"
}
]
Note that as peak points out maketable could be replaced with
def maketable: INDEX($id_array[]; .ID_EXT) | map_values(del(.ID_EXT)) ;
if the more general INDEX builtin (definition below) is available.
def INDEX(stream; idx_expr):
reduce stream as $row (
{}
; .[$row|idx_expr| if type != "string" then tojson else . end] |= $row
)
;

Fuseki on OpenShift: Can UPDATE but not SELECT

I have installed a Jena Fuseki server on OpenShift.
The --config services.ttl configuration file is as shown below.
What I observe is the following:
If I perform a SPARQL update from the Control Panel I get Update Succeeded and some TDB files do change on the server (in ./app-root/data/DB/).
However when I perform a SPARQL query such as SELECT ?s ?p ?o WHERE { ?s ?p ?o. } again in the Control Panel I get zero statements back. This same is true for this GET request:
http://<obfuscated>.rhcloud.com/ds/query?query=SELECT+%3Fs+%3Fp+%3Fo+WHERE+{+%3Fs+%3Fp+%3Fo.+}&output=text&stylesheet=
The log file on OpenShift contains these entries:
INFO [24] GET http://<obfuscated>.rhcloud.com/ds/query?query=SELECT+%3Fs+%3Fp+%3Fo+WHERE+{+%3Fs+%3Fp+%3Fo.+}+&output=text&stylesheet=
INFO [24] Query = SELECT ?s ?p ?o WHERE { ?s ?p ?o. }
INFO [24] exec/select
INFO [24] 200 OK (2 ms)
So it appears as if RDF statements can be written to TDB but not retrieved. If I try the same on a local installation of Fuseki the problem does not manifest.
What else can I do to diagnose and resolve this problem with Fuseki on OpenShift?
UPDATE Apparently the problem does not manifest if I INSERT statements into a named GRAPH (not the default graph).
#prefix : <#> .
#prefix fuseki: <http://jena.apache.org/fuseki#> .
#prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
#prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
#prefix tdb: <http://jena.hpl.hp.com/2008/tdb#> .
#prefix ja: <http://jena.hpl.hp.com/2005/11/Assembler#> .
[] rdf:type fuseki:Server ;
fuseki:services (
<#service>
) .
[] ja:loadClass "com.hp.hpl.jena.tdb.TDB" .
tdb:DatasetTDB rdfs:subClassOf ja:RDFDataset .
tdb:GraphTDB rdfs:subClassOf ja:Model .
<#service> a fuseki:Service ;
fuseki:name "ds" ;
fuseki:serviceQuery "sparql" ;
fuseki:serviceQuery "query" ;
fuseki:serviceUpdate "update" ;
fuseki:serviceUpload "upload" ;
fuseki:serviceReadWriteGraphStore "data" ;
fuseki:dataset <#dataset> ;
.
<#dataset> a tdb:DatasetTDB ;
tdb:location "../data/DB" ;
tdb:unionDefaultGraph true ;
.
tdb:unionDefaultGraph true turned out to be the culprit. From the documentation:
An assembler can specify that the default graph for query is the union
of the named graphs. This is done by adding tdb:unionDefaultGraph.
Since this does not mention the default graph as part of the union I guess with this configuration there is no default graph other than the union of the named graph and hence updates that do not name a graph are ignored.
The described problem disappears with the alternative configuration tdb:unionDefaultGraph false.