{
    // Sort fields for submissions
    SORT_SUB_FIELDS[SearchOrder.ID.ordinal()] = "subId";
    SORT_SUB_FIELDS[SearchOrder.STUDENT_EMAIL.ordinal()] = "studentEmail";
    SORT_SUB_FIELDS[SearchOrder.STUDENT_NAME.ordinal()] = "studentName";
    SORT_SUB_FIELDS[SearchOrder.STUDENT_ID.ordinal()] = "institutionalIdentifier";
    SORT_SUB_FIELDS[SearchOrder.STATE.ordinal()] = "state";
    SORT_SUB_FIELDS[SearchOrder.ASSIGNEE.ordinal()] = "sortAssigned";
    SORT_SUB_FIELDS[SearchOrder.DOCUMENT_TITLE.ordinal()] = "documentTitle";
    SORT_SUB_FIELDS[SearchOrder.DOCUMENT_ABSTRACT.ordinal()] = "documentAbstract";
    SORT_SUB_FIELDS[SearchOrder.DOCUMENT_KEYWORDS.ordinal()] = "documentKeywords";
    SORT_SUB_FIELDS[SearchOrder.DOCUMENT_SUBJECTS.ordinal()] = "documentSubjects";
    SORT_SUB_FIELDS[SearchOrder.DOCUMENT_LANGUAGE.ordinal()] = "documentLanguage";
    SORT_SUB_FIELDS[SearchOrder.PUBLISHED_MATERIAL.ordinal()] = "publishedMaterial";
    SORT_SUB_FIELDS[SearchOrder.PRIMARY_DOCUMENT.ordinal()] = "primaryDocument";
    SORT_SUB_FIELDS[SearchOrder.GRADUATION_DATE.ordinal()] = "graduationSemester";
    SORT_SUB_FIELDS[SearchOrder.DEFENSE_DATE.ordinal()] = "defenseDate";
    SORT_SUB_FIELDS[SearchOrder.SUBMISSION_DATE.ordinal()] = "submissionDate";
    SORT_SUB_FIELDS[SearchOrder.LICENSE_AGREEMENT_DATE.ordinal()] = "licenseAgreementDate";
    SORT_SUB_FIELDS[SearchOrder.APPROVAL_DATE.ordinal()] = "approvalDate";
    SORT_SUB_FIELDS[SearchOrder.COMMITTEE_APPROVAL_DATE.ordinal()] = "committeeApprovalDate";
    SORT_SUB_FIELDS[SearchOrder.COMMITTEE_EMBARGO_APPROVAL_DATE.ordinal()] =
        "committeeEmbargoApprovalDate";
    SORT_SUB_FIELDS[SearchOrder.COMMITTEE_MEMBERS.ordinal()] = "committeeMembers";
    SORT_SUB_FIELDS[SearchOrder.COMMITTEE_CONTACT_EMAIL.ordinal()] = "committeeContactEmail";
    SORT_SUB_FIELDS[SearchOrder.DEGREE.ordinal()] = "degree";
    SORT_SUB_FIELDS[SearchOrder.DEGREE_LEVEL.ordinal()] = "degreeLevel";
    SORT_SUB_FIELDS[SearchOrder.PROGRAM.ordinal()] = "program";
    SORT_SUB_FIELDS[SearchOrder.COLLEGE.ordinal()] = "college";
    SORT_SUB_FIELDS[SearchOrder.DEPARTMENT.ordinal()] = "department";
    SORT_SUB_FIELDS[SearchOrder.MAJOR.ordinal()] = "major";
    SORT_SUB_FIELDS[SearchOrder.EMBARGO_TYPE.ordinal()] = "embargo";
    SORT_SUB_FIELDS[SearchOrder.DOCUMENT_TYPE.ordinal()] = "documentType";
    SORT_SUB_FIELDS[SearchOrder.UMI_RELEASE.ordinal()] = "umiRelease";
    SORT_SUB_FIELDS[SearchOrder.CUSTOM_ACTIONS.ordinal()] = "customActions";
    SORT_SUB_FIELDS[SearchOrder.DEPOSIT_ID.ordinal()] = "depositId";
    SORT_SUB_FIELDS[SearchOrder.REVIEWER_NOTES.ordinal()] = "reviewerNotes";
    SORT_SUB_FIELDS[SearchOrder.LAST_EVENT_ENTRY.ordinal()] = "lastEventEntry";
    SORT_SUB_FIELDS[SearchOrder.LAST_EVENT_TIME.ordinal()] = "lastEventTime";
    SORT_SUB_FIELDS[SearchOrder.ORCID.ordinal()] = "orcid";

    // Sort fields for action logs
    for (int i = 0; i < SearchOrder.values().length; i++) SORT_LOG_FIELDS[i] = SORT_SUB_FIELDS[i];
    SORT_LOG_FIELDS[SearchOrder.ID.ordinal()] = "logId";

    // Sort types for both submissions and action logs
    for (int i = 0; i < SearchOrder.values().length; i++) SORT_TYPES[i] = SortField.STRING;
    SORT_TYPES[SearchOrder.ID.ordinal()] = SortField.LONG;
    SORT_TYPES[SearchOrder.GRADUATION_DATE.ordinal()] = SortField.LONG;
    SORT_TYPES[SearchOrder.DEFENSE_DATE.ordinal()] = SortField.LONG;
    SORT_TYPES[SearchOrder.SUBMISSION_DATE.ordinal()] = SortField.LONG;
    SORT_TYPES[SearchOrder.LICENSE_AGREEMENT_DATE.ordinal()] = SortField.LONG;
    SORT_TYPES[SearchOrder.APPROVAL_DATE.ordinal()] = SortField.LONG;
    SORT_TYPES[SearchOrder.COMMITTEE_APPROVAL_DATE.ordinal()] = SortField.LONG;
    SORT_TYPES[SearchOrder.COMMITTEE_EMBARGO_APPROVAL_DATE.ordinal()] = SortField.LONG;
    SORT_TYPES[SearchOrder.CUSTOM_ACTIONS.ordinal()] = SortField.STRING;
    SORT_TYPES[SearchOrder.LAST_EVENT_TIME.ordinal()] = SortField.LONG;
  }
