/* * The contents of this file are subject to the Open Software License * Version 3.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.opensource.org/licenses/osl-3.0.txt * * 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. */ package org.mulgara.query; import java.io.Serializable; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import java.util.List; import org.apache.log4j.Logger; import org.jrdf.graph.BlankNode; import org.jrdf.graph.Literal; import org.mulgara.query.rdf.BlankNodeImpl; /** * An Answer that represents a graph. * * @created Jun 30, 2008 * @author Paula Gearon * @copyright © 2008 <a href="http://www.topazproject.org/">The Topaz Project</a> * @licence <a href="{@docRoot}/../../LICENCE.txt">Open Software License v3.0</a> */ public class GraphAnswer extends AbstractAnswer implements Answer, Serializable { /** The serialization ID. */ private static final long serialVersionUID = -5499236950928116988L; /** Logger */ private static final Logger logger = Logger.getLogger(GraphAnswer.class.getName()); /** The variable name for the first column. */ private static final String CONSTANT_VAR_SUBJECT = "subject"; /** The variable name for the second column. */ private static final String CONSTANT_VAR_PREDICATE = "predicate"; /** The variable name for the third column. */ private static final String CONSTANT_VAR_OBJECT = "object"; /** The first column variable. */ private static final Variable SUBJECT_VAR = new Variable(CONSTANT_VAR_SUBJECT); /** The second column variable. */ private static final Variable PREDICATE_VAR = new Variable(CONSTANT_VAR_PREDICATE); /** The third column variable. */ private static final Variable OBJECT_VAR = new Variable(CONSTANT_VAR_OBJECT); /** An array containing the variable. */ private static final Variable[] CONSTANT_VAR_ARR = new Variable[] { SUBJECT_VAR, PREDICATE_VAR, OBJECT_VAR }; /** The raw answer to wrap. */ private Answer rawAnswer; /** The column counter for emulating rows. */ private int colOffset = 0; /** The number of rows per solution. A solution is a row from the raw answer. */ private final int rowsPerSoln; /** The normal variables that must be bound. */ private Map<Variable,List<Integer>> stdVarIndexes; /** The bnode generating variables. */ private Map<Variable,List<Integer>> bnodeVars; /** All the nodes generated for a single solution */ private Object[] rowNodes; /** Internal counter for generating blank node identifiers */ private long blankNodeId = 0; /** * Constructs a new BooleanAnswer. * @param rawAnswer The result this answer represents. */ public GraphAnswer(Answer rawAnswer) { int cols = rawAnswer.getNumberOfVariables(); if (cols % 3 != 0) throw new IllegalArgumentException("Cannot construct a graph with " + cols + " columns."); rowsPerSoln = cols / 3; this.rawAnswer = rawAnswer; rowNodes = new Object[cols]; stdVarIndexes = new HashMap<Variable,List<Integer>>(); bnodeVars = new HashMap<Variable,List<Integer>>(); Variable[] vars = rawAnswer.getVariables(); for (int i = 0; i < vars.length; i++) { Variable v = vars[i]; List<Integer> nodeColumns; if (v.isBnodeVar()) { // record which columns this bnode will generate nodeColumns = bnodeVars.get(v); if (nodeColumns == null) { // first time this bnode was encountered nodeColumns = new ArrayList<Integer>(); bnodeVars.put(v, nodeColumns); } } else { // record which columns this bnode will generate nodeColumns = stdVarIndexes.get(v); if (nodeColumns == null) { // first time this variable was encountered nodeColumns = new ArrayList<Integer>(); stdVarIndexes.put(v, nodeColumns); } } nodeColumns.add(i); } resetBlankNodes(); } /** * @see org.mulgara.query.Answer#getObject(int) */ public Object getObject(int column) throws TuplesException { int c = column + colOffset; assert (rawAnswer.getVariables()[c].isBnodeVar() && rowNodes[c] instanceof BlankNode) || !rawAnswer.getVariables()[c].isBnodeVar(); return rowNodes[c]; } /** * @see org.mulgara.query.Answer#getObject(java.lang.String) */ public Object getObject(String columnName) throws TuplesException { // use an unrolled loop if (CONSTANT_VAR_SUBJECT.equals(columnName)) return getObject(0); if (CONSTANT_VAR_PREDICATE.equals(columnName)) return getObject(1); if (CONSTANT_VAR_OBJECT.equals(columnName)) return getObject(2); throw new TuplesException("Unknown variable: " + columnName); } /** @see org.mulgara.query.Cursor#beforeFirst() */ public void beforeFirst() throws TuplesException { rawAnswer.beforeFirst(); colOffset = (rowsPerSoln - 1) * 3; resetBlankNodes(); } /** @see org.mulgara.query.Cursor#close() */ public void close() throws TuplesException { rawAnswer.close(); } /** * @see org.mulgara.query.Cursor#getColumnIndex(org.mulgara.query.Variable) */ public int getColumnIndex(Variable column) throws TuplesException { // use an unrolled loop if (SUBJECT_VAR.equals(column)) return 0; if (PREDICATE_VAR.equals(column)) return 1; if (OBJECT_VAR.equals(column)) return 2; throw new TuplesException("Unknown variable: " + column); } /** * @see org.mulgara.query.Cursor#getNumberOfVariables() */ public int getNumberOfVariables() { return 3; } /** * @see org.mulgara.query.Cursor#getRowCardinality() */ public int getRowCardinality() throws TuplesException { int rawCardinality = rawAnswer.getRowCardinality() * rowsPerSoln; if (rawCardinality == 0) return 0; // get a copy to work with GraphAnswer answerCopy = (GraphAnswer)clone(); try { answerCopy.beforeFirst(); // test if one row if (!answerCopy.next()) return 0; // test if we know it can't be more than 1, or if there is no second row if (rawCardinality == 1) return 1; if (!answerCopy.next()) return rowsPerSoln; // Return the raw cardinality return rawCardinality; } finally { try { answerCopy.close(); } catch (TuplesException e) { logger.warn("Exception closing cloned answer", e); } } } /** * @see org.mulgara.query.Cursor#isEmpty() */ public boolean isEmpty() throws TuplesException { return rawAnswer.isEmpty(); } /** * @see org.mulgara.query.Cursor#getRowCount() */ public long getRowCount() throws TuplesException { // Urk. Doing this the hard way... // get a copy to work with GraphAnswer answerCopy = (GraphAnswer)clone(); try { answerCopy.beforeFirst(); long result = 0; while (answerCopy.next()) result++; return result * rowsPerSoln; } finally { try { answerCopy.close(); } catch (TuplesException e) { logger.warn("Exception closing cloned answer", e); } } } /** * @see org.mulgara.query.Cursor#getRowUpperBound() */ public long getRowUpperBound() throws TuplesException { return rawAnswer.getRowUpperBound() * rowsPerSoln; } /** * @see org.mulgara.query.Cursor#getRowExpectedCount() */ public long getRowExpectedCount() throws TuplesException { return rawAnswer.getRowExpectedCount() * rowsPerSoln; } /** * @see org.mulgara.query.Cursor#getVariables() */ public Variable[] getVariables() { return CONSTANT_VAR_ARR; } /** * Since the returned variables are static, provide them statically as well. */ public static Variable[] getGraphVariables() { return CONSTANT_VAR_ARR; } /** * @see org.mulgara.query.Cursor#isUnconstrained() */ public boolean isUnconstrained() throws TuplesException { return false; } /** * @see org.mulgara.query.Cursor#next() */ public boolean next() throws TuplesException { boolean nextAvailable; do { nextAvailable = internalNext(); } while (nextAvailable && !graphable()); return nextAvailable; } /** @see java.lang.Object#clone() */ public Object clone() { GraphAnswer a = (GraphAnswer)super.clone(); a.rawAnswer = (Answer)rawAnswer.clone(); return a; } public boolean equals(Object object) { if ((object != null) && (object instanceof Answer)) { try { return AnswerOperations.equal(this, (Answer) object); } catch (TuplesException e) { logger.fatal("Couldn't test equality of answers", e); } } return false; } /** * Added to match {@link #equals(Object)}. */ public int hashCode() { return super.hashCode(); } /** * An internal method for moving on to the next row, without testing validity. * @return <code>true</code> if this call has not exhausted the rows. * @throws TuplesException Due to an error in the underlying rawAnswer. */ private boolean internalNext() throws TuplesException { if ((colOffset += 3) < (rowsPerSoln * 3)) return true; colOffset = 0; // test if the next solution can be used // this requires that there are no unbound variables boolean nextResult; do { nextResult = rawAnswer.next(); } while (nextResult && hasUnboundVar()); generateBlanks(); return nextResult; } /** * Tests if the current row of the raw answer has any unbound variables. * This has a side effect of filling the row with all the tested values, * meaning that there is no longer a need to call rawAnswer.getObject on any values. * @return <code>true</code> if at least one variable is unbound. If there is an * unbound variable then the contents of rowNodes are not defined. * @throws TuplesException If there is an error reading a variable. */ private boolean hasUnboundVar() throws TuplesException { for (Map.Entry<Variable,List<Integer>> varNodes: stdVarIndexes.entrySet()) { List<Integer> objIndexes = varNodes.getValue(); // get the value for the first occurrence Object o = rawAnswer.getObject(objIndexes.get(0)); // short circuit on invalid rows if (o == null) return true; // fill in the row for (int v: objIndexes) rowNodes[v] = o; } return false; } /** * Test if the current row is expressible as a graph row. * @return <code>true</code> if the subject-predicate-object have valid node types. * @throws TuplesException The row could not be accessed. */ private boolean graphable() throws TuplesException { if (rowNodes[colOffset] instanceof Literal) return false; Object predicate = rowNodes[1 + colOffset]; return !(predicate instanceof Literal || predicate instanceof BlankNode); } /** * Resets the blank node identifier to the start of the document. */ private void resetBlankNodes() { blankNodeId = 1; } /** * Creates all the generated blank nodes for this row. */ private void generateBlanks() { for (Map.Entry<Variable,List<Integer>> varNodes: bnodeVars.entrySet()) { // generate the blank node for this variable BlankNode b = new BlankNodeImpl(blankNodeId++); // put this blank node in every position the variable appears in for (int i: varNodes.getValue()) rowNodes[i] = b; } } }