/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.jena.riot.lang; import org.apache.jena.atlas.json.io.parser.TokenizerJSON ; import org.apache.jena.datatypes.TypeMapper ; import org.apache.jena.graph.Node ; import org.apache.jena.graph.Triple ; import org.apache.jena.riot.Lang ; import org.apache.jena.riot.RDFLanguages ; import org.apache.jena.riot.system.ParserProfile ; import org.apache.jena.riot.system.StreamRDF ; import org.apache.jena.riot.tokens.Token ; import org.apache.jena.riot.tokens.TokenType ; import org.apache.jena.riot.tokens.Tokenizer ; /** * RDF-JSON. * * @see <a href="http://www.w3.org/TR/rdf-json/">http://www.w3.org/TR/rdf-json/</a> */ public class LangRDFJSON extends LangBase { public LangRDFJSON(Tokenizer tokenizer, ParserProfile profile, StreamRDF dest) { super(tokenizer, profile, dest) ; if (!(tokenizer instanceof TokenizerJSON)) { throw new IllegalArgumentException("Tokenizer for the RDF/JSON parser must be an instance of "+TokenizerJSON.class.getName()) ; } } @Override public Lang getLang() { return RDFLanguages.RDFJSON ; } @Override protected void runParser() { this.tryParseGraph() ; } private void tryParseGraph() { //Must be a { to start the JSON Object if (lookingAt(TokenType.LBRACE)) { //Can discard the { safely nextToken() ; //Then want to try parsing triples tryParseTriples(); //Ensure that there is a } to end the JSON Object and discard it if (!lookingAt(TokenType.RBRACE)) { exception(peekToken(), "Expected a } character to end a JSON Object but got %s", peekToken()) ; } nextToken() ; //Should now be at the end of the file if (!lookingAt(TokenType.EOF)) { exception(peekToken(), "Expected the end of the JSON but there is additional content beyond the end of the JSON Object") ; } nextToken(); } else { exception(peekToken(), "Expected a { character to start a JSON Object but got %s", peekToken()) ; } } private void tryParseTriples() { //First we expect to see a Property Name which is the Subject for //our Triples //Note that subjectExpected starts as false because we can get an empty graph //in which case we won't see any subjects boolean subjectExpected = false; while (moreTokens()) { if (lookingAt(TokenType.RBRACE)) { if (subjectExpected) { exception(peekToken(), "Expected a Property Name after a comma to represent the Subject of the next block of triples but got %s", peekToken()) ; } //Otherwise this is the end of the JSON Object representing the Graph so just return return; } else if (isPropertyName()) { subjectExpected = false; //Is a Property Name so represents a Subject Token t = nextToken() ; Node subj; if (t.getImage().startsWith("_:")) { subj = profile.createBlankNode(null, t.getImage().substring(2), t.getLine(), t.getColumn()) ; } else { subj = profile.createURI(t.getImage(), t.getLine(), t.getColumn()) ; } //Should always be a : after a Property Name checkColon() ; //Now try and parse the Predicate Object List tryParsePredicateObjectList(subj) ; //After the end of a Predicate Object List may optionally have a comma //to denote there are further if (lookingAt(TokenType.COMMA)) { nextToken(); subjectExpected = true; } } else { if (subjectExpected) { exception(peekToken(), "Expected a Property Name after a comma to represent the Subject of the next block of triples but got %s", peekToken()) ; } else { exception(peekToken(), "Expected either the end of the JSON Object (the } character) or a JSON Property Name (String) to set the Subject for some Triples but got %s", peekToken()) ; } } } } private void tryParsePredicateObjectList(Node subj) { //RDF/JSON defines a Predicate Object list to be a JSON Object //where each Predicate is a Property Name and the Object List //is a JSON Array //First must see the { to start the object which we can then discard if (!lookingAt(TokenType.LBRACE)) { exception(peekToken(), "Expected a { character to start the JSON Object for a Predicate Object List but got a %s", peekToken()) ; } nextToken() ; //Then we must see a Property Name or the end of the predicate object list boolean first = true; boolean propertyNameExpected = true; while (true) { if (isPropertyName()) { first = false; propertyNameExpected = false; Token t = nextToken(); Node pred = profile.createURI(t.getImage(), t.getLine(), t.getColumn()) ; //Must be a : after Property Name checkColon() ; //Then we can try and parse the Object List tryParseObjectList(subj, pred) ; //After this we may optionally see a , which //means that there are more property names present //i.e. further predicates for this subject if (lookingAt(TokenType.COMMA)) { nextToken() ; propertyNameExpected = true; } } else if (!first && lookingAt(TokenType.RBRACE)) { //This is the end of the Predicate Object List Object if (propertyNameExpected) { exception(peekToken(), "Expected a further Property Name to represent a Predicate after a comma in a Predicate Object List but got %s", peekToken()) ; } nextToken(); return; } else { if (propertyNameExpected) { exception(peekToken(), "Expected a Property Name to represent a Predicate as part of a Predicate Object List but got %s", peekToken()) ; } else { exception(peekToken(), "Expected a Property Name or the end of the Predicate Object List but got %s", peekToken()) ; } } } } private void tryParseObjectList(Node subj, Node pred) { //RDF/JSON defines an Object List to be a JSON Array where //the array may consist of JSON Objects which represent //the objects of Triples //First must see the [ to start the array if (lookingAt(TokenType.LBRACKET)) { nextToken(); boolean first = true; boolean objectExpected = true; while (true) { if (lookingAt(TokenType.LBRACE)) { if (!objectExpected) { exception(peekToken(), "Expected the end of the JSON Array for the Object List as no comma was seen after the preceding } but got %s", peekToken()); } first = false; objectExpected = false; //Parse the Object and Emit the Triple Node obj = tryParseObject(); Triple t = profile.createTriple(subj, pred, obj, currLine, currCol) ; dest.triple(t) ; //After this may optionally see a comma to indicate //that there are further objects in the object list if (lookingAt(TokenType.COMMA)) { nextToken(); objectExpected = true; } } else if (!first && lookingAt(TokenType.RBRACKET)) { //This is the end of the Object List Object if (objectExpected) { exception(peekToken(), "Expected a further JSON Object to represent an Object after a comma in a Object List but got %s", peekToken()) ; } nextToken(); return; } else { if (objectExpected) { exception(peekToken(), "Expected a JSON Object to represent an Object as part of a Object List but got %s", peekToken()) ; } else { exception(peekToken(), "Expected a JSON Object or the end of the Object List but got %s", peekToken()) ; } } } } else { exception(peekToken(), "Expected a [ character to start a JSON Array for the Object List but got %s", peekToken()) ; } } private Node tryParseObject() { //RDF/JSON defines the Object of a Triple to be encoded as a //JSON Object //It has mandatory properties 'value' and 'type' plus optional //properties 'lang', 'xml:lang' and 'datatype' Node obj = null; Token value = null, type = null, lang = null, datatype = null; //First we expect to see the { character to start the JSON Object if (lookingAt(TokenType.LBRACE)) { //Discard the { nextToken(); //Then see a stream of tokens which are property value pairs //representing the properties of the object boolean first = true; boolean propertyNameExpected = true; while (true) { if (isPropertyName()) { first = false; propertyNameExpected = false; Token t = nextToken(); String name = t.getImage(); //Must always be a : after a property name checkColon(); //Is this one of our valid properties switch ( name ) { case "value": if ( value == null ) { value = checkValidForObjectProperty(); } else { exception( t, "Encountered the value property on an Object when the value property has already been specified" ); } break; case "type": if ( type == null ) { type = checkValidForObjectProperty(); } else { exception( t, "Encountered the type property on an Object when the type property has already been specified" ); } break; case "lang": case "xml:lang": if ( lang == null && datatype == null ) { lang = checkValidForObjectProperty(); } else { exception( t, "Encountered the %s property on an Object when lang/datatype has already been specified", name ); } break; case "datatype": if ( lang == null && datatype == null ) { datatype = checkValidForObjectProperty(); } else { exception( t, "Encountered the %s property on an Object when lang/datatype has already been specified", name ); } break; default: exception( t, "Unexpected Property Name %s encountered, expected one of value, type, lang or datatype", t.getImage() ); break; } //After each Property Value pair we may optionally //see a comma to indicate further pairs are present if (lookingAt(TokenType.COMMA)) { nextToken(); propertyNameExpected = true; } } else if (!first && lookingAt(TokenType.RBRACE)) { if (propertyNameExpected) { exception(peekToken(), "Expected a further Property Name to represent a property of the Object of a Triple after a comma but got %s", peekToken()) ; } break; } else { exception(peekToken(), "Expected a Property Name to define a property relating to the Object of a Triple but got %s", peekToken()) ; } } //Discard the } which terminated the Object nextToken(); //Next up validate the tokens we've got if (type == null) exception(peekToken(), "Unable to parse the Object for a Triple from a JSON Object as the required 'type' property is not present") ; if (value == null) exception(peekToken(), "Unable to parse the Object for a Triple from a JSON Object as the required 'value' property is not present") ; //Use these to create the Object String typeStr = type.getImage(); switch ( typeStr ) { case "uri": obj = profile.createURI( value.getImage(), value.getLine(), value.getColumn() ); break; case "bnode": obj = profile.createBlankNode( null, value.getImage().substring( 2 ), value.getLine(), value.getColumn() ); break; case "literal": if ( lang != null ) { obj = profile.createLangLiteral( value.getImage(), lang.getImage(), value.getLine(), value.getColumn() ); } else if ( datatype != null ) { obj = profile.createTypedLiteral( value.getImage(), TypeMapper.getInstance().getSafeTypeByName( datatype.getImage() ), value.getLine(), value.getColumn() ); } else { obj = profile.createStringLiteral( value.getImage(), value.getLine(), value.getColumn() ); } break; default: exception( type, "Unable to parse the Object for a Triple from a JSON Object as the value %s given for the 'type' property is not one of uri, bnode or literal", typeStr ); break; } } else { exception(peekToken(), "Expected a { character to start a JSON Object to represent the Object of a Triple but got %s", peekToken()) ; } return obj; } private boolean isPropertyName() { return lookingAt(TokenType.STRING1) || lookingAt(TokenType.STRING2); } private Token checkValidForObjectProperty() { Token t = null; if (lookingAt(TokenType.STRING1) || lookingAt(TokenType.STRING2)) t = nextToken(); else exception(peekToken(), "JSON Values given for properties for an Object must be Strings") ; return t; } private void checkColon() { if (!lookingAt(TokenType.COLON)) exception(peekToken(), "Expected a : character after a JSON Property Name but got %s", peekToken()) ; nextToken() ; } }