/********************************************************************************** * * Copyright (c) 2003, 2004, 2007, 2008, 2009 The Sakai Foundation * * Licensed under the Educational Community 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.opensource.org/licenses/ECL-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 edu.indiana.lib.twinpeaks.search.singlesearch.web2; import edu.indiana.lib.twinpeaks.net.*; import edu.indiana.lib.twinpeaks.search.*; import edu.indiana.lib.twinpeaks.search.singlesearch.CqlParser; import edu.indiana.lib.twinpeaks.util.*; import java.io.*; import java.net.*; import java.util.*; import javax.xml.parsers.*; import javax.xml.transform.*; import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamSource; import org.w3c.dom.*; import org.xml.sax.*; /** * Send a query to the Muse Web2 interface */ public class Web2Query extends HttpTransactionQueryBase { private static org.apache.commons.logging.Log _log = LogUtils.getLog(Web2Query.class); /** * Records displayed "per page" */ public static final String RECORDS_PER_PAGE = "10"; /** * Records to fetch from each search target */ private static final String RECORDS_PER_TARGET = "30"; /** * Unique name for this search application */ private final String APPLICATION = SessionContext.uniqueSessionName(this); /** * Web2 Bridge error code: No logged-in session */ private static final String NO_SESSION = "904"; /** * Database for this request */ private String _database; /** * Muse syntax search criteria for this request (see parseRequest()) */ private String _museSearchString; /** * Web2 input */ private Document _web2Document; /** * Active reference ID # */ private static long _referenceId = System.currentTimeMillis(); /** * Local ID (for the current transaction) */ private long _transactionId; /** * Local version of server response (modified to contain SFX URL data) */ private byte _localResponseBytes[]; /** * Local byte array ready for use? */ private boolean _localResponseBytesReady = false; /** * Next RESULT record to request */ private int _nextResult = 0; /** * General synchronization */ private static Object _sync = new Object(); /** * Constructor */ public Web2Query() { super(); } /** * Parse user request parameters. * @param parameterMap Request details (name=value pairs) */ public void parseRequest(Map parameterMap) { String action; super.parseRequest(parameterMap); /* * These cannot be null by the time we get here */ if ((getRequestParameter("guid") == null) || (getRequestParameter("url") == null)) { throw new IllegalArgumentException("Missing GUID or URL"); } action = getRequestParameter("action"); if ("startsearch".equalsIgnoreCase(action)) { if ((getRequestParameter("targets") == null) || (getRequestParameter("username") == null) || (getRequestParameter("password") == null)) { throw new IllegalArgumentException("Missing target list, username, or password"); } } /* * Now deal with the search criteria (CQL syntax) */ _museSearchString = parseCql(getRequestParameter("searchString")); } /** * Search */ public void doQuery() { Document document; String action; /* * Get the logical "database" (a name for the configuration for this search) */ _database = getRequestParameter("database"); /* * We'll manage redirects, and submit with POST */ setRedirectBehavior(REDIRECT_MANAGED); setQueryMethod(METHOD_POST); /* * Save the URL and query text */ setUrl(getRequestParameter("url")); setSearchString(getSearchString()); /* * Request additional results (pagination)? Save the requested * pagesize and starting record */ action = getRequestParameter("action"); if (action.equalsIgnoreCase("requestRange")) { getSessionContext().putInt("startRecord", getIntegerRequestParameter("startRecord").intValue()); getSessionContext().putInt("pageSize", getIntegerRequestParameter("pageSize").intValue()); } /* * New search? */ if (action.equalsIgnoreCase("startSearch")) { /* * Initialize a new session context block */ StatusUtils.initialize(getSessionContext(), getRequestParameter("targets")); /* * LOGOFF any previous session */ clearParameters(); doLogoffCommand(); submit(); try { _log.debug(DomUtils.serialize(getResponseDocument())); } catch (Exception ignore) { } /* * LOGON */ clearParameters(); doLogonCommand(); submit(); validateResponse("LOGON"); /* * FIND */ clearParameters(); doFindCommand(); submit(); validateResponse("FIND"); setFindStatus(); try { _log.debug("Search response:"); _log.debug(DomUtils.serialize(getResponseDocument())); } catch (Exception ignore) { } /* doSearchCommand(); submit(); validateResponse("SEARCH"); setSearchStatus(); try { System.out.println(); System.out.println(DomUtils.serialize(getResponseDocument())); System.out.println(); } catch (Exception ignore) { } */ return; } /* * Request additional SEARCH results */ /* System.out.println("Result request starts"); clearParameters(); doResultsCommand(getTransactionResultSetName()); submit(); validateResponse("RESULTS"); */ /* * Request FIND results */ clearParameters(); doResultsCommand(getFindResultSetId()); submit(); validateResponse("RESULTS"); /* * Combine results */ /* System.out.println("Combine request starts"); clearParameters(); doCombineCommand(); submit(); validateResponse("COMBINE"); try { System.out.println(); System.out.println(DomUtils.serialize(getResponseDocument())); System.out.println(); } catch (Exception ignore) { } System.out.println("Result request starts"); clearParameters(); doResultsCommand(getTransactionResultSetName()); submit(); validateResponse("RESULTS"); try { System.out.println(); System.out.println(DomUtils.serialize(getResponseDocument())); System.out.println(); } catch (Exception ignore) { } */ } /** * Custom submit behavior (override HttpTransactionQueryBase) */ public int submit() { setWeb2InputMessage(); return super.submit(); } /* * Helpers */ /** * Generate a LOGON command */ private void doLogonCommand() throws SearchException { Element logonElement; String username, password; username = getRequestParameter("username"); password = getRequestParameter("password"); try { doWeb2InputHeader(); logonElement = addWeb2Input("LOGON"); addWeb2Input(logonElement, "USER_ID", username); addWeb2Input(logonElement, "USER_PWD", password); doWeb2InputClose(); } catch (DomException exception) { throw new SearchException(exception.toString()); } } /** * Generate a LOGOFF command */ private void doLogoffCommand() throws SearchException { try { doWeb2InputHeader(); addWeb2Input("LOGOFF"); doWeb2InputClose(); } catch (DomException exception) { throw new SearchException(exception.toString()); } } /** * Generate a SEARCH command */ private void doSearchCommand() throws SearchException { Element element, searchElement; String sortBy, targets; /* * Pick up the database(s) to examine, sort mode */ targets = getRequestParameter("targets"); _log.debug("Targets for search source " + _database + ": " + targets); _log.debug("SEARCH FOR: " + getSearchString()); sortBy = getRequestParameter("sortBy"); if (StringUtils.isNull(sortBy)) { sortBy = "ICERankingKeyRelevance"; } _log.debug("RANKING_KEY: " + sortBy); /* * And generate the search command */ try { doWeb2InputHeader(); searchElement = addWeb2Input("SEARCH"); addWeb2Input(searchElement, "TERMS", getSearchString()); addWeb2Input(searchElement, "QUERY_TYPE", "Muse"); addWeb2Input(searchElement, "TARGETS", targets); addWeb2Input(searchElement, "START", "1"); addWeb2Input(searchElement, "PER_TARGET", RECORDS_PER_TARGET); addWeb2Input(searchElement, "PER_PAGE", getIntegerRequestParameter("pageSize").toString()); addWeb2Input(searchElement, "RESULT_SET", getTransactionResultSetName()); addWeb2Input(searchElement, "APPEND", "false"); addWeb2Input(searchElement, "JITTERBUG_KEY"); element = addWeb2Input(searchElement, "DEDUPE_KEY"); element.setAttribute("dedupeMode", ""); element.setAttribute("dedupeMixMode", ""); element = addWeb2Input(searchElement, "RANKING_KEY", sortBy); element.setAttribute("rankingMode", ""); element.setAttribute("rankingOrder", ""); doWeb2InputClose(); } catch (DomException exception) { throw new SearchException(exception.toString()); } } /** * Generate a FIND command */ private void doFindCommand() throws SearchException { Element element, searchElement; String pageSize, sortBy, targets; String searchCriteria, searchFilter; int active, targetCount; /* * Set search criteria (use the search filter, if any is configured) */ searchFilter = SearchSource.getConfiguredParameter(_database, "searchFilter"); searchCriteria = (searchFilter == null) ? "" : (searchFilter + " "); searchCriteria += getSearchString(); /* * Pick up the database(s) to examine, sort mode */ targets = getRequestParameter("targets"); targetCount = new StringTokenizer(targets).countTokens(); _log.debug("Targets for search source " + _database + ", " + targetCount + " targets: " + targets); _log.debug("Search for: " + searchCriteria); sortBy = getRequestParameter("sortBy"); if (StringUtils.isNull(sortBy)) { sortBy = "ICERankingKeyRelevance"; } sortBy = ""; _log.debug("RANKING_KEY: " + sortBy); pageSize = getIntegerRequestParameter("pageSize").toString(); _log.debug("PAGE SIZE: " + pageSize); /* * And generate the FIND command */ try { doWeb2InputHeader(); searchElement = addWeb2Input("FIND"); addWeb2Input(searchElement, "TERMS", searchCriteria); addWeb2Input(searchElement, "QUERY_TYPE", "Muse"); addWeb2Input(searchElement, "TARGETS", targets); addWeb2Input(searchElement, "FIND_SET", "sakaibrary"); addWeb2Input(searchElement, "JITTERBUG_KEY"); addWeb2Input(searchElement, "PER_PAGE", pageSize); addWeb2Input(searchElement, "PER_TARGET", pageSize); element = addWeb2Input(searchElement, "DEDUPE_KEY", ""); element.setAttribute("dedupeMode", ""); element.setAttribute("dedupeMixMode", ""); element = addWeb2Input(searchElement, "RANKING_KEY", sortBy); element.setAttribute("rankingMode", ""); element.setAttribute("rankingOrder", ""); doWeb2InputClose(); saveFindReferenceId(getTransactionId()); } catch (DomException exception) { throw new SearchException(exception.toString()); } } /** * Generate a STATUS command */ private void doStatusCommand() throws SearchException { Element statusElement; try { doWeb2InputHeader(); statusElement = addWeb2Input("STATUS"); addWeb2Input(statusElement, "ID", getFindReferenceId()); doWeb2InputClose(); } catch (DomException exception) { throw new SearchException(exception.toString()); } } /** * Generate a COMBINE command */ private void doCombineCommand() throws SearchException { Element combineElement; _log.debug("COMBINE find sets: " + getFindResultSetId()); _log.debug("COMBINE output: " + getTransactionResultSetName()); try { Element element; doWeb2InputHeader(); combineElement = addWeb2Input("COMBINE"); addWeb2Input(combineElement, "RESULT_SET", getFindResultSetId()); addWeb2Input(combineElement, "OUTPUT_RESULT_SET", getTransactionResultSetName()); doWeb2InputClose(); } catch (DomException exception) { throw new SearchException(exception.toString()); } } /** * Generate a RESULTS command */ private void doResultsCommand(String resultSetId) throws SearchException { Element resultsElement; int active, start, pageSize, perTarget; active = getSessionContext().getInt("active"); start = getSessionContext().getInt("startRecord"); pageSize = getSessionContext().getInt("pageSize"); perTarget = pageSize; _nextResult += Math.min(start, pageSize); _log.debug("Results commmand: " + resultSetId); _log.debug("Active = " + active + ", start record = " + _nextResult + ", page size = " + pageSize + ", per=target = " + perTarget); try { doWeb2InputHeader(); resultsElement = addWeb2Input("RESULTS"); addWeb2Input(resultsElement, "START", String.valueOf(_nextResult)); addWeb2Input(resultsElement, "PER_PAGE", String.valueOf(pageSize)); addWeb2Input(resultsElement, "PER_TARGET", String.valueOf(perTarget)); addWeb2Input(resultsElement, "RESULT_SET", resultSetId); doWeb2InputClose(); } catch (DomException exception) { throw new SearchException(exception.toString()); } } /** * Create the Web2 input Document, add the standard Web2 XML header */ private void doWeb2InputHeader() throws DomException { setTransactionId(); _web2Document = DomUtils.createXmlDocument("MUSEWEB2-INPUT"); } /** * Format the standard Web2 XML close */ private void doWeb2InputClose() { addReferenceId(); } /** * Fetch the (Muse format) search string (overrides HttpTransactionQueryBase) * @return The native Muse query text */ public String getSearchString() { return _museSearchString; } /** * Parse CQL search queries into a crude take on the Muse format. * @param cql String containing a cql query * @return Muse search criteria */ private String parseCql(String cql) throws IllegalArgumentException { CqlParser parser; String result; _log.debug( "Initial CQL Criteria: " + cql ); parser = new CqlParser(); result = parser.doCQL2MetasearchCommand(cql); _log.debug("Processed Result: " + result); return result; } /** * Merge the STATUS and RESULTS response documents */ private void mergeResponseDocuments(Document statusDocument, Document resultsDocument) { _localResponseBytesReady = false; try { Element statusElement = DomUtils.getElement(statusDocument.getDocumentElement(), "STATUS"); DomUtils.copyDocumentNode(statusElement, resultsDocument); _localResponseBytes = DomUtils.serialize(resultsDocument).getBytes("UTF-8"); _localResponseBytesReady = true; } catch (Exception exception) { throw new SearchException(exception.toString()); } } /** * Set the xmlMessage parameter (this is the "command" sent to Web2) * @param xml XML command */ private void setWeb2InputMessage() throws SearchException { try { setParameter("xmlMessage", DomUtils.serialize(_web2Document)); } catch (DomException exception) { throw new SearchException(exception.toString()); } } /** * Format the current reference id * @return XML id */ private Element addReferenceId() { return addWeb2Input("REFERENCE_ID", getTransactionId()); } /** * Establish a transaction ID for the current activity (LOGIN, SEARCH, etc) */ private void setTransactionId() { synchronized (_sync) { _transactionId = _referenceId++; } } /** * Fetch the current transaction id * @return The ID */ private String getTransactionId() { return Long.toHexString(_transactionId); } /** * Returns a new result set name for this transaction * @return Result set name (constant portion + reference ID) */ private synchronized String saveFindReferenceId(String transactionId) { removeSessionParameter(APPLICATION, "findReferenceId"); setSessionParameter(APPLICATION, "findReferenceId", transactionId); return getFindReferenceId(); } /** * Returns a new result set name for this transaction * @return Result set name (constant portion + reference ID) */ private synchronized String getFindReferenceId() { return getSessionParameter(APPLICATION, "findReferenceId"); } public Iterator getStatusMapEntrySetIterator() { HashMap statusMap = (HashMap) getSessionContext().get("searchStatus"); Set entrySet = statusMap.entrySet(); return entrySet.iterator(); } /** * Returns a new result set name for this transaction * @return Active result set name(s) (name1|name2|name3), null if none active */ private String getFindResultSetId() { String ids = ""; int active = 0; for (Iterator iterator = getStatusMapEntrySetIterator(); iterator.hasNext(); ) { Map.Entry entry = (Map.Entry) iterator.next(); HashMap systemMap = (HashMap) entry.getValue(); String status = (String) systemMap.get("STATUS"); String id; if (!status.equals("ACTIVE")) { continue; } id = (String) systemMap.get("RESULT_SET"); if (ids.length() == 0) { ids = id; } else { ids = ids + "|" + id; } active++; } _log.debug(active + " result set ids: " + ids); getSessionContext().putInt("active", active); return (ids.length() == 0) ? null : ids; } /** * Returns a new result set name for this transaction * @return Result set name (constant portion + reference ID) */ private synchronized String getNewTransactionResultSetName() { removeSessionParameter(APPLICATION, "resultSetName"); return getTransactionResultSetName(); } /** * Returns the result set name for this transaction (SEARCH) * @return Result set name (constant portion + reference ID) */ private synchronized String getTransactionResultSetName() { String resultSetName = getSessionParameter(APPLICATION, "resultSetName"); if (resultSetName == null) { StringBuilder name = new StringBuilder("sakaibrary"); name.append(getTransactionId()); name.append(".xml"); resultSetName = name.toString(); setSessionParameter(APPLICATION, "resultSetName", resultSetName); } _log.debug("Transaction result set name: " + resultSetName); return resultSetName; } /** * Add Element and child text * @param parentElement Add new element here * @param newElementName New element name * @param text Child text (for the new element) */ private Element addWeb2Input(Element parentElement, String newElementName, String text) { Element element; element = DomUtils.createElement(parentElement, newElementName); if (!StringUtils.isNull(text)) { DomUtils.addText(element, text); } return element; } /** * Add Element and child text to document root * @param newElementName New element name * @param text Child text (for the new element) */ private Element addWeb2Input(String newElementName, String text) { return addWeb2Input(_web2Document.getDocumentElement(), newElementName, text); } /** * Add Element to parent * @param parentElement Add new element here * @param newElementName New element name */ private Element addWeb2Input(Element parentElement, String newElementName) { return addWeb2Input(parentElement, newElementName, null); } /** * Add Element to document root * @param newElementName New element name */ private Element addWeb2Input(String newElementName) { return addWeb2Input(_web2Document.getDocumentElement(), newElementName, null); } /** * Get an element from the server response * @Element parent Look for named element here * @param elementName Element name * @return The first occurance of the named element (null if none) */ private Element getElement(Element parent, String elementName) { try { Element root = parent; if (root == null) { root = getResponseDocument().getDocumentElement(); } return DomUtils.getElement(root, elementName); } catch (Exception exception) { throw new SearchException(exception.toString()); } } /** * Get an element from the server response (search from document root) * @param elementName Element name * @return The first occurance of the named element (null if none) */ private Element getElement(String elementName) { return getElement(null, elementName); } /** * Initial response validation. Verify: * <ul> * <li>Error code * <li>Correct <REFERENCE_ID> value * </ul> *<p> * @param action Server activity (SEARCH, LOGON, etc) */ private void validateResponse(String action) throws SearchException { Document document; Element element; String error, id, message, status; document = getResponseDocument(); element = getElement(document.getDocumentElement(), action); error = element.getAttribute("ERROR"); status = element.getAttribute("STATUS"); element = getElement(document.getDocumentElement(), "REFERENCE_ID"); id = DomUtils.getText(element); if (!"false".equalsIgnoreCase(error)) { String text = "Error " + error + ", status = " + status + ", for activity " + action; LogUtils.displayXml(_log, text, document); if (status.equals(NO_SESSION)) { /* * Session timeout is a special case * o Re-initialize (clear the query URL) * o Set "global failure" status * o Throw the exception */ removeQueryUrl(APPLICATION); StatusUtils.setGlobalError(getSessionContext(), status, "Session timed out"); throw new SessionTimeoutException(); } element = getElement(document.getDocumentElement(), "DATA"); if ((message = DomUtils.getText(element)) == null) { message = ""; } StatusUtils.setGlobalError(getSessionContext(), status, message); if (!StringUtils.isNull(message)) { text = "Error " + status + ": " + message; } throw new SearchException(text); } if (!getTransactionId().equalsIgnoreCase(id)) { String text = "Transaction ID mismatch, expected " + getTransactionId() + ", found " + id; LogUtils.displayXml(_log, text, document); StatusUtils.setGlobalError(getSessionContext(), "<internal>", text); throw new SearchException(text); } } /** * Save the initial status (find set name(s), estimated hits, etc.) as * session context information * @return A Map of status details (keyed by target name) */ private void setFindStatus() throws SearchException { NodeList nodeList; String target; int active, total; nodeList = DomUtils.getElementList(getResponseDocument().getDocumentElement(), "RECORD"); active = 0; total = 0; /* * Update the status map for each target */ for (int i = 0; i < nodeList.getLength(); i++) { Element recordElement = (Element) nodeList.item(i); HashMap map; String text; Element element; int estimate, hits; /* * Database */ element = DomUtils.getElement(recordElement, "TARGET"); target = DomUtils.getText(element); map = StatusUtils.getStatusMapForTarget(getSessionContext(), target); /* * Result set */ element = DomUtils.getElement(recordElement, "RESULT_SET"); text = DomUtils.getText(element); map.put("RESULT_SET", ((text == null) ? "<none>" : text)); /* * Get the estimated result count */ element = DomUtils.getElement(recordElement, "ESTIMATE"); if ((text = DomUtils.getText(element)) == null) { text = "0"; } estimate = Integer.parseInt(text); /* * Any hits available? */ element = DomUtils.getElement(recordElement, "HITS"); text = DomUtils.getText(element); hits = (text == null) ? 0 : Integer.parseInt(text); /* * One common failure mode for the database connectors is to return a * positive estimated result count with no actual hits. * * So, to use results from this database, we need to find both an * estimate and some hits. */ map.put("ESTIMATE", "0"); map.put("STATUS", "DONE"); if ((estimate > 0) && (hits > 0)) { map.put("ESTIMATE", String.valueOf(estimate)); total += estimate; map.put("STATUS", "ACTIVE"); active++; } } /* * Save in session context: * * -- The largest number of records we could possibly return * -- The count of "in progress" searches */ getSessionContext().put("maxRecords", String.valueOf(total)); getSessionContext().putInt("active", active); } /** * Save the initial SEARCH command status (find set name, estimated hits) * @return A Map of status details (keyed by target name) */ private void setSearchStatus() throws SearchException { List nodeList; String target; int active, total; nodeList = DomUtils.selectElementsByAttributeValue(getResponseDocument().getDocumentElement(), "RECORD", "type", "status"); active = 0; total = 0; for (int i = 0; i < nodeList.size(); i++) { Element recordElement = (Element) nodeList.get(i); HashMap map; String text; Element element; int max; target = getSourceId(recordElement.getAttribute("source")); if (target.equals("unavailable")) { target = recordElement.getAttribute("source"); } map = StatusUtils.getStatusMapForTarget(getSessionContext(), target); map.put("RESULT_SET", getTransactionResultSetName()); map.put("HITS", "0"); element = DomUtils.getElement(recordElement, "ESTIMATE"); text = DomUtils.getText(element); map.put("ESTIMATE", text); max = Integer.parseInt(text); total += max; map.put("STATUS", "DONE"); if (max > 0) { map.put("STATUS", "ACTIVE"); active++; } } /* * Save in session context: * * -- The largest number of records we could possibly return * -- The count of "in progress" searches */ getSessionContext().put("maxRecords", String.valueOf(total)); getSessionContext().putInt("active", active); } /** * Look up the "sourceID" attribute (the target name) for a specified * RECORD element "source" * @param source Source attribute text * @return The sourceID attribute */ private String getSourceId(String source) { NodeList nodeList; nodeList = DomUtils.getElementList(getResponseDocument().getDocumentElement(), "RECORD"); for (int i = 0; i < nodeList.getLength(); i++) { Element recordElement = (Element) nodeList.item(i); if (source.equals(recordElement.getAttribute("source"))) { return recordElement.getAttribute("sourceID"); } } return "unavailable"; } }