Commit 7c73e6a8 authored by R.W.Majeed's avatar R.W.Majeed
Browse files

Parse transformation rules from RDF/SKOS (incomplete)

parent 6ef3c96e
......@@ -4,6 +4,10 @@ Uniqueness of notation
According to SKOS, a single skos:Concept may contain 0, 1 or more notations.
Each notation (literal+datatype) uniquely identifies a Concept within a ConceptScheme.
TODO: write validator script, which checks that
1. Every concept with a notation has specified at least one ConceptScheme
2. Notations are unique (within a ConceptScheme)
Processing ontologies with Jena
-------------------------------
......
......@@ -7,25 +7,32 @@
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix i2b2: <http://sekmi.de/skos-i2b2#> .
@prefix dwh: <http://sekmi.de/histream/dwh#> .
@prefix snomed: <http://purl.bioontology.org/ontology/SNOMEDCT/>
@prefix snomed: <http://purl.bioontology.org/ontology/SNOMEDCT/> .
@prefix : <http://www.pulmonary-fibrosis.net/dzl/> .
:eurIPFreg a skos:ConceptScheme .
# mapFact [ dwh:condition | dwh:choose | dwh:value ]
# dwh:choose ( {[ {dwh:condition | dwh:value}+; dwh:target?; dwh:modify? ]+ } )
# if no dwh:target is specified, the fact is dropped
# dwh:mapFact implies skos:mappingRelation
:FF.2235.qspab_60 a dwh:Concept ;
skos:inScheme :eurIPFreg ;
skos:notation "FF.2235.qspab_60" ;
dwh:mapFact [
a dwh:MapRule
dwh:condition "./value='1'"^^dwh:XPath ;
dwh:target snomed:11399002 ; # pulmonary arterial hypertension
rdf:value rdf:nil
dwh:modify dwh:removeValue
] ;
dwh:mapFact [
dwh:condition dwh:otherwise ; # none of the previous conditions matches
dwh:target snomed:11399002 ; # pulmonary arterial hypertension
rdf:value rdf:nil
dwh:choose ([rdf:value "1"; dwh:target :t1] [rdf:value "1"; dwh:target :t2]) ;
dwh:otherwise [dwh:target snomed:11399002 ; dwh:modify dwh:removeValue]; # pulmonary arterial hypertension
dwh:modify dwh:removeValue
] ;
dwh:generateFact [
dwh:condition "true"^^<http://www.w3.org/TR/xpath-31/>
dwh:condition "true"^^<http://www.w3.org/TR/xpath-31/> ;
dwh:target snomed:11399002 ; # pulmonary arterial hypertension
dwh:value rdf:nil
# use string literal with xsl:if@test boolean-expression
......
# For the turtle syntax, see http://www.w3.org/TR/turtle/
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix dc: <http://purl.org/dc/elements/1.1/> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix i2b2: <http://sekmi.de/skos-i2b2#> .
@prefix dwh: <http://sekmi.de/histream/dwh#> .
@prefix snomed: <http://purl.bioontology.org/ontology/SNOMEDCT/> .
@prefix : <http://sekmi.de/histream/examples/mapping#> .
# Target schema
:target a skos:ConceptScheme .
# Target concepts
:t1 a dwh:Concept ;
skos:inScheme :target ;
skos:prefLabel "Target Concept 1"@en ;
skos:notation "T:t1" .
:t2 a dwh:Concept ;
skos:inScheme :target ;
skos:prefLabel "Target Concept 1"@en ;
skos:notation "T:t1" .
# Source schema
:source a skos:ConceptScheme .
# Source concepts
:TestMapConditionXP a dwh:Concept ;
skos:inScheme :source ;
skos:notation "source1" ;
dwh:mapFact [
a dwh:MapRule ; # optional
dwh:condition "./value='1'"^^dwh:XPath ;
dwh:target :t1 ;
dwh:modify dwh:removeValue
] .
:TestMapConditionJS a dwh:Concept ;
skos:inScheme :source ;
skos:notation "source2" ;
dwh:mapFact [
a dwh:MapRule ; # optional
dwh:condition "fact.value==1"^^dwh:Javascript ;
dwh:target :t1 ;
dwh:modify dwh:removeValue
] .
:MapChooseValue a dwh:Concept ;
skos:inScheme :source ;
skos:notation "map2" ;
dwh:mapFact [
dwh:choose ([rdf:value "1"; dwh:target :t1] [rdf:value "1"; dwh:target :t2]) ;
dwh:otherwise [dwh:target snomed:11399002 ; dwh:modify dwh:removeValue]; # pulmonary arterial hypertension
dwh:modify dwh:removeValue
] .
......@@ -8,10 +8,22 @@ public class HIStreamOntology {
public static final String DWH_NAMESPACE = "http://sekmi.de/histream/dwh#";
public static final URI DWH_RESTRICTION;
public static final URI DWH_MAPFACT;
public static final URI DWH_CONDITION;
public static final URI DWH_CHOOSE;
public static final URI DWH_TARGET;
public static final URI DWH_MODIFY;
public static final URI DWH_OTHERWISE;
static {
final ValueFactory f = ValueFactoryImpl.getInstance();
DWH_RESTRICTION = f.createURI(DWH_NAMESPACE, "restriction");
DWH_MAPFACT = f.createURI(DWH_NAMESPACE, "mapFact");
DWH_CONDITION = f.createURI(DWH_NAMESPACE, "condition");
DWH_CHOOSE = f.createURI(DWH_NAMESPACE, "choose");
DWH_TARGET = f.createURI(DWH_NAMESPACE, "target");
DWH_MODIFY = f.createURI(DWH_NAMESPACE, "modify");
DWH_OTHERWISE = f.createURI(DWH_NAMESPACE, "otherwise");
}
}
package de.sekmi.histream.ontology.skos;
import java.util.function.Consumer;
import de.sekmi.histream.Observation;
import de.sekmi.histream.ObservationFactory;
import de.sekmi.histream.io.Transformation;
public class RDFTransformation implements Transformation {
private ObservationFactory factory;
public RDFTransformation(ObservationFactory factory){
this.factory = factory;
}
@Override
public Observation transform(Observation fact,
Consumer<Observation> generatedReceiver) {
// TODO Auto-generated method stub
return null;
}
}
......@@ -23,6 +23,7 @@ public class RDFUtils {
rs.close();
}
}
public static void forEachRDFListItem(RepositoryConnection connection, Resource rdfList, Consumer<Value> consumer) throws RepositoryException{
do{
// get value
......
......@@ -77,7 +77,7 @@ public class RestrictionImpl implements ValueRestriction {
o = RDFUtils.getObject(rdf, resource, OWL.ONEOF);
if( o != null ){
final List<String> list = new ArrayList<>();
RDFUtils.forEachRDFListItem(rdf, (Resource)o, v -> {
RDFUtils.forEachRDFListItem(rdf,(Resource)o, v -> {
try {
Literal literal = RDFUtils.getLiteralObject(rdf, (Resource)v, RDF.VALUE);
// TODO: use correct types from literal
......
package de.sekmi.histream.ontology.skos;
import org.openrdf.model.Resource;
import de.sekmi.histream.ontology.OntologyException;
public class SKOSException extends OntologyException {
/**
*
*/
private static final long serialVersionUID = 1L;
private Resource node;
public SKOSException(Resource node, String message){
super(message);
this.node = node;
}
public SKOSException(Throwable cause) {
super(cause);
}
public String toString(){
return "SKOSException for Node "+node+": "+getMessage();
}
}
......@@ -4,13 +4,18 @@ import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import org.openrdf.model.Literal;
import org.openrdf.model.Resource;
import org.openrdf.model.Statement;
import org.openrdf.model.URI;
import org.openrdf.model.Value;
import org.openrdf.model.util.Literals;
import org.openrdf.model.vocabulary.RDF;
import org.openrdf.model.vocabulary.SKOS;
import org.openrdf.repository.Repository;
import org.openrdf.repository.RepositoryConnection;
......@@ -25,6 +30,8 @@ import de.sekmi.histream.Plugin;
import de.sekmi.histream.ontology.Concept;
import de.sekmi.histream.ontology.Ontology;
import de.sekmi.histream.ontology.OntologyException;
import de.sekmi.histream.ontology.skos.transform.Rule;
import de.sekmi.histream.ontology.skos.transform.TransformationRules;
public class Store implements Ontology, Plugin {
private Repository repo;
......@@ -153,8 +160,8 @@ public class Store implements Ontology, Plugin {
@Override
public Concept getConceptByNotation(String id) throws OntologyException {
public ConceptImpl getConceptByNotation(String id) throws OntologyException {
// TODO use getConceptByNotation(id, scheme)
try {
RepositoryResult<Statement> rs = rc.getStatements(null, SKOS.NOTATION, Literals.createLiteral(rc.getValueFactory(), id), true);
try{
......@@ -162,18 +169,50 @@ public class Store implements Ontology, Plugin {
Resource s = rs.next().getSubject();
if( rs.hasNext() ){
// multiple concepts with same id
// this should not happen
throw new OntologyException("Notation '"+id+"' with multiple concepts");
// TODO: check ConceptScheme
throw new SKOSException(s, "Notation '"+id+"' with multiple concepts");
}
return new ConceptImpl(this, s);
}finally{
rs.close();
}
}catch( RepositoryException e ){
throw new OntologyException(e);
throw new SKOSException(e);
}
}
/**
* Find a concept by notation and concept scheme.
* <p>
* If {@code null} is specified for the scheme, any concept with the specified notation
* is returned - regardless of scheme.
*
* @param notation notation to find
* @param conceptScheme scheme for the concept or {@code null}
* @return concept
* @throws RepositoryException for repository errors
*/
public ConceptImpl getConceptByNotation(String notation, URI conceptScheme) throws RepositoryException {
RepositoryResult<Statement> rs = rc.getStatements(null, SKOS.NOTATION, Literals.createLiteral(rc.getValueFactory(), notation), true);
Resource concept = null;
try{
while( rs.hasNext() ){
concept = rs.next().getSubject();
// check scheme
if( rc.hasStatement(concept, SKOS.IN_SCHEME, conceptScheme, false) ){
// found!
break;
}else{
// other scheme / no scheme
concept = null;
}
}
}finally{
rs.close();
}
return new ConceptImpl(this, concept);
}
String getLocalString(Resource subject, URI predicate, String language) throws OntologyException {
try {
return RDFUtils.getLocalString(rc, subject, predicate, language);
......@@ -198,5 +237,141 @@ public class Store implements Ontology, Plugin {
return lastModified;
}
protected void forEachStatement(Resource subject, URI predicate, Consumer<Statement> consumer) throws RepositoryException{
RepositoryResult<Statement> rs = rc.getStatements(subject, predicate, null, false);
try{
while( rs.hasNext() ){
consumer.accept(rs.next());
}
}finally{
rs.close();
}
}
protected void forEachObject(Resource subject, URI predicate, Consumer<Value> consumer) throws RepositoryException{
forEachStatement(subject, predicate, s -> consumer.accept(s.getObject()));
}
/**
* Retrieve transformation rules for the concept identified by the given notation (restricted to a schema)
* TODO move method to ConceptImpl
* @param notation notation for the concept
* @param schemaURI schema in which to search for the notation. can be null
* @return transformation rules
* @throws RepositoryException XXX
* @throws OntologyException
*/
public TransformationRules getConceptTransformations(String notation, String schemaURI) throws OntologyException{
ArrayList<Rule> rules = new ArrayList<Rule>();
try {
ConceptImpl c = getConceptByNotation(notation, rc.getValueFactory().createURI(schemaURI));
if( c == null )return null;
ArrayList<Value> list = new ArrayList<>();
forEachObject(c.getResource(), HIStreamOntology.DWH_MAPFACT, list::add);
for( Value v : list ){
if( !(v instanceof Resource) ){
// value should be a resource
throw new RepositoryException("Not a resource: ("+c.getResource()+", "+HIStreamOntology.DWH_MAPFACT+", [object] <--!! )");
}
Rule rule = loadMapFactRule((Resource)v, false);
rules.add(rule);
}
} catch (RepositoryException e) {
throw new OntologyException(e);
}
return new TransformationRules(rules.toArray(new Rule[rules.size()]));
}
public void forEachRDFListItem(Resource rdfList, Consumer<Value> consumer) throws RepositoryException{
do{
// get value
Value obj = RDFUtils.getObject(rc, rdfList, RDF.FIRST);
if( obj != null )consumer.accept(obj);
// get rest
obj = RDFUtils.getObject(rc, rdfList, RDF.REST);
if( obj == null ){
// illegal termination of list
// TODO throw exception
rdfList = null;
}else if( obj.equals(RDF.NIL) ){
// end of list reached
rdfList = null;
}else if( obj instanceof Resource ){
// remaining list
rdfList = (Resource)obj;
}else{
// illegal termination of list
// TODO throw exception
rdfList = null;
}
}while( rdfList != null );
}
private Rule loadMapFactRule(Resource r, boolean disableChoose) throws OntologyException, RepositoryException{
final Value[] v = new Value[6];
forEachStatement(r, null, s -> {
if( s.getPredicate().equals(HIStreamOntology.DWH_CONDITION) ){
v[0] = s.getObject();
}else if( s.getPredicate().equals(HIStreamOntology.DWH_CHOOSE) ){
v[1] = s.getObject();
}else if( s.getPredicate().equals(HIStreamOntology.DWH_TARGET) ){
v[2] = s.getObject();
}else if( s.getPredicate().equals(HIStreamOntology.DWH_MODIFY) ){
v[3] = s.getObject();
}else if( s.getPredicate().equals(RDF.VALUE) ){
v[4] = s.getObject();
}else if( s.getPredicate().equals(HIStreamOntology.DWH_OTHERWISE) ){
v[5] = s.getObject();
}//TODO: else if DWH_OTHERWISE
// TODO add exceptions to errors
});
if( v[0] != null ){
// condition specified
// need target
if( v[2] == null )throw new SKOSException(r, "dwh:mapFact with dwh:condition must also contain dwh:target");
// choose not allowed
if( v[1] != null )throw new SKOSException(r, "dwh:condition and dwh:choose cannot be combined");
// load condition
if( !(v[0] instanceof Literal) )throw new SKOSException(r, "dwh:condition needs literal object");
Literal condition = (Literal)v[0];
// TODO: load otherwise
return new Rule(condition, new ConceptImpl(this, (Resource)v[2]));
}else if( v[1] != null ){
// choose specified
if( !(v[1] instanceof Resource) )throw new SKOSException(r, "dwh:choose must be a resource");
// target not allowed
if( v[2] != null )throw new SKOSException(r, "dwh:choose and dwh:target cannot be combined");
// condition not allowed (v[0] implied)
final ArrayList<Rule> list = new ArrayList<>();
final List<Throwable> errors = new LinkedList<>();
RDFUtils.forEachRDFListItem(rc, (Resource)v[1], l -> {
// one level of recursion
try {
list.add(loadMapFactRule((Resource)l,true));
} catch (Exception e) {
errors.add(e);
}
});
// TODO load otherwise
if( errors.isEmpty() ){
return new Rule(list.toArray(new Rule[list.size()]), null);
}else{
final Throwable first = errors.get(0);
SKOSException error = new SKOSException(first);
for( Throwable e : errors ){
if( e != first )error.addSuppressed(e);
}
throw error;
}
}else if( v[4] != null ){
// rdf:value specified (match single value)
if( !(v[4] instanceof Literal) )throw new SKOSException(r, "rdf:value needs literal object");
if( v[2] == null )throw new SKOSException(r, "dwh:mapFact with rdf:value must also contain dwh:target");
Literal value = (Literal)v[4];
return new Rule(value, new ConceptImpl(this, (Resource)v[2]));
}else{
// unsupported mapping
throw new SKOSException(r, "dwh:mapFact must contain one of dwh:condition, dwh:choose, dwh:value");
}
}
}
package de.sekmi.histream.ontology.skos.transform;
import java.util.HashMap;
import java.util.function.Consumer;
import de.sekmi.histream.Observation;
import de.sekmi.histream.ObservationFactory;
import de.sekmi.histream.io.Transformation;
import de.sekmi.histream.io.TransformationException;
import de.sekmi.histream.ontology.OntologyException;
import de.sekmi.histream.ontology.skos.Store;
public class RDFTransformation implements Transformation {
private ObservationFactory factory;
private Store store;
private String schema;
private boolean dropFactsWithoutRules;
private HashMap<String, TransformationRules> cache;
public RDFTransformation(ObservationFactory factory, Store store, String schema, boolean dropFactsWithoutRules){
this.store = store;
this.factory = factory;
this.schema = schema;
this.dropFactsWithoutRules = dropFactsWithoutRules;
this.cache = new HashMap<>();
}
@Override
public Observation transform(Observation fact,
Consumer<Observation> generatedReceiver) throws TransformationException {
// get rules
String notation = fact.getConceptId();
TransformationRules rules;
if( cache.containsKey(notation) ){
rules = cache.get(notation);
}else{
// retrieve from store
try {
rules = store.getConceptTransformations(notation, schema);
} catch (OntologyException e) {
// put in cache to prevent repeating errors
cache.put(notation, null);
throw new TransformationException("Error retrieving transformation rules",e);
}
// put in cache
cache.put(notation, rules);
}
if( rules == null ){
return dropFactsWithoutRules?null:fact;
}
// TODO perform transformation
// TODO perform generations
if( factory != null ){}
return fact;
}
}
package de.sekmi.histream.ontology.skos.transform;
import org.openrdf.model.Literal;
import de.sekmi.histream.ontology.skos.ConceptImpl;
public class Rule {
protected String condition;
protected String conditionType;
protected Rule[] choose;
protected ConceptImpl target;
protected Rule otherwise;
public Rule(Literal condition, ConceptImpl target){
this.condition = condition.stringValue();
this.conditionType = condition.getDatatype().stringValue();
this.target = target;
}
public Rule(Rule[] choose, Rule otherwise){
this.choose = choose;
}
public ConceptImpl getTarget(){
return target;
}
public String toString(){
return "Condition: "+condition+" ("+conditionType+") ->"+target;
}
}
package de.sekmi.histream.ontology.skos.transform;
public class TransformationRules {
private Rule[] rules;
public TransformationRules(Rule[] rules){
this.rules = rules;
}
// TODO manage mappings
// TODO manage generators
public Rule[] getMapRules(){
return rules;
}
public Rule[] getGeneratorRules(){
return null;
}
}
package de.sekmi.histream.ontology.skos.transform;
import java.io.File;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import de.sekmi.histream.ontology.OntologyException;
import de.sekmi.histream.ontology.skos.Store;
import de.sekmi.histream.ontology.skos.transform.Rule;
import de.sekmi.histream.ontology.skos.transform.TransformationRules;
public class