java.fedora.server.search.FieldSearchResultSQLImpl Maven / Gradle / Ivy
/*
* -----------------------------------------------------------------------------
*
* License and Copyright: The contents of this file are subject to 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.fedora-commons.org/licenses.
*
* 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 entire file consists of original code.
* Copyright © 2008 Fedora Commons, Inc.
*
Copyright © 2002-2007 The Rector and Visitors of the University of
* Virginia and Cornell University
* All rights reserved.
*
* -----------------------------------------------------------------------------
*/
package fedora.server.search;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.sql.Connection;
import java.sql.Statement;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.apache.log4j.Logger;
import fedora.server.ReadOnlyContext;
import fedora.server.Server;
import fedora.server.errors.UnrecognizedFieldException;
import fedora.server.errors.ObjectIntegrityException;
import fedora.server.errors.QueryParseException;
import fedora.server.errors.RepositoryConfigurationException;
import fedora.server.errors.StreamIOException;
import fedora.server.errors.StorageDeviceException;
import fedora.server.errors.ServerException;
import fedora.server.storage.ConnectionPool;
import fedora.server.storage.BMechReader;
import fedora.server.storage.DOReader;
import fedora.server.storage.RepositoryReader;
import fedora.server.storage.types.DatastreamXMLMetadata;
import fedora.server.storage.types.Disseminator;
import fedora.server.utilities.DateUtility;
import fedora.server.utilities.MD5Utility;
/**
* A FieldSearchResults object returned as the result of
* a FieldSearchSQLImpl search.
*
* A FieldSearchResultSQLImpl is intended to be re-used in cases where
* the results of a query require more than one call to the server.
*
* @author [email protected]
* @version $Id: FieldSearchResultSQLImpl.java 5220 2006-11-20 13:52:20Z cwilper $
*/
public class FieldSearchResultSQLImpl
implements FieldSearchResult {
/** Logger for this class. */
private static final Logger LOG = Logger.getLogger(
FieldSearchResultSQLImpl.class.getName());
/* fields supporting public accessors */
private ArrayList m_objectFields;
private String m_token;
private long m_cursor=-1;
private long m_completeListSize=-1;
private Date m_expirationDate;
/* invariants */
private Connection m_conn;
private ConnectionPool m_cPool;
private RepositoryReader m_repoReader;
private String[] m_resultFields;
private int m_maxResults;
private int m_maxSeconds;
private long m_startMillis;
/* internal state */
private Statement m_statement;
private ResultSet m_resultSet;
private long m_nextCursor=0;
private boolean m_expired;
/**
* Construct a FieldSearchResultSQLImpl object.
*
* Upon construction, a connection is obtained from the connectionPool,
* and the query is executed. (The connection will be returned to the
* pool only after the last result has been obtained from the ResultSet,
* the session is expired, or some non-recoverable error has occurred)
*
* Once the ResultSet is obtained, one result is requested of it
* (and remembered for use in step()); then the call returns.
*
* @param cPool the connectionPool
* @param repoReader the provider of object field information for results
* @param resultFields which fields should be returned in results
* @param maxResults how many results should be returned at one time. This
* should be the smaller of a) the FieldSearchImpl's limit [the server
* limit] and b) the requested limit [the client limit]
* @param query the end-user query
*/
protected FieldSearchResultSQLImpl(ConnectionPool cPool,
RepositoryReader repoReader, String[] resultFields, int maxResults,
int maxSeconds, FieldSearchQuery query)
throws SQLException, QueryParseException {
m_cPool=cPool;
m_repoReader=repoReader;
m_resultFields=resultFields;
m_maxResults=maxResults;
m_maxSeconds=maxSeconds;
m_conn=m_cPool.getConnection();
try {
m_statement=m_conn.createStatement();
m_resultSet=m_statement.executeQuery(logAndGetQueryText(query, m_resultFields)); //2004.05.02 wdn5e
} catch (SQLException sqle) {
// if there's any kind of problem getting the resultSet,
// give the connection back to the pool
try {
if (m_resultSet != null) m_resultSet.close();
if (m_statement != null) m_statement.close();
if (m_conn!=null) m_cPool.free(m_conn);
throw sqle;
} catch (SQLException sqle2) {
throw sqle2;
} finally {
m_resultSet=null;
m_statement=null;
}
}
}
//2004.05.02 wdn5e -- sort on selected fields
private String logAndGetQueryText(FieldSearchQuery query, String[] resultFields) //2004.05.02 wdn5e
throws SQLException, QueryParseException {
StringBuffer queryText=new StringBuffer("SELECT");
if (query.getType()==FieldSearchQuery.TERMS_TYPE) {
queryText.append(" doFields.pid FROM doFields" + getWhereClause(query.getTerms()));
} else {
StringBuffer resultFieldsString = new StringBuffer();
if (resultFields.length > 0) {
String delimiter = " ";
for (int i=0; i < resultFields.length; i++) {
String dbColumn = "doFields." + dcFixup(resultFields[i]);
resultFieldsString.append(delimiter + dbColumn);
delimiter = ", ";
}
}
queryText.append(resultFieldsString);
queryText.append(" FROM doFields");
queryText.append(getWhereClause(query.getConditions()));
// disabled sorting: see bug 78
// queryText.append(" ORDER BY");
// queryText.append(resultFieldsString);
}
String qt=queryText.toString();
LOG.debug(qt);
return qt;
}
private String getWhereClause(String terms)
throws QueryParseException {
if (terms.indexOf("'")!=-1) {
throw new QueryParseException("Query cannot contain the ' character.");
}
StringBuffer whereClause=new StringBuffer();
if (!terms.equals("*") && !terms.equals("")) {
whereClause.append(" WHERE");
// formulate the where clause if the terms aren't * or ""
int usedCount=0;
boolean needsEscape=false;
for (int i=0; i0) {
whereClause.append(" OR");
}
String qPart=toSql(column, terms);
if (qPart.charAt(0)==' ') {
needsEscape=true;
} else {
whereClause.append(" ");
}
whereClause.append(qPart);
usedCount++;
}
}
if (needsEscape) {
// whereClause.append(" {escape '/'}");
}
}
return whereClause.toString();
}
private String getWhereClause(List conditions)
throws QueryParseException {
StringBuffer whereClause=new StringBuffer();
boolean willJoin=false;
if (conditions.size()>0) {
boolean needsEscape=false;
whereClause.append(" WHERE");
for (int i=0; i0) {
whereClause.append(" AND");
}
String op=cond.getOperator().getSymbol();
String prop=cond.getProperty();
if (prop.toLowerCase().endsWith("date")) {
// deal with dates ... cDate mDate dcmDate date
if (op.equals("~")) {
if (prop.equals("date")) {
// query for dcDate as string
String sqlPart=toSql("doFields.dcDate", cond.getValue());
if (sqlPart.startsWith(" ")) {
needsEscape=true;
} else {
whereClause.append(' ');
}
whereClause.append(sqlPart);
} else {
throw new QueryParseException("The ~ operator "
+ "cannot be used with cDate, mDate, "
+ "or dcmDate because they are not "
+ "string-valued fields.");
}
} else { // =, <, <=, >, >=
// property must be parsable as a date... if ok,
// do (cDate, mDate, dcmDate)
// or (date) <- dcDate from dcDates table
Date dt=DateUtility.parseDateAsUTC(cond.getValue());
if (dt==null) {
throw new QueryParseException("When using "
+ "equality or inequality operators "
+ "with a date-based value, the date "
+ "must be in yyyy-MM-DD[THH:mm:ss[.SSS][Z]] "
+ "form.");
}
if (prop.equals("date")) {
// do a left join on the dcDates table...dcDate
// query will be of form:
// select pid
// from doFields
// left join dcDates on doFields.pid=dcDates.pid
// where...
if (!willJoin) {
willJoin=true;
whereClause.insert(0, " LEFT JOIN dcDates "
+ "ON doFields.pid=dcDates.pid");
}
whereClause.append(" dcDates.dcDate" + op
+ dt.getTime() );
} else {
whereClause.append(" doFields." + prop + op
+ dt.getTime() );
}
}
} else {
if (op.equals("=")) {
if (isDCProp(prop) || prop.equals("bDef") || prop.equals("bMech") ) {
throw new QueryParseException("The = operator "
+ "can only be used with dates and "
+ "non-repeating fields.");
} else {
// do a real equals check... do a toSql but
// reject it if it uses "LIKE"
String sqlPart=toSql("doFields." + prop, cond.getValue());
if (sqlPart.indexOf("LIKE ")!=-1) {
throw new QueryParseException("The = "
+ "operator cannot be used with "
+ "wildcards.");
}
if (sqlPart.startsWith(" ")) {
needsEscape=true;
} else {
whereClause.append(' ');
}
whereClause.append(sqlPart);
}
} else if (op.equals("~")) {
if (isDCProp(prop)) {
// prepend dc and caps the first char first...
prop="dc" + prop.substring(0,1).toUpperCase()
+ prop.substring(1);
}
// the field name is ok, so toSql it
String sqlPart=toSql("doFields." + prop,
cond.getValue());
if (sqlPart.startsWith(" ")) {
needsEscape=true;
} else {
whereClause.append(' ');
}
whereClause.append(sqlPart);
} else {
throw new QueryParseException("Can't use >, >=, <, "
+ "or <= operator on a string-based field.");
}
}
}
if (needsEscape) {
// whereClause.append(" {escape '/'}");
}
}
return whereClause.toString();
}
protected boolean isExpired() {
long passedSeconds=(System.currentTimeMillis() - m_startMillis)/1000;
m_expired=(passedSeconds > m_maxSeconds);
if (m_expired) {
// clean up
try {
if (m_resultSet!=null) m_resultSet.close();
if (m_statement!=null) m_statement.close();
if (m_conn!=null) m_cPool.free(m_conn);
} catch (SQLException sqle) {
} finally {
m_resultSet=null;
m_statement=null;
}
}
return m_expired;
}
/**
* Update object with the next chunk of results.
*
* if getToken() is null after this call, the resultSet was exhausted.
*/
protected void step()
throws UnrecognizedFieldException, ObjectIntegrityException,
RepositoryConfigurationException, StreamIOException,
ServerException {
m_objectFields=new ArrayList();
int resultCount=0;
// run through resultSet, adding each result to m_objectFields
// for up to maxResults objects, or until the result set is
// empty, whichever comes first.
try {
while (resultCount0 && !m_resultSet.isAfterLast()) {
// yes, so generate a token, make sure the cursor is set,
// and make sure the expirationDate is set
long now=System.currentTimeMillis();
m_token=MD5Utility.getBase16Hash(this.hashCode() + "" + now);
m_cursor=m_nextCursor;
// keep m_nextCursor updated for next block
m_nextCursor+=resultCount;
m_startMillis=now;
Date dt=new Date();
dt.setTime(m_startMillis + (1000 * m_maxSeconds));
m_expirationDate=dt;
} else {
// no, so make sure the token is null and clean up
m_token=null;
try {
if (m_resultSet!=null) m_resultSet.close();
if (m_statement!=null) m_statement.close();
if (m_conn!=null) m_cPool.free(m_conn);
} catch (SQLException sqle2) {
throw new StorageDeviceException("Error closing statement "
+ "or result set." + sqle2.getMessage());
} finally {
m_resultSet=null;
m_statement=null;
}
}
} catch (SQLException sqle) {
try {
if (m_resultSet!=null) m_resultSet.close();
if (m_statement!=null) m_statement.close();
if (m_conn!=null) m_cPool.free(m_conn);
throw new StorageDeviceException("Error with sql database. "
+ sqle.getMessage());
} catch (SQLException sqle2) {
throw new StorageDeviceException("Error closing statement "
+ "or result set." + sqle.getMessage() + sqle2.getMessage());
} finally {
m_resultSet=null;
m_statement=null;
}
}
}
/**
* For the given pid, get a reader on the object from the repository
* and return an ObjectFields object with resultFields fields populated.
*
* @param pid the unique identifier of the object for which the information
* is requested.
* @return ObjectFields populated with the requested fields
* @throws UnrecognizedFieldException if a resultFields value isn't valid
* @throws ObjectIntegrityException if the underlying digital object can't
* be parsed
* @throws RepositoryConfigurationException if the sax parser can't
* be constructed
* @throws StreamIOException if an error occurs while reading the serialized
* digital object stream
* @throws ServerException if any other kind of error occurs while reading
* the underlying object
*/
private ObjectFields getObjectFields(String pid)
throws UnrecognizedFieldException, ObjectIntegrityException,
RepositoryConfigurationException, StreamIOException,
ServerException {
DOReader r=m_repoReader.getReader(Server.USE_DEFINITIVE_STORE, ReadOnlyContext.EMPTY, pid);
ObjectFields f;
// If there's a DC record available, use SAX to parse the most
// recent version of it into f.
DatastreamXMLMetadata dcmd=null;
try {
dcmd=(DatastreamXMLMetadata) r.GetDatastream("DC", null);
} catch (ClassCastException cce) {
throw new ObjectIntegrityException("Object " + r.GetObjectPID()
+ " has a DC datastream, but it's not inline XML.");
}
if (dcmd!=null) {
f=new ObjectFields(m_resultFields, dcmd.getContentStream());
// add dcmDate if wanted
for (int i=0; i
* If the string has any characters that need to be escaped, it will
* begin with a space, indicating to the caller that the entire WHERE
* clause should end with " {escape '/'}".
*
* @param name the name of the field in the database
* @param in the query string, where * and ? are treated as wildcards
* @return String a suitable string for use in a SQL WHERE clause,
* as described above
*/
private static String toSql(String name, String in) {
if ( !name.endsWith("pid")
&& !name.endsWith("bDef")
&& !name.endsWith("bMech")) in=in.toLowerCase(); // if it's not a PID-type field,
// it's case insensitive
if (name.startsWith("dc") || (name.startsWith("doFields.dc"))
|| (name.equals("bDef")) || (name.equals("doFields.bDef"))
|| (name.equals("bMech")) || (name.equals("doFields.bMech")) ) {
StringBuffer newIn=new StringBuffer();
if (!in.startsWith("*")) {
newIn.append("* ");
}
newIn.append(in);
if (!in.endsWith("*")) {
newIn.append(" *");
}
in=newIn.toString();
}
if (in.indexOf("\\")!=-1) {
// has one or more escapes, un-escape and translate
StringBuffer out=new StringBuffer();
out.append("\'");
boolean needLike=false;
boolean needEscape=false;
boolean lastWasEscape=false;
for (int i=0; i 1 places
private static final String dcFixup (String st) {
String dcFixed;
if (isDCProp(st)) {
dcFixed = "dc" + st.substring(0,1).toUpperCase() + st.substring(1);
} else {
dcFixed = st;
}
return dcFixed;
}
}