/* * OpenSearchGenerator.java * * Version: $Revision: 1.4 $ * * Date: $Date: 2006/01/10 04:28:19 $ * * Copyright (c) 2002, Hewlett-Packard Company and Massachusetts * Institute of Technology. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * - Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * - Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * - Neither the name of the Hewlett-Packard Company nor the name of the * Massachusetts Institute of Technology nor the names of their * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH * DAMAGE. */ package org.dspace.app.xmlui.cocoon; import java.io.IOException; import java.io.Serializable; import java.net.URLDecoder; import java.sql.SQLException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.ResourceBundle; import org.apache.avalon.excalibur.pool.Recyclable; import org.apache.avalon.framework.parameters.Parameters; import org.apache.avalon.framework.parameters.ParameterException; import org.apache.cocoon.ProcessingException; import org.apache.cocoon.ResourceNotFoundException; import org.apache.cocoon.caching.CacheableProcessingComponent; import org.apache.cocoon.environment.ObjectModelHelper; import org.apache.cocoon.environment.Request; import org.apache.cocoon.environment.SourceResolver; import org.apache.cocoon.environment.http.HttpRequest; import org.apache.cocoon.generation.AbstractGenerator; import org.apache.cocoon.util.HashUtil; import org.apache.cocoon.xml.dom.DOMStreamer; import org.apache.excalibur.source.SourceValidity; import org.apache.excalibur.source.impl.validity.ExpiresValidity; import org.apache.log4j.Logger; import org.dspace.app.util.OpenSearch; import org.dspace.app.util.SyndicationFeed; import org.dspace.app.xmlui.utils.ContextUtil; import org.dspace.app.xmlui.utils.FeedUtils; import org.dspace.search.DSQuery; import org.dspace.search.QueryArgs; import org.dspace.search.QueryResults; import org.dspace.sort.SortOption; import org.dspace.content.Collection; import org.dspace.content.Community; import org.dspace.content.DSpaceObject; import org.dspace.core.ConfigurationManager; import org.dspace.core.Constants; import org.dspace.core.Context; import org.dspace.handle.HandleManager; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; /** * * Generate an OpenSearch-compliant search results document for DSpace, either * a community or collection or the whole repository. * * Once thing that has been modified from DSpace's JSP implementation is what * is placed inside an item's description, we've changed it so that the list * of metadata fields is scanned until a value is found and the first one * found is used as the description. This means that we look at the abstract, * description, alternative title, title, etc... to and the first metadata * value found is used. * * I18N: Feed's are internationalized, meaning that they may contain references * to messages contained in the global messages.xml file using cocoon's i18n * schema. However the library used to build the feeds does not understand * this schema to work around this limitation I created a little hack. It * basically works like this, when text that needs to be localized is put into * the feed it is always mangled such that a prefix is added to the messages's * key. Thus if the key were "xmlui.feed.text" then the resulting text placed * into the feed would be "I18N:xmlui.feed.text". After the library is finished * and produced it's final result the output is traversed to find these * occurrences and replace them with proper cocoon i18n elements. * * @author Richard Rodgers */ public class OpenSearchGenerator extends AbstractGenerator implements CacheableProcessingComponent, Recyclable { private static final Logger log = Logger.getLogger(OpenSearchGenerator.class); /** Cache of this object's validity */ private ExpiresValidity validity = null; /** The results requested format */ private String format = null; /** the search query string */ private String query = null; /** optional search scope (= handle of container) or null */ private String scope = null; /** optional sort specification */ private int sort = 0; /** sort order, see SortOption **/ private String sortOrder = null; /** results per page */ private int rpp = 0; /** first result index */ private int start = 0; /** request type */ private String type = null; /** the results document (cached) */ private Document resultsDoc = null; /** * Generate the unique caching key. * This key must be unique inside the space of this component. */ public Serializable getKey() { StringBuffer key = new StringBuffer("key:"); if (scope != null) key.append(scope); key.append(query); if (format != null) key.append(format); key.append(sort); key.append(start); key.append(rpp); key.append(sortOrder); return HashUtil.hash(key.toString()); } /** * Generate the cache validity object. * */ public SourceValidity getValidity() { if (this.validity == null) { long expiry = System.currentTimeMillis() + ConfigurationManager.getIntProperty("websvc.opensearch.validity") * 60 * 60 * 1000; this.validity = new ExpiresValidity(expiry); } return this.validity; } /** * Setup configuration for this request */ public void setup(SourceResolver resolver, Map objectModel, String src, Parameters par) throws ProcessingException, SAXException, IOException { super.setup(resolver, objectModel, src, par); Request request = ObjectModelHelper.getRequest(objectModel); this.query = request.getParameter("query"); if (query == null) query = ""; query = URLDecoder.decode(query, "UTF-8"); this.format = request.getParameter("format"); if (format == null || format.length() == 0) format = "atom"; this.scope = request.getParameter("scope"); String srt = request.getParameter("sort_by"); this.sort = (srt == null || srt.length() == 0) ? 0 : Integer.valueOf(srt); String order = request.getParameter("order"); this.sortOrder = (order == null || order.length() == 0 || order.toLowerCase().startsWith("asc")) ? SortOption.ASCENDING : SortOption.DESCENDING; String st = request.getParameter("start"); this.start = (st == null || st.length() == 0) ? 0 : Integer.valueOf(st); String pp = request.getParameter("rpp"); this.rpp = (pp == null || pp.length() == 0) ? 0 : Integer.valueOf(pp); try { this.type = par.getParameter("type"); } catch(ParameterException e) { this.type = "results"; } } /** * Generate the search results document. */ public void generate() throws IOException, SAXException, ProcessingException { Document retDoc = null; try { if (type != null && type.equals("description")) { retDoc = OpenSearch.getDescriptionDoc(scope); } else if (resultsDoc != null) { // use cached document if available retDoc = resultsDoc; } else { Context context = ContextUtil.obtainContext(objectModel); QueryArgs qArgs = new QueryArgs(); // can't start earlier than 0 in the results! if (start < 0) { start = 0; } qArgs.setStart(start); if (rpp > 0) { qArgs.setPageSize(rpp); } qArgs.setSortOrder(sortOrder); if (sort > 0) { try { qArgs.setSortOption(SortOption.getSortOption(sort)); } catch(Exception e) { // invalid sort id - do nothing } } qArgs.setSortOrder(sortOrder); // If there is a scope parameter, attempt to dereference it // failure will only result in its being ignored DSpaceObject container = null; if ((scope != null) && !scope.equals("")) { container = HandleManager.resolveToObject(context, scope); } qArgs.setQuery(query); // Perform the search QueryResults qResults = null; if (container == null) { qResults = DSQuery.doQuery(context, qArgs); } else if (container instanceof Collection) { qResults = DSQuery.doQuery(context, qArgs, (Collection)container); } else if (container instanceof Community) { qResults = DSQuery.doQuery(context, qArgs, (Community)container); } // now instantiate the results DSpaceObject[] results = new DSpaceObject[qResults.getHitHandles().size()]; for (int i = 0; i < qResults.getHitHandles().size(); i++) { String myHandle = (String)qResults.getHitHandles().get(i); DSpaceObject dso = HandleManager.resolveToObject(context, myHandle); if (dso == null) { throw new SQLException("Query \"" + query + "\" returned unresolvable handle: " + myHandle); } results[i] = dso; } resultsDoc = OpenSearch.getResultsDoc(format, query, qResults, container, results, FeedUtils.i18nLabels); FeedUtils.unmangleI18N(resultsDoc); retDoc = resultsDoc; } if (retDoc != null) { DOMStreamer streamer = new DOMStreamer(contentHandler, lexicalHandler); streamer.stream(retDoc); } } catch (IOException e) { throw new SAXException(e); } catch (SQLException sqle) { throw new SAXException(sqle); } } /** * Recycle */ public void recycle() { this.format = null; this.query = null; this.scope = null; this.sort = 0; this.rpp = 0; this.start = 0; this.type = null; this.sortOrder = null; this.resultsDoc = null; this.validity = null; super.recycle(); } }