/*
* OpenSearch.java
*
* Version: $Revision: 1.20 $
*
* Date: $Date: 2005/08/25 17:20:27 $
*
* Copyright (c) 2002-2009, The DSpace Foundation. 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 DSpace Foundation nor the names of its
* 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.util;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
import java.net.URLEncoder;
import java.io.UnsupportedEncodingException;
import org.w3c.dom.Document;
import org.jdom.DocType;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.Namespace;
import org.jdom.output.DOMOutputter;
import org.jdom.output.XMLOutputter;
import org.apache.log4j.Logger;
import org.dspace.content.Bitstream;
import org.dspace.content.Collection;
import org.dspace.content.Community;
import org.dspace.content.DCDate;
import org.dspace.content.DCValue;
import org.dspace.content.DSpaceObject;
import org.dspace.content.Item;
import org.dspace.core.ConfigurationManager;
import org.dspace.core.Constants;
import org.dspace.handle.HandleManager;
import org.dspace.search.QueryResults;
import com.sun.syndication.feed.WireFeed;
import com.sun.syndication.feed.atom.Category;
import com.sun.syndication.feed.atom.Content;
import com.sun.syndication.feed.atom.Feed;
import com.sun.syndication.feed.atom.Entry;
import com.sun.syndication.feed.atom.Link;
import com.sun.syndication.feed.atom.Person;
import com.sun.syndication.feed.rss.Channel;
import com.sun.syndication.feed.rss.Description;
import com.sun.syndication.feed.rss.Image;
import com.sun.syndication.io.WireFeedOutput;
import com.sun.syndication.feed.module.opensearch.OpenSearchModule;
import com.sun.syndication.feed.module.opensearch.entity.OSQuery;
import com.sun.syndication.feed.module.opensearch.impl.OpenSearchModuleImpl;
import com.sun.syndication.io.FeedException;
/**
* Utility Class with static methods for producing OpenSearch-compliant search results,
* and the OpenSearch description document.
* <p>
* OpenSearch is a specification for describing and advertising search-engines
* and their result formats. Commonly, RSS and Atom formats are used, which
* the current implementation supports, as is HTML (used directly in browsers).
* NB: this is baseline OpenSearch, no extensions currently supported.
* </p>
* <p>
* The value of the "scope" parameter should either be absent (which means no
* scope restriction), or the handle of a community or collection.
* </p>
*
* @author Richard Rodgers
*
*/
public class OpenSearch
{
private static final Logger log = Logger.getLogger(OpenSearch.class);
// are open search queries enabled?
private static boolean enabled = false;
// supported results formats
private static List<String> formats = null;
// Namespaces used
private static final String osNs = "http://a9.com/-/spec/opensearch/1.1/";
// base search UI URL
private static String uiUrl = null;
// base search service URL
private static String svcUrl = null;
static
{
enabled = ConfigurationManager.getBooleanProperty("websvc.opensearch.enable");
svcUrl = ConfigurationManager.getProperty("dspace.url") + "/" +
ConfigurationManager.getProperty("websvc.opensearch.svccontext");
uiUrl = ConfigurationManager.getProperty("dspace.url") + "/" +
ConfigurationManager.getProperty("websvc.opensearch.uicontext");
// read rest of config info if enabled
formats = new ArrayList<String>();
if (enabled)
{
String fmtsStr = ConfigurationManager.getProperty("websvc.opensearch.formats");
if ( fmtsStr != null )
{
for (String fmt : fmtsStr.split(","))
{
formats.add(fmt);
}
}
}
}
/**
* Returns list of supported formats
*
* @return list of format names - 'rss', 'atom' or 'html'
*/
public static List<String> getFormats()
{
return formats;
}
/**
* Returns a mime-type associated with passed format
*
* @param format the results document format (rss, atom, html)
* @return content-type mime-type
*/
public static String getContentType(String format)
{
return "html".equals(format) ? "text/html" :
"application/" + format + "+xml; charset=UTF-8";
}
/**
* Returns the OpenSearch service document appropriate for given scope
*
* @param scope - null for entire repository, or handle or community or collection
* @return document the service document
* @throws IOException
*/
public static Document getDescriptionDoc(String scope) throws IOException
{
return jDomToW3(getServiceDocument(scope));
}
/**
* Returns OpenSearch Servic Document as a string
*
* @param scope - null for entire repository, or handle or community or collection
* @return service document as a string
*/
public static String getDescription(String scope)
{
return new XMLOutputter().outputString(getServiceDocument(scope));
}
/**
* Returns a formatted set of search results as a string
*
* @param format results format - html, rss or atom
* @param query - the search query
* @param qResults - the query results to be formatted
* @param scope - search scope, null or community/collection handle
* @param results the retreived DSpace objects satisfying search
* @param labels labels to apply - format specific
* @return formatted search results
* @throws IOException
*/
public static String getResultsString(String format, String query, QueryResults qResults,
DSpaceObject scope, DSpaceObject[] results,
Map<String, String> labels) throws IOException
{
try
{
return getResults(format, query, qResults, scope, results, labels).outputString();
}
catch (FeedException e)
{
log.error(e.toString(), e);
throw new IOException("Unable to generate feed");
}
}
/**
* Returns a formatted set of search results as a document
*
* @param format results format - html, rss or atom
* @param query - the search query
* @param qResults - the query results to be formatted
* @param scope - search scope, null or community/collection handle
* @param results the retreived DSpace objects satisfying search
* @param labels labels to apply - format specific
* @return formatted search results
* @throws IOException
*/
public static Document getResultsDoc(String format, String query, QueryResults qResults,
DSpaceObject scope, DSpaceObject[] results, Map<String, String> labels)
throws IOException
{
try
{
return getResults(format, query, qResults, scope, results, labels).outputW3CDom();
}
catch (FeedException e)
{
log.error(e.toString(), e);
throw new IOException("Unable to generate feed");
}
}
private static SyndicationFeed getResults(String format, String query, QueryResults qResults,
DSpaceObject scope, DSpaceObject[] results, Map<String, String> labels)
{
// Encode results in requested format
if ("rss".equals(format))
format = "rss_2.0";
else if ("atom".equals(format))
format = "atom_1.0";
SyndicationFeed feed = new SyndicationFeed(labels.get(SyndicationFeed.MSG_UITYPE));
feed.populate(null, scope, results, labels);
feed.setType(format);
feed.addModule(openSearchMarkup(query, qResults));
return feed;
}
/*
* Generates the OpenSearch elements which are added to the RSS or Atom feeds as foreign markup
* wrapped in a module
*
* @param query the search query
* @param qRes the search results
* @return module
*/
private static OpenSearchModule openSearchMarkup(String query, QueryResults qRes)
{
OpenSearchModule osMod = new OpenSearchModuleImpl();
osMod.setTotalResults(qRes.getHitCount());
osMod.setStartIndex(qRes.getStart());
osMod.setItemsPerPage(qRes.getPageSize());
OSQuery osq = new OSQuery();
osq.setRole("request");
try
{
osq.setSearchTerms(URLEncoder.encode(query, "UTF-8"));
}
catch(UnsupportedEncodingException e)
{
log.error(e);
}
osq.setStartPage(1 + (qRes.getStart() / qRes.getPageSize()));
osMod.addQuery(osq);
return osMod;
}
/**
* Returns as a document the OpenSearch service document
*
* @param scope - null for the entire repository, or a collection/community handle
* @return Service Document
*/
private static org.jdom.Document getServiceDocument(String scope)
{
Namespace ns = Namespace.getNamespace(osNs);
Element root = new Element("OpenSearchDescription", ns);
root.addContent(new Element("ShortName", ns).setText(ConfigurationManager.getProperty("websvc.opensearch.shortname")));
root.addContent(new Element("LongName", ns).setText(ConfigurationManager.getProperty("websvc.opensearch.longname")));
root.addContent(new Element("Description", ns).setText(ConfigurationManager.getProperty("websvc.opensearch.description")));
root.addContent(new Element("InputEncoding", ns).setText("UTF-8"));
root.addContent(new Element("OutputEncoding", ns).setText("UTF-8"));
// optional elements
String sample = ConfigurationManager.getProperty("websvc.opensearch.samplequery");
if (sample != null && sample.length() > 0)
{
Element sq = new Element("Query", ns).setAttribute("role", "example");
root.addContent(sq.setAttribute("searchTerms", sample));
}
String tags = ConfigurationManager.getProperty("websvc.opensearch.tags");
if (tags != null && tags.length() > 0)
{
root.addContent(new Element("Tags", ns).setText(tags));
}
String contact = ConfigurationManager.getProperty("mail.admin");
if (contact != null && contact.length() > 0)
{
root.addContent(new Element("Contact", ns).setText(contact));
}
String faviconUrl = ConfigurationManager.getProperty("websvc.opensearch.faviconurl");
if (faviconUrl != null && faviconUrl.length() > 0)
{
String dim = String.valueOf(16);
String type = faviconUrl.endsWith("ico") ? "image/vnd.microsoft.icon" : "image/png";
Element fav = new Element("Image", ns).setAttribute("height", dim).setAttribute("width", dim).
setAttribute("type", type).setText(faviconUrl);
root.addContent(fav);
}
// service URLs
for (String format : formats)
{
Element url = new Element("Url", ns).setAttribute("type", getContentType(format));
StringBuffer template = new StringBuffer();
if ("html".equals(format))
{
template.append(uiUrl);
}
else
{
template.append(svcUrl);
}
template.append("?query={searchTerms}");
if(! "html".equals(format))
{
template.append("&start={startIndex?}&rpp={count?}&format=");
template.append(format);
}
if (scope != null)
{
template.append("&scope=");
template.append(scope);
}
url.setAttribute("template", template.toString());
root.addContent(url);
}
return new org.jdom.Document(root);
}
/**
* Converts a JDOM document to a W3C one
* @param jdomDoc
* @return W3C Document object
* @throws IOException
*/
private static Document jDomToW3(org.jdom.Document jdomDoc) throws IOException
{
DOMOutputter domOut = new DOMOutputter();
try
{
return domOut.output(jdomDoc);
}
catch(JDOMException jde)
{
throw new IOException("JDOM output exception");
}
}
}