/*
* Copyright (2005-2012) Schibsted ASA
* This file is part of Possom.
*
* Possom is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Possom is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Possom. If not, see <http://www.gnu.org/licenses/>.
*/
package no.sesat.search.http.servlet;
import java.io.IOException;
import java.util.Properties;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.parsers.DocumentBuilder;
import no.sesat.commons.ioc.BaseContext;
import no.sesat.commons.ioc.ContextWrapper;
import no.sesat.search.datamodel.DataModel;
import no.sesat.search.datamodel.DataModelFactory;
import no.sesat.search.datamodel.access.ControlLevel;
import no.sesat.search.datamodel.generic.StringDataObject;
import no.sesat.search.datamodel.request.ParametersDataObject;
import no.sesat.search.http.servlet.FactoryReloads.ReloadArg;
import no.sesat.search.mode.SearchMode;
import no.sesat.search.mode.SearchModeFactory;
import no.sesat.search.run.QueryFactory;
import no.sesat.search.run.RunningQuery;
import no.sesat.search.run.RunningQueryUtility;
import no.sesat.search.site.Site;
import no.sesat.search.site.SiteContext;
import no.sesat.search.site.SiteKeyedFactoryInstantiationException;
import no.sesat.search.site.config.BytecodeLoader;
import no.sesat.search.site.config.DocumentLoader;
import no.sesat.search.site.config.PropertiesLoader;
import no.sesat.search.site.config.SiteConfiguration;
import no.sesat.search.site.config.TextMessages;
import no.sesat.search.site.config.UrlResourceLoader;
import no.sesat.search.view.config.SearchTab;
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.lang.time.StopWatch;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
/** The Central Controller to incoming queries.
* Controls the SearchMode -> RunningQuery creation and handling.
*
*
*
* @version <tt>$Id$</tt>
*/
public final class SearchServlet extends HttpServlet {
// Constants -----------------------------------------------------
/** The serialVersionUID. */
private static final long serialVersionUID = 3068140845772756438L;
private static final String REMOTE_ADDRESS_KEY = "REMOTE_ADDR";
private static final Logger LOG = Logger.getLogger(SearchServlet.class);
private static final Logger ACCESS_LOG = Logger.getLogger("no.sesat.Access");
private static final Logger STATISTICS_LOG = Logger.getLogger("no.sesat.Statistics");
private static final String ERR_MISSING_MODE = "No existing implementation for mode ";
// Attributes ----------------------------------------------------
// Attributes ----------------------------------------------------
// Important that a Servlet does not have instance fields for synchronisation reasons.
//
// Static --------------------------------------------------------
static{
// when the root logger is set to DEBUG do not limit connection times
if(Logger.getRootLogger().getLevel().isGreaterOrEqual(Level.INFO)){
System.setProperty("http.keepAlive", "false");
System.setProperty("sun.net.client.defaultConnectTimeout", "1000");
System.setProperty("sun.net.client.defaultReadTimeout", "3000");
System.setProperty("sun.net.http.errorstream.enableBuffering", "true");
}
}
// Constructors --------------------------------------------------
// Public --------------------------------------------------------
@Override
public void destroy() {
super.destroy();
}
// Package protected ---------------------------------------------
// Protected -----------------------------------------------------
@Override
protected void doGet(
final HttpServletRequest request,
final HttpServletResponse response)
throws ServletException, IOException {
logAccessRequest(request);
LOG.trace("doGet()");
final DataModel datamodel = (DataModel) request.getSession().getAttribute(DataModel.KEY);
final ParametersDataObject parametersDO = datamodel.getParameters();
final Site site = datamodel.getSite().getSite();
// BaseContext providing SiteContext and ResourceContext.
// We need it casted as a SiteContext for the ResourceContext code to be happy.
final SiteContext genericCxt = new SiteContext(){
public PropertiesLoader newPropertiesLoader(
final SiteContext siteCxt,
final String resource,
final Properties properties) {
return UrlResourceLoader.newPropertiesLoader(siteCxt, resource, properties);
}
public DocumentLoader newDocumentLoader(
final SiteContext siteCxt,
final String resource,
final DocumentBuilder builder) {
return UrlResourceLoader.newDocumentLoader(siteCxt, resource, builder);
}
public BytecodeLoader newBytecodeLoader(SiteContext context, String className, final String jar) {
return UrlResourceLoader.newBytecodeLoader(context, className, jar);
}
@Override
public Site getSite() {
return site;
}
};
final DataModelFactory dmFactory;
try{
dmFactory = DataModelFactory.instanceOf(ContextWrapper.wrap(DataModelFactory.Context.class, genericCxt));
}catch(SiteKeyedFactoryInstantiationException skfe){
throw new ServletException(skfe);
}
// DataModel's ControlLevel will be VIEW_CONSTRUCTION (safe setting set by DataModelFilter)
// Bring it back to VIEW_CONSTRUCTION.
dmFactory.assignControlLevel(datamodel, ControlLevel.REQUEST_CONSTRUCTION);
try{
final String cParameter = null != parametersDO.getValue(SearchTab.PARAMETER_KEY)
? parametersDO.getValue(SearchTab.PARAMETER_KEY).getString()
: null;
final SearchTab searchTab = RunningQueryUtility
.findSearchTabByKey(datamodel, cParameter, dmFactory, genericCxt);
if (null!= searchTab) {
// this is legacy. shorter to write in templates than $datamodel.page.currentTab
request.setAttribute("tab", searchTab);
LOG.debug("Character encoding =" + request.getCharacterEncoding());
final StopWatch stopWatch = new StopWatch();
stopWatch.start();
final String ipAddr = null != request.getAttribute(REMOTE_ADDRESS_KEY)
? (String) request.getAttribute(REMOTE_ADDRESS_KEY)
: request.getRemoteAddr();
performFactoryReloads(
request.getParameter("reload"),
genericCxt,
ipAddr,
datamodel.getSite().getSiteConfiguration());
// If the rss is hidden, require a partnerId.
// The security by obscurity has been somewhat improved by the
// addition of rssPartnerId as a md5-protected parameter (MD5ProtectedParametersFilter).
final StringDataObject layout = parametersDO.getValue("layout");
boolean hiddenRssWithoutPartnerId = null != layout
&& "rss".equals(layout.getString())
&& searchTab.isRssHidden()
&& null == parametersDO.getValues().get("rssPartnerId");
if (hiddenRssWithoutPartnerId) {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
} else if (null != layout && "rss".equals(layout.getString()) && "".equals(searchTab.getRssResultName())) {
LOG.warn("RSS not supported for requested vertical " + searchTab.toString());
response.sendError(HttpServletResponse.SC_NOT_FOUND);
} else {
performSearch(request, response, genericCxt, searchTab, stopWatch);
getServletContext().getRequestDispatcher("/WEB-INF/jsp/start.jsp").forward(request, response);
}
}else{
response.sendError(HttpServletResponse.SC_NOT_FOUND);
}
}finally{
// DataModel's ControlLevel will be REQUEST_CONSTRUCTION or RUNNING_QUERY_HANDLING
// Increment it onwards to VIEW_CONSTRUCTION.
dmFactory.assignControlLevel(datamodel, ControlLevel.VIEW_CONSTRUCTION);
}
}
// Private -------------------------------------------------------
private static void performFactoryReloads(
final String reload,
final SiteContext genericCxt,
final String ipAddr,
final SiteConfiguration siteConf){
// check if user ipaddress is permitted to perform a factory reload.
boolean allowed =
ipAddr.startsWith("127.") || ipAddr.startsWith("10.") || ipAddr.startsWith("0:0:0:0:0:0:0:1%0");
final String[] ipaddress = null != siteConf.getProperty("sesat.factoryReload.ipaddresses.allowed")
? siteConf.getProperty("sesat.factoryReload.ipaddresses.allowed").split(",")
: new String[]{};
for (String s : ipaddress) { allowed |= ipAddr.startsWith(s); }
if (null != reload && reload.length() > 0){
if (allowed){
try{
final ReloadArg arg = ReloadArg.valueOf(reload.toUpperCase());
FactoryReloads.performReloads(genericCxt, arg);
}catch(IllegalArgumentException ex){
LOG.info("Invalid reload parameter -->" + reload);
}
}else{
LOG.warn("ipaddress " + ipAddr + " not allowed to performFactoryReload(..)");
}
}
}
private static void updateAttributes(
final HttpServletRequest request,
final RunningQuery.Context rqCxt){
final DataModel datamodel = (DataModel) request.getSession().getAttribute(DataModel.KEY);
// TODO remove next two, access through datamodel instead.
request.setAttribute("text",TextMessages.valueOf(ContextWrapper.wrap(
TextMessages.Context.class,
rqCxt,new SiteContext(){
@Override
public Site getSite() {
return datamodel.getSite().getSite();
}
})));
request.setAttribute("no.sesat.Statistics", new StringBuffer());
}
private static void performSearch(
final HttpServletRequest request,
final HttpServletResponse response,
final SiteContext genericCxt,
final SearchTab searchTab,
final StopWatch stopWatch) throws IOException{
final SearchMode mode = SearchModeFactory.instanceOf(
ContextWrapper.wrap(SearchModeFactory.Context.class, genericCxt))
.getMode(searchTab.getMode());
if (mode == null) {
LOG.error(ERR_MISSING_MODE + searchTab.getMode());
throw new UnsupportedOperationException(ERR_MISSING_MODE + searchTab.getMode());
}
final DataModel datamodel = (DataModel) request.getSession().getAttribute(DataModel.KEY);
final StringDataObject layout = datamodel.getParameters().getValue("layout");
final RunningQuery.Context rqCxt = ContextWrapper.wrap(
RunningQuery.Context.class,
new BaseContext() {
public DataModel getDataModel(){
return datamodel;
}
public SearchMode getSearchMode() {
return mode;
}
public SearchTab getSearchTab() {
return searchTab;
}
},
genericCxt
);
updateAttributes(request, rqCxt);
if(null == layout || !"opensearch".equalsIgnoreCase(layout.getString())){
try {
// DataModel's ControlLevel will be REQUEST_CONSTRUCTION
// Increment it onwards to RUNNING_QUERY_CONSTRUCTION.
DataModelFactory
.instanceOf(ContextWrapper.wrap(DataModelFactory.Context.class, genericCxt))
.assignControlLevel(datamodel, ControlLevel.RUNNING_QUERY_CONSTRUCTION);
final RunningQuery query = QueryFactory.getInstance().createQuery(rqCxt, request, response);
if( !datamodel.getQuery().getQuery().isBlank() || searchTab.isExecuteOnBlank() ){
query.run();
stopWatch.stop();
LOG.info("Search took " + stopWatch + " " + datamodel.getQuery().getString());
if(!"NOCOUNT".equals(request.getParameter("IGNORE"))){
STATISTICS_LOG.info(
"<search-servlet"
+ (null != layout ? " layout=\"" + layout.getXmlEscaped() + "\">" : ">")
+ "<query>" + datamodel.getQuery().getXmlEscaped() + "</query>"
+ "<time>" + stopWatch + "</time>"
+ ((StringBuffer)request.getAttribute("no.sesat.Statistics")).toString()
+ "</search-servlet>");
}
}
} catch (InterruptedException e) {
LOG.error("Task timed out");
} catch (SiteKeyedFactoryInstantiationException e) {
LOG.error(e.getMessage(), e);
}
}
}
// next to duplicate from SiteLocatorFilter
private static void logAccessRequest(final HttpServletRequest request){
final String url = request.getRequestURI()
+ (null != request.getQueryString() ? '?' + request.getQueryString() : "");
final String referer = request.getHeader("Referer");
final String method = request.getMethod();
final String ip = request.getRemoteAddr();
final String userAgent = request.getHeader("User-Agent");
final String sesamId = getCookieValue(request, "SesamID");
final String sesamUser = getCookieValue(request, "SesamUser");
ACCESS_LOG.info("<search-servlet>"
+ "<real-url method=\"" + method + "\">" + StringEscapeUtils.escapeXml(url) + "</real-url>"
+ (null != referer ? "<referer>" + StringEscapeUtils.escapeXml(referer) + "</referer>" : "")
+ "<browser ipaddress=\"" + ip + "\">" + StringEscapeUtils.escapeXml(userAgent) + "</browser>"
+ "<user id=\"" + sesamId + "\">" + sesamUser + "</user>"
+ "</search-servlet>");
}
// probably apache commons could simplify this // duplicated in SiteLocatorFilter
static String getCookieValue(final HttpServletRequest request, final String cookieName){
String value = "";
// Look in attributes (it could have already been updated this request)
if( null != request ){
// Look through cookies
if( null != request.getCookies() ){
for( Cookie c : request.getCookies()){
if( c.getName().equals( cookieName ) ){
value = c.getValue();
break;
}
}
}
}
return value;
}
// Inner classes -------------------------------------------------
}