Java tutorial
/******************************************************************************* * Copyright (c) 2015 Eclipse RDF4J contributors, Aduna, and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Distribution License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/org/documents/edl-v10.php. *******************************************************************************/ package org.eclipse.rdf4j.rio.ntriples; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.io.UnsupportedEncodingException; import java.nio.charset.Charset; import java.util.Collection; import java.util.HashSet; import org.apache.commons.io.input.BOMInputStream; import org.eclipse.rdf4j.model.IRI; import org.eclipse.rdf4j.model.Literal; import org.eclipse.rdf4j.model.Resource; import org.eclipse.rdf4j.model.Statement; import org.eclipse.rdf4j.model.Value; import org.eclipse.rdf4j.model.ValueFactory; import org.eclipse.rdf4j.model.impl.SimpleValueFactory; import org.eclipse.rdf4j.rio.RDFFormat; import org.eclipse.rdf4j.rio.RDFHandlerException; import org.eclipse.rdf4j.rio.RDFParseException; import org.eclipse.rdf4j.rio.RioSetting; import org.eclipse.rdf4j.rio.helpers.AbstractRDFParser; import org.eclipse.rdf4j.rio.helpers.BasicParserSettings; import org.eclipse.rdf4j.rio.helpers.NTriplesParserSettings; /** * RDF parser for N-Triples files. A specification of NTriples can be found in * <a href="http://www.w3.org/TR/rdf-testcases/#ntriples">this section</a> of the RDF Test Cases document. * This parser is not thread-safe, therefore its public methods are synchronized. * * @author Arjohn Kampman */ public class NTriplesParser extends AbstractRDFParser { /*-----------* * Variables * *-----------*/ protected Reader reader; protected long lineNo; protected Resource subject; protected IRI predicate; protected Value object; /*--------------* * Constructors * *--------------*/ /** * Creates a new NTriplesParser that will use a {@link SimpleValueFactory} to create object for resources, * bNodes and literals. */ public NTriplesParser() { super(); } /** * Creates a new NTriplesParser that will use the supplied <tt>ValueFactory</tt> to create RDF model * objects. * * @param valueFactory * A ValueFactory. */ public NTriplesParser(ValueFactory valueFactory) { super(valueFactory); } /*---------* * Methods * *---------*/ @Override public RDFFormat getRDFFormat() { return RDFFormat.NTRIPLES; } /** * Implementation of the <tt>parse(InputStream, String)</tt> method defined in the RDFParser interface. * * @param in * The InputStream from which to read the data, must not be <tt>null</tt>. The InputStream is * supposed to contain 7-bit US-ASCII characters, as per the N-Triples specification. * @param baseURI * The URI associated with the data in the InputStream, must not be <tt>null</tt>. * @throws IOException * If an I/O error occurred while data was read from the InputStream. * @throws RDFParseException * If the parser has found an unrecoverable parse error. * @throws RDFHandlerException * If the configured statement handler encountered an unrecoverable error. * @throws IllegalArgumentException * If the supplied input stream or base URI is <tt>null</tt>. */ @Override public synchronized void parse(InputStream in, String baseURI) throws IOException, RDFParseException, RDFHandlerException { if (in == null) { throw new IllegalArgumentException("Input stream can not be 'null'"); } // Note: baseURI will be checked in parse(Reader, String) try { parse(new InputStreamReader(new BOMInputStream(in, false), Charset.forName("UTF-8")), baseURI); } catch (UnsupportedEncodingException e) { // Every platform should support the UTF-8 encoding... throw new RuntimeException(e); } } /** * Implementation of the <tt>parse(Reader, String)</tt> method defined in the RDFParser interface. * * @param reader * The Reader from which to read the data, must not be <tt>null</tt>. * @param baseURI * The URI associated with the data in the Reader, must not be <tt>null</tt>. * @throws IOException * If an I/O error occurred while data was read from the InputStream. * @throws RDFParseException * If the parser has found an unrecoverable parse error. * @throws RDFHandlerException * If the configured statement handler encountered an unrecoverable error. * @throws IllegalArgumentException * If the supplied reader or base URI is <tt>null</tt>. */ @Override public synchronized void parse(Reader reader, String baseURI) throws IOException, RDFParseException, RDFHandlerException { clear(); try { if (reader == null) { throw new IllegalArgumentException("Reader can not be 'null'"); } if (baseURI == null) { throw new IllegalArgumentException("base URI can not be 'null'"); } if (rdfHandler != null) { rdfHandler.startRDF(); } this.reader = reader; lineNo = 1; reportLocation(lineNo, 1); int c = readCodePoint(); c = skipWhitespace(c); while (c != -1) { if (c == '#') { // Comment, ignore c = skipLine(c); } else if (c == '\r' || c == '\n') { // Empty line, ignore c = skipLine(c); } else { c = parseTriple(c); } c = skipWhitespace(c); } } finally { clear(); } if (rdfHandler != null) { rdfHandler.endRDF(); } } /** * Reads characters from reader until it finds a character that is not a space or tab, and returns this * last character code point. In case the end of the character stream has been reached, -1 is returned. */ protected int skipWhitespace(int c) throws IOException { while (c == ' ' || c == '\t') { c = readCodePoint(); } return c; } /** * Verifies that there is only whitespace or comments until the end of the line. */ protected int assertLineTerminates(int c) throws IOException, RDFParseException { c = readCodePoint(); c = skipWhitespace(c); if (c == '#') { // c = skipToEndOfLine(c); } else { if (c != -1 && c != '\r' && c != '\n') { reportFatalError("Content after '.' is not allowed"); } } return c; } /** * Reads characters from reader until the first EOL has been read. The EOL character or -1 is returned. */ protected int skipToEndOfLine(int c) throws IOException { while (c != -1 && c != '\r' && c != '\n') { c = readCodePoint(); } return c; } /** * Reads characters from reader until the first EOL has been read. The first character after the EOL is * returned. In case the end of the character stream has been reached, -1 is returned. */ protected int skipLine(int c) throws IOException { while (c != -1 && c != '\r' && c != '\n') { c = readCodePoint(); } // c is equal to -1, \r or \n. In case of a \r, we should // check whether it is followed by a \n. if (c == '\n') { c = readCodePoint(); lineNo++; reportLocation(lineNo, 1); } else if (c == '\r') { c = readCodePoint(); if (c == '\n') { c = readCodePoint(); } lineNo++; reportLocation(lineNo, 1); } return c; } private int parseTriple(int c) throws IOException, RDFParseException, RDFHandlerException { boolean ignoredAnError = false; try { c = parseSubject(c); c = skipWhitespace(c); c = parsePredicate(c); c = skipWhitespace(c); c = parseObject(c); c = skipWhitespace(c); if (c == -1) { throwEOFException(); } else if (c != '.') { reportError("Expected '.', found: " + new String(Character.toChars(c)), NTriplesParserSettings.FAIL_ON_NTRIPLES_INVALID_LINES); } c = assertLineTerminates(c); } catch (RDFParseException rdfpe) { if (getParserConfig().isNonFatalError(NTriplesParserSettings.FAIL_ON_NTRIPLES_INVALID_LINES)) { reportError(rdfpe, NTriplesParserSettings.FAIL_ON_NTRIPLES_INVALID_LINES); ignoredAnError = true; } else { throw rdfpe; } } c = skipLine(c); if (!ignoredAnError) { Statement st = createStatement(subject, predicate, object); if (rdfHandler != null) { rdfHandler.handleStatement(st); } } subject = null; predicate = null; object = null; return c; } protected int parseSubject(int c) throws IOException, RDFParseException { StringBuilder sb = new StringBuilder(100); // subject is either an uriref (<foo://bar>) or a nodeID (_:node1) if (c == '<') { // subject is an uriref c = parseUriRef(c, sb); subject = createURI(sb.toString()); } else if (c == '_') { // subject is a bNode c = parseNodeID(c, sb); subject = createNode(sb.toString()); } else if (c == -1) { throwEOFException(); } else { reportFatalError("Expected '<' or '_', found: " + new String(Character.toChars(c))); } return c; } protected int parsePredicate(int c) throws IOException, RDFParseException { StringBuilder sb = new StringBuilder(100); // predicate must be an uriref (<foo://bar>) if (c == '<') { // predicate is an uriref c = parseUriRef(c, sb); predicate = createURI(sb.toString()); } else if (c == -1) { throwEOFException(); } else { reportFatalError("Expected '<', found: " + new String(Character.toChars(c))); } return c; } protected int parseObject(int c) throws IOException, RDFParseException { StringBuilder sb = getBuffer(); // object is either an uriref (<foo://bar>), a nodeID (_:node1) or a // literal ("foo"-en or "1"^^<xsd:integer>). if (c == '<') { // object is an uriref c = parseUriRef(c, sb); object = createURI(sb.toString()); } else if (c == '_') { // object is a bNode c = parseNodeID(c, sb); object = createNode(sb.toString()); } else if (c == '"') { // object is a literal StringBuilder lang = getLanguageTagBuffer(); StringBuilder datatype = getDatatypeUriBuffer(); c = parseLiteral(c, sb, lang, datatype); object = createLiteral(sb.toString(), lang.toString(), datatype.toString()); } else if (c == -1) { throwEOFException(); } else { reportFatalError("Expected '<', '_' or '\"', found: " + new String(Character.toChars(c)) + ""); } return c; } protected int parseUriRef(int c, StringBuilder uriRef) throws IOException, RDFParseException { if (c != '<') { reportError("Supplied char should be a '<', is: " + new String(Character.toChars(c)), NTriplesParserSettings.FAIL_ON_NTRIPLES_INVALID_LINES); } // Read up to the next '>' character c = readCodePoint(); while (c != '>') { if (c == -1) { throwEOFException(); } if (c == ' ') { reportError("IRI included an unencoded space: " + new String(Character.toChars(c)), BasicParserSettings.VERIFY_URI_SYNTAX); } uriRef.append(Character.toChars(c)); if (c == '\\') { // This escapes the next character, which might be a '>' c = readCodePoint(); if (c == -1) { throwEOFException(); } if (c != 'u' && c != 'U') { reportError("IRI includes string escapes: '\\" + c + "'", BasicParserSettings.VERIFY_URI_SYNTAX); } uriRef.append(Character.toChars(c)); } c = readCodePoint(); } // c == '>', read next char c = readCodePoint(); return c; } protected int parseNodeID(int c, StringBuilder name) throws IOException, RDFParseException { if (c != '_') { reportError("Supplied char should be a '_', is: " + new String(Character.toChars(c)), NTriplesParserSettings.FAIL_ON_NTRIPLES_INVALID_LINES); } c = readCodePoint(); if (c == -1) { throwEOFException(); } else if (c != ':') { reportError("Expected ':', found: " + new String(Character.toChars(c)), NTriplesParserSettings.FAIL_ON_NTRIPLES_INVALID_LINES); } c = readCodePoint(); if (c == -1) { throwEOFException(); } else if (!NTriplesUtil.isLetterOrNumber(c)) { reportError("Expected a letter or number, found: " + new String(Character.toChars(c)), NTriplesParserSettings.FAIL_ON_NTRIPLES_INVALID_LINES); } name.append(Character.toChars(c)); // Read all following letter and numbers, they are part of the name c = readCodePoint(); while (c != -1 && NTriplesUtil.isLetterOrNumber(c)) { name.append(Character.toChars(c)); c = readCodePoint(); } return c; } private int parseLiteral(int c, StringBuilder value, StringBuilder lang, StringBuilder datatype) throws IOException, RDFParseException { if (c != '"') { reportError("Supplied char should be a '\"', is: " + c, NTriplesParserSettings.FAIL_ON_NTRIPLES_INVALID_LINES); } // Read up to the next '"' character c = readCodePoint(); while (c != '"') { if (c == -1) { throwEOFException(); } value.append(Character.toChars(c)); if (c == '\\') { // This escapes the next character, which might be a double quote c = readCodePoint(); if (c == -1) { throwEOFException(); } value.append(Character.toChars(c)); } c = readCodePoint(); } // c == '"', read next char c = readCodePoint(); if (c == '@') { // Read language c = readCodePoint(); if (!NTriplesUtil.isLetter(c)) { reportError("Expected a letter, found: " + new String(Character.toChars(c)), NTriplesParserSettings.FAIL_ON_NTRIPLES_INVALID_LINES); } while (c != -1 && c != '.' && c != '^' && c != ' ' && c != '\t') { lang.append(Character.toChars(c)); c = readCodePoint(); } } else if (c == '^') { // Read datatype c = readCodePoint(); // c should be another '^' if (c == -1) { throwEOFException(); } else if (c != '^') { reportError("Expected '^', found: " + new String(Character.toChars(c)), NTriplesParserSettings.FAIL_ON_NTRIPLES_INVALID_LINES); } c = readCodePoint(); // c should be a '<' if (c == -1) { throwEOFException(); } else if (c != '<') { reportError("Expected '<', found: " + new String(Character.toChars(c)), NTriplesParserSettings.FAIL_ON_NTRIPLES_INVALID_LINES); } c = parseUriRef(c, datatype); } return c; } @Override protected IRI createURI(String uri) throws RDFParseException { try { uri = NTriplesUtil.unescapeString(uri); } catch (IllegalArgumentException e) { reportError(e.getMessage(), NTriplesParserSettings.FAIL_ON_NTRIPLES_INVALID_LINES); } return super.createURI(uri); } /** * Reads the next Unicode code point. * * @return the next Unicode code point, or -1 if the end of the stream has been reached. * @throws IOException */ protected int readCodePoint() throws IOException { int next = reader.read(); if (Character.isHighSurrogate((char) next)) { next = Character.toCodePoint((char) next, (char) reader.read()); } return next; } protected Literal createLiteral(String label, String lang, String datatype) throws RDFParseException { try { label = NTriplesUtil.unescapeString(label); } catch (IllegalArgumentException e) { reportFatalError(e); } if (lang.length() == 0) { lang = null; } if (datatype.length() == 0) { datatype = null; } IRI dtURI = null; if (datatype != null) { dtURI = createURI(datatype); } return super.createLiteral(label, lang, dtURI, lineNo, -1); } /** * Overrides {@link AbstractRDFParser#reportWarning(String)}, adding line number information to the error. */ @Override protected void reportWarning(String msg) { reportWarning(msg, lineNo, -1); } /** * Overrides {@link AbstractRDFParser#reportError(String, RioSetting)}, adding line number information to * the error. */ @Override protected void reportError(String msg, RioSetting<Boolean> setting) throws RDFParseException { reportError(msg, lineNo, -1, setting); } protected void reportError(Exception e, RioSetting<Boolean> setting) throws RDFParseException { reportError(e, lineNo, -1, setting); } /** * Overrides {@link AbstractRDFParser#reportFatalError(String)}, adding line number information to the * error. */ @Override protected void reportFatalError(String msg) throws RDFParseException { reportFatalError(msg, lineNo, -1); } /** * Overrides {@link AbstractRDFParser#reportFatalError(Exception)}, adding line number information to the * error. */ @Override protected void reportFatalError(Exception e) throws RDFParseException { reportFatalError(e, lineNo, -1); } protected void throwEOFException() throws RDFParseException { throw new RDFParseException("Unexpected end of file"); } /** * Return a buffer of zero length and non-zero capacity. The same buffer is reused for each thing which is * parsed. This reduces the heap churn substantially. However, you have to watch out for side-effects and * convert the buffer to a {@link String} before the buffer is reused. * * @return a buffer of zero length and non-zero capacity. */ private StringBuilder getBuffer() { buffer.setLength(0); return buffer; } private final StringBuilder buffer = new StringBuilder(100); /** * Return a buffer for the use of parsing literal language tags. The buffer is of zero length and non-zero * capacity. The same buffer is reused for each tag which is parsed. This reduces the heap churn * substantially. However, you have to watch out for side-effects and convert the buffer to a * {@link String} before the buffer is reused. * * @return a buffer of zero length and non-zero capacity, for the use of parsing literal language tags. */ private StringBuilder getLanguageTagBuffer() { languageTagBuffer.setLength(0); return languageTagBuffer; } private final StringBuilder languageTagBuffer = new StringBuilder(8); /** * Return a buffer for the use of parsing literal datatype URIs. The buffer is of zero length and non-zero * capacity. The same buffer is reused for each datatype which is parsed. This reduces the heap churn * substantially. However, you have to watch out for side-effects and convert the buffer to a * {@link String} before the buffer is reused. * * @return a buffer of zero length and non-zero capacity, for the user of parsing literal datatype URIs. */ private StringBuilder getDatatypeUriBuffer() { datatypeUriBuffer.setLength(0); return datatypeUriBuffer; } private final StringBuilder datatypeUriBuffer = new StringBuilder(40); @Override protected void clear() { super.clear(); // get rid of anything large left in the buffers. buffer.setLength(0); buffer.trimToSize(); languageTagBuffer.setLength(0); languageTagBuffer.trimToSize(); datatypeUriBuffer.setLength(0); datatypeUriBuffer.trimToSize(); } /* * N-Triples parser supports these settings. */ @Override public Collection<RioSetting<?>> getSupportedSettings() { Collection<RioSetting<?>> result = new HashSet<RioSetting<?>>(super.getSupportedSettings()); result.add(NTriplesParserSettings.FAIL_ON_NTRIPLES_INVALID_LINES); return result; } }