You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commonsrdf.apache.org by st...@apache.org on 2016/10/26 11:35:51 UTC

[25/50] [abbrv] incubator-commonsrdf git commit: RDFParser moved to org.apache.commons.rdf.experimental

http://git-wip-us.apache.org/repos/asf/incubator-commonsrdf/blob/a189f91e/rdf4j/src/main/java/org/apache/commons/rdf/rdf4j/experimental/RDF4JParser.java
----------------------------------------------------------------------
diff --git a/rdf4j/src/main/java/org/apache/commons/rdf/rdf4j/experimental/RDF4JParser.java b/rdf4j/src/main/java/org/apache/commons/rdf/rdf4j/experimental/RDF4JParser.java
new file mode 100644
index 0000000..c185419
--- /dev/null
+++ b/rdf4j/src/main/java/org/apache/commons/rdf/rdf4j/experimental/RDF4JParser.java
@@ -0,0 +1,197 @@
+/**
+ * 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.commons.rdf.rdf4j.experimental;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Optional;
+import java.util.function.Consumer;
+
+import org.apache.commons.rdf.api.IRI;
+import org.apache.commons.rdf.api.Quad;
+import org.apache.commons.rdf.api.RDFSyntax;
+import org.apache.commons.rdf.experimental.RDFParser;
+import org.apache.commons.rdf.rdf4j.RDF4JDataset;
+import org.apache.commons.rdf.rdf4j.RDF4JGraph;
+import org.apache.commons.rdf.rdf4j.RDF4JTermFactory;
+import org.apache.commons.rdf.simple.experimental.AbstractRDFParser;
+import org.eclipse.rdf4j.model.Model;
+import org.eclipse.rdf4j.repository.util.RDFInserter;
+import org.eclipse.rdf4j.repository.util.RDFLoader;
+import org.eclipse.rdf4j.rio.ParserConfig;
+import org.eclipse.rdf4j.rio.RDFFormat;
+import org.eclipse.rdf4j.rio.RDFHandler;
+import org.eclipse.rdf4j.rio.RDFHandlerException;
+import org.eclipse.rdf4j.rio.Rio;
+import org.eclipse.rdf4j.rio.helpers.AbstractRDFHandler;
+
+/**
+ * RDF4J-based parser.
+ * <p>
+ * This can handle the RDF syntaxes {@link RDFSyntax#JSONLD},
+ * {@link RDFSyntax#NQUADS}, {@link RDFSyntax#NTRIPLES},
+ * {@link RDFSyntax#RDFXML}, {@link RDFSyntax#TRIG} and {@link RDFSyntax#TURTLE}
+ * - additional syntaxes can be supported by including the corresponding
+ * <em>rdf4j-rio-*</em> module on the classpath.
+ *
+ */
+public class RDF4JParser extends AbstractRDFParser<RDF4JParser> implements RDFParser {
+
+	private final class AddToQuadConsumer extends AbstractRDFHandler {
+		private final Consumer<Quad> quadTarget;
+
+		private AddToQuadConsumer(Consumer<Quad> quadTarget) {
+			this.quadTarget = quadTarget;
+		}
+
+		public void handleStatement(org.eclipse.rdf4j.model.Statement st)
+				throws org.eclipse.rdf4j.rio.RDFHandlerException {
+			// TODO: if getRdfTermFactory() is a non-rdf4j factory, should
+			// we use factory.createQuad() instead?
+			// Unsure what is the promise of setting getRdfTermFactory() --
+			// does it go all the way down to creating BlankNode, IRI and
+			// Literal?
+			quadTarget.accept(rdf4jTermFactory.asQuad(st));
+			// Performance note:
+			// Graph/Quad.add should pick up again our
+			// RDF4JGraphLike.asStatement()
+			// and avoid double conversion.
+			// Additionally the RDF4JQuad and RDF4JTriple implementations
+			// are lazily converting subj/obj/pred/graph.s
+		}
+	}
+
+	private final static class AddToModel extends AbstractRDFHandler {
+		private final Model model;
+
+		public AddToModel(Model model) {
+			this.model = model;
+		}
+
+		public void handleStatement(org.eclipse.rdf4j.model.Statement st)
+				throws org.eclipse.rdf4j.rio.RDFHandlerException {
+			model.add(st);
+		}
+
+		@Override
+		public void handleNamespace(String prefix, String uri) throws RDFHandlerException {
+			model.setNamespace(prefix, uri);
+		}
+	}
+
+	private RDF4JTermFactory rdf4jTermFactory;
+
+	@Override
+	protected RDF4JTermFactory createRDFTermFactory() {
+		return new RDF4JTermFactory();
+	}
+
+	@Override
+	protected RDF4JParser prepareForParsing() throws IOException, IllegalStateException {
+		RDF4JParser c = prepareForParsing();
+		// Ensure we have an RDF4JTermFactory for conversion.
+		// We'll make a new one if user has provided a non-RDF4J factory
+		c.rdf4jTermFactory = (RDF4JTermFactory) getRdfTermFactory().filter(RDF4JTermFactory.class::isInstance)
+				.orElseGet(c::createRDFTermFactory);
+		return c;
+	}
+
+	@Override
+	protected void parseSynchronusly() throws IOException {		
+		Optional<RDFFormat> formatByMimeType = getContentType().flatMap(Rio::getParserFormatForMIMEType);
+		String base = getBase().map(IRI::getIRIString).orElse(null);
+				
+		ParserConfig parserConfig = new ParserConfig();
+		// TODO: Should we need to set anything?
+		RDFLoader loader = new RDFLoader(parserConfig, rdf4jTermFactory.getValueFactory());
+		RDFHandler rdfHandler = makeRDFHandler();		
+		if (getSourceFile().isPresent()) {			
+			// NOTE: While we could have used  
+			// loader.load(sourcePath.toFile()
+			// if the path fs provider == FileSystems.getDefault(), 			
+			// that RDFLoader method does not use absolute path
+			// as the base URI, so to be consistent 
+			// we'll always do it with our own input stream
+			//
+			// That means we may have to guess format by extensions:			
+			Optional<RDFFormat> formatByFilename = getSourceFile().map(Path::getFileName).map(Path::toString)
+					.flatMap(Rio::getParserFormatForFileName);
+			// TODO: for the excited.. what about the extension after following symlinks? 
+			
+			RDFFormat format = formatByMimeType.orElse(formatByFilename.orElse(null));
+			try (InputStream in = Files.newInputStream(getSourceFile().get())) {
+				loader.load(in, base, format, rdfHandler);
+			}
+		} else if (getSourceIri().isPresent()) {
+			try {
+				// TODO: Handle international IRIs properly
+				// (Unicode support for for hostname, path and query)
+				URL url = new URL(getSourceIri().get().getIRIString());
+				// TODO: This probably does not support https:// -> http:// redirections
+				loader.load(url, base, formatByMimeType.orElse(null), makeRDFHandler());
+			} catch (MalformedURLException ex) {
+				throw new IOException("Can't handle source URL: " + getSourceIri().get(), ex);
+			}			
+		}
+		// must be getSourceInputStream then, this is guaranteed by super.checkSource(); 		
+		loader.load(getSourceInputStream().get(), base, formatByMimeType.orElse(null), rdfHandler);
+	}
+
+	protected RDFHandler makeRDFHandler() {
+
+		// TODO: Can we join the below DF4JDataset and RDF4JGraph cases
+		// using RDF4JGraphLike<TripleLike<BlankNodeOrIRI,IRI,RDFTerm>>
+		// or will that need tricky generics types?
+
+		if (getTargetDataset().filter(RDF4JDataset.class::isInstance).isPresent()) {
+			// One of us, we can add them as Statements directly
+			RDF4JDataset dataset = (RDF4JDataset) getTargetDataset().get();
+			if (dataset.asRepository().isPresent()) {
+				return new RDFInserter(dataset.asRepository().get().getConnection());
+			}
+			if (dataset.asModel().isPresent()) {
+				Model model = dataset.asModel().get();
+				return new AddToModel(model);
+			}
+			// Not backed by Repository or Model?
+			// Third-party RDF4JDataset subclass, so we'll fall through to the
+			// getTarget() handling further down
+		} else if (getTargetGraph().filter(RDF4JGraph.class::isInstance).isPresent()) {
+			RDF4JGraph graph = (RDF4JGraph) getTargetGraph().get();
+
+			if (graph.asRepository().isPresent()) {
+				RDFInserter inserter = new RDFInserter(graph.asRepository().get().getConnection());
+				graph.getContextFilter().ifPresent(inserter::enforceContext);
+				return inserter;
+			}
+			if (graph.asModel().isPresent() && graph.getContextFilter().isPresent()) {
+				Model model = graph.asModel().get();
+				return new AddToModel(model);
+			}
+			// else - fall through
+		}
+
+		// Fall thorough: let target() consume our converted quads.
+		return new AddToQuadConsumer(getTarget());
+	}
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-commonsrdf/blob/a189f91e/rdf4j/src/main/java/org/apache/commons/rdf/rdf4j/experimental/package-info.java
----------------------------------------------------------------------
diff --git a/rdf4j/src/main/java/org/apache/commons/rdf/rdf4j/experimental/package-info.java b/rdf4j/src/main/java/org/apache/commons/rdf/rdf4j/experimental/package-info.java
new file mode 100644
index 0000000..192a148
--- /dev/null
+++ b/rdf4j/src/main/java/org/apache/commons/rdf/rdf4j/experimental/package-info.java
@@ -0,0 +1,34 @@
+/*
+ * 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.
+ */
+/**
+ * Experimental Commons RDF RDF4J implementations.
+ * <p>
+ * Classes in this package should be considered <strong>at
+ * risk</strong>; they might change or be removed in the next minor update of
+ * Commons RDF.
+ * <p>
+ * When a class has stabilized, it will move to the
+ * {@link org.apache.commons.rdf.rdf4j} package.
+ * <p>
+ * <ul>
+ * <li>{@link RDF4JParser} - an RDF4J-backed
+ * implementations of 
+ * {@link org.apache.commons.rdf.api.experimental.RDFParser}.</li>
+ * </ul>
+ */
+package org.apache.commons.rdf.rdf4j.experimental;
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-commonsrdf/blob/a189f91e/rdf4j/src/main/java/org/apache/commons/rdf/rdf4j/package-info.java
----------------------------------------------------------------------
diff --git a/rdf4j/src/main/java/org/apache/commons/rdf/rdf4j/package-info.java b/rdf4j/src/main/java/org/apache/commons/rdf/rdf4j/package-info.java
index 036b54b..e384f3d 100644
--- a/rdf4j/src/main/java/org/apache/commons/rdf/rdf4j/package-info.java
+++ b/rdf4j/src/main/java/org/apache/commons/rdf/rdf4j/package-info.java
@@ -41,11 +41,11 @@
  * {@link org.apache.commons.rdf.rdf4j.RDF4JDataset} provide access to the
  * underlying RDF4J implementations.
  * <p>
- * The {@link org.apache.commons.rdf.rdf4j.RDF4JParser} can be used to
+ * The {@link org.apache.commons.rdf.rdf4j.experimental.RDF4JParser} can be used to
  * parse RDF files using RDF4j. It should be most efficient if used with
- * {@link org.apache.commons.rdf.rdf4j.RDF4JParser#target(org.apache.commons.rdf.api.Dataset)}
+ * {@link org.apache.commons.rdf.rdf4j.experimental.RDF4JParser#target(org.apache.commons.rdf.api.Dataset)}
  * and an adapted {@link org.apache.commons.rdf.rdf4j.RDF4JDataset}, or
- * {@link org.apache.commons.rdf.rdf4j.RDF4JParser#target(org.apache.commons.rdf.api.Graph)}
+ * {@link org.apache.commons.rdf.rdf4j.experimental.RDF4JParser#target(org.apache.commons.rdf.api.Graph)}
  * and a an adapted {@link org.apache.commons.rdf.rdf4j.RDF4JGraph}
  * 
  *

http://git-wip-us.apache.org/repos/asf/incubator-commonsrdf/blob/a189f91e/simple/src/main/java/org/apache/commons/rdf/simple/AbstractRDFParser.java
----------------------------------------------------------------------
diff --git a/simple/src/main/java/org/apache/commons/rdf/simple/AbstractRDFParser.java b/simple/src/main/java/org/apache/commons/rdf/simple/AbstractRDFParser.java
deleted file mode 100644
index e58fced..0000000
--- a/simple/src/main/java/org/apache/commons/rdf/simple/AbstractRDFParser.java
+++ /dev/null
@@ -1,541 +0,0 @@
-/**
- * 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.commons.rdf.simple;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.URI;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.Optional;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
-import java.util.function.Consumer;
-
-import org.apache.commons.rdf.api.Dataset;
-import org.apache.commons.rdf.api.Graph;
-import org.apache.commons.rdf.api.IRI;
-import org.apache.commons.rdf.api.Quad;
-import org.apache.commons.rdf.api.RDFParser;
-import org.apache.commons.rdf.api.RDFSyntax;
-import org.apache.commons.rdf.api.RDFTermFactory;
-
-/**
- * Abstract RDFParser
- * <p>
- * This abstract class keeps the properties in protected fields like
- * {@link #sourceFile} using {@link Optional}. Some basic checking like
- * {@link #checkIsAbsolute(IRI)} is performed.
- * <p>
- * This class and its subclasses are {@link Cloneable}, immutable and
- * (therefore) thread-safe - each call to option methods like
- * {@link #contentType(String)} or {@link #source(IRI)} will return a cloned,
- * mutated copy.
- * <p>
- * By default, parsing is done by the abstract method
- * {@link #parseSynchronusly()} - which is executed in a cloned snapshot - hence
- * multiple {@link #parse()} calls are thread-safe. The default {@link #parse()}
- * uses a thread pool in {@link #threadGroup} - but implementations can override
- * {@link #parse()} (e.g. because it has its own threading model or use
- * asynchronous remote execution).
- */
-public abstract class AbstractRDFParser<T extends AbstractRDFParser<T>> 
-	implements RDFParser, Cloneable {	
-	
-	public static final ThreadGroup threadGroup = new ThreadGroup("Commons RDF parsers");
-	private static final ExecutorService threadpool = Executors.newCachedThreadPool(r -> new Thread(threadGroup, r));
-
-	// Basically only used for creating IRIs
-	private static RDFTermFactory internalRdfTermFactory = new SimpleRDFTermFactory();
-
-	/**
-	 * Get the set {@link RDFTermFactory}, if any.
-	 */
-	public Optional<RDFTermFactory> getRdfTermFactory() {
-		return rdfTermFactory;
-	}
-
-	/**
-	 * Get the set content-type {@link RDFSyntax}, if any.
-	 * <p>
-	 * If this is {@link Optional#isPresent()}, then 
-	 * {@link #getContentType()} contains the 
-	 * value of {@link RDFSyntax#mediaType}. 
-	 */
-	public Optional<RDFSyntax> getContentTypeSyntax() {
-		return contentTypeSyntax;
-	}
-	
-	/**
-	 * Get the set content-type String, if any.
-	 * <p>
-	 * If this is {@link Optional#isPresent()} and 
-	 * is recognized by {@link RDFSyntax#byMediaType(String)}, then
-	 * the corresponding {@link RDFSyntax} is set on 
-	 * {@link #getContentType()}, otherwise that is
-	 * {@link Optional#empty()}. 
-	 */
-	public final Optional<String> getContentType() {
-		return contentType;
-	}
-
-	/**
-	 * Get the target to consume parsed Quads.
-	 * <p>
-	 * From the call to {@link #parseSynchronusly()}, this
-	 * method is always {@link Optional#isPresent()}.
-	 * 
-	 */	
-	public Consumer<Quad> getTarget() {
-		return target;
-	}
-
-	/**
-	 * Get the target dataset as set by {@link #target(Dataset)}.
-	 * <p>
-	 * The return value is {@link Optional#isPresent()} if and only if
-	 * {@link #target(Dataset)} has been set, meaning that the implementation
-	 * may choose to append parsed quads to the {@link Dataset} directly instead
-	 * of relying on the generated {@link #getTarget()} consumer.
-	 * <p>
-	 * If this value is present, then {@link #getTargetGraph()} MUST 
-	 * be {@link Optional#empty()}.
-	 * 
-	 * @return The target Dataset, or {@link Optional#empty()} if another kind of target has been set.
-	 */
-	public Optional<Dataset> getTargetDataset() {
-		return targetDataset;
-	}
-
-	/**
-	 * Get the target graph as set by {@link #target(Graph)}.
-	 * <p>
-	 * The return value is {@link Optional#isPresent()} if and only if
-	 * {@link #target(Graph)} has been set, meaning that the implementation
-	 * may choose to append parsed triples to the {@link Graph} directly instead
-	 * of relying on the generated {@link #getTarget()} consumer.
-	 * <p>
-	 * If this value is present, then {@link #getTargetDataset()} MUST 
-	 * be {@link Optional#empty()}.
-	 * 
-	 * @return The target Graph, or {@link Optional#empty()} if another kind of target has been set.
-	 */	
-	public Optional<Graph>  getTargetGraph() {
-		return targetGraph;
-	}
-	
-	/**
-	 * Get the set base {@link IRI}, if present.
-	 * <p>
-	 * 
-	 */	
-	public Optional<IRI> getBase() {
-		return base;
-	}
-
-	/**
-	 * Get the set source {@link InputStream}.
-	 * <p>
-	 * If this is {@link Optional#isPresent()}, then 
-	 * {@link #getSourceFile()} and {@link #getSourceIri()}
-	 * are {@link Optional#empty()}.
-	 */
-	public Optional<InputStream> getSourceInputStream() {
-		return sourceInputStream;
-	}
-
-	/**
-	 * Get the set source {@link Path}.
-	 * <p>
-	 * If this is {@link Optional#isPresent()}, then 
-	 * {@link #getSourceInputStream()} and {@link #getSourceIri()}
-	 * are {@link Optional#empty()}.
-	 */	
-	public Optional<Path> getSourceFile() {
-		return sourceFile;
-	}
-
-	/**
-	 * Get the set source {@link Path}.
-	 * <p>
-	 * If this is {@link Optional#isPresent()}, then 
-	 * {@link #getSourceInputStream()} and {@link #getSourceInputStream()()}
-	 * are {@link Optional#empty()}.
-	 */		
-	public Optional<IRI> getSourceIri() {
-		return sourceIri;
-	}
-
-
-	private Optional<RDFTermFactory> rdfTermFactory = Optional.empty();
-	private Optional<RDFSyntax> contentTypeSyntax = Optional.empty();
-	private Optional<String> contentType = Optional.empty();
-	private Optional<IRI> base = Optional.empty();
-	private Optional<InputStream> sourceInputStream = Optional.empty();
-	private Optional<Path> sourceFile = Optional.empty();
-	private Optional<IRI> sourceIri = Optional.empty();
-	private Consumer<Quad> target;
-	private Optional<Dataset> targetDataset;
-	private Optional<Graph> targetGraph;
-
-	@SuppressWarnings("unchecked")
-	@Override
-	public T clone() {
-		try {
-			return (T) super.clone();
-		} catch (CloneNotSupportedException e) {
-			throw new RuntimeException(e);
-		}
-	}
-
-	@SuppressWarnings("unchecked")
-	protected T asT() { 
-		return (T) this;
-	}
-	
-	@Override
-	public T rdfTermFactory(RDFTermFactory rdfTermFactory) {
-		AbstractRDFParser<T> c = clone();
-		c.rdfTermFactory = Optional.ofNullable(rdfTermFactory);
-		return c.asT();
-	}
-
-	@Override
-	public T contentType(RDFSyntax rdfSyntax) throws IllegalArgumentException {
-		AbstractRDFParser<T> c = clone();
-		c.contentTypeSyntax = Optional.ofNullable(rdfSyntax);
-		c.contentType = c.contentTypeSyntax.map(syntax -> syntax.mediaType);
-		return c.asT();
-	}
-
-	@Override
-	public T contentType(String contentType) throws IllegalArgumentException {
-		AbstractRDFParser<T> c = clone();
-		c.contentType = Optional.ofNullable(contentType);
-		c.contentTypeSyntax = c.contentType.flatMap(RDFSyntax::byMediaType);
-		return c.asT();
-	}
-
-	@Override
-	public T base(IRI base) {
-		AbstractRDFParser<T> c = clone();
-		c.base = Optional.ofNullable(base);
-		c.base.ifPresent(i -> checkIsAbsolute(i));
-		return c.asT();
-	}
-
-	@Override
-	public T base(String base) throws IllegalArgumentException {
-		return base(internalRdfTermFactory.createIRI(base));
-	}
-
-	@Override
-	public T source(InputStream inputStream) {
-		AbstractRDFParser<T> c = clone();
-		c.resetSource();
-		c.sourceInputStream = Optional.ofNullable(inputStream);
-		return c.asT();
-	}
-
-	@Override
-	public T source(Path file) {
-		AbstractRDFParser<T> c = clone();
-		c.resetSource();
-		c.sourceFile = Optional.ofNullable(file);
-		return c.asT();
-	}
-
-	@Override
-	public T source(IRI iri) {
-		AbstractRDFParser<T> c = clone();
-		c.resetSource();
-		c.sourceIri = Optional.ofNullable(iri);
-		c.sourceIri.ifPresent(i -> checkIsAbsolute(i));
-		return c.asT();
-	}
-
-	@Override
-	public T source(String iri) throws IllegalArgumentException {
-		AbstractRDFParser<T> c = clone();
-		c.resetSource();
-		c.sourceIri = Optional.ofNullable(iri).map(internalRdfTermFactory::createIRI);
-		c.sourceIri.ifPresent(i -> checkIsAbsolute(i));
-		return source(internalRdfTermFactory.createIRI(iri));
-	}
-
-	/**
-	 * Check if an iri is absolute.
-	 * <p>
-	 * Used by {@link #source(String)} and {@link #base(String)}
-	 * 
-	 * @param iri
-	 */
-	protected void checkIsAbsolute(IRI iri) {
-		if (!URI.create(iri.getIRIString()).isAbsolute()) {
-			throw new IllegalArgumentException("IRI is not absolute: " + iri);
-		}
-	}
-
-	/**
-	 * Check that one and only one source is present and valid.
-	 * <p>
-	 * Used by {@link #parse()}.
-	 * <p>
-	 * Subclasses might override this method, e.g. to support other
-	 * source combinations, or to check if the sourceIri is 
-	 * resolvable. 
-	 * 
-	 * @throws IOException If a source file can't be read
-	 */
-	protected void checkSource() throws IOException {
-		if (!sourceFile.isPresent() && !sourceInputStream.isPresent() && !sourceIri.isPresent()) {
-			throw new IllegalStateException("No source has been set");
-		}
-		if (sourceIri.isPresent() && sourceInputStream.isPresent()) {
-			throw new IllegalStateException("Both sourceIri and sourceInputStream have been set");
-		}
-		if (sourceIri.isPresent() && sourceFile.isPresent()) {
-			throw new IllegalStateException("Both sourceIri and sourceFile have been set");
-		}
-		if (sourceInputStream.isPresent() && sourceFile.isPresent()) {
-			throw new IllegalStateException("Both sourceInputStream and sourceFile have been set");
-		}
-		if (sourceFile.isPresent() && !sourceFile.filter(Files::isReadable).isPresent()) {
-			throw new IOException("Can't read file: " + sourceFile);
-		}
-	}
-
-	/**
-	 * Check if base is required.
-	 * 
-	 * @throws IllegalStateException if base is required, but not set.
-	 */
-	protected void checkBaseRequired() {
-		if (!base.isPresent() && sourceInputStream.isPresent()
-				&& !contentTypeSyntax.filter(t -> t == RDFSyntax.NQUADS || t == RDFSyntax.NTRIPLES).isPresent()) {
-			throw new IllegalStateException("base iri required for inputstream source");
-		}
-	}
-
-	/**
-	 * Reset all source* fields to Optional.empty()
-	 * <p>
-	 * Subclasses should override this and call <code>super.resetSource()</code>
-	 * if they need to reset any additional source* fields.
-	 * 
-	 */
-	protected void resetSource() {
-		sourceInputStream = Optional.empty();
-		sourceIri = Optional.empty();
-		sourceFile = Optional.empty();
-	}
-
-
-	/**
-	 * Reset all optional target* fields to Optional.empty()</code>
-	 * <p>
-	 * Note that the consumer set for {@link #getTarget()} is
-	 * NOT reset.
-	 * <p>
-	 * Subclasses should override this and call <code>super.resetTarget()</code>
-	 * if they need to reset any additional target* fields.
-	 * 
-	 */
-	protected void resetTarget() {
-		targetDataset = Optional.empty();
-		targetGraph = Optional.empty();
-	}	
-	
-	/**
-	 * Parse {@link #sourceInputStream}, {@link #sourceFile} or
-	 * {@link #sourceIri}.
-	 * <p>
-	 * One of the source fields MUST be present, as checked by {@link #checkSource()}.
-	 * <p>
-	 * {@link #checkBaseRequired()} is called to verify if {@link #getBase()} is required.
-	 * 
-	 * @throws IOException If the source could not be read 
-	 * @throws RDFParseException If the source could not be parsed (e.g. a .ttl file was not valid Turtle)
-	 */
-	protected abstract void parseSynchronusly() throws IOException, RDFParseException;
-
-	/**
-	 * Prepare a clone of this RDFParser which have been checked and
-	 * completed.
-	 * <p>
-	 * The returned clone will always have
-	 * {@link #getTarget()} and {@link #getRdfTermFactory()} present.
-	 * <p>
-	 * If the {@link #getSourceFile()} is present, but the 
-	 * {@link #getBase()} is not present, the base will be set to the
-	 * <code>file:///</code> IRI for the Path's real path (e.g. resolving any 
-	 * symbolic links).  
-	 *  
-	 * @return A completed and checked clone of this RDFParser
-	 * @throws IOException If the source was not accessible (e.g. a file was not found)
-	 * @throws IllegalStateException If the parser was not in a compatible setting (e.g. contentType was an invalid string) 
-	 */
-	protected T prepareForParsing() throws IOException, IllegalStateException {
-		checkSource();
-		checkBaseRequired();		
-		checkContentType();
-		checkTarget();
-
-		// We'll make a clone of our current state which will be passed to
-		// parseSynchronously()
-		AbstractRDFParser<T> c = clone();
-
-		// Use a fresh SimpleRDFTermFactory for each parse
-		if (!c.rdfTermFactory.isPresent()) {
-			c.rdfTermFactory = Optional.of(createRDFTermFactory());
-		}
-		// sourceFile, but no base? Let's follow any symlinks and use
-		// the file:/// URI
-		if (c.sourceFile.isPresent() && !c.base.isPresent()) {
-			URI baseUri = c.sourceFile.get().toRealPath().toUri();
-			c.base = Optional.of(internalRdfTermFactory.createIRI(baseUri.toString()));
-		}
-
-		return c.asT();
-	}
-	
-	/**
-	 * Subclasses can override this method to check the target is 
-	 * valid.
-	 * <p>
-	 * The default implementation throws an IllegalStateException if the 
-	 * target has not been set.
-	 */
-	protected void checkTarget() {
-		if (target == null) {
-			throw new IllegalStateException("target has not been set");
-		}
-		if (targetGraph.isPresent() && targetDataset.isPresent()) {
-			// This should not happen as each target(..) method resets the optionals
-			throw new IllegalStateException("targetGraph and targetDataset can't both be set");
-		}
-	}
-
-	/**
-	 * Subclasses can override this method to check compatibility with the
-	 * contentType setting.
-	 * 
-	 * @throws IllegalStateException
-	 *             if the {@link #getContentType()} or
-	 *             {@link #getContentTypeSyntax()} is not compatible or invalid
-	 */
-	protected void checkContentType() throws IllegalStateException {
-	}
-
-	/**
-	 * Guess RDFSyntax from a local file's extension.
-	 * <p>
-	 * This method can be used by subclasses if {@link #getContentType()} is not
-	 * present and {@link #getSourceFile()} is set.
-	 * 
-	 * @param path Path which extension should be checked
-	 * @return The {@link RDFSyntax} which has a matching {@link RDFSyntax#fileExtension}, 
-	 * 	otherwise {@link Optional#empty()}. 
-	 */
-	protected static Optional<RDFSyntax> guessRDFSyntax(Path path) {
-			return fileExtension(path).flatMap(RDFSyntax::byFileExtension);
-	}
-
-	/**
-	 * Return the file extension of a Path - if any.
-	 * <p>
-	 * The returned file extension includes the leading <code>.</code>
-	 * <p>
-	 * Note that this only returns the last extension, e.g. the 
-	 * file extension for <code>archive.tar.gz</code> would be <code>.gz</code>
-	 * 
-	 * @param path Path which filename might contain an extension
-	 * @return File extension (including the leading <code>.</code>, 
-	 * 	or {@link Optional#empty()} if the path has no extension
-	 */
-	private static Optional<String> fileExtension(Path path) {
-		Path fileName = path.getFileName();
-		if (fileName == null) { 
-			return Optional.empty();
-		}
-		String filenameStr = fileName.toString();
-		int last = filenameStr.lastIndexOf(".");
-		if (last > -1) { 
-			return Optional.of(filenameStr.substring(last));				
-		}
-		return Optional.empty();
-	}
-	
-
-	/**
-	 * Create a new {@link RDFTermFactory} for a parse session.
-	 * <p>
-	 * This is called by {@link #parse()} to set 
-	 * {@link #rdfTermFactory(RDFTermFactory)} if it is
-	 * {@link Optional#empty()}.
-	 * <p>
-	 * As parsed blank nodes might be made with 
-	 * {@link RDFTermFactory#createBlankNode(String)}, 
-	 * each call to this method SHOULD return 
-	 * a new RDFTermFactory instance.
-	 * 
-	 * @return A new {@link RDFTermFactory}
-	 */
-	protected RDFTermFactory createRDFTermFactory() {
-		return new SimpleRDFTermFactory();
-	}
-
-	@Override
-	public Future<ParseResult> parse() throws IOException, IllegalStateException {
-		final AbstractRDFParser<T> c = prepareForParsing();
-		return threadpool.submit(() -> {
-			c.parseSynchronusly();
-			return null;
-		});
-	}
-
-	@Override
-	public T target(Consumer<Quad> consumer) {
-		AbstractRDFParser<T> c = clone();
-		c.resetTarget();
-		c.target = consumer;
-		return c.asT();
-	}
-	
-	@Override
-	public T target(Dataset dataset) {
-		@SuppressWarnings({ "rawtypes", "unchecked" })
-		AbstractRDFParser<T> c = (AbstractRDFParser) RDFParser.super.target(dataset);
-		c.resetTarget();
-		c.targetDataset = Optional.of(dataset);
-		return c.asT();
-	}
-	
-	@Override
-	public T target(Graph graph) {
-		@SuppressWarnings({ "rawtypes", "unchecked" }) // super calls our .clone()
-		AbstractRDFParser<T> c = (AbstractRDFParser) RDFParser.super.target(graph);
-		c.resetTarget();
-		c.targetGraph = Optional.of(graph);
-		return c.asT();
-	}
-	
-
-
-}

http://git-wip-us.apache.org/repos/asf/incubator-commonsrdf/blob/a189f91e/simple/src/main/java/org/apache/commons/rdf/simple/RDFParseException.java
----------------------------------------------------------------------
diff --git a/simple/src/main/java/org/apache/commons/rdf/simple/RDFParseException.java b/simple/src/main/java/org/apache/commons/rdf/simple/RDFParseException.java
index d1cdd57..adb5b0e 100644
--- a/simple/src/main/java/org/apache/commons/rdf/simple/RDFParseException.java
+++ b/simple/src/main/java/org/apache/commons/rdf/simple/RDFParseException.java
@@ -18,7 +18,7 @@
 
 package org.apache.commons.rdf.simple;
 
-import org.apache.commons.rdf.api.RDFParser;
+import org.apache.commons.rdf.experimental.RDFParser;
 
 public class RDFParseException extends Exception {
 	private static final long serialVersionUID = 5427752643780702976L;

http://git-wip-us.apache.org/repos/asf/incubator-commonsrdf/blob/a189f91e/simple/src/main/java/org/apache/commons/rdf/simple/experimental/AbstractRDFParser.java
----------------------------------------------------------------------
diff --git a/simple/src/main/java/org/apache/commons/rdf/simple/experimental/AbstractRDFParser.java b/simple/src/main/java/org/apache/commons/rdf/simple/experimental/AbstractRDFParser.java
new file mode 100644
index 0000000..f675459
--- /dev/null
+++ b/simple/src/main/java/org/apache/commons/rdf/simple/experimental/AbstractRDFParser.java
@@ -0,0 +1,543 @@
+/**
+ * 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.commons.rdf.simple.experimental;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Optional;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.function.Consumer;
+
+import org.apache.commons.rdf.api.Dataset;
+import org.apache.commons.rdf.api.Graph;
+import org.apache.commons.rdf.api.IRI;
+import org.apache.commons.rdf.api.Quad;
+import org.apache.commons.rdf.api.RDFSyntax;
+import org.apache.commons.rdf.api.RDFTermFactory;
+import org.apache.commons.rdf.experimental.RDFParser;
+import org.apache.commons.rdf.simple.RDFParseException;
+import org.apache.commons.rdf.simple.SimpleRDFTermFactory;
+
+/**
+ * Abstract RDFParser
+ * <p>
+ * This abstract class keeps the properties in protected fields like
+ * {@link #sourceFile} using {@link Optional}. Some basic checking like
+ * {@link #checkIsAbsolute(IRI)} is performed.
+ * <p>
+ * This class and its subclasses are {@link Cloneable}, immutable and
+ * (therefore) thread-safe - each call to option methods like
+ * {@link #contentType(String)} or {@link #source(IRI)} will return a cloned,
+ * mutated copy.
+ * <p>
+ * By default, parsing is done by the abstract method
+ * {@link #parseSynchronusly()} - which is executed in a cloned snapshot - hence
+ * multiple {@link #parse()} calls are thread-safe. The default {@link #parse()}
+ * uses a thread pool in {@link #threadGroup} - but implementations can override
+ * {@link #parse()} (e.g. because it has its own threading model or use
+ * asynchronous remote execution).
+ */
+public abstract class AbstractRDFParser<T extends AbstractRDFParser<T>> 
+	implements RDFParser, Cloneable {	
+	
+	public static final ThreadGroup threadGroup = new ThreadGroup("Commons RDF parsers");
+	private static final ExecutorService threadpool = Executors.newCachedThreadPool(r -> new Thread(threadGroup, r));
+
+	// Basically only used for creating IRIs
+	private static RDFTermFactory internalRdfTermFactory = new SimpleRDFTermFactory();
+
+	/**
+	 * Get the set {@link RDFTermFactory}, if any.
+	 */
+	public Optional<RDFTermFactory> getRdfTermFactory() {
+		return rdfTermFactory;
+	}
+
+	/**
+	 * Get the set content-type {@link RDFSyntax}, if any.
+	 * <p>
+	 * If this is {@link Optional#isPresent()}, then 
+	 * {@link #getContentType()} contains the 
+	 * value of {@link RDFSyntax#mediaType}. 
+	 */
+	public Optional<RDFSyntax> getContentTypeSyntax() {
+		return contentTypeSyntax;
+	}
+	
+	/**
+	 * Get the set content-type String, if any.
+	 * <p>
+	 * If this is {@link Optional#isPresent()} and 
+	 * is recognized by {@link RDFSyntax#byMediaType(String)}, then
+	 * the corresponding {@link RDFSyntax} is set on 
+	 * {@link #getContentType()}, otherwise that is
+	 * {@link Optional#empty()}. 
+	 */
+	public final Optional<String> getContentType() {
+		return contentType;
+	}
+
+	/**
+	 * Get the target to consume parsed Quads.
+	 * <p>
+	 * From the call to {@link #parseSynchronusly()}, this
+	 * method is always {@link Optional#isPresent()}.
+	 * 
+	 */	
+	public Consumer<Quad> getTarget() {
+		return target;
+	}
+
+	/**
+	 * Get the target dataset as set by {@link #target(Dataset)}.
+	 * <p>
+	 * The return value is {@link Optional#isPresent()} if and only if
+	 * {@link #target(Dataset)} has been set, meaning that the implementation
+	 * may choose to append parsed quads to the {@link Dataset} directly instead
+	 * of relying on the generated {@link #getTarget()} consumer.
+	 * <p>
+	 * If this value is present, then {@link #getTargetGraph()} MUST 
+	 * be {@link Optional#empty()}.
+	 * 
+	 * @return The target Dataset, or {@link Optional#empty()} if another kind of target has been set.
+	 */
+	public Optional<Dataset> getTargetDataset() {
+		return targetDataset;
+	}
+
+	/**
+	 * Get the target graph as set by {@link #target(Graph)}.
+	 * <p>
+	 * The return value is {@link Optional#isPresent()} if and only if
+	 * {@link #target(Graph)} has been set, meaning that the implementation
+	 * may choose to append parsed triples to the {@link Graph} directly instead
+	 * of relying on the generated {@link #getTarget()} consumer.
+	 * <p>
+	 * If this value is present, then {@link #getTargetDataset()} MUST 
+	 * be {@link Optional#empty()}.
+	 * 
+	 * @return The target Graph, or {@link Optional#empty()} if another kind of target has been set.
+	 */	
+	public Optional<Graph>  getTargetGraph() {
+		return targetGraph;
+	}
+	
+	/**
+	 * Get the set base {@link IRI}, if present.
+	 * <p>
+	 * 
+	 */	
+	public Optional<IRI> getBase() {
+		return base;
+	}
+
+	/**
+	 * Get the set source {@link InputStream}.
+	 * <p>
+	 * If this is {@link Optional#isPresent()}, then 
+	 * {@link #getSourceFile()} and {@link #getSourceIri()}
+	 * are {@link Optional#empty()}.
+	 */
+	public Optional<InputStream> getSourceInputStream() {
+		return sourceInputStream;
+	}
+
+	/**
+	 * Get the set source {@link Path}.
+	 * <p>
+	 * If this is {@link Optional#isPresent()}, then 
+	 * {@link #getSourceInputStream()} and {@link #getSourceIri()}
+	 * are {@link Optional#empty()}.
+	 */	
+	public Optional<Path> getSourceFile() {
+		return sourceFile;
+	}
+
+	/**
+	 * Get the set source {@link Path}.
+	 * <p>
+	 * If this is {@link Optional#isPresent()}, then 
+	 * {@link #getSourceInputStream()} and {@link #getSourceInputStream()()}
+	 * are {@link Optional#empty()}.
+	 */		
+	public Optional<IRI> getSourceIri() {
+		return sourceIri;
+	}
+
+
+	private Optional<RDFTermFactory> rdfTermFactory = Optional.empty();
+	private Optional<RDFSyntax> contentTypeSyntax = Optional.empty();
+	private Optional<String> contentType = Optional.empty();
+	private Optional<IRI> base = Optional.empty();
+	private Optional<InputStream> sourceInputStream = Optional.empty();
+	private Optional<Path> sourceFile = Optional.empty();
+	private Optional<IRI> sourceIri = Optional.empty();
+	private Consumer<Quad> target;
+	private Optional<Dataset> targetDataset;
+	private Optional<Graph> targetGraph;
+
+	@SuppressWarnings("unchecked")
+	@Override
+	public T clone() {
+		try {
+			return (T) super.clone();
+		} catch (CloneNotSupportedException e) {
+			throw new RuntimeException(e);
+		}
+	}
+
+	@SuppressWarnings("unchecked")
+	protected T asT() { 
+		return (T) this;
+	}
+	
+	@Override
+	public T rdfTermFactory(RDFTermFactory rdfTermFactory) {
+		AbstractRDFParser<T> c = clone();
+		c.rdfTermFactory = Optional.ofNullable(rdfTermFactory);
+		return c.asT();
+	}
+
+	@Override
+	public T contentType(RDFSyntax rdfSyntax) throws IllegalArgumentException {
+		AbstractRDFParser<T> c = clone();
+		c.contentTypeSyntax = Optional.ofNullable(rdfSyntax);
+		c.contentType = c.contentTypeSyntax.map(syntax -> syntax.mediaType);
+		return c.asT();
+	}
+
+	@Override
+	public T contentType(String contentType) throws IllegalArgumentException {
+		AbstractRDFParser<T> c = clone();
+		c.contentType = Optional.ofNullable(contentType);
+		c.contentTypeSyntax = c.contentType.flatMap(RDFSyntax::byMediaType);
+		return c.asT();
+	}
+
+	@Override
+	public T base(IRI base) {
+		AbstractRDFParser<T> c = clone();
+		c.base = Optional.ofNullable(base);
+		c.base.ifPresent(i -> checkIsAbsolute(i));
+		return c.asT();
+	}
+
+	@Override
+	public T base(String base) throws IllegalArgumentException {
+		return base(internalRdfTermFactory.createIRI(base));
+	}
+
+	@Override
+	public T source(InputStream inputStream) {
+		AbstractRDFParser<T> c = clone();
+		c.resetSource();
+		c.sourceInputStream = Optional.ofNullable(inputStream);
+		return c.asT();
+	}
+
+	@Override
+	public T source(Path file) {
+		AbstractRDFParser<T> c = clone();
+		c.resetSource();
+		c.sourceFile = Optional.ofNullable(file);
+		return c.asT();
+	}
+
+	@Override
+	public T source(IRI iri) {
+		AbstractRDFParser<T> c = clone();
+		c.resetSource();
+		c.sourceIri = Optional.ofNullable(iri);
+		c.sourceIri.ifPresent(i -> checkIsAbsolute(i));
+		return c.asT();
+	}
+
+	@Override
+	public T source(String iri) throws IllegalArgumentException {
+		AbstractRDFParser<T> c = clone();
+		c.resetSource();
+		c.sourceIri = Optional.ofNullable(iri).map(internalRdfTermFactory::createIRI);
+		c.sourceIri.ifPresent(i -> checkIsAbsolute(i));
+		return source(internalRdfTermFactory.createIRI(iri));
+	}
+
+	/**
+	 * Check if an iri is absolute.
+	 * <p>
+	 * Used by {@link #source(String)} and {@link #base(String)}
+	 * 
+	 * @param iri
+	 */
+	protected void checkIsAbsolute(IRI iri) {
+		if (!URI.create(iri.getIRIString()).isAbsolute()) {
+			throw new IllegalArgumentException("IRI is not absolute: " + iri);
+		}
+	}
+
+	/**
+	 * Check that one and only one source is present and valid.
+	 * <p>
+	 * Used by {@link #parse()}.
+	 * <p>
+	 * Subclasses might override this method, e.g. to support other
+	 * source combinations, or to check if the sourceIri is 
+	 * resolvable. 
+	 * 
+	 * @throws IOException If a source file can't be read
+	 */
+	protected void checkSource() throws IOException {
+		if (!sourceFile.isPresent() && !sourceInputStream.isPresent() && !sourceIri.isPresent()) {
+			throw new IllegalStateException("No source has been set");
+		}
+		if (sourceIri.isPresent() && sourceInputStream.isPresent()) {
+			throw new IllegalStateException("Both sourceIri and sourceInputStream have been set");
+		}
+		if (sourceIri.isPresent() && sourceFile.isPresent()) {
+			throw new IllegalStateException("Both sourceIri and sourceFile have been set");
+		}
+		if (sourceInputStream.isPresent() && sourceFile.isPresent()) {
+			throw new IllegalStateException("Both sourceInputStream and sourceFile have been set");
+		}
+		if (sourceFile.isPresent() && !sourceFile.filter(Files::isReadable).isPresent()) {
+			throw new IOException("Can't read file: " + sourceFile);
+		}
+	}
+
+	/**
+	 * Check if base is required.
+	 * 
+	 * @throws IllegalStateException if base is required, but not set.
+	 */
+	protected void checkBaseRequired() {
+		if (!base.isPresent() && sourceInputStream.isPresent()
+				&& !contentTypeSyntax.filter(t -> t == RDFSyntax.NQUADS || t == RDFSyntax.NTRIPLES).isPresent()) {
+			throw new IllegalStateException("base iri required for inputstream source");
+		}
+	}
+
+	/**
+	 * Reset all source* fields to Optional.empty()
+	 * <p>
+	 * Subclasses should override this and call <code>super.resetSource()</code>
+	 * if they need to reset any additional source* fields.
+	 * 
+	 */
+	protected void resetSource() {
+		sourceInputStream = Optional.empty();
+		sourceIri = Optional.empty();
+		sourceFile = Optional.empty();
+	}
+
+
+	/**
+	 * Reset all optional target* fields to Optional.empty()</code>
+	 * <p>
+	 * Note that the consumer set for {@link #getTarget()} is
+	 * NOT reset.
+	 * <p>
+	 * Subclasses should override this and call <code>super.resetTarget()</code>
+	 * if they need to reset any additional target* fields.
+	 * 
+	 */
+	protected void resetTarget() {
+		targetDataset = Optional.empty();
+		targetGraph = Optional.empty();
+	}	
+	
+	/**
+	 * Parse {@link #sourceInputStream}, {@link #sourceFile} or
+	 * {@link #sourceIri}.
+	 * <p>
+	 * One of the source fields MUST be present, as checked by {@link #checkSource()}.
+	 * <p>
+	 * {@link #checkBaseRequired()} is called to verify if {@link #getBase()} is required.
+	 * 
+	 * @throws IOException If the source could not be read 
+	 * @throws RDFParseException If the source could not be parsed (e.g. a .ttl file was not valid Turtle)
+	 */
+	protected abstract void parseSynchronusly() throws IOException, RDFParseException;
+
+	/**
+	 * Prepare a clone of this RDFParser which have been checked and
+	 * completed.
+	 * <p>
+	 * The returned clone will always have
+	 * {@link #getTarget()} and {@link #getRdfTermFactory()} present.
+	 * <p>
+	 * If the {@link #getSourceFile()} is present, but the 
+	 * {@link #getBase()} is not present, the base will be set to the
+	 * <code>file:///</code> IRI for the Path's real path (e.g. resolving any 
+	 * symbolic links).  
+	 *  
+	 * @return A completed and checked clone of this RDFParser
+	 * @throws IOException If the source was not accessible (e.g. a file was not found)
+	 * @throws IllegalStateException If the parser was not in a compatible setting (e.g. contentType was an invalid string) 
+	 */
+	protected T prepareForParsing() throws IOException, IllegalStateException {
+		checkSource();
+		checkBaseRequired();		
+		checkContentType();
+		checkTarget();
+
+		// We'll make a clone of our current state which will be passed to
+		// parseSynchronously()
+		AbstractRDFParser<T> c = clone();
+
+		// Use a fresh SimpleRDFTermFactory for each parse
+		if (!c.rdfTermFactory.isPresent()) {
+			c.rdfTermFactory = Optional.of(createRDFTermFactory());
+		}
+		// sourceFile, but no base? Let's follow any symlinks and use
+		// the file:/// URI
+		if (c.sourceFile.isPresent() && !c.base.isPresent()) {
+			URI baseUri = c.sourceFile.get().toRealPath().toUri();
+			c.base = Optional.of(internalRdfTermFactory.createIRI(baseUri.toString()));
+		}
+
+		return c.asT();
+	}
+	
+	/**
+	 * Subclasses can override this method to check the target is 
+	 * valid.
+	 * <p>
+	 * The default implementation throws an IllegalStateException if the 
+	 * target has not been set.
+	 */
+	protected void checkTarget() {
+		if (target == null) {
+			throw new IllegalStateException("target has not been set");
+		}
+		if (targetGraph.isPresent() && targetDataset.isPresent()) {
+			// This should not happen as each target(..) method resets the optionals
+			throw new IllegalStateException("targetGraph and targetDataset can't both be set");
+		}
+	}
+
+	/**
+	 * Subclasses can override this method to check compatibility with the
+	 * contentType setting.
+	 * 
+	 * @throws IllegalStateException
+	 *             if the {@link #getContentType()} or
+	 *             {@link #getContentTypeSyntax()} is not compatible or invalid
+	 */
+	protected void checkContentType() throws IllegalStateException {
+	}
+
+	/**
+	 * Guess RDFSyntax from a local file's extension.
+	 * <p>
+	 * This method can be used by subclasses if {@link #getContentType()} is not
+	 * present and {@link #getSourceFile()} is set.
+	 * 
+	 * @param path Path which extension should be checked
+	 * @return The {@link RDFSyntax} which has a matching {@link RDFSyntax#fileExtension}, 
+	 * 	otherwise {@link Optional#empty()}. 
+	 */
+	protected static Optional<RDFSyntax> guessRDFSyntax(Path path) {
+			return fileExtension(path).flatMap(RDFSyntax::byFileExtension);
+	}
+
+	/**
+	 * Return the file extension of a Path - if any.
+	 * <p>
+	 * The returned file extension includes the leading <code>.</code>
+	 * <p>
+	 * Note that this only returns the last extension, e.g. the 
+	 * file extension for <code>archive.tar.gz</code> would be <code>.gz</code>
+	 * 
+	 * @param path Path which filename might contain an extension
+	 * @return File extension (including the leading <code>.</code>, 
+	 * 	or {@link Optional#empty()} if the path has no extension
+	 */
+	private static Optional<String> fileExtension(Path path) {
+		Path fileName = path.getFileName();
+		if (fileName == null) { 
+			return Optional.empty();
+		}
+		String filenameStr = fileName.toString();
+		int last = filenameStr.lastIndexOf(".");
+		if (last > -1) { 
+			return Optional.of(filenameStr.substring(last));				
+		}
+		return Optional.empty();
+	}
+	
+
+	/**
+	 * Create a new {@link RDFTermFactory} for a parse session.
+	 * <p>
+	 * This is called by {@link #parse()} to set 
+	 * {@link #rdfTermFactory(RDFTermFactory)} if it is
+	 * {@link Optional#empty()}.
+	 * <p>
+	 * As parsed blank nodes might be made with 
+	 * {@link RDFTermFactory#createBlankNode(String)}, 
+	 * each call to this method SHOULD return 
+	 * a new RDFTermFactory instance.
+	 * 
+	 * @return A new {@link RDFTermFactory}
+	 */
+	protected RDFTermFactory createRDFTermFactory() {
+		return new SimpleRDFTermFactory();
+	}
+
+	@Override
+	public Future<ParseResult> parse() throws IOException, IllegalStateException {
+		final AbstractRDFParser<T> c = prepareForParsing();
+		return threadpool.submit(() -> {
+			c.parseSynchronusly();
+			return null;
+		});
+	}
+
+	@Override
+	public T target(Consumer<Quad> consumer) {
+		AbstractRDFParser<T> c = clone();
+		c.resetTarget();
+		c.target = consumer;
+		return c.asT();
+	}
+	
+	@Override
+	public T target(Dataset dataset) {
+		@SuppressWarnings({ "rawtypes", "unchecked" })
+		AbstractRDFParser<T> c = (AbstractRDFParser) RDFParser.super.target(dataset);
+		c.resetTarget();
+		c.targetDataset = Optional.of(dataset);
+		return c.asT();
+	}
+	
+	@Override
+	public T target(Graph graph) {
+		@SuppressWarnings({ "rawtypes", "unchecked" }) // super calls our .clone()
+		AbstractRDFParser<T> c = (AbstractRDFParser) RDFParser.super.target(graph);
+		c.resetTarget();
+		c.targetGraph = Optional.of(graph);
+		return c.asT();
+	}
+	
+
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-commonsrdf/blob/a189f91e/simple/src/main/java/org/apache/commons/rdf/simple/experimental/package-info.java
----------------------------------------------------------------------
diff --git a/simple/src/main/java/org/apache/commons/rdf/simple/experimental/package-info.java b/simple/src/main/java/org/apache/commons/rdf/simple/experimental/package-info.java
new file mode 100644
index 0000000..5196f42
--- /dev/null
+++ b/simple/src/main/java/org/apache/commons/rdf/simple/experimental/package-info.java
@@ -0,0 +1,34 @@
+/*
+ * 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.
+ */
+/**
+ * Experimental Commons RDF Simple implementations.
+ * <p>
+ * Classes in this package should be considered <strong>at
+ * risk</strong>; they might change or be removed in the next minor update of
+ * Commons RDF.
+ * <p>
+ * When a class has stabilized, it will move to the
+ * {@link org.apache.commons.rdf.simple} package.
+ * <p>
+ * <ul>
+ * <li>{@link AbstractRDFParser} - an abstract helper class
+ * for implementations of 
+ * {@link org.apache.commons.rdf.api.experimental.RDFParser}.</li>
+ * </ul>
+ */
+package org.apache.commons.rdf.simple.experimental;
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-commonsrdf/blob/a189f91e/simple/src/test/java/org/apache/commons/rdf/simple/AbstractRDFParserBuilderTest.java
----------------------------------------------------------------------
diff --git a/simple/src/test/java/org/apache/commons/rdf/simple/AbstractRDFParserBuilderTest.java b/simple/src/test/java/org/apache/commons/rdf/simple/AbstractRDFParserBuilderTest.java
index acb75b7..f115e94 100644
--- a/simple/src/test/java/org/apache/commons/rdf/simple/AbstractRDFParserBuilderTest.java
+++ b/simple/src/test/java/org/apache/commons/rdf/simple/AbstractRDFParserBuilderTest.java
@@ -32,11 +32,12 @@ import java.util.concurrent.TimeUnit;
 import org.apache.commons.rdf.api.Graph;
 import org.apache.commons.rdf.api.IRI;
 import org.apache.commons.rdf.api.Literal;
-import org.apache.commons.rdf.api.RDFParser;
 import org.apache.commons.rdf.api.RDFSyntax;
 import org.apache.commons.rdf.api.RDFTerm;
 import org.apache.commons.rdf.api.RDFTermFactory;
 import org.apache.commons.rdf.api.Triple;
+import org.apache.commons.rdf.experimental.RDFParser;
+import org.apache.commons.rdf.simple.experimental.AbstractRDFParser;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;

http://git-wip-us.apache.org/repos/asf/incubator-commonsrdf/blob/a189f91e/simple/src/test/java/org/apache/commons/rdf/simple/DummyRDFParserBuilder.java
----------------------------------------------------------------------
diff --git a/simple/src/test/java/org/apache/commons/rdf/simple/DummyRDFParserBuilder.java b/simple/src/test/java/org/apache/commons/rdf/simple/DummyRDFParserBuilder.java
index fe0b36e..99ed576 100644
--- a/simple/src/test/java/org/apache/commons/rdf/simple/DummyRDFParserBuilder.java
+++ b/simple/src/test/java/org/apache/commons/rdf/simple/DummyRDFParserBuilder.java
@@ -23,8 +23,9 @@ import java.util.function.Consumer;
 
 import org.apache.commons.rdf.api.IRI;
 import org.apache.commons.rdf.api.Quad;
-import org.apache.commons.rdf.api.RDFParser;
 import org.apache.commons.rdf.api.RDFTermFactory;
+import org.apache.commons.rdf.experimental.RDFParser;
+import org.apache.commons.rdf.simple.experimental.AbstractRDFParser;
 
 /** 
  * For test purposes - a {@link RDFParser} that inserts information