From 73024a4d03a2ca8f0e75e2ad59ab87aa61b74b72 Mon Sep 17 00:00:00 2001 From: rjy7 Date: Fri, 27 Aug 2010 15:52:30 +0000 Subject: [PATCH] Reimplement search controller and search results display template in Freemarker. Error cases not handled yet. --- webapp/config/web.xml | 13 +- .../freemarker/FreemarkerHttpServlet.java | 1 + .../controller/freemarker/UrlBuilder.java | 15 +- .../FreemarkerPagedSearchController.java | 866 ++++++++++++++++++ .../search/controller/IndexController.java | 4 +- .../controller/PagedSearchController.java | 6 +- .../web/templatemodels/BaseTemplateModel.java | 14 +- .../IndividualTemplateModel.java | 2 +- .../web/templatemodels/LinkTemplateModel.java | 56 ++ .../freemarker/body/individualList.ftl | 18 +- .../freemarker/body/pagedSearchResults.ftl | 55 ++ .../class/view/search/view-search-default.ftl | 15 +- webapp/web/templates/search/searchPaged.jsp | 3 +- 13 files changed, 1042 insertions(+), 26 deletions(-) create mode 100644 webapp/src/edu/cornell/mannlib/vitro/webapp/search/controller/FreemarkerPagedSearchController.java create mode 100644 webapp/src/edu/cornell/mannlib/vitro/webapp/web/templatemodels/LinkTemplateModel.java create mode 100644 webapp/web/templates/freemarker/body/pagedSearchResults.ftl diff --git a/webapp/config/web.xml b/webapp/config/web.xml index 770d9fdf1..e8a973593 100644 --- a/webapp/config/web.xml +++ b/webapp/config/web.xml @@ -843,7 +843,7 @@ SearchController - edu.cornell.mannlib.vitro.webapp.search.controller.PagedSearchController + edu.cornell.mannlib.vitro.webapp.search.controller.FreemarkerPagedSearchController SearchController @@ -875,7 +875,16 @@ AutocompleteController /populateselect - + + + ReorderController + edu.cornell.mannlib.vitro.webapp.controller.edit.ReorderController + + + ReorderController + /edit/reorder + + AdminController edu.cornell.mannlib.vitro.webapp.controller.AdminController diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/FreemarkerHttpServlet.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/FreemarkerHttpServlet.java index 12d8cf988..8b40744bd 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/FreemarkerHttpServlet.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/FreemarkerHttpServlet.java @@ -251,6 +251,7 @@ public class FreemarkerHttpServlet extends VitroHttpServlet { urls.put("siteAdmin", urlBuilder.getPortalUrl(Route.LOGIN)); urls.put("siteIcons", urlBuilder.getPortalUrl(themeDir + "/site_icons")); + return urls; } diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/UrlBuilder.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/UrlBuilder.java index a9194a636..fb99415ef 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/UrlBuilder.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/UrlBuilder.java @@ -6,6 +6,7 @@ import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.net.URLEncoder; import java.util.HashMap; +import java.util.Map; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -143,14 +144,19 @@ public class UrlBuilder { public Params() { } public Params(String...strings) { - this(); int stringCount = strings.length; for (int i = 0; i < stringCount; i=i+2) { // Skip the last item if there's an odd number if (i == stringCount-1) { break; } + // Skip a param with a null value + if (strings[i+1] == null) { continue; } this.put(strings[i], strings[i+1]); } - } + } + + public Params(Map map) { + this.putAll(map); + } } /********** Static utility methods **********/ @@ -164,6 +170,11 @@ public class UrlBuilder { return path.isEmpty() ? "/" : path; } + public static String getUrl(String path, String...params) { + Params urlParams = new Params(params); + return getUrl(path, urlParams); + } + public static String getUrl(String path, Params params) { path = getPath(path, params); return getUrl(path); diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/search/controller/FreemarkerPagedSearchController.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/search/controller/FreemarkerPagedSearchController.java new file mode 100644 index 000000000..83e9f4d4a --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/search/controller/FreemarkerPagedSearchController.java @@ -0,0 +1,866 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.search.controller; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.servlet.RequestDispatcher; +import javax.servlet.ServletConfig; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.document.Document; +import org.apache.lucene.document.Field; +import org.apache.lucene.index.CorruptIndexException; +import org.apache.lucene.index.Term; +import org.apache.lucene.queryParser.QueryParser; +import org.apache.lucene.search.BooleanClause; +import org.apache.lucene.search.BooleanQuery; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.TermQuery; +import org.apache.lucene.search.TopDocs; +import org.apache.lucene.search.WildcardQuery; +import org.apache.lucene.store.Directory; +import org.apache.lucene.store.FSDirectory; + +import edu.cornell.mannlib.vitro.webapp.beans.DataProperty; +import edu.cornell.mannlib.vitro.webapp.beans.DataPropertyStatement; +import edu.cornell.mannlib.vitro.webapp.beans.Individual; +import edu.cornell.mannlib.vitro.webapp.beans.IndividualImpl; +import edu.cornell.mannlib.vitro.webapp.beans.ObjectProperty; +import edu.cornell.mannlib.vitro.webapp.beans.ObjectPropertyStatement; +import edu.cornell.mannlib.vitro.webapp.beans.Portal; +import edu.cornell.mannlib.vitro.webapp.beans.VClass; +import edu.cornell.mannlib.vitro.webapp.beans.VClassGroup; +import edu.cornell.mannlib.vitro.webapp.controller.Controllers; +import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest; +import edu.cornell.mannlib.vitro.webapp.controller.freemarker.FreemarkerHttpServlet; +import edu.cornell.mannlib.vitro.webapp.controller.freemarker.UrlBuilder.Params; +import edu.cornell.mannlib.vitro.webapp.dao.DataPropertyDao; +import edu.cornell.mannlib.vitro.webapp.dao.IndividualDao; +import edu.cornell.mannlib.vitro.webapp.dao.ObjectPropertyDao; +import edu.cornell.mannlib.vitro.webapp.dao.VClassDao; +import edu.cornell.mannlib.vitro.webapp.dao.VClassGroupDao; +import edu.cornell.mannlib.vitro.webapp.dao.VitroVocabulary; +import edu.cornell.mannlib.vitro.webapp.flags.PortalFlag; +import edu.cornell.mannlib.vitro.webapp.search.SearchException; +import edu.cornell.mannlib.vitro.webapp.search.beans.Searcher; +import edu.cornell.mannlib.vitro.webapp.search.beans.VitroHighlighter; +import edu.cornell.mannlib.vitro.webapp.search.beans.VitroQuery; +import edu.cornell.mannlib.vitro.webapp.search.beans.VitroQueryFactory; +import edu.cornell.mannlib.vitro.webapp.search.lucene.Entity2LuceneDoc; +import edu.cornell.mannlib.vitro.webapp.search.lucene.LuceneIndexer; +import edu.cornell.mannlib.vitro.webapp.search.lucene.LuceneSetup; +import edu.cornell.mannlib.vitro.webapp.search.lucene.SimpleLuceneHighlighter; +import edu.cornell.mannlib.vitro.webapp.utils.FlagMathUtils; +import edu.cornell.mannlib.vitro.webapp.utils.Html2Text; +import edu.cornell.mannlib.vitro.webapp.utils.StringUtils; +import edu.cornell.mannlib.vitro.webapp.web.templatemodels.IndividualTemplateModel; +import edu.cornell.mannlib.vitro.webapp.web.templatemodels.LinkTemplateModel; +import freemarker.template.Configuration; + +/** + * PagedSearchController is the new search controller that interacts + * directly with the lucene API and returns paged, relevance ranked results. + * + * @author bdc34 + * + * Rewritten to use Freemarker: rjy7 + * + */ +public class FreemarkerPagedSearchController extends FreemarkerHttpServlet implements Searcher { + + private static final long serialVersionUID = 1L; + private IndexSearcher searcher = null; + private static final Log log = LogFactory.getLog(FreemarkerPagedSearchController.class.getName()); + String NORESULT_MSG = "The search returned no results."; + private int defaultHitsPerPage = 25; + private int defaultMaxSearchSize= 1000; + + public void init(ServletConfig config) throws ServletException { + super.init(config); + LuceneIndexer indexer=(LuceneIndexer)getServletContext() + .getAttribute(LuceneIndexer.class.getName()); + indexer.addSearcher(this); + + try{ + String indexDir = getIndexDir(getServletContext()); + getIndexSearcher(indexDir); + }catch(Exception ex){ + + } + } + + protected String getBody(VitroRequest vreq, Map body, Configuration config) { + try { + + Portal portal = vreq.getPortal(); + PortalFlag portalFlag = vreq.getPortalFlag(); + + //make sure an IndividualDao is available + if( vreq.getWebappDaoFactory() == null + || vreq.getWebappDaoFactory().getIndividualDao() == null ){ + log.error("makeUsableBeans() could not get IndividualDao "); + //doSearchError(request, response, "Could not access Model", portalFlag); + //return; + } + IndividualDao iDao = vreq.getWebappDaoFactory().getIndividualDao(); + VClassGroupDao grpDao = vreq.getWebappDaoFactory().getVClassGroupDao(); + VClassDao vclassDao = vreq.getWebappDaoFactory().getVClassDao(); + String alphaFilter = vreq.getParameter("alpha"); + + int startIndex = 0; + try{ + startIndex = Integer.parseInt(vreq.getParameter("startIndex")); + }catch (Throwable e) { + startIndex = 0; + } + + int hitsPerPage = defaultHitsPerPage; + try{ + hitsPerPage = Integer.parseInt(vreq.getParameter("hitsPerPage")); + } catch (Throwable e) { + hitsPerPage = defaultHitsPerPage; + } + + int maxHitSize = defaultMaxSearchSize; + if( startIndex >= defaultMaxSearchSize - hitsPerPage ) + maxHitSize = startIndex + defaultMaxSearchSize; + if( alphaFilter != null ){ + maxHitSize = maxHitSize * 2; + hitsPerPage = maxHitSize; + } + + String indexDir = getIndexDir(getServletContext()); + + String qtxt = vreq.getParameter(VitroQuery.QUERY_PARAMETER_NAME); + Analyzer analyzer = getAnalyzer(getServletContext()); + Query query = getQuery(vreq, portalFlag, analyzer, indexDir, qtxt); + log.debug("query for '" + qtxt +"' is " + query.toString()); + + if (query == null ) { + //doNoQuery(vreq, response); + //return; + } + + IndexSearcher searcherForRequest = getIndexSearcher(indexDir); + + TopDocs topDocs = null; + try{ + topDocs = searcherForRequest.search(query,null,maxHitSize); + }catch(Throwable t){ + log.error("in first pass at search: " + t); + // this is a hack to deal with odd cases where search and index threads interact + try{ + wait(150); + topDocs = searcherForRequest.search(query,null,maxHitSize); + }catch (Exception ex){ + log.error(ex); + String msg = makeBadSearchMessage(qtxt,ex.getMessage()); + if(msg == null ) msg = "

The search request contained errors.

"; + //doFailedSearch(vreq, response, msg, qtxt); + //return; + } + } + + if( topDocs == null || topDocs.scoreDocs == null){ + log.error("topDocs for a search was null"); + String msg = "

The search request contained errors.

"; + //doFailedSearch(request, response, msg, qtxt); + //return; + } + + int hitsLength = topDocs.scoreDocs.length; + if ( hitsLength < 1 ){ + //doFailedSearch(request, response, NORESULT_MSG, qtxt); + //return; + } + log.debug("found "+hitsLength+" hits"); + + int lastHitToShow = 0; + if((startIndex + hitsPerPage) > hitsLength ) { + lastHitToShow = hitsLength; + } else { + lastHitToShow = startIndex + hitsPerPage - 1; + } + + List beans = new LinkedList(); + for(int i=startIndex; i= startIndex) && (i <= lastHitToShow) ){ + Document doc = searcherForRequest.doc(topDocs.scoreDocs[i].doc); + String uri = doc.get(Entity2LuceneDoc.term.URI); + Individual ent = new IndividualImpl(); + ent.setURI(uri); + ent = iDao.getIndividualByURI(uri); + if(ent!=null) + beans.add(ent); + } + }catch(Exception e){ + log.error("problem getting usable Individuals from search " + + "hits" + e.getMessage()); + } + } + + Params pagingLinkParams = new Params(); + pagingLinkParams.put("querytext", qtxt); + + String classGroupParam = vreq.getParameter("classgroup"); + String typeParam = vreq.getParameter("type"); + + // Search request includes no classgroup and no type, so add classgroup search refinement links. + if ( classGroupParam == null && typeParam == null) { + List classgroups = getClassGroups(grpDao, topDocs, searcherForRequest); + List classGroupLinks = new ArrayList(classgroups.size()); + for (VClassGroup vcg : classgroups) { + classGroupLinks.add(new VClassGroupSearchLink(qtxt, vcg)); + } + body.put("classGroupLinks", classGroupLinks); + + // Search request is for a classgroup, so add rdf:type search refinement links + // but try to filter out classes that are subclasses + } else if ( classGroupParam != null && typeParam == null ) { + List vClasses = getVClasses(vclassDao,topDocs,searcherForRequest); + List vClassLinks = new ArrayList(vClasses.size()); + for (VClass vc : vClasses) { + vClassLinks.add(new VClassSearchLink(qtxt, vc)); + } + body.put("classLinks", vClassLinks); + pagingLinkParams.put("classgroup", classGroupParam); + + // This case is never displayed + } else if ( !StringUtils.isEmpty(alphaFilter) ) { + body.put("alphas", getAlphas(topDocs, searcherForRequest)); + alphaSortIndividuals(beans); + + } else { + pagingLinkParams.put("type", typeParam); + } + + beans = highlightBeans( beans , + vreq.getWebappDaoFactory().getDataPropertyDao(), + vreq.getWebappDaoFactory().getObjectPropertyDao(), + new SimpleLuceneHighlighter(query,analyzer) ); + + // Convert search result individuals to template model objects + List individuals = new ArrayList(beans.size()); + for (Individual i : beans) { + individuals.add(new IndividualTemplateModel(i)); + } + body.put("individuals", individuals); + + body.put("querytext", qtxt); + body.put("title", qtxt+" - "+portal.getAppName()+" Search Results" ); + + if ( !StringUtils.isEmpty(classGroupParam) ) { + VClassGroup grp = grpDao.getGroupByURI(classGroupParam); + if( grp != null && grp.getPublicName() != null ) + body.put("classgroupName", grp.getPublicName()); + } + + if ( !StringUtils.isEmpty(typeParam) ) { + VClass type = vclassDao.getVClassByURI(typeParam); + if( type != null && type.getName() != null ) + body.put("typeName", type.getName()); + } + + body.put("pagingLinks", getPagingLinks(startIndex, hitsPerPage, hitsLength, maxHitSize, vreq.getServletPath(), pagingLinkParams)); + + } catch (Throwable e) { + log.error(e, e); + //doSearchError(request, response, e.getMessage(), null); + //return; + } + + return mergeBodyToTemplate("pagedSearchResults.ftl", body, config); + } + + private void alphaSortIndividuals(List beans) { + Collections.sort(beans, new Comparator< Individual >(){ + public int compare(Individual o1, Individual o2) { + if( o1 == null || o1.getName() == null ) + return 1; + else + return o1.getName().compareTo(o2.getName()); + }}); + } + + private List getAlphas(TopDocs topDocs, IndexSearcher searcher) { + Set alphas = new HashSet(); + for(int i=0;i 0) + alphas.add( name.substring(0, 1)); + } catch (CorruptIndexException e) { + log.debug("Could not get alphas for document",e); + } catch (IOException e) { + log.debug("Could not get alphas for document",e); + } + + } + return new ArrayList(alphas); + } + + /** + * Get the class groups represented for the individuals in the topDocs. + */ + private List getClassGroups(VClassGroupDao grpDao, TopDocs topDocs, + IndexSearcher searcherForRequest) { + LinkedHashMap grpMap = grpDao.getClassGroupMap(); + int n = grpMap.size(); + + HashSet classGroupsInHits = new HashSet(n); + int grpsFound = 0; + + for(int i=0; i grpsFound ;i++){ + try{ + Document doc = searcherForRequest.doc(topDocs.scoreDocs[i].doc); + Field[] grps = doc.getFields(Entity2LuceneDoc.term.CLASSGROUP_URI); + if(grps != null || grps.length > 0){ + for(int j=0;j= n ) + break; + } + } + } + }catch(Exception e){ + log.error("problem getting VClassGroups from search hits " + + e.getMessage()); + } + } + + List classgroupURIs= Collections.list(Collections.enumeration(classGroupsInHits)); + List classgroups = new ArrayList( classgroupURIs.size() ); + for(String cgUri: classgroupURIs){ + if( cgUri != null && ! "".equals(cgUri) ){ + VClassGroup vcg = grpDao.getGroupByURI( cgUri ); + if( vcg == null ){ + log.debug("could not get classgroup for URI " + cgUri); + }else{ + classgroups.add(vcg); + } + } + } + grpDao.sortGroupList(classgroups); + + return classgroups; + } + + private class VClassGroupSearchLink extends LinkTemplateModel { + + VClassGroupSearchLink(String querytext, VClassGroup classgroup) { + super(classgroup.getPublicName(), "/search", "querytext", querytext, "classgroup", classgroup.getURI()); + } + } + + private class VClassSearchLink extends LinkTemplateModel { + + VClassSearchLink(String querytext, VClass type) { + super(type.getName(), "/search", "querytext", querytext, "type", type.getURI()); + } + } + + private List getPagingLinks(int startIndex, int hitsPerPage, int hitsLength, int maxHitSize, String baseUrl, Params params) { + + List pagingLinks = new ArrayList(); + + // No paging links if only one page of results + if (hitsLength <= hitsPerPage) { + return pagingLinks; + } + + int pageNumber; + + params.put("hitsPerPage", String.valueOf(hitsPerPage)); + + for (int i = 0; i < hitsLength; i += hitsPerPage) { + params.put("startIndex", String.valueOf(i)); + if ( i < maxHitSize - hitsPerPage) { + pageNumber = i/hitsPerPage + 1; + if (i >= startIndex && i < (startIndex + hitsPerPage)) { + pagingLinks.add(new PagingLink(pageNumber)); + } else { + pagingLinks.add(new PagingLink(pageNumber, baseUrl, params)); + } + } else { + pagingLinks.add(new PagingLink("more...", baseUrl, params)); + } + } + + return pagingLinks; + } + + private class PagingLink extends LinkTemplateModel { + + PagingLink(int pageNumber, String baseUrl, Params params) { + super(String.valueOf(pageNumber), baseUrl, params); + } + + // Constructor for current page item: not a link, so no url value. + PagingLink(int pageNumber) { + setText(String.valueOf(pageNumber)); + } + + // Constructor for "more..." item + PagingLink(String text, String baseUrl, Params params) { + super(text, baseUrl, params); + } + + + + } + + private List getVClasses(VClassDao vclassDao, TopDocs topDocs, + IndexSearcher searherForRequest){ + HashSet typesInHits = getVClassUrisForHits(topDocs,searherForRequest); + List classes = new ArrayList(typesInHits.size()); + + Iterator it = typesInHits.iterator(); + while(it.hasNext()){ + String typeUri = it.next(); + try{ + if( VitroVocabulary.OWL_THING.equals(typeUri)) + continue; + VClass type = vclassDao.getVClassByURI(typeUri); + if( ! type.isAnonymous() && + type.getName() != null && !"".equals(type.getName()) && + type.getGroupURI() != null ) //don't display classes that aren't in classgroups + classes.add(type); + }catch(Exception ex){ + if( log.isDebugEnabled() ) + log.debug("could not add type " + typeUri, ex); + } + } + Collections.sort(classes, new Comparator(){ + public int compare(VClass o1, VClass o2) { + return o1.compareTo(o2); + }}); + return classes; + } + + private HashSet getVClassUrisForHits(TopDocs topDocs, + IndexSearcher searcherForRequest){ + HashSet typesInHits = new HashSet(); + for(int i=0; i MAX_QUERY_LENGTH ){ + log.debug("The search was too long. The maximum " + + "query length is " + MAX_QUERY_LENGTH ); + return null; + } + QueryParser parser = getQueryParser(analyzer); + query = parser.parse(querystr); + + String alpha = request.getParameter("alpha"); + if( alpha != null && !"".equals(alpha) && alpha.length() == 1){ + BooleanQuery boolQuery = new BooleanQuery(); + boolQuery.add( query, BooleanClause.Occur.MUST ); + boolQuery.add( + new WildcardQuery(new Term(Entity2LuceneDoc.term.NAME, alpha+'*')), + BooleanClause.Occur.MUST); + query = boolQuery; + } + + //check if this is classgroup filtered + Object param = request.getParameter("classgroup"); + if( param != null && !"".equals(param)){ + BooleanQuery boolQuery = new BooleanQuery(); + boolQuery.add( query, BooleanClause.Occur.MUST); + boolQuery.add( new TermQuery( + new Term(Entity2LuceneDoc.term.CLASSGROUP_URI, + (String)param)), + BooleanClause.Occur.MUST); + query = boolQuery; + } + + //check if this is rdf:type filtered + param = request.getParameter("type"); + if( param != null && !"".equals(param)){ + BooleanQuery boolQuery = new BooleanQuery(); + boolQuery.add( query, BooleanClause.Occur.MUST); + boolQuery.add( new TermQuery( + new Term(Entity2LuceneDoc.term.RDFTYPE, + (String)param)), + BooleanClause.Occur.MUST); + query = boolQuery; + } + + //if we have a flag/portal query then we add + //it by making a BooelanQuery. + Query flagQuery = makeFlagQuery( portalState ); + if( flagQuery != null ){ + BooleanQuery boolQuery = new BooleanQuery(); + boolQuery.add( query, BooleanClause.Occur.MUST); + boolQuery.add( flagQuery, BooleanClause.Occur.MUST); + query = boolQuery; + } + + log.debug("Query: " + query); + + }catch (Exception ex){ + throw new SearchException(ex.getMessage()); + } + + return query; + } + + @SuppressWarnings("static-access") + private QueryParser getQueryParser(Analyzer analyzer){ + //defaultSearchField indicates which field search against when there is no term + //indicated in the query string. + //The analyzer is needed so that we use the same analyzer on the search queries as + //was used on the text that was indexed. + QueryParser qp = new QueryParser(defaultSearchField,analyzer); + //this sets the query parser to AND all of the query terms it finds. + qp.setDefaultOperator(QueryParser.AND_OPERATOR); + //set up the map of stemmed field names -> unstemmed field names +// HashMap map = new HashMap(); +// map.put(Entity2LuceneDoc.term.ALLTEXT,Entity2LuceneDoc.term.ALLTEXTUNSTEMMED); +// qp.setStemmedToUnstemmed(map); + return qp; + } + + /** + * Makes a flag based query clause. This is where searches can filtered + * by portal. + * + * If you think that search is not working correctly with protals and + * all that kruft then this is a method you want to look at. + * + * It only takes into account "the portal flag" and flag1Exclusive must + * be set. Where does that stuff get set? Look in vitro.flags.PortalFlag + * + * One thing to keep in mind with portal filtering and search is that if + * you want to search a portal that is different then the portal the user + * is 'in' then the home parameter should be set to force the user into + * the new portal. + * + * Ex. Bob requests the search page for vivo in portal 3. You want to + * have a drop down menu so bob can search the all CALS protal, id 60. + * You need to have a home=60 on your search form. If you don't set + * home=60 with your search query, then the search will not be in the + * all portal AND the WebappDaoFactory will be filtered to only show + * things in portal 3. + * + * Notice: flag1 as a parameter is ignored. bdc34 2009-05-22. + */ + @SuppressWarnings("static-access") + private Query makeFlagQuery( PortalFlag flag){ + if( flag == null || !flag.isFilteringActive() + || flag.getFlag1DisplayStatus() == flag.SHOW_ALL_PORTALS ) + return null; + + // make one term for each bit in the numeric flag that is set + Collection terms = new LinkedList(); + int portalNumericId = flag.getFlag1Numeric(); + Long[] bits = FlagMathUtils.numeric2numerics(portalNumericId); + for (Long bit : bits) { + terms.add(new TermQuery(new Term(Entity2LuceneDoc.term.PORTAL, Long + .toString(bit)))); + } + + // make a boolean OR query for all of those terms + BooleanQuery boolQuery = new BooleanQuery(); + if (terms.size() > 0) { + for (TermQuery term : terms) { + boolQuery.add(term, BooleanClause.Occur.SHOULD); + } + return boolQuery; + } else { + //we have no flags set, so no flag filtering + return null; + } + } + + private synchronized IndexSearcher getIndexSearcher(String indexDir) { + if( searcher == null ){ + try { + Directory fsDir = FSDirectory.getDirectory(indexDir); + searcher = new IndexSearcher(fsDir); + } catch (IOException e) { + log.error("LuceneSearcher: could not make indexSearcher "+e); + log.error("It is likely that you have not made a directory for the lucene index. "+ + "Create the directory indicated in the error and set permissions/ownership so"+ + " that the tomcat server can read/write to it."); + //The index directory is created by LuceneIndexer.makeNewIndex() + } + } + return searcher; + } + + private List highlightBeans(List beans, + DataPropertyDao dpDao, ObjectPropertyDao opDao, VitroHighlighter highlighter) { + if( beans == null ){ + log.debug("List of beans passed to highlightBeans() was null"); + return Collections.EMPTY_LIST; + }else if( highlighter == null ){ + log.debug("Null highlighter passed to highlightBeans()"); + return beans; + } + Iterator it = beans.iterator(); + while(it.hasNext()){ + Individual ent = it.next(); + try{ + dpDao.fillDataPropertiesForIndividual(ent); + opDao.fillObjectPropertiesForIndividual(ent); + fragmentHighlight(ent, highlighter); + }catch( Exception ex ){ + log.debug("Error while doing search highlighting" , ex); + } + } + return beans; + } + + /** + * Highlights the name and then replaces the description with + * highlighted fragments. + * @param ent + * @param highlighter + */ + public void fragmentHighlight(Individual ent, VitroHighlighter hl){ + try{ + if( ent == null ) return; + + Html2Text h2t = new Html2Text(); + StringBuffer sb = new StringBuffer(""); + if(ent.getBlurb() != null) + sb.append(ent.getBlurb()).append(' '); + + if(ent.getDescription() != null ) + sb.append(ent.getDescription()).append(' '); + + if(ent.getDataPropertyList() != null) { + Iterator edIt = ent.getDataPropertyList().iterator(); + while (edIt.hasNext()) { + try{ + DataProperty dp = (DataProperty)edIt.next(); + if( getDataPropertyBlacklist().contains(dp.getURI())) + continue; + for(DataPropertyStatement dps : dp.getDataPropertyStatements()){ + sb.append(dp.getPublicName()).append(' ') + .append(dps.getData()).append(' '); + } + }catch(Throwable e){ + log.debug("Error highlighting data property statment " + + "for individual "+ent.getURI()); + } + } + } + + if(ent.getObjectPropertyList() != null) { + Iterator edIt = ent.getObjectPropertyList().iterator(); + String t = null; + while (edIt.hasNext()) { + try { + ObjectProperty op = (ObjectProperty)edIt.next(); + if( getObjectPropertyBlacklist().contains(op.getURI())) + continue; + for( ObjectPropertyStatement stmt : op.getObjectPropertyStatements()){ + sb.append( ( (t = op.getDomainPublic()) != null) ? t : "" ); + sb.append(' '); + sb.append( ( (t = stmt.getObject().getName()) != null) ? t : "" ); + sb.append(' '); + } + } catch (Throwable e) { + log.debug("Error highlighting object property " + + "statement for individual "+ent.getURI()); + } + } + } + + String keywords = ent.getKeywordString(); + if( keywords != null ) + sb.append(keywords); + + ent.setDescription(hl.getHighlightFragments( h2t.stripHtml( sb.toString() ))); + }catch(Throwable th){ + log.debug("could not hightlight for entity " + ent.getURI(),th); + } + } + + private void doNoQuery(HttpServletRequest request, + HttpServletResponse response) + throws ServletException, IOException { + Portal portal = (new VitroRequest(request)).getPortal(); + request.setAttribute("title", "Search "+portal.getAppName()); + request.setAttribute("bodyJsp", Controllers.SEARCH_FORM_JSP); + + RequestDispatcher rd = request + .getRequestDispatcher(Controllers.BASIC_JSP); + rd.forward(request, response); + } + + private void doFailedSearch(HttpServletRequest request, + HttpServletResponse response, String message, String querytext) + throws ServletException, IOException { + Portal portal = (new VitroRequest(request)).getPortal(); + if( querytext != null ){ + request.setAttribute("querytext", querytext); + request.setAttribute("title", querytext+" - "+portal.getAppName()+" Search" ); + }else{ + request.setAttribute("title", portal.getAppName()+" Search" ); + request.setAttribute("querytext", ""); + } + if( message != null && message.length() > 0) + request.setAttribute("message", message); + + request.setAttribute("bodyJsp", Controllers.SEARCH_FAILED_JSP); + RequestDispatcher rd = request.getRequestDispatcher(Controllers.BASIC_JSP); + rd.forward(request, response); + } + + + /** + * Makes a message to display to user for a bad search term. + * @param query + * @param exceptionMsg + */ + private String makeBadSearchMessage(String querytext, String exceptionMsg){ + String rv = ""; + try{ + //try to get the column in the search term that is causing the problems + int coli = exceptionMsg.indexOf("column"); + if( coli == -1) return ""; + int numi = exceptionMsg.indexOf(".", coli+7); + if( numi == -1 ) return ""; + String part = exceptionMsg.substring(coli+7,numi ); + int i = Integer.parseInt(part) - 1; + + // figure out where to cut preview and post-view + int errorWindow = 5; + int pre = i - errorWindow; + if (pre < 0) + pre = 0; + int post = i + errorWindow; + if (post > querytext.length()) + post = querytext.length(); + // log.warn("pre: " + pre + " post: " + post + " term len: + // " + term.length()); + + // get part of the search term before the error and after + String before = querytext.substring(pre, i); + String after = ""; + if (post > i) + after = querytext.substring(i + 1, post); + + rv = "The search term had an error near " + + before + "" + querytext.charAt(i) + + "" + after + ""; + } catch (Throwable ex) { + return ""; + } + return rv; + } + + @SuppressWarnings("unchecked") + private HashSet getDataPropertyBlacklist(){ + HashSetdpBlacklist = (HashSet) + getServletContext().getAttribute(LuceneSetup.SEARCH_DATAPROPERTY_BLACKLIST); + return dpBlacklist; + } + + @SuppressWarnings("unchecked") + private HashSet getObjectPropertyBlacklist(){ + HashSetopBlacklist = (HashSet) + getServletContext().getAttribute(LuceneSetup.SEARCH_OBJECTPROPERTY_BLACKLIST); + return opBlacklist; + } + + private void doSearchError(HttpServletRequest request, + HttpServletResponse response, String message, Object object) + throws ServletException, IOException { + Portal portal = (new VitroRequest(request)).getPortal(); + + request.setAttribute("bodyJsp", Controllers.SEARCH_ERROR_JSP); + RequestDispatcher rd = request.getRequestDispatcher(Controllers.BASIC_JSP); + rd.forward(request, response); + } + private final String defaultSearchField = "ALLTEXT"; + public static final int MAX_QUERY_LENGTH = 500; + + + /** + * Need to accept notification from indexer that the index has been changed. + */ + public void close() { + searcher = null; + } + + public VitroHighlighter getHighlighter(VitroQuery q) { + throw new Error("PagedSearchController.getHighlighter() is unimplemented"); + } + + public VitroQueryFactory getQueryFactory() { + throw new Error("PagedSearchController.getQueryFactory() is unimplemented"); + } + + public List search(VitroQuery query) throws SearchException { + throw new Error("PagedSearchController.search() is unimplemented"); + } + +} diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/search/controller/IndexController.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/search/controller/IndexController.java index b02fc8823..dfda2ea57 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/search/controller/IndexController.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/search/controller/IndexController.java @@ -89,7 +89,7 @@ public class IndexController extends FreemarkerHttpServlet { ! "authenticated".equalsIgnoreCase(loginHandler.getLoginStatus()) || Integer.parseInt(loginHandler.getLoginRole()) <= 5 ){ - body.put("message","You must login to rebuild the search index."); + body.put("message","You must log in to rebuild the search index."); return mergeBodyToTemplate("message.ftl", body, config); } @@ -108,7 +108,7 @@ public class IndexController extends FreemarkerHttpServlet { return mergeBodyToTemplate("errorMessage.ftl", body, config); } - body.put("message","Rebuilding of index started."); + body.put("message","Rebuilding of index started."); return mergeBodyToTemplate("message.ftl", body, config); } } diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/search/controller/PagedSearchController.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/search/controller/PagedSearchController.java index 9e63dba9e..94c2c3847 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/search/controller/PagedSearchController.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/search/controller/PagedSearchController.java @@ -220,7 +220,7 @@ public class PagedSearchController extends VitroHttpServlet implements Searcher{ if( request.getParameter("classgroup") == null && request.getParameter("type") == null){ request.setAttribute("classgroups", getClassGroups(grpDao, topDocs, searcherForRequest)); - request.setAttribute("refinment", ""); + request.setAttribute("refinement", ""); }else{ //search request for a classgroup so add rdf:type search refinement links @@ -228,14 +228,14 @@ public class PagedSearchController extends VitroHttpServlet implements Searcher{ if( request.getParameter("classgroup") != null && request.getParameter("type") == null ){ request.setAttribute("types", getVClasses(vclassDao,topDocs,searcherForRequest)); - request.setAttribute("refinment", "&classgroup=" + request.setAttribute("refinement", "&classgroup=" + URLEncoder.encode(request.getParameter("classgroup"),"UTF-8")); }else{ if( alphaFilter != null && !"".equals(alphaFilter)){ request.setAttribute("alphas", getAlphas(topDocs, searcherForRequest)); alphaSortIndividuals(beans); }else{ - request.setAttribute("refinment", "&type=" + request.setAttribute("refinement", "&type=" + URLEncoder.encode(request.getParameter("type"),"UTF-8")); } } diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/web/templatemodels/BaseTemplateModel.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/web/templatemodels/BaseTemplateModel.java index 797bf2103..ff74de800 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/web/templatemodels/BaseTemplateModel.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/web/templatemodels/BaseTemplateModel.java @@ -2,6 +2,9 @@ package edu.cornell.mannlib.vitro.webapp.web.templatemodels; +import java.util.ArrayList; +import java.util.List; + import javax.servlet.ServletContext; import org.apache.commons.logging.Log; @@ -13,7 +16,7 @@ import edu.cornell.mannlib.vitro.webapp.controller.freemarker.UrlBuilder.Params; public abstract class BaseTemplateModel { - private static final Log log = LogFactory.getLog(BaseTemplateModel.class.getName()); + private static final Log log = LogFactory.getLog(BaseTemplateModel.class); protected static ServletContext servletContext = null; protected static VitroRequest vreq = null; @@ -23,8 +26,13 @@ public abstract class BaseTemplateModel { return UrlBuilder.getUrl(path); } - // Wrap UrlBuilder method so templates can call ${item.url} - public String getUrl(String path, Params params) { + // Convenience method so subclasses can call getUrl(path, params) + protected String getUrl(String path, Params params) { + return UrlBuilder.getUrl(path, params); + } + + // Convenience method so subclasses can call getUrl(path, params) + protected String getUrl(String path, String... params) { return UrlBuilder.getUrl(path, params); } diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/web/templatemodels/IndividualTemplateModel.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/web/templatemodels/IndividualTemplateModel.java index b2677cca5..f6a18bc5b 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/web/templatemodels/IndividualTemplateModel.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/web/templatemodels/IndividualTemplateModel.java @@ -19,7 +19,7 @@ import edu.cornell.mannlib.vitro.webapp.web.ViewFinder.ClassView; public class IndividualTemplateModel extends BaseTemplateModel { - private static final Log log = LogFactory.getLog(IndividualTemplateModel.class.getName()); + private static final Log log = LogFactory.getLog(IndividualTemplateModel.class); private static final String PATH = Route.INDIVIDUAL.path(); diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/web/templatemodels/LinkTemplateModel.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/web/templatemodels/LinkTemplateModel.java new file mode 100644 index 000000000..ae678a706 --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/web/templatemodels/LinkTemplateModel.java @@ -0,0 +1,56 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.web.templatemodels; + +import org.apache.commons.lang.StringEscapeUtils; + +import edu.cornell.mannlib.vitro.webapp.controller.freemarker.UrlBuilder; +import edu.cornell.mannlib.vitro.webapp.controller.freemarker.UrlBuilder.Params; + +public class LinkTemplateModel extends BaseTemplateModel { + + private String url; + private String text; + + public LinkTemplateModel() { } + + public LinkTemplateModel(String text, String path) { + setText(text); + setUrl(path); + } + + public LinkTemplateModel(String text, String path, String...params) { + setText(text); + setUrl(path, params); + } + + public LinkTemplateModel(String text, String path, Params params) { + setText(text); + setUrl(path, params); + } + + public String getUrl() { + return url; + } + + protected void setUrl(String path) { + url = UrlBuilder.getUrl(path); + } + + protected void setUrl(String path, String... params) { + url = UrlBuilder.getUrl(path, params); + } + + protected void setUrl(String path, Params params) { + url = UrlBuilder.getUrl(path, params); + } + + public String getText() { + return text; + } + + protected void setText(String text) { + this.text = StringEscapeUtils.escapeHtml(text); + } + +} diff --git a/webapp/web/templates/freemarker/body/individualList.ftl b/webapp/web/templates/freemarker/body/individualList.ftl index 0fd4419ac..2fa7c4645 100644 --- a/webapp/web/templates/freemarker/body/individualList.ftl +++ b/webapp/web/templates/freemarker/body/individualList.ftl @@ -2,6 +2,8 @@ <#-- List individual members of a class. --> +<#import "listMacros.ftl" as l> +

${title}

@@ -15,8 +17,20 @@
    <#list individuals as individual>
  • - <#-- Currently we just use the search view here; there's no custom list view defined. --> - <#include "${individual.searchView}"> + <#-- The old JSP version uses a custom search view if one is defined, but it doesn't make sense + to do that in the current system, because we don't use the default search view. We could define + a custom list or use the custom short view, but for now just hard-code the view here; it will not be + customizable. + <#include "${individual.searchView}"> --> + ${individual.name} +
      + <@l.firstLastList> + <#if individual.moniker??>
    • ${individual.moniker}
    • , + <#list individual.links as link> +
    • ${link.anchor}
    • , + + +
diff --git a/webapp/web/templates/freemarker/body/pagedSearchResults.ftl b/webapp/web/templates/freemarker/body/pagedSearchResults.ftl new file mode 100644 index 000000000..d7946e3c2 --- /dev/null +++ b/webapp/web/templates/freemarker/body/pagedSearchResults.ftl @@ -0,0 +1,55 @@ +<#-- $This file is distributed under the terms of the license in /doc/license.txt$ --> + +<#-- Template for displaying paged search results --> + +

+ Search Results for '${querytext}' + <#if classgroupName?has_content>limited to type '${classgroupName}' + <#if typeName?has_content>limited to type '${typeName}' +

+ +
+ + <#-- Refinement links --> + <#if classGroupLinks?has_content> +
+ Show only results of this type: + <#list classGroupLinks as link> + ${link.text} + +
+ + + <#if classLinks?has_content> +
+ Show only results of this subtype: + <#list classLinks as link> + ${link.text} + +
+ + + <#-- Search results --> +
    + <#list individuals as individual> +
  • + <#include "${individual.searchView}"> +
  • + +
+ + <#-- Paging controls --> + <#if (pagingLinks?size > 0)> +
+ Pages: + <#list pagingLinks as link> + <#if link.url??> + ${link.text} + <#else> + ${link.text} <#-- no link if current page --> + + +
+ + +
\ No newline at end of file diff --git a/webapp/web/templates/freemarker/body/partials/class/view/search/view-search-default.ftl b/webapp/web/templates/freemarker/body/partials/class/view/search/view-search-default.ftl index 0c5416f08..0e9a0342c 100644 --- a/webapp/web/templates/freemarker/body/partials/class/view/search/view-search-default.ftl +++ b/webapp/web/templates/freemarker/body/partials/class/view/search/view-search-default.ftl @@ -2,14 +2,9 @@ <#-- Default individual search view --> -<#import "listMacros.ftl" as l> - ${individual.name} -
    - <@l.firstLastList> - <#if individual.moniker??>
  • ${individual.moniker}
  • , - <#list individual.links as link> -
  • ${link.anchor}
  • , - - -
+<#if individual.moniker?has_content> | ${individual.moniker} + +<#if individual.description?has_content> +
${individual.description}
+ \ No newline at end of file diff --git a/webapp/web/templates/search/searchPaged.jsp b/webapp/web/templates/search/searchPaged.jsp index b77edb848..9b604099a 100644 --- a/webapp/web/templates/search/searchPaged.jsp +++ b/webapp/web/templates/search/searchPaged.jsp @@ -7,6 +7,7 @@ <%@ page import="edu.cornell.mannlib.vitro.webapp.beans.Portal" %> <%@ taglib uri="http://java.sun.com/jstl/core" prefix="c" %> <%@ page errorPage="/error.jsp"%> + <% /*********************************************** Display Paged Search Results @@ -153,7 +154,7 @@ if( request.getAttribute("types") != null ){ String basePageUrl = request.getContextPath() + "/search?querytext=" +URLEncoder.encode(request.getParameter("querytext"),"UTF-8") + - request.getAttribute("refinment"); + request.getAttribute("refinement"); out.println("
"); out.println("Pages:");