diff --git a/webapp/config/web.xml b/webapp/config/web.xml
index b63cd727f..78c73c2fc 100644
--- a/webapp/config/web.xml
+++ b/webapp/config/web.xml
@@ -874,6 +874,19 @@
/searchcontroller
+
+ AutocompleteController
+ edu.cornell.mannlib.vitro.webapp.search.controller.AutocompleteController
+
+
+ AutocompleteController
+ /autocomplete
+
+
+ AutocompleteController
+ /populateselect
+
+
AdminControlleredu.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 a2ee3e141..c173b0fe8 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
@@ -349,6 +349,11 @@ public class FreeMarkerHttpServlet extends VitroHttpServlet {
writeTemplate(templateName, root);
}
+ protected void ajaxWrite(String templateName, Map map) {
+ templateName = "ajax/" + templateName;
+ writeTemplate(templateName, map);
+ }
+
protected void writeTemplate(String templateName, Map map) {
StringWriter sw = mergeToTemplate(templateName, map);
write(sw);
diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/TestController.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/TestController.java
index 3544267c6..d5cbb14cd 100644
--- a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/TestController.java
+++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/TestController.java
@@ -36,6 +36,19 @@ public class TestController extends FreeMarkerHttpServlet {
Date now = cal.getTime();
body.put("now", now);
// In template: ${now?date}, ${now?datetime}, ${now?time}
+
+ // You can add to a collection AFTER putting it in the template data model
+ List fruit = new ArrayList();
+ fruit.add("apples");
+ fruit.add("bananas");
+ body.put("fruit", fruit);
+ fruit.add("oranges");
+
+ // But you cannot modify a scalar after putting it in the data model - the
+ // template still gets the old value
+ String animal = "elephant";
+ body.put("animal", animal);
+ animal = "camel";
// Create the template to see the examples live.
String bodyTemplate = "test.ftl";
diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/search/controller/AutocompleteController.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/search/controller/AutocompleteController.java
new file mode 100644
index 000000000..0765d0a91
--- /dev/null
+++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/search/controller/AutocompleteController.java
@@ -0,0 +1,393 @@
+/* $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.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+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.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.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.Individual;
+import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest;
+import edu.cornell.mannlib.vitro.webapp.controller.freemarker.FreeMarkerHttpServlet;
+import edu.cornell.mannlib.vitro.webapp.dao.IndividualDao;
+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.utils.FlagMathUtils;
+
+/**
+ * AutocompleteController is used to generate autocomplete and select element content
+ * through a Lucene search. The search logic is copied from PagedSearchController.
+ */
+
+/* rjy7 We should have a SearchController that is subclassed by both PagedSearchController
+ * and AjaxSearchController, so the methods don't all have to be copied into both places.
+ * The parent SearchController should extend FreeMarkerHttpServlet. Can only be done
+ * once PagedSearchController has been moved to FreeMarker.
+ */
+public class AutocompleteController extends FreeMarkerHttpServlet implements Searcher{
+
+ private static final long serialVersionUID = 1L;
+ private static final Log log = LogFactory.getLog(AutocompleteController.class.getName());
+
+ private IndexSearcher searcher = null;
+ String NORESULT_MSG = "";
+ private String QUERY_PARAMETER_NAME = "term";
+ 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){
+
+ }
+ }
+
+ public void doPost(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ doGet(request, response);
+ }
+
+ public void doGet(HttpServletRequest request, HttpServletResponse response)
+ throws IOException, ServletException {
+
+ String templateName = request.getServletPath().equals("/autocomplete") ? "autocompleteResults.ftl" : "selectResults.ftl";
+ Map map = new HashMap();
+
+ try {
+ doSetup(request, response);
+
+ 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(templateName, map);
+ return;
+ }
+ IndividualDao iDao = vreq.getWebappDaoFactory().getIndividualDao();
+
+ int maxHitSize = defaultMaxSearchSize;
+
+ String indexDir = getIndexDir(getServletContext());
+
+ String qtxt = vreq.getParameter(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(templateName, map);
+ 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);
+ doFailedSearch(templateName, map);
+ return;
+ }
+ }
+
+ if( topDocs == null || topDocs.scoreDocs == null){
+ log.error("topDocs for a search was null");
+ doFailedSearch(templateName, map);
+ return;
+ }
+
+ int hitsLength = topDocs.scoreDocs.length;
+ if ( hitsLength < 1 ){
+ doFailedSearch(templateName, map);
+ return;
+ }
+ log.debug("found "+hitsLength+" hits");
+
+ List results = new ArrayList();
+ 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;
+ }
+
+ // The way the analyzer is set up, name:Sm* returns no results,
+ // but name:sm* does.
+ querystr = querystr.toLowerCase();
+
+ {
+ BooleanQuery boolQuery = new BooleanQuery();
+ boolQuery.add(
+ new WildcardQuery(new Term(Entity2LuceneDoc.term.NAME, querystr+'*')),
+ BooleanClause.Occur.MUST);
+ Object param = request.getParameter("type");
+ boolQuery.add( new TermQuery(
+ new Term(Entity2LuceneDoc.term.RDFTYPE,
+ (String)param)),
+ 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;
+// }
+
+ //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;
+ }
+
+
+ }catch (Exception ex){
+ throw new SearchException(ex.getMessage());
+ }
+
+ return query;
+ }
+
+ /**
+ * 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 void doNoQuery(String templateName, Map map) {
+ ajaxWrite(templateName, map);
+ }
+
+ private void doFailedSearch(String templateName, Map map) {
+ ajaxWrite(templateName, map);
+ }
+
+ private void doSearchError(String templateName, Map map) {
+ ajaxWrite(templateName, map);
+ }
+
+ public static final int MAX_QUERY_LENGTH = 500;
+
+ public class SearchResult implements Comparable