2222import java .lang .reflect .*;
2323import java .math .BigDecimal ;
2424import java .math .BigInteger ;
25+ import java .net .MalformedURLException ;
2526import java .net .URI ;
2627import java .net .URISyntaxException ;
28+ import java .net .URL ;
2729import java .text .ParseException ;
2830import java .text .SimpleDateFormat ;
2931import java .time .ZonedDateTime ;
@@ -39,6 +41,8 @@ class Parser {
3941
4042 private final Logger logger = LoggerFactory .getLogger (Parser .class );
4143
44+ private static final URI blankNodeIdPropertyUri = URI .create ("https://admin-shell.io/aas/blankNodeId" );
45+
4246 static Map <String , String > knownNamespaces = new HashMap <>();
4347
4448 /**
@@ -55,20 +59,30 @@ private <T> T handleObject(Model inputModel, String objectUri, Class<T> targetCl
5559
5660 //if(!targetClass.getSimpleName().endsWith("Impl")) //This would not work for "TypedLiteral", "RdfResource" and so on
5761 //Check whether we are dealing with an instantiable class (i.e. no interface and no abstract class)
62+ boolean currentObjectIsBlankNode = false ;
5863 if (targetClass .isInterface () || Modifier .isAbstract (targetClass .getModifiers ())) {
5964 //We don't know the desired class yet (current targetClass is not instantiable). This is only known for the root object
6065 ArrayList <Class <?>> implementingClasses = getImplementingClasses (targetClass );
61-
66+ String queryString ;
6267 //Get a list of all "rdf:type" statements in our model
6368 //In case of a blank node, the "object URI" will just be a string and no valid URI. In that case, we need a different query syntax
6469 try {
65- new URI (objectUri ). toURL ( );
70+ new URL (objectUri );
6671 }
67- catch (URISyntaxException | IllegalArgumentException e )
72+ catch (MalformedURLException e )
6873 {
69- e .printStackTrace ();
74+ currentObjectIsBlankNode = true ;
75+ }
76+ if (currentObjectIsBlankNode )
77+ {
78+ //Object is a blank node, so the subject URI cannot be used
79+ queryString = "SELECT ?type { ?s <" + blankNodeIdPropertyUri + "> \" " + objectUri + "\" ; a ?type . }" ;
80+ }
81+ else
82+ {
83+ //Not a blank node, so we can work with the subject URI
84+ queryString = "SELECT ?type { BIND(<" + objectUri + "> AS ?s). ?s a ?type . }" ;
7085 }
71- String queryString = "SELECT ?type { BIND(<" + objectUri + "> AS ?s). ?s a ?type . }" ;
7286 Query query = QueryFactory .create (queryString );
7387 QueryExecution queryExecution = QueryExecutionFactory .create (query , inputModel );
7488 ResultSet resultSet = queryExecution .execSelect ();
@@ -215,9 +229,19 @@ private <T> T handleObject(Model inputModel, String objectUri, Class<T> targetCl
215229 //Start the "WHERE" part - Fuseki does not expect the "WHERE" keyword, but just an "{"
216230 queryStringBuilder .append (" { " );
217231
232+ //In case of blank nodes, we can't work with the subject URI
233+ if (currentObjectIsBlankNode )
234+ {
235+ queryStringBuilder .append ("?s <" ).append (blankNodeIdPropertyUri ).append ("> \" " ).append (objectUri ).append ("\" ;" );
236+ }
237+ else
238+ {
239+ queryStringBuilder .append (" <" ).append (objectUri ).append (">" );
240+ }
241+
218242 //Make sure that the object is of the correct type
219243 //This is particularly relevant in case of all fields being optional -- then one could simply parse a random object
220- queryStringBuilder .append (" <" ). append ( objectUri ). append ( "> a " ).append (wrapIfUri (targetClass .getAnnotation (IRI .class ).value ()[0 ])).append (". " );
244+ queryStringBuilder .append (" a " ).append (wrapIfUri (targetClass .getAnnotation (IRI .class ).value ()[0 ])).append (". " );
221245
222246 for (Map .Entry <String , Method > entry : methodMap .entrySet ()) {
223247 //Is this a field which is annotated by NOT NULL?
@@ -229,7 +253,13 @@ private <T> T handleObject(Model inputModel, String objectUri, Class<T> targetCl
229253 //In AAS, every field is optional, as there are no validation annotations in the model
230254 queryStringBuilder .append (" OPTIONAL {" );
231255
232- queryStringBuilder .append (" <" ).append (objectUri ).append ("> " ); //subject, as passed to the function
256+ if (currentObjectIsBlankNode )
257+ {
258+ queryStringBuilder .append (" ?s " );
259+ }
260+ else {
261+ queryStringBuilder .append (" <" ).append (objectUri ).append ("> " ); //subject, as passed to the function
262+ }
233263 //For the field, get the JsonAlias annotation (present for all classes generated by the CodeGen tool)
234264 //Find the annotation value containing a colon and interpret this as "prefix:predicate"
235265 boolean foundAnnotation = false ;
@@ -276,13 +306,6 @@ private <T> T handleObject(Model inputModel, String objectUri, Class<T> targetCl
276306 queryForOtherProperties .append (" <" ).append (entry .getValue ()).append (">\n " );
277307 }
278308
279- for (Map .Entry <String , String > entry : knownNamespaces .entrySet ()) {
280- queryForOtherProperties .append ("PREFIX " ).append (entry .getKey ());
281- if (!entry .getKey ().endsWith (":" )) {
282- queryForOtherProperties .append (":" );
283- }
284- queryForOtherProperties .append (" <" ).append (entry .getValue ()).append (">\n " );
285- }
286309
287310 //Respect ALL properties and values
288311 queryForOtherProperties .append (" SELECT ?p ?o { <" ).append (objectUri ).append ("> ?p ?o .\n " );
@@ -442,13 +465,48 @@ private <T> T handleObject(Model inputModel, String objectUri, Class<T> targetCl
442465 if (Collection .class .isAssignableFrom (currentType )) {
443466 sparqlParameterName += "s" ; //plural form for the concatenated values
444467 }
445- if (querySolution .contains (sparqlParameterName )) {
468+ querySolution .varNames ().forEachRemaining (System .out ::println );
469+ System .out .println (querySolution .contains (sparqlParameterName ));
470+ if (querySolution .contains (sparqlParameterName )) { //TODO why is this false for keys?! Answer: Because you can't do a GROUP_CONCAT on blank nodes
446471 String currentSparqlBinding = querySolution .get (sparqlParameterName ).toString ();
472+
447473 boolean objectIsBlankNode = querySolution .get (sparqlParameterName ).isResource () && querySolution .get (sparqlParameterName ).asNode ().isBlank ();
448- System .out .println (currentSparqlBinding + " is blank node? " + objectIsBlankNode );
474+ String blankNodeId = "" ;
475+ //If the object is a blank node, we will struggle to make follow-up queries starting at the blank node as subject
476+ //For that case, we add some artificial identifiers here
477+ if (objectIsBlankNode ) {
478+ blankNodeId = querySolution .get (sparqlParameterName ).asNode ().getBlankNodeId ().toString ();
479+ inputModel .add (
480+ ResourceFactory .createStatement (
481+ querySolution .get (sparqlParameterName ).asResource (), //subject: the blank node
482+ ResourceFactory .createProperty (blankNodeIdPropertyUri .toString ()), //some artificial property
483+ ResourceFactory .createStringLiteral (blankNodeId ))); //object: the ID of the blank node
484+ }
449485
450486 if (currentType .isEnum ()) {
451- entry .getValue ().invoke (returnObject , handleEnum (currentType , currentSparqlBinding ));
487+ //Two possibilities:
488+ //1: The URI of the enum value is given directly e.g. ?s ?p <someUri>
489+ //2: The URI of the enum value is encapsulated in a blank node, e.g.
490+ // ?s ?p [ a demo:myEnum, demo:enumValue ]
491+ if (objectIsBlankNode )
492+ {
493+ Query innerEnumQuery = QueryFactory .create ("SELECT ?type { ?s <" + blankNodeIdPropertyUri + "> + \" " + blankNodeId + "\" ; a ?type } " );
494+ QueryExecution innerEnumQueryExecution = QueryExecutionFactory .create (innerEnumQuery , inputModel );
495+ ResultSet innerEnumQueryExecutionResultSet = innerEnumQueryExecution .execSelect ();
496+ while (innerEnumQueryExecutionResultSet .hasNext ())
497+ {
498+ try {
499+ entry .getValue ().invoke (returnObject , handleEnum (currentType , innerEnumQueryExecutionResultSet .next ().get ("type" ).toString ()));
500+ break ; //Stop after the first successful execution
501+ }
502+ catch (IOException ignored ) //There might be errors, if multiple types are present, see example above
503+ {}
504+ }
505+ innerEnumQueryExecution .close ();
506+ }
507+ else {
508+ entry .getValue ().invoke (returnObject , handleEnum (currentType , currentSparqlBinding ));
509+ }
452510 continue ;
453511 }
454512
@@ -527,7 +585,13 @@ private <T> T handleObject(Model inputModel, String objectUri, Class<T> targetCl
527585
528586 } else {
529587 //Not a primitive object, but a complex sub-object. Recursively call this function to handle it
530- entry .getValue ().invoke (returnObject , handleObject (inputModel , currentSparqlBinding , entry .getValue ().getParameterTypes ()[0 ]));
588+ if (objectIsBlankNode ) {
589+ System .out .println ("Handling sub-object as blank node" );
590+ entry .getValue ().invoke (returnObject , handleObject (inputModel , blankNodeId , entry .getValue ().getParameterTypes ()[0 ]));
591+ } else {
592+
593+ entry .getValue ().invoke (returnObject , handleObject (inputModel , currentSparqlBinding , entry .getValue ().getParameterTypes ()[0 ]));
594+ }
531595 }
532596 }
533597 }
@@ -690,7 +754,7 @@ private <T> T handleEnum(Class<T> enumClass, String url) throws IOException {
690754 url = url .substring (url .lastIndexOf ("/" ) + 1 );
691755 }
692756 for (T constant : constants ) {
693- if (url .equals (constant .toString ())) {
757+ if (url .equalsIgnoreCase (constant .toString ())) {
694758 return constant ;
695759 }
696760 }
0 commit comments