diff --git a/webapp/config/web.xml b/webapp/config/web.xml index 24e86255f..fc1c291b4 100644 --- a/webapp/config/web.xml +++ b/webapp/config/web.xml @@ -875,6 +875,12 @@ IndividualListController edu.cornell.mannlib.vitro.webapp.controller.freemarker.IndividualListController + IndividualListController /individuallist @@ -976,12 +982,6 @@ JSON Service edu.cornell.mannlib.vitro.webapp.controller.JSONServlet - JSON Service /dataservice diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/SolrIndividualListController.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/SolrIndividualListController.java new file mode 100644 index 000000000..eec707bec --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/SolrIndividualListController.java @@ -0,0 +1,337 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.controller.freemarker; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.servlet.ServletContext; +import javax.servlet.ServletException; + +import org.apache.commons.lang.StringUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.lucene.document.Document; +import org.apache.lucene.index.CorruptIndexException; +import org.apache.lucene.index.Term; +import org.apache.lucene.search.BooleanClause; +import org.apache.lucene.search.BooleanQuery; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.PrefixQuery; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.ScoreDoc; +import org.apache.lucene.search.Sort; +import org.apache.lucene.search.TermQuery; +import org.apache.lucene.search.TopDocs; + +import edu.cornell.mannlib.vitro.webapp.beans.Individual; +import edu.cornell.mannlib.vitro.webapp.beans.VClass; +import edu.cornell.mannlib.vitro.webapp.beans.VClassGroup; +import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest; +import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.ExceptionResponseValues; +import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.ResponseValues; +import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.TemplateResponseValues; +import edu.cornell.mannlib.vitro.webapp.dao.IndividualDao; +import edu.cornell.mannlib.vitro.webapp.search.lucene.Entity2LuceneDoc; +import edu.cornell.mannlib.vitro.webapp.search.lucene.LuceneIndexFactory; +import edu.cornell.mannlib.vitro.webapp.web.templatemodels.individual.ListedIndividualTemplateModel; +import freemarker.ext.beans.BeansWrapper; +import freemarker.template.TemplateModel; + +/** + * Generates a list of individuals for display in a template + */ +public class SolrIndividualListController extends FreemarkerHttpServlet { + + private static final long serialVersionUID = 1L; + private static final Log log = LogFactory.getLog(SolrIndividualListController.class.getName()); + + public static final int ENTITY_LIST_CONTROLLER_MAX_RESULTS = 30000; + public static final int INDIVIDUALS_PER_PAGE = 30; + public static final int MAX_PAGES = 40; //must be even + + private static final String TEMPLATE_DEFAULT = "individualList.ftl"; + + @Override + protected ResponseValues processRequest(VitroRequest vreq) { + + String templateName = TEMPLATE_DEFAULT; + Map body = new HashMap(); + String errorMessage = null; + String message = null; + + try { + Object obj = vreq.getAttribute("vclass"); + VClass vclass = null; + if ( obj == null ) { // look for vitroclass id parameter + String vitroClassIdStr = vreq.getParameter("vclassId"); + if ( !StringUtils.isEmpty(vitroClassIdStr)) { + try { + //TODO have to change this so vclass's group and entity count are populated + vclass = vreq.getWebappDaoFactory().getVClassDao().getVClassByURI(vitroClassIdStr); + if (vclass == null) { + log.error("Couldn't retrieve vclass " + vitroClassIdStr); + errorMessage = "Class " + vitroClassIdStr + " not found"; + } + } catch (Exception ex) { + throw new HelpException("IndividualListController: request parameter 'vclassId' must be a URI string."); + } + } + } else if (obj instanceof VClass) { + vclass = (VClass)obj; + } else { + throw new HelpException("IndividualListController: attribute 'vclass' must be of type " + + VClass.class.getName() + "."); + } + + body.put("vclassId", vclass.getURI()); + + if (vclass != null) { + String alpha = getAlphaParameter(vreq); + int page = getPageParameter(vreq); + Map map = getResultsForVClass( + vclass.getURI(), + page, + alpha, + vreq.getWebappDaoFactory().getIndividualDao(), + getServletContext()); + body.putAll(map); + + List inds = (List)map.get("entities"); + List indsTm = new ArrayList(); + for(Individual ind : inds ){ + indsTm.add(new ListedIndividualTemplateModel(ind,vreq)); + } + body.put("individuals", indsTm); + + List wpages = new ArrayList(); + List pages = (List)body.get("pages"); + BeansWrapper wrapper = new BeansWrapper(); + for( PageRecord pr: pages ){ + wpages.add( wrapper.wrap(pr) ); + } + + // Set title and subtitle. Title will be retrieved later in getTitle(). + VClassGroup classGroup = vclass.getGroup(); + String title; + if (classGroup == null) { + title = vclass.getName(); + } else { + title = classGroup.getPublicName(); + body.put("subtitle", vclass.getName()); + } + body.put("title", title); + body.put("redirecturl", vreq.getContextPath()+"/entityurl/"); + getServletContext().setAttribute("classuri", vclass.getURI()); + } + + } catch (HelpException help){ + errorMessage = "Request attribute 'vclass' or request parameter 'vclassId' must be set before calling. Its value must be a class uri."; + } catch (Throwable e) { + return new ExceptionResponseValues(e); + } + + if (errorMessage != null) { + templateName = Template.ERROR_MESSAGE.toString(); + body.put("errorMessage", errorMessage); + } else if (message != null) { + body.put("message", message); + } + + return new TemplateResponseValues(templateName, body); + } + + private class HelpException extends Throwable { + private static final long serialVersionUID = 1L; + + public HelpException(String string) { + super(string); + } + } + + public static String getAlphaParameter(VitroRequest request){ + return request.getParameter("alpha"); + } + + public static int getPageParameter(VitroRequest request) { + String pageStr = request.getParameter("page"); + if( pageStr != null ){ + try{ + return Integer.parseInt(pageStr); + }catch(NumberFormatException nfe){ + log.debug("could not parse page parameter"); + return 1; + } + }else{ + return 1; + } + } + + /** + * This method is now called in a couple of places. It should be refactored + * into a DAO or similar object. + */ + public static Map getResultsForVClass(String vclassURI, int page, String alpha, IndividualDao indDao, ServletContext context) + throws CorruptIndexException, IOException, ServletException{ + Map rvMap = new HashMap(); + + //make lucene query for this rdf:type + Query query = getQuery(vclassURI, alpha); + + //execute lucene query for individuals of the specified type + IndexSearcher index = LuceneIndexFactory.getIndexSearcher(context); + TopDocs docs = null; + try{ + docs = index.search(query, null, + ENTITY_LIST_CONTROLLER_MAX_RESULTS, + new Sort(Entity2LuceneDoc.term.NAME_LOWERCASE)); + }catch(Throwable th){ + log.error("Could not run search. " + th.getMessage()); + docs = null; + } + + if( docs == null ) + throw new ServletException("Could not run search in IndividualListController"); + + //get list of individuals for the search results + int size = docs.totalHits; + log.debug("Number of search results: " + size); + + // don't get all the results, only get results for the requestedSize + List individuals = new ArrayList(INDIVIDUALS_PER_PAGE); + int individualsAdded = 0; + int ii = (page-1)*INDIVIDUALS_PER_PAGE; + while( individualsAdded < INDIVIDUALS_PER_PAGE && ii < size ){ + ScoreDoc hit = docs.scoreDocs[ii]; + if (hit != null) { + Document doc = index.doc(hit.doc); + if (doc != null) { + String uri = doc.getField(Entity2LuceneDoc.term.URI).stringValue(); + Individual ind = indDao.getIndividualByURI( uri ); + if( ind != null ){ + individuals.add( ind ); + individualsAdded++; + } + } else { + log.warn("no document found for lucene doc id " + hit.doc); + } + } else { + log.debug("hit was null"); + } + ii++; + } + + rvMap.put("count", size); + + if( size > INDIVIDUALS_PER_PAGE ){ + rvMap.put("showPages", Boolean.TRUE); + List pageRecords = makePagesList(size, INDIVIDUALS_PER_PAGE, page); + rvMap.put("pages", pageRecords); + }else{ + rvMap.put("showPages", Boolean.FALSE); + rvMap.put("pages", Collections.emptyList()); + } + + rvMap.put("alpha",alpha); + + rvMap.put("totalCount", size); + rvMap.put("entities",individuals); + if (individuals == null) + log.debug("entities list is null for vclass " + vclassURI ); + + return rvMap; + } + + private static BooleanQuery getQuery(String vclassUri, String alpha){ + BooleanQuery query = new BooleanQuery(); + try{ + //query term for rdf:type + query.add( + new TermQuery( new Term(Entity2LuceneDoc.term.RDFTYPE, vclassUri)), + BooleanClause.Occur.MUST ); + + //Add alpha filter if it is needed + Query alphaQuery = null; + if( alpha != null && !"".equals(alpha) && alpha.length() == 1){ + alphaQuery = + new PrefixQuery(new Term(Entity2LuceneDoc.term.NAME_LOWERCASE, alpha.toLowerCase())); + query.add(alphaQuery,BooleanClause.Occur.MUST); + } + + log.debug("Query: " + query); + return query; + } catch (Exception ex){ + log.error(ex,ex); + return new BooleanQuery(); + } + } + + public static List makePagesList( int count, int pageSize, int selectedPage){ + + List records = new ArrayList( MAX_PAGES + 1 ); + int requiredPages = count/pageSize ; + int remainder = count % pageSize ; + if( remainder > 0 ) + requiredPages++; + + if( selectedPage < MAX_PAGES && requiredPages > MAX_PAGES ){ + //the selected pages is within the first maxPages, just show the normal pages up to maxPages. + for(int page = 1; page < requiredPages && page <= MAX_PAGES ; page++ ){ + records.add( new PageRecord( "page=" + page, Integer.toString(page), Integer.toString(page), selectedPage == page ) ); + } + records.add( new PageRecord( "page="+ (MAX_PAGES+1), Integer.toString(MAX_PAGES+1), "more...", false)); + }else if( requiredPages > MAX_PAGES && selectedPage+1 > MAX_PAGES && selectedPage < requiredPages - MAX_PAGES){ + //the selected pages is in the middle of the list of page + int startPage = selectedPage - MAX_PAGES / 2; + int endPage = selectedPage + MAX_PAGES / 2; + for(int page = startPage; page <= endPage ; page++ ){ + records.add( new PageRecord( "page=" + page, Integer.toString(page), Integer.toString(page), selectedPage == page ) ); + } + records.add( new PageRecord( "page="+ endPage+1, Integer.toString(endPage+1), "more...", false)); + }else if ( requiredPages > MAX_PAGES && selectedPage > requiredPages - MAX_PAGES ){ + //the selected page is in the end of the list + int startPage = requiredPages - MAX_PAGES; + double max = Math.ceil(count/pageSize); + for(int page = startPage; page <= max; page++ ){ + records.add( new PageRecord( "page=" + page, Integer.toString(page), Integer.toString(page), selectedPage == page ) ); + } + }else{ + //there are fewer than maxPages pages. + for(int i = 1; i <= requiredPages; i++ ){ + records.add( new PageRecord( "page=" + i, Integer.toString(i), Integer.toString(i), selectedPage == i ) ); + } + } + return records; + } + + public static class PageRecord { + public PageRecord(String param, String index, String text, boolean selected) { + this.param = param; + this.index = index; + this.text = text; + this.selected = selected; + } + public String param; + public String index; + public String text; + public boolean selected=false; + + public String getParam() { + return param; + } + public String getIndex() { + return index; + } + public String getText() { + return text; + } + public boolean getSelected(){ + return selected; + } + } + +}