/**
 * Lucene implementation of the Searcher interface.
 *
 * <p>This implementation is directly tied to the LuceneIndexerImpl, in fact it requires it as a
 * spring-based dependency.
 *
 * @author <a href="http://www.scottphillips.com">Scott Phillips</a>
 */
public class LuceneSearcherImpl implements Searcher {

  // Static constants
  public static String[] SORT_SUB_FIELDS = new String[SearchOrder.values().length];
  public static String[] SORT_LOG_FIELDS = new String[SearchOrder.values().length];
  public static int[] SORT_TYPES = new int[SearchOrder.values().length];

  {
    // Sort fields for submissions
    SORT_SUB_FIELDS[SearchOrder.ID.ordinal()] = "subId";
    SORT_SUB_FIELDS[SearchOrder.STUDENT_EMAIL.ordinal()] = "studentEmail";
    SORT_SUB_FIELDS[SearchOrder.STUDENT_NAME.ordinal()] = "studentName";
    SORT_SUB_FIELDS[SearchOrder.STUDENT_ID.ordinal()] = "institutionalIdentifier";
    SORT_SUB_FIELDS[SearchOrder.STATE.ordinal()] = "state";
    SORT_SUB_FIELDS[SearchOrder.ASSIGNEE.ordinal()] = "sortAssigned";
    SORT_SUB_FIELDS[SearchOrder.DOCUMENT_TITLE.ordinal()] = "documentTitle";
    SORT_SUB_FIELDS[SearchOrder.DOCUMENT_ABSTRACT.ordinal()] = "documentAbstract";
    SORT_SUB_FIELDS[SearchOrder.DOCUMENT_KEYWORDS.ordinal()] = "documentKeywords";
    SORT_SUB_FIELDS[SearchOrder.DOCUMENT_SUBJECTS.ordinal()] = "documentSubjects";
    SORT_SUB_FIELDS[SearchOrder.DOCUMENT_LANGUAGE.ordinal()] = "documentLanguage";
    SORT_SUB_FIELDS[SearchOrder.PUBLISHED_MATERIAL.ordinal()] = "publishedMaterial";
    SORT_SUB_FIELDS[SearchOrder.PRIMARY_DOCUMENT.ordinal()] = "primaryDocument";
    SORT_SUB_FIELDS[SearchOrder.GRADUATION_DATE.ordinal()] = "graduationSemester";
    SORT_SUB_FIELDS[SearchOrder.DEFENSE_DATE.ordinal()] = "defenseDate";
    SORT_SUB_FIELDS[SearchOrder.SUBMISSION_DATE.ordinal()] = "submissionDate";
    SORT_SUB_FIELDS[SearchOrder.LICENSE_AGREEMENT_DATE.ordinal()] = "licenseAgreementDate";
    SORT_SUB_FIELDS[SearchOrder.APPROVAL_DATE.ordinal()] = "approvalDate";
    SORT_SUB_FIELDS[SearchOrder.COMMITTEE_APPROVAL_DATE.ordinal()] = "committeeApprovalDate";
    SORT_SUB_FIELDS[SearchOrder.COMMITTEE_EMBARGO_APPROVAL_DATE.ordinal()] =
        "committeeEmbargoApprovalDate";
    SORT_SUB_FIELDS[SearchOrder.COMMITTEE_MEMBERS.ordinal()] = "committeeMembers";
    SORT_SUB_FIELDS[SearchOrder.COMMITTEE_CONTACT_EMAIL.ordinal()] = "committeeContactEmail";
    SORT_SUB_FIELDS[SearchOrder.DEGREE.ordinal()] = "degree";
    SORT_SUB_FIELDS[SearchOrder.DEGREE_LEVEL.ordinal()] = "degreeLevel";
    SORT_SUB_FIELDS[SearchOrder.PROGRAM.ordinal()] = "program";
    SORT_SUB_FIELDS[SearchOrder.COLLEGE.ordinal()] = "college";
    SORT_SUB_FIELDS[SearchOrder.DEPARTMENT.ordinal()] = "department";
    SORT_SUB_FIELDS[SearchOrder.MAJOR.ordinal()] = "major";
    SORT_SUB_FIELDS[SearchOrder.EMBARGO_TYPE.ordinal()] = "embargo";
    SORT_SUB_FIELDS[SearchOrder.DOCUMENT_TYPE.ordinal()] = "documentType";
    SORT_SUB_FIELDS[SearchOrder.UMI_RELEASE.ordinal()] = "umiRelease";
    SORT_SUB_FIELDS[SearchOrder.CUSTOM_ACTIONS.ordinal()] = "customActions";
    SORT_SUB_FIELDS[SearchOrder.DEPOSIT_ID.ordinal()] = "depositId";
    SORT_SUB_FIELDS[SearchOrder.REVIEWER_NOTES.ordinal()] = "reviewerNotes";
    SORT_SUB_FIELDS[SearchOrder.LAST_EVENT_ENTRY.ordinal()] = "lastEventEntry";
    SORT_SUB_FIELDS[SearchOrder.LAST_EVENT_TIME.ordinal()] = "lastEventTime";
    SORT_SUB_FIELDS[SearchOrder.ORCID.ordinal()] = "orcid";

    // Sort fields for action logs
    for (int i = 0; i < SearchOrder.values().length; i++) SORT_LOG_FIELDS[i] = SORT_SUB_FIELDS[i];
    SORT_LOG_FIELDS[SearchOrder.ID.ordinal()] = "logId";

    // Sort types for both submissions and action logs
    for (int i = 0; i < SearchOrder.values().length; i++) SORT_TYPES[i] = SortField.STRING;
    SORT_TYPES[SearchOrder.ID.ordinal()] = SortField.LONG;
    SORT_TYPES[SearchOrder.GRADUATION_DATE.ordinal()] = SortField.LONG;
    SORT_TYPES[SearchOrder.DEFENSE_DATE.ordinal()] = SortField.LONG;
    SORT_TYPES[SearchOrder.SUBMISSION_DATE.ordinal()] = SortField.LONG;
    SORT_TYPES[SearchOrder.LICENSE_AGREEMENT_DATE.ordinal()] = SortField.LONG;
    SORT_TYPES[SearchOrder.APPROVAL_DATE.ordinal()] = SortField.LONG;
    SORT_TYPES[SearchOrder.COMMITTEE_APPROVAL_DATE.ordinal()] = SortField.LONG;
    SORT_TYPES[SearchOrder.COMMITTEE_EMBARGO_APPROVAL_DATE.ordinal()] = SortField.LONG;
    SORT_TYPES[SearchOrder.CUSTOM_ACTIONS.ordinal()] = SortField.STRING;
    SORT_TYPES[SearchOrder.LAST_EVENT_TIME.ordinal()] = SortField.LONG;
  }

