/*
* The contents of this file are subject to the Mozilla Public License
* Version 1.1 (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.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS"
* basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
* the License for the specific language governing rights and limitations
* under the License.
*
* The Original Code is the Kowari Metadata Store.
*
* The Initial Developer of the Original Code is Plugged In Software Pty
* Ltd (http://www.pisoftware.com, mailto:info@pisoftware.com). Portions
* created by Plugged In Software Pty Ltd are Copyright (C) 2001,2002
* Plugged In Software Pty Ltd. All Rights Reserved.
*
* Contributor(s): N/A.
*
* [NOTE: The text of this Exhibit A may differ slightly from the text
* of the notices in the Source Code files of the Original Code. You
* should use the text of this Exhibit A rather than the text found in the
* Original Code Source Code for Your Modifications.]
*
*/
package org.mulgara.content.n3;
// Java 2 standard packages
// Third party packages
import java.util.Collections;
import java.util.List;
import org.apache.log4j.Logger; // Apache Log4J
import org.jrdf.graph.*; // JRDF
// Locally written packages
import org.mulgara.content.Content;
import org.mulgara.content.NotModifiedException;
import org.mulgara.content.NotModifiedTuplesException;
import org.mulgara.query.Cursor;
import org.mulgara.query.TuplesException;
import org.mulgara.query.Variable;
import org.mulgara.resolver.spi.LocalizeException;
import org.mulgara.resolver.spi.ResolverSession;
import org.mulgara.resolver.spi.Statements;
import org.mulgara.resolver.spi.StatementsWrapperResolution;
import org.mulgara.store.tuples.AbstractTuples;
import org.mulgara.store.tuples.Tuples;
/**
* Parses an {@link java.io.InputStream} into {@link Statements}.
*
* This particular implementation is complicated by the need to adapt the Jena
* N3 <q>push</q> parser to be a <q>pull</q> parser instead.
*
* @created 2004-04-02
* @author <a href="http://staff.pisoftware.com/raboczi">Simon Raboczi</a>
* @version $Revision: 1.8 $
* @modified $Date: 2005/01/05 04:58:02 $ @maintenanceAuthor $Author: newmana $
* @company <a href="mailto:info@PIsoftware.com">Plugged In Software</a>
* @copyright © 2004 <a href="http://www.PIsoftware.com/">Plugged In
* Software Pty Ltd</a>
* @licence <a href="{@docRoot}/../../LICENCE">Mozilla Public License v1.1</a>
*/
public class N3Statements extends AbstractTuples implements Statements {
/** Logger. */
private static final Logger logger = Logger.getLogger(N3Statements.class.getName());
/** The RDF/XML formatted document. */
private Content content;
/** The session used to globalize the RDF nodes from the stream. */
private ResolverSession resolverSession;
/** The current row. if the cursor is not on a row, this will be <code>null</code> */
private Triple triple;
private Parser parser = null;
private long rowCount;
private boolean rowCountIsValid = false;
//
// Constructors
//
/**
* Construct a Notation-3 stream parser.
*
* @param content the Notation-3 content
* @param resolverSession session against which to localize RDF nodes
* @throws IllegalArgumentException if <var>inputStream</var> or
* <var>resolverSession</var> are <code>null</code>
*/
N3Statements(Content content, ResolverSession resolverSession) {
// Validate "content" parameter
if (content == null) {
throw new IllegalArgumentException( "Null \"content\" parameter");
}
// Validate "resolverSession" parameter
if (resolverSession == null) {
throw new IllegalArgumentException("Null \"resolverSession\" parameter");
}
// Initialize fields
this.content = content;
this.resolverSession = resolverSession;
// Fix the magical column names for RDF statements
setVariables(new Variable[] { new Variable("subject"),
new Variable("predicate"),
new Variable("object") });
}
//
// Methods implementing Statements
//
public long getSubject() throws TuplesException {
return getColumnValue(0);
}
public long getPredicate() throws TuplesException {
return getColumnValue(1);
}
public long getObject() throws TuplesException {
return getColumnValue(2);
}
//
// Methods implementing AbstractTuples
//
/**
* {@inheritDoc}
*
* Non-zero length <var>prefix</var> values don't need to be supported by
* this class because prefix filtration is implemented by the
* {@link StatementsWrapperResolution} which the existing external resolvers
* always apply to their content before returning it.
*
* @param prefix {@inheritDoc}; for this particular implementation, non-zero
* length prefixes are not supported
* @throws NotModifiedTuplesException if the underlying tuples are cached
* @throws TuplesException {@inheritDoc}; also if <var>prefix</var> is non-zero length
*/
public void beforeFirst(long[] prefix, int suffixTruncation) throws TuplesException {
if (logger.isDebugEnabled()) {
logger.debug("Before first");
}
// Validate "prefix" parameter
if (prefix == null) {
throw new IllegalArgumentException("Null \"prefix\" parameter");
}
if (prefix.length != 0) {
throw new TuplesException(
getClass() + ".beforeFirst isn't implemented for non-zero length prefix"
);
}
// Validate "suffixTruncation" parameter
if (suffixTruncation != 0) {
throw new IllegalArgumentException("Null \"suffixTruncation\" parameter");
}
// Shut down any existing parsing thread
if (parser != null) {
stopThread();
}
// Create the parser and start the parsing thread
try {
parser = new Parser(content, resolverSession);
} catch (NotModifiedException e) {
throw new NotModifiedTuplesException(e);
}
parser.start();
// TODO skip forward to the first triple that matches prefix
}
/**
* The cursor position isn't cloned by this method.
*/
public Object clone() {
N3Statements cloned = (N3Statements) super.clone();
// Copy immutable fields by reference
cloned.content = content;
cloned.resolverSession = resolverSession;
// The cursor position is not cloned.
cloned.triple = null;
cloned.parser = null;
return cloned;
}
/**
* Close the RDF/XML formatted input stream.
*/
public void close() throws TuplesException {
stopThread();
}
/**
* @param column 0 for the subject, 1 for the predicate, 2 for the object
*/
public long getColumnValue(int column) throws TuplesException {
if (triple == null) {
throw new TuplesException("There is no current row");
}
// Pull the appropriate field from the current triple as a JRDF Node
Node node;
switch (column) {
case 0: node = triple.getSubject(); break;
case 1: node = triple.getPredicate(); break;
case 2: node = triple.getObject(); break;
default: throw new TuplesException("No such column " + column);
}
assert node != null;
// Localize the node
try {
return resolverSession.localize(node);
}
catch (LocalizeException e) {
throw new TuplesException("Couldn't get column " + column + " value", e);
}
}
public List<Tuples> getOperands() {
return Collections.emptyList();
}
public int getRowCardinality() throws TuplesException {
long statementCount;
if (rowCountIsValid) {
statementCount = rowCount;
} else {
Parser p;
boolean newParser;
if (parser != null) {
// Use the existing parser.
p = parser;
newParser = false;
} else {
// Create a new parser.
try {
p = new Parser(content, resolverSession);
p.start();
} catch (NotModifiedException e) {
throw new NotModifiedTuplesException(e);
}
newParser = true;
}
// We can do this since the queue holds more than two triples.
try {
synchronized (p) {
while (p.getStatementCount() < 2 && !p.isStatementCountTotal()) {
try {
// Wait on the parser for changes to the statement count or
// completion status.
p.wait();
} catch (InterruptedException ex) {
throw new TuplesException("Abort");
}
}
statementCount = p.getStatementCount();
}
} catch (TuplesException ex) {
p.abort();
if (!newParser) {
// We just aborted the main parser, so nullify the reference.
parser = null;
}
throw ex; // rethrow.
} finally {
if (newParser) {
// Stop the thread.
p.abort();
}
}
}
// Convert the statement count into a cardinality class
return statementCount == 0 ? Cursor.ZERO :
statementCount == 1 ? Cursor.ONE :
Cursor.MANY;
}
public long getRowCount() throws TuplesException {
if (!rowCountIsValid) {
if (parser != null && parser.isStatementCountTotal()) {
// Get the statement count from the parser.
rowCount = parser.getStatementCount();
} else {
// Create a new parser.
Parser p;
try {
p = new Parser(content, resolverSession);
} catch (NotModifiedException e) {
throw new NotModifiedTuplesException(e);
}
// Consume the entire file.
p.start();
try {
rowCount = p.waitForStatementTotal();
} finally {
p.abort();
}
}
rowCountIsValid = true;
}
return rowCount;
}
public long getRowUpperBound() throws TuplesException {
// If the row count isn't yet available, return an absurdly huge value
return parser != null && parser.isStatementCountTotal() ?
parser.getStatementCount() : Long.MAX_VALUE;
}
/** Guess at a large number */
private static final Long LARGE_FILE_SIZE = 1000000L;
public long getRowExpectedCount() throws TuplesException {
// If the row count isn't yet available, return an absurdly huge value
return parser != null && parser.isStatementCountTotal() ?
parser.getStatementCount() : LARGE_FILE_SIZE;
}
public boolean hasNoDuplicates() throws TuplesException {
return false;
}
public boolean isColumnEverUnbound(int column) throws TuplesException {
switch (column) {
case 0: case 1: case 2:
return false;
default:
throw new TuplesException("No such column " + column);
}
}
public boolean next() throws TuplesException {
if (parser == null) {
// no current row
return false;
}
try {
triple = parser.getTriple();
} catch (TuplesException ex) {
stopThread();
throw ex; // rethrow
}
if (triple == null) {
// Hit the end of the file.
assert parser.isStatementCountTotal();
rowCount = parser.getStatementCount();
rowCountIsValid = true;
stopThread();
}
return triple != null;
}
/**
* Stops the thread if it is running, and clears the current row.
*/
private void stopThread() {
if (parser != null) {
parser.abort();
parser = null;
}
triple = null;
}
}