edu.indiana.lib.twinpeaks.search.singlesearch.web2.Web2Query.java Source code

Java tutorial

Introduction

Here is the source code for edu.indiana.lib.twinpeaks.search.singlesearch.web2.Web2Query.java

Source

/**********************************************************************************
*
 * 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";
    }
}