  // Spring dependencies
  public LuceneIndexerImpl indexer = null;
  public SubmissionRepository subRepo = null;
  public StateManager stateManager = null;

  /**
   * Spring injection for the LuceneIndexerImpl. Note that this implementation is tied directly to
   * the indexer implementation, that is why the datatype is what it is.
   *
   * @param indexer The lucene indexer.
   */
  public void setLuceneIndexerImpl(LuceneIndexerImpl indexer) {
    this.indexer = indexer;
  }

  /** @param subRepo The submission repository */
  public void setSubmissionRepository(SubmissionRepository subRepo) {
    this.subRepo = subRepo;
  }

  /** @param stateManager The state manager (used to resolve state names into display names) */
  public void setStateManager(StateManager stateManager) {
    this.stateManager = stateManager;
  }

  @Override
  public SearchResult<Submission> submissionSearch(
      SearchFilter filter, SearchOrder orderBy, SearchDirection direction, int offset, int limit) {
    try {
      IndexReader reader = IndexReader.open(indexer.index);
      try {
        IndexSearcher searcher = new IndexSearcher(reader);

        BooleanQuery andQuery = new BooleanQuery();
        andQuery.add(new TermQuery(new Term("type", "submission")), Occur.MUST);
        buildQuery(andQuery, filter, true); // <-- This does most of the work.

        boolean reverse = (direction == SearchDirection.ASCENDING) ? false : true;

        SortField dynamicSortField =
            new SortField(
                SORT_SUB_FIELDS[orderBy.ordinal()], SORT_TYPES[orderBy.ordinal()], reverse);
        SortField idSortField = new SortField("subId", SortField.LONG, reverse);
        Sort sort = new Sort(dynamicSortField, idSortField);

        Logger.debug("Submission Query: " + andQuery.toString());

        // Run the search
        TopDocs topDocs = searcher.search(andQuery, offset + limit, sort);

        List<Long> sortedIds = new ArrayList<Long>();
        for (int i = offset; i < offset + limit; i++) {
          if (i >= topDocs.scoreDocs.length) break;
          Document doc = searcher.doc(topDocs.scoreDocs[i].doc);

          sortedIds.add(Long.valueOf(doc.get("subId")));
        }

        searcher.close();

        List<Submission> results = subRepo.findSubmissions(sortedIds);
        Collections.sort(results, new ModelComparator(sortedIds));

        return new LuceneSearchResults<Submission>(
            filter, direction, orderBy, offset, limit, results, topDocs.totalHits);
      } finally {
        reader.close();
      }
    } catch (IOException ioe) {
      Logger.error(ioe, "Unable to search");
    }
    return null;
  }

  @Override
  public SearchResult<ActionLog> actionLogSearch(
      SearchFilter filter, SearchOrder orderBy, SearchDirection direction, int offset, int limit) {

    try {
      IndexReader reader = IndexReader.open(indexer.index);
      try {
        IndexSearcher searcher = new IndexSearcher(reader);

        BooleanQuery andQuery = new BooleanQuery();
        andQuery.add(new TermQuery(new Term("type", "actionlog")), Occur.MUST);
        buildQuery(andQuery, filter, false); // <-- This does most of the work.

        boolean reverse = (direction == SearchDirection.ASCENDING) ? false : true;

        SortField dynamicSortField =
            new SortField(
                SORT_LOG_FIELDS[orderBy.ordinal()], SORT_TYPES[orderBy.ordinal()], reverse);
        SortField idSortField = new SortField("logId", SortField.LONG, reverse);
        Sort sort = new Sort(dynamicSortField, idSortField);

        Logger.debug("Log Query: " + andQuery.toString());

        // Run the search
        TopDocs topDocs = searcher.search(andQuery, offset + limit, sort);

        List<Long> sortedIds = new ArrayList<Long>();
        for (int i = offset; i < offset + limit; i++) {
          if (i >= topDocs.scoreDocs.length) break;
          Document doc = searcher.doc(topDocs.scoreDocs[i].doc);

          sortedIds.add(Long.valueOf(doc.get("logId")));
        }

        searcher.close();

        List<ActionLog> results = subRepo.findActionLogs(sortedIds);
        Collections.sort(results, new ModelComparator(sortedIds));

        return new LuceneSearchResults<ActionLog>(
            filter, direction, orderBy, offset, limit, results, topDocs.totalHits);
      } finally {
        reader.close();
      }
    } catch (IOException ioe) {
      Logger.error(ioe, "Unable to search");
    }
    return null;
  }

  @Override
  public long[] submissionSearch(
      SearchFilter filter, SearchOrder orderBy, SearchDirection direction) {

    try {
      IndexReader reader = IndexReader.open(indexer.index);
      try {
        IndexSearcher searcher = new IndexSearcher(reader);

        BooleanQuery andQuery = new BooleanQuery();
        andQuery.add(new TermQuery(new Term("type", "submission")), Occur.MUST);
        buildQuery(andQuery, filter, true); // <-- This does most of the work.

        boolean reverse = (direction == SearchDirection.ASCENDING) ? false : true;

        SortField dynamicSortField =
            new SortField(
                SORT_SUB_FIELDS[orderBy.ordinal()], SORT_TYPES[orderBy.ordinal()], reverse);
        SortField idSortField = new SortField("subId", SortField.LONG, reverse);
        Sort sort = new Sort(dynamicSortField, idSortField);

        Logger.debug("Submission ID Query: " + andQuery.toString());

        TopDocs topDocs = searcher.search(andQuery, Integer.MAX_VALUE, sort);

        long[] sortedIds = new long[topDocs.scoreDocs.length];
        for (int i = 0; i < topDocs.scoreDocs.length; i++) {
          Document doc = searcher.doc(topDocs.scoreDocs[i].doc);
          sortedIds[i] = Long.valueOf(doc.get("subId")).longValue();
        }

        searcher.close();

        return sortedIds;

      } finally {
        reader.close();
      }
    } catch (IOException ioe) {
      Logger.error(ioe, "Unable to search");
    }
    return null;
  }

  @Override
  public long[] actionLogSearch(
      SearchFilter filter, SearchOrder orderBy, SearchDirection direction) {

    try {
      IndexReader reader = IndexReader.open(indexer.index);
      try {
        IndexSearcher searcher = new IndexSearcher(reader);

        BooleanQuery andQuery = new BooleanQuery();
        andQuery.add(new TermQuery(new Term("type", "actionlog")), Occur.MUST);
        buildQuery(andQuery, filter, true); // <-- This does most of the work.

        boolean reverse = (direction == SearchDirection.ASCENDING) ? false : true;

        SortField dynamicSortField =
            new SortField(
                SORT_SUB_FIELDS[orderBy.ordinal()], SORT_TYPES[orderBy.ordinal()], reverse);
        SortField idSortField = new SortField("logId", SortField.LONG, reverse);
        Sort sort = new Sort(dynamicSortField, idSortField);

        Logger.debug("Log ID Query: " + andQuery.toString());

        TopDocs topDocs = searcher.search(andQuery, Integer.MAX_VALUE, sort);

        long[] sortedIds = new long[topDocs.scoreDocs.length];
        for (int i = 0; i < topDocs.scoreDocs.length; i++) {
          Document doc = searcher.doc(topDocs.scoreDocs[i].doc);
          sortedIds[i] = Long.valueOf(doc.get("logId")).longValue();
        }
        searcher.close();

        return sortedIds;

      } finally {
        reader.close();
      }
    } catch (IOException ioe) {
      Logger.error(ioe, "Unable to search");
    }
    return null;
  }

  /**
   * This method produces the common part of the query handle the filter search clauses.
   *
   * @param andQuery The existing and-based query
   * @param filter The filter search paramaters.
   * @param submissions Whether this is for submissions or action logs
   */
  public void buildQuery(BooleanQuery andQuery, SearchFilter filter, boolean submissions) {
    QueryParser parser = new QueryParser(indexer.version, "searchText", indexer.standardAnalyzer);

    // Include Submission filter
    if (filter.getIncludedSubmissions().size() > 0) {
      BooleanQuery orQuery = new BooleanQuery();
      for (Submission sub : filter.getIncludedSubmissions()) {
        orQuery.add(
            new TermQuery(new Term("subId", NumericUtils.longToPrefixCoded(sub.getId()))),
            Occur.SHOULD);
      }
      andQuery.add(orQuery, Occur.MUST);
    }

    // Include Log filter
    if (filter.getIncludedActionLogs().size() > 0) {
      BooleanQuery orQuery = new BooleanQuery();
      for (ActionLog log : filter.getIncludedActionLogs()) {
        orQuery.add(
            new TermQuery(new Term("logId", NumericUtils.longToPrefixCoded(log.getId()))),
            Occur.SHOULD);
      }
      andQuery.add(orQuery, Occur.MUST);
    }

    // Exclude Submission filter
    for (Submission sub : filter.getExcludedSubmissions()) {
      andQuery.add(
          new TermQuery(new Term("subId", NumericUtils.longToPrefixCoded(sub.getId()))),
          Occur.MUST_NOT);
    }

    // Exclude Log filter
    for (ActionLog log : filter.getExcludedActionLogs()) {
      andQuery.add(
          new TermQuery(new Term("logId", NumericUtils.longToPrefixCoded(log.getId()))),
          Occur.MUST_NOT);
    }

    // Search Text Filter
    if (filter.getSearchText().size() > 0) {
      BooleanQuery orQuery = new BooleanQuery();
      for (String searchText : filter.getSearchText()) {
        try {
          // First try to interpret it as a complex lucene search string.
          orQuery.add(parser.parse(searchText), Occur.SHOULD);
        } catch (ParseException e) {
          // If that fails just fall back to a term query.
          orQuery.add(new TermQuery(new Term("searchText", searchText)), Occur.SHOULD);
        }
      }
      andQuery.add(orQuery, Occur.MUST);
    }

    // State Filter
    if (filter.getStates().size() > 0) {
      BooleanQuery orQuery = new BooleanQuery();
      for (String stateName : filter.getStates()) {
        State state = stateManager.getState(stateName);
        orQuery.add(new TermQuery(new Term("state", state.getDisplayName())), Occur.SHOULD);
      }
      andQuery.add(orQuery, Occur.MUST);
    }

    // Assignee Filter
    if (filter.getAssignees().size() > 0) {
      BooleanQuery orQuery = new BooleanQuery();
      for (Person assignee : filter.getAssignees()) {

        long assigneeId = 0;
        if (assignee != null) assigneeId = assignee.getId();

        orQuery.add(
            new TermQuery(new Term("searchAssigned", NumericUtils.longToPrefixCoded(assigneeId))),
            Occur.SHOULD);
      }
      andQuery.add(orQuery, Occur.MUST);
    }

    // Embargo Filter
    if (filter.getEmbargoTypes().size() > 0) {
      BooleanQuery orQuery = new BooleanQuery();
      for (EmbargoType embargo : filter.getEmbargoTypes()) {
        orQuery.add(
            new TermQuery(
                new Term("embargo", embargo.getName() + " " + embargo.getGuarantor().name())),
            Occur.SHOULD);
      }
      andQuery.add(orQuery, Occur.MUST);
    }

    // Graduation Semester Filter
    if (filter.getGraduationSemesters().size() > 0) {
      BooleanQuery orQuery = new BooleanQuery();
      for (Semester semester : filter.getGraduationSemesters()) {

        // We can't index it if it dosn't have a date.
        if (semester.year == null) continue;

        Calendar cal = Calendar.getInstance();
        cal.clear();
        cal.set(Calendar.YEAR, semester.year);
        if (semester.month != null) {
          cal.set(Calendar.MONTH, semester.month);
        }
        orQuery.add(
            new TermQuery(
                new Term(
                    "graduationSemester", NumericUtils.longToPrefixCoded(cal.getTimeInMillis()))),
            Occur.SHOULD);
      }
      andQuery.add(orQuery, Occur.MUST);
    }

    // Degree Filter
    if (filter.getDegrees().size() > 0) {
      BooleanQuery orQuery = new BooleanQuery();
      for (String degree : filter.getDegrees()) {
        orQuery.add(new TermQuery(new Term("degree", degree)), Occur.SHOULD);
      }
      andQuery.add(orQuery, Occur.MUST);
    }

    // Department Filter
    if (filter.getDepartments().size() > 0) {
      BooleanQuery orQuery = new BooleanQuery();
      for (String dept : filter.getDepartments()) {
        orQuery.add(new TermQuery(new Term("department", dept)), Occur.SHOULD);
      }
      andQuery.add(orQuery, Occur.MUST);
    }

    // Program Filter
    if (filter.getPrograms().size() > 0) {
      BooleanQuery orQuery = new BooleanQuery();
      for (String program : filter.getPrograms()) {
        orQuery.add(new TermQuery(new Term("program", program)), Occur.SHOULD);
      }
      andQuery.add(orQuery, Occur.MUST);
    }

    // College Filter
    if (filter.getColleges().size() > 0) {
      BooleanQuery orQuery = new BooleanQuery();
      for (String college : filter.getColleges()) {
        orQuery.add(new TermQuery(new Term("college", college)), Occur.SHOULD);
      }
      andQuery.add(orQuery, Occur.MUST);
    }

    // Major Filter
    if (filter.getMajors().size() > 0) {
      BooleanQuery orQuery = new BooleanQuery();
      for (String major : filter.getMajors()) {
        orQuery.add(new TermQuery(new Term("major", major)), Occur.SHOULD);
      }
      andQuery.add(orQuery, Occur.MUST);
    }

    // Document Type Filter
    if (filter.getDocumentTypes().size() > 0) {
      BooleanQuery orQuery = new BooleanQuery();
      for (String docType : filter.getDocumentTypes()) {
        orQuery.add(new TermQuery(new Term("documentType", docType)), Occur.SHOULD);
      }
      andQuery.add(orQuery, Occur.MUST);
    }

    // UMI Release Filter
    if (filter.getUMIRelease() != null) {
      if (filter.getUMIRelease()) {
        andQuery.add(new TermQuery(new Term("umiRelease", "yes")), Occur.MUST);
      } else {
        andQuery.add(new TermQuery(new Term("umiRelease", "no")), Occur.MUST);
      }
    }

    // Date Range Filter
    if (filter.getDateRangeStart() != null || filter.getDateRangeEnd() != null) {

      long startTime = 0;
      if (filter.getDateRangeStart() != null) startTime = filter.getDateRangeStart().getTime();

      long endTime = Long.MAX_VALUE;
      if (filter.getDateRangeEnd() != null) endTime = filter.getDateRangeEnd().getTime();

      if (submissions)
        andQuery.add(
            NumericRangeQuery.newLongRange("submissionDate", startTime, endTime, true, true),
            Occur.MUST);
      else
        andQuery.add(
            NumericRangeQuery.newLongRange("lastEventTime", startTime, endTime, true, true),
            Occur.MUST);
    }
    // Custom Action Filter
    if (filter.getCustomActions().size() > 0) {
      BooleanQuery orQuery = new BooleanQuery();
      for (CustomActionDefinition customAction : filter.getCustomActions()) {
        orQuery.add(new TermQuery(new Term("customAction", customAction.getLabel())), Occur.SHOULD);
      }
      andQuery.add(orQuery, Occur.MUST);
    }
  }

  /**
   * Search results object.
   *
   * <p>This method encapsulates the result of a search query for a particular type of object. It
   * records what the parameters where that generated the query, and access to the results. In
   * addition there are some helpful pagination options to aid the front-end in generating
   * pagination results.
   *
   * @param <T> Either Submission or ActionLog
   */
  public static class LuceneSearchResults<T extends AbstractModel> implements SearchResult<T> {

    // The search parameters
    public final SearchFilter filter;
    public final SearchDirection direction;
    public final SearchOrder orderBy;
    public final int offset;
    public final int limit;
    public final List<T> results;
    public final int total;

    /**
     * Construct a new search results object.
     *
     * @param filter The filter which produced these results.
     * @param direction The direction of the results.
     * @param orderBy How the results are ordered.
     * @param offset The pagination offset of the results.
     * @param limit The number of items per page.
     * @param results The actual results.
     * @param total How many objects matched this query regardless of pagination limits.
     */
    public LuceneSearchResults(
        SearchFilter filter,
        SearchDirection direction,
        SearchOrder orderBy,
        int offset,
        int limit,
        List<T> results,
        int total) {
      this.filter = filter;
      this.direction = direction;
      this.orderBy = orderBy;
      this.offset = offset;
      this.limit = limit;
      this.results = results;
      this.total = total;
    }

    @Override
    public SearchFilter getFilter() {
      return filter;
    }

    @Override
    public SearchDirection getDirection() {
      return direction;
    }

    @Override
    public SearchOrder getOrderBy() {
      return orderBy;
    }

    @Override
    public int getOffset() {
      return offset;
    }

    @Override
    public int getLimit() {
      return limit;
    }

    @Override
    public List<T> getResults() {
      return results;
    }

    @Override
    public int getTotal() {
      return total;
    }

    @Override
    public List<Pagination> getPagination(int windowSize) {

      List<Pagination> pagination = new ArrayList<Pagination>();

      // Create the backwards entries
      for (int i = -((windowSize - 1) / 2); i < 0; i++) {

        int offset = getOffset() + (i * getLimit());
        int page = (getOffset() / getLimit()) + i + 1;

        if (offset >= 0) pagination.add(new Pagination(page, offset, false));
      }

      // Add the current entry
      pagination.add(new Pagination((getOffset() / limit) + 1, getOffset(), true));

      // Create the forward entries
      for (int i = 1; i <= windowSize; i++) {

        int offset = getOffset() + (i * getLimit());
        int page = (getOffset() / getLimit()) + i + 1;

        if (offset < getTotal() && pagination.size() < windowSize)
          pagination.add(new Pagination(page, offset, false));
      }

      return pagination;
    }
  } // LuceneSearchResults

  /** Sort vireo model objects based upon a provided list of ids. */
  public static class ModelComparator implements Comparator<AbstractModel> {

    // The predefined list of sorted ids.
    public final List<Long> sortedIds;

    /**
     * Construct a new comparator.
     *
     * @param sortedIds A predefined list of sorted ids.
     */
    public ModelComparator(List<Long> sortedIds) {
      this.sortedIds = sortedIds;
    }

    @Override
    public int compare(AbstractModel a, AbstractModel b) {

      Long aId = a.getId();
      Long bId = b.getId();

      if (aId == bId) return 0;
      if (sortedIds.indexOf(aId) < sortedIds.indexOf(bId)) return -1;
      return 1;
    }
  }
}