org.ambraproject.article.service.BrowseServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.ambraproject.article.service.BrowseServiceImpl.java

Source

/*
* $HeadURL$
not* $Id$
*
* Copyright (c) 2006-2011 by Public Library of Science
*     http://plos.org
*     http://ambraproject.org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*     http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.ambraproject.article.service;

import org.ambraproject.ApplicationException;
import org.ambraproject.article.BrowseParameters;
import org.ambraproject.article.BrowseResult;
import org.ambraproject.article.action.TOCArticleGroup;
import org.ambraproject.cache.Cache;
import org.ambraproject.journal.JournalService;
import org.ambraproject.model.IssueInfo;
import org.ambraproject.model.VolumeInfo;
import org.ambraproject.model.article.ArticleInfo;
import org.ambraproject.model.article.ArticleType;
import org.ambraproject.model.article.Years;
import org.ambraproject.search.SearchHit;
import org.ambraproject.search.service.SolrServerFactory;
import org.ambraproject.service.HibernateServiceImpl;
import org.ambraproject.solr.SolrServiceUtil;
import org.apache.commons.configuration.Configuration;
import org.apache.commons.configuration.HierarchicalConfiguration;
import org.apache.commons.lang.StringUtils;
import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.response.FacetField;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrDocumentList;
import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.orm.hibernate3.HibernateCallback;
import org.springframework.transaction.annotation.Transactional;
import org.topazproject.ambra.models.Issue;
import org.topazproject.ambra.models.Journal;
import org.topazproject.ambra.models.Volume;

import java.io.IOException;
import java.net.URI;
import java.sql.SQLException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.StringTokenizer;
import java.util.TreeMap;

/**
 * Class to get all Articles in system and organize them by date and by category
 * from the SOLR index
 *
 * @author Jen Song
 * @author Joe Osowski
 */
public class BrowseServiceImpl extends HibernateServiceImpl implements BrowseService {
    private static final Logger log = LoggerFactory.getLogger(BrowseServiceImpl.class);

    private static final String ARTBYCAT_LIST_KEY = "ArtByCat-";
    private static final String DATE_LIST_KEY = "DateList-";
    private static final String ARTBYDATE_LIST_KEY = "ArtByDate-";

    private Cache browseSolrCache;
    private JournalService journalService;
    private ArticleService articleService;
    private SolrServerFactory serverFactory;

    private int cacheTimeToLive = 15;
    private Boolean useCache = true;

    //We have two collections here, as list supports ordering
    //And we want to keep the sorts in the order in which they are defined
    private List displaySorts = null;
    private Map validSorts = null;

    private static final Integer getYear(String date) {
        return Integer.valueOf(date.substring(0, 4));
    }

    private static final Integer getMonth(String date) {
        return Integer.valueOf(date.substring(5, 7));
    }

    private static final Integer getDay(String date) {
        return Integer.valueOf(date.substring(8, 10));
    }

    /**
     * @param journalService The journal-service to use.
     */
    @Required
    public void setJournalService(JournalService journalService) {
        this.journalService = journalService;
    }

    /**
     * @param articleService The articleService to use.
     */
    @Required
    public void setArticleService(ArticleService articleService) {
        this.articleService = articleService;
    }

    /**
     * @param browseSolrCache The browse solr cache to use.
     */
    @Required
    public void setBrowseSolrCache(Cache browseSolrCache) {
        this.browseSolrCache = browseSolrCache;
        // the time to live will be short enough that we are not going to worry about invalidator logic
    }

    /**
     * Set the configuration class
     *
     * @param config the configuration class to use
     * @throws ApplicationException if required configuration settings are missing
     */
    @Required
    public void setConfiguration(Configuration config) throws ApplicationException {
        if (config.containsKey("ambra.services.browse.sortOptions.option")) {
            validSorts = new HashMap();
            displaySorts = new ArrayList();

            HierarchicalConfiguration hc = (HierarchicalConfiguration) config;
            List<HierarchicalConfiguration> sorts = hc.configurationsAt("ambra.services.browse.sortOptions.option");

            for (HierarchicalConfiguration s : sorts) {
                String key = s.getString("[@displayName]");
                String value = s.getString("");
                validSorts.put(key, value);
                displaySorts.add(key);
            }

            ((HierarchicalConfiguration) config).setExpressionEngine(null);
        } else {
            throw new ApplicationException(
                    "ambra.services.browse.sortOptions.option not defined " + "in configuration.");
        }

        this.cacheTimeToLive = config.getInt("ambra.services.browse.time", 15) * 60;
        this.useCache = config.getBoolean("ambra.services.browse.cache", true);
    }

    /**
     * The map of sorts that are valid for this provider
     * @return
     */
    public List getSorts() {
        return this.displaySorts;
    }

    /**
     * Get the dates of all articles with a <code>state</code> of <code>ACTIVE</code> (meaning the articles have been
     * published). The outer map is a map of years, the next inner map a map of months, and finally the innermost is a
     * list of days. <br/>
     *
     * @param  journalKey the current journal
     *
     * @return the article dates.
     */
    @Transactional(readOnly = true)
    public Years getArticleDatesForJournal(final String journalKey) {
        if (this.useCache) {
            String cacheKey = DATE_LIST_KEY + journalKey;

            return browseSolrCache.get(cacheKey, this.cacheTimeToLive,
                    new Cache.SynchronizedLookup<Years, RuntimeException>(cacheKey.intern()) {
                        @SuppressWarnings("synthetic-access")
                        @Override
                        public Years lookup() throws RuntimeException {
                            return loadArticleDates(journalKey);
                        }
                    });
        } else {
            return loadArticleDates(journalKey);
        }
    }

    /**
     * Get articles in the given category. One "page" of articles will be returned, i.e. articles pageNum * pageSize ..
     * (pageNum + 1) * pageSize - 1 . Note that less than a pageSize articles may be returned, either because it's the end
     * of the list or because some articles are not accessible.
     *
     * @param params A collection filters / parameters to browse by
     * @return the articles.
     */
    @Transactional(readOnly = true)
    public BrowseResult getArticlesBySubject(final BrowseParameters params) {
        BrowseResult result;

        if (this.useCache) {
            final String cacheKey = ARTBYCAT_LIST_KEY + params.getJournalKey() + "-"
                    + StringUtils.join(params.getSubjects(), "-") + "-" + params.getSort() + "-" + "-"
                    + params.getPageNum() + "-" + params.getPageSize();

            result = browseSolrCache.get(cacheKey, this.cacheTimeToLive,
                    new Cache.SynchronizedLookup<BrowseResult, RuntimeException>(cacheKey.intern()) {
                        @Override
                        public BrowseResult lookup() throws RuntimeException {
                            return getArticlesBySubjectViaSolr(params);
                        }
                    });
        } else {
            result = getArticlesBySubjectViaSolr(params);
        }

        return result;
    }

    /**
     * Get articles in the given date range, from newest to oldest, of the given article type(s). One "page" of articles
     * will be returned, i.e. articles pageNum * pageSize .. (pageNum + 1) * pageSize - 1 . Note that less than a pageSize
     * articles may be returned, either because it's the end of the list or because some articles are not accessible.
     * <p/>
     * Note: this method assumes the dates are truly just dates, i.e. no hours, minutes, etc.
     * <p/>
     * If the <code>articleTypes</code> parameter is null or empty, then all types of articles are returned.
     * <p/>
     * This method should never return null.
     *
     * @param params A collection filters / parameters to browse by
     * @return the articles.
     */
    @Transactional(readOnly = true)
    public BrowseResult getArticlesByDate(final BrowseParameters params) {

        BrowseResult result;

        if (this.useCache) {
            String mod = params.getJournalKey() + "-" + params.getStartDate().getTimeInMillis() + "-"
                    + params.getEndDate().getTimeInMillis() + "-" + params.getSort();
            String cacheKey = ARTBYDATE_LIST_KEY + mod + "-" + params.getPageNum() + "-" + params.getPageSize();

            result = browseSolrCache.get(cacheKey, this.cacheTimeToLive,
                    new Cache.SynchronizedLookup<BrowseResult, RuntimeException>(cacheKey.intern()) {
                        @Override
                        public BrowseResult lookup() throws RuntimeException {
                            return getArticlesByDateViaSolr(params);
                        }
                    });
        } else {
            result = getArticlesByDateViaSolr(params);
        }

        return result;
    }

    /**
     * Get a list of article-counts for each category.
     *
     * @param journalKey The current journal
     * @return the category infos.
     */
    @Transactional(readOnly = true)
    public SortedMap<String, Long> getSubjectsForJournal(final String journalKey) {
        if (this.useCache) {
            final String cacheKey = ARTBYCAT_LIST_KEY + journalKey;

            return browseSolrCache.get(cacheKey, this.cacheTimeToLive,
                    new Cache.SynchronizedLookup<SortedMap<String, Long>, RuntimeException>(cacheKey.intern()) {
                        @Override
                        public SortedMap<String, Long> lookup() throws RuntimeException {
                            return getSubjectsForJournalViaSolr(journalKey);
                        }
                    });
        } else {
            return getSubjectsForJournalViaSolr(journalKey);
        }
    }

    /**
     * Get Issue information.
     *
     * @param issueDoi DOI of Issue.
     * @return the Issue information.
     */
    @Transactional(readOnly = true)
    public IssueInfo getIssueInfo(final URI issueDoi) {
        return (IssueInfo) hibernateTemplate.execute(new HibernateCallback() {
            public Object doInHibernate(Session session) throws HibernateException, SQLException {
                // get the Issue
                final Issue issue = (Issue) hibernateTemplate.get(Issue.class, issueDoi);
                if (issue == null) {
                    log.error("Failed to retrieve Issue for doi='" + issueDoi.toString() + "'");
                    return null;
                }

                // get the image Article
                URI imageArticle = null;
                String description = issue.getDescription();
                if ((issue.getImage() != null) && (issue.getImage().toString().length() != 0)) {
                    imageArticle = issue.getImage();
                }

                // derive prev/next Issue, "parent" Volume
                URI prevIssueURI = null;
                URI nextIssueURI = null;
                Volume parentVolume = null;

                List<String> results = session
                        .createSQLQuery("select aggregationUri from VolumeIssueList where issueUri = :doi")
                        .setParameter("doi", issueDoi.toString()).list();
                if (results.size() > 0) {
                    URI volumeURI = URI.create(results.get(0));
                    parentVolume = (Volume) session.load(Volume.class, volumeURI);
                }
                if (parentVolume != null) {
                    final List<URI> issues = parentVolume.getIssueList();
                    final int issuePos = issues.indexOf(issueDoi);
                    prevIssueURI = (issuePos == 0) ? null : issues.get(issuePos - 1);
                    nextIssueURI = (issuePos == issues.size() - 1) ? null : issues.get(issuePos + 1);
                } else {
                    log.warn("Issue: " + issue.getId() + ", not contained in any Volumes");
                }

                IssueInfo issueInfo = new IssueInfo(issue.getId(), issue.getDisplayName(), prevIssueURI,
                        nextIssueURI, imageArticle, description,
                        parentVolume == null ? null : parentVolume.getId());
                issueInfo.setArticleUriList(getArticleList(issue));
                return issueInfo;

            }
        });
    }

    /**
     * Return the ID of the latest issue from the latest volume. If no issue exists in the latest volume, then look at the
     * previous volume and so on. The Current Issue for each Journal should be configured via the admin console. This
     * method is a reasonable way to get the most recent Issue if Current Issue was not set.
     *
     * @param journal The journal in which to seek the most recent Issue
     * @return The most recent Issue from the most recent Volume, or null if there are no Issues
     */
    public URI getLatestIssueFromLatestVolume(Journal journal) {
        List<VolumeInfo> vols = getVolumeInfosForJournal(journal);
        if (vols.size() > 0) {
            for (VolumeInfo volInfo : vols) {
                IssueInfo latestIssue = null;
                List<IssueInfo> issuesInVol = volInfo.getIssueInfos();
                if (issuesInVol.size() > 0) {
                    latestIssue = issuesInVol.get(issuesInVol.size() - 1);
                }
                if (latestIssue != null) {
                    return latestIssue.getId();
                }
            }
        }
        return null;
    }

    /**
     * Returns the list of ArticleInfos contained in this Issue. The list will contain only ArticleInfos for Articles that
     * the current user has permission to view.
     *
     * @param issueDOI Issue ID
     * @param authId the AuthId of the current user
     * @return List of ArticleInfo objects.
     */
    @Transactional(readOnly = true)
    public List<ArticleInfo> getArticleInfosForIssue(final URI issueDOI, String authId) {

        IssueInfo iInfo = getIssueInfo(issueDOI);
        List<ArticleInfo> aInfos = new ArrayList<ArticleInfo>();

        for (URI articleDoi : iInfo.getArticleUriList()) {
            try {
                ArticleInfo ai = articleService.getArticleInfo(articleDoi.toString(), authId);
                aInfos.add(ai);
            } catch (NoSuchArticleIdException ex) {
                log.warn("Article " + articleDoi + " missing; member of Issue " + issueDOI);
            }
        }
        return aInfos;
    }

    /**
     * Get a VolumeInfo for the given id. This only works if the volume is in the current journal.
     *
     * @param id Volume ID
     * @return VolumeInfo
     */
    @Transactional(readOnly = true)
    public VolumeInfo getVolumeInfo(URI id, String journalKey) {
        // Attempt to get the volume infos from the cached journal list...
        List<VolumeInfo> volumes = getVolumeInfosForJournal(journalService.getJournal(journalKey));
        for (VolumeInfo vol : volumes) {
            if (id.equals(vol.getId())) {
                return vol;
            }
        }

        /*
         * If we have no luck with the cached journal list, attempt to load the volume re-using
         * loadVolumeInfos();
         */
        List<URI> l = new ArrayList<URI>();
        l.add(id);
        List<VolumeInfo> vols = loadVolumeInfos(l);
        if ((vols != null) && (vols.size() > 0)) {
            return vols.get(0);
        }
        return null;
    }

    /**
     * Returns a list of VolumeInfos for the given Journal. VolumeInfos are sorted in reverse order to reflect most common
     * usage. Uses the pull-through cache.
     *
     * @param journal To find VolumeInfos for.
     * @return VolumeInfos for journal in reverse order.
     */
    @Transactional(readOnly = true)
    public List<VolumeInfo> getVolumeInfosForJournal(final Journal journal) {
        final List<URI> volumeDois = journal.getVolumes();
        List<VolumeInfo> volumeInfos = loadVolumeInfos(volumeDois);
        Collections.reverse(volumeInfos);
        return volumeInfos;
    }

    /**
     * Get VolumeInfos. Note that the IssueInfos contained in the volumes have not been instantiated with the
     * ArticleInfos.
     *
     * @param volumeDois to look up.
     * @return volumeInfos.
     */
    private List<VolumeInfo> loadVolumeInfos(final List<URI> volumeDois) {
        List<VolumeInfo> volumeInfos = new ArrayList<VolumeInfo>();

        // get the Volumes
        for (int onVolumeDoi = 0; onVolumeDoi < volumeDois.size(); onVolumeDoi++) {
            final URI volumeDoi = volumeDois.get(onVolumeDoi);
            final Volume volume = (Volume) hibernateTemplate.get(Volume.class, volumeDoi);
            if (volume == null) {
                log.error("unable to load Volume: " + volumeDoi);
                continue;
            }

            List<IssueInfo> issueInfos = new ArrayList<IssueInfo>();
            for (final URI issueDoi : volume.getIssueList()) {
                issueInfos.add(getIssueInfo(issueDoi));
            }

            // calculate prev/next
            final URI prevVolumeDoi = (onVolumeDoi == 0) ? null : volumeDois.get(onVolumeDoi - 1);
            final URI nextVolumeDoi = (onVolumeDoi == volumeDois.size() - 1) ? null
                    : volumeDois.get(onVolumeDoi + 1);
            final VolumeInfo volumeInfo = new VolumeInfo(volume.getId(), volume.getDisplayName(), prevVolumeDoi,
                    nextVolumeDoi, volume.getImage(), volume.getDescription(), issueInfos);
            volumeInfos.add(volumeInfo);
        }

        return volumeInfos;
    }

    private Years loadArticleDates(String journalKey) {
        Years dates = new Years();

        SolrQuery query = createCommonQuery(journalKey);

        query.setFacet(true);
        query.addFacetField("publication_date");
        query.setFacetLimit(-1);
        query.setFacetMinCount(1);

        query.setRows(0);

        try {
            QueryResponse response = this.serverFactory.getServer().query(query);
            FacetField ff = response.getFacetField("publication_date");
            List<FacetField.Count> counts = ff.getValues();
            for (FacetField.Count count : counts) {
                String publicationDate = count.getName();
                Integer y = getYear(publicationDate);
                Integer m = getMonth(publicationDate);
                Integer d = getDay(publicationDate);
                dates.getMonths(y).getDays(m).add(d);
            }
        } catch (SolrServerException e) {
            log.error("Unable to execute a query on the Solr Server.", e);
        }

        return dates;
    }

    /**
     * Given a list of Article Groups with correctly ordered articles create a CSV string of article URIs. The URIs will
     * be in the order that they show up on the TOC.
     *
     * @param articleGroups the list of TOCArticleGroup to process.
     * @return a string of a comma separated list of article URIs
     */
    public String articleGrpListToCSV(List<TOCArticleGroup> articleGroups) {
        StringBuilder articleList = new StringBuilder();
        Iterator i = articleGroups.listIterator();

        // Group Loop
        while (i.hasNext()) {
            TOCArticleGroup ag = (TOCArticleGroup) i.next();
            Iterator y = ag.articles.listIterator();

            // Article Loop
            while (y.hasNext()) {
                ArticleInfo ai = (ArticleInfo) y.next();
                articleList.append(ai.doi);

                if (y.hasNext())
                    articleList.append(',');
            }
            if (i.hasNext())
                articleList.append(',');
        }
        return articleList.toString();
    }

    /**
     *
     */
    public List<TOCArticleGroup> getArticleGrpList(URI issueURI, String authId) {
        Issue issue = (Issue) hibernateTemplate.get(Issue.class, issueURI);

        // If the issue does not exist then return an empty list
        if (issue == null)
            return new ArrayList<TOCArticleGroup>();

        return getArticleGrpList(issue, authId);
    }

    /**
     *
     */
    public List<TOCArticleGroup> getArticleGrpList(Issue issue, String authId) {
        List<TOCArticleGroup> groupList = new ArrayList<TOCArticleGroup>();

        for (ArticleType at : ArticleType.getOrderedListForDisplay()) {
            TOCArticleGroup newArticleGroup = new TOCArticleGroup(at);
            groupList.add(newArticleGroup);
        }

        return buildArticleGroups(issue, groupList, authId);
    }

    /**
     *
     */
    public List<TOCArticleGroup> buildArticleGroups(Issue issue, List<TOCArticleGroup> articleGroups,
            String authId) {
        List<ArticleInfo> articlesInIssue = getArticleInfosForIssue(issue.getId(), authId);
        /*
         * For every article that is of the same ArticleType as a TOCArticleGroup, add it to that group.
         * Articles can appear in multiple TOCArticleGroups.
         */
        for (ArticleInfo ai : articlesInIssue)
            for (TOCArticleGroup ag : articleGroups)
                for (ArticleType articleType : ai.getArticleTypes())
                    if (ag.getArticleType().equals(articleType)) {
                        ag.addArticle(ai);
                        break;
                    }

        Iterator iter = articleGroups.listIterator();
        Integer i = 0;

        while (iter.hasNext()) {
            TOCArticleGroup ag = (TOCArticleGroup) iter.next();
            // remove the group if it has no articles
            if (ag.articles.size() == 0) {
                iter.remove();
                continue;
            }
            // If we respect order then don't sort.
            if (!issue.getRespectOrder()) {
                ag.setId("tocGrp_" + (i++));
                ag.sortArticles();
            }
        }
        return articleGroups;
    }

    /**
     * Get ordered list of articles. Either from articleList or from simpleCollection if articleList is empty.
     *
     * @param issue
     * @return List of article URI's
     */
    public List<URI> getArticleList(Issue issue) {
        List<URI> articleList = issue.getArticleList();
        if (articleList.isEmpty() && !issue.getSimpleCollection().isEmpty())
            return new ArrayList<URI>(issue.getSimpleCollection());

        return articleList;
    }

    /**
     * Sets the solr server factory object
     * @param serverFactory solr server factory
     */
    public void setServerFactory(SolrServerFactory serverFactory) {
        this.serverFactory = serverFactory;
    }

    /**
     * Get a list of article counts for each category
     *
     * @param journalKey the current journal
     *
     * @return category info
     */
    private SortedMap<String, Long> getSubjectsForJournalViaSolr(String journalKey) {

        SortedMap<String, Long> categories = new TreeMap<String, Long>();

        SolrQuery query = createCommonQuery(journalKey);

        query.setFacet(true);
        query.addFacetField("subject_level_1");
        query.setFacetLimit(-1);
        query.setFacetMinCount(1);

        query.setRows(0);

        try {
            QueryResponse response = this.serverFactory.getServer().query(query);
            FacetField ff = response.getFacetField("subject_level_1");
            List<FacetField.Count> counts = ff.getValues();
            if (counts != null) {
                for (FacetField.Count count : counts) {
                    categories.put(count.getName(), count.getCount());
                }
            }
        } catch (SolrServerException e) {
            log.error("Unable to execute a query on the Solr Server.", e);
        }

        return categories;
    }

    /**
     * Returns a list of articles for a given category
     * @param params a collection filters / parameters to browse by
     * @return articles
     */
    private BrowseResult getArticlesBySubjectViaSolr(BrowseParameters params) {
        BrowseResult result = new BrowseResult();
        ArrayList<SearchHit> articles = new ArrayList<SearchHit>();
        long total = 0;

        SolrQuery query = createCommonQuery(params.getJournalKey());

        query.addField("title_display");
        query.addField("author_display");
        query.addField("article_type");
        query.addField("publication_date");
        query.addField("id");
        query.addField("abstract_primary_display");
        query.addField("eissn");

        if (params.getSubjects() != null && params.getSubjects().length > 0) {
            StringBuffer subjectQuery = new StringBuffer();
            for (String subject : params.getSubjects()) {
                subjectQuery.append("\"").append(subject).append("\"").append(" AND ");
            }
            // remove the last " AND "
            query.setQuery("subject_level_1:(" + subjectQuery.substring(0, subjectQuery.length() - 5) + ")");
        }

        // we use subject_level_1 field instead of subject_facet field because
        // we are only interested in the top level subjects
        query.setFacet(true);
        query.addFacetField("subject_level_1");
        query.setFacetMinCount(1);
        query.setFacetSort("index");

        setSort(query, params);

        query.setStart(params.getPageNum() * params.getPageSize());
        query.setRows(params.getPageSize());

        try {
            QueryResponse response = this.serverFactory.getServer().query(query);
            SolrDocumentList documentList = response.getResults();
            total = documentList.getNumFound();

            for (SolrDocument document : documentList) {
                SearchHit sh = createArticleBrowseDisplay(document, query.toString());
                articles.add(sh);
            }

            result.setSubjectFacet(facetCountsToHashMap(response.getFacetField("subject_level_1")));
        } catch (SolrServerException e) {
            log.error("Unable to execute a query on the Solr Server.", e);
        }

        result.setTotal(total);
        result.setArticles(articles);

        return result;
    }

    /**
     * Sorting values stored in the config, this parses through those values and
     * sets up the correct parameters for SOLR
     *
     * @param query The SolrQuery which will have a <i>sort</i> clause attached
     * @param params The SearchParameters DTO which contains the <code>sort</code> field used by this method
     */
    private void setSort(SolrQuery query, BrowseParameters params) throws RuntimeException {
        log.debug("SearchParameters.sort = {}", params.getSort());

        String sortKey = params.getSort();

        if (sortKey != null && sortKey.trim().length() > 0) {
            String sortValue = (String) validSorts.get(sortKey);

            if (sortValue == null) {
                throw new RuntimeException("Invalid sort of '" + params.getSort() + "' specified.");
            }

            //First tokenize up defined sorts into tokens on comma: ","
            StringTokenizer sortTokens = new StringTokenizer(sortValue, ",");

            while (sortTokens.hasMoreTokens()) {
                //Now tokenize each sort command on space
                StringTokenizer curSort = new StringTokenizer(sortTokens.nextToken());
                String fieldName = curSort.nextToken(); // First token
                String sortDirection = null;

                if (curSort.hasMoreTokens()) {
                    sortDirection = curSort.nextToken(); // Second token
                }

                if (sortDirection == null || !sortDirection.toLowerCase().equals("asc")) {
                    query.addSortField(fieldName, SolrQuery.ORDER.desc);
                } else {
                    query.addSortField(fieldName, SolrQuery.ORDER.asc);
                }
            }

        } else {
            //If no sort is specified default to publication_date
            query.addSortField("publication_date", SolrQuery.ORDER.desc);
        }
        //If everything else is equal, order by id
        query.addSortField("id", SolrQuery.ORDER.desc);
    }

    /**
     * Returns list of articles in a given date range, from newest to oldest
     * @param params the collection class of parameters.
     * @return the articles
     */
    private BrowseResult getArticlesByDateViaSolr(BrowseParameters params) {
        BrowseResult result = new BrowseResult();
        ArrayList<SearchHit> articles = new ArrayList<SearchHit>();
        long totalSize = 0;

        SolrQuery query = createCommonQuery(params.getJournalKey());

        query.addField("title_display");
        query.addField("author_display");
        query.addField("article_type");
        query.addField("publication_date");
        query.addField("id");
        query.addField("abstract_primary_display");
        query.addField("eissn");

        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        String sDate = sdf.format(params.getStartDate().getTime());
        String eDate = sdf.format(params.getEndDate().getTime());

        sDate = sDate + "T00:00:00Z";
        eDate = eDate + "T00:00:00Z";

        query.addFilterQuery("publication_date:[" + sDate + " TO " + eDate + "]");

        StringBuffer sb = new StringBuffer();
        if (params.getArticleTypes() != null && params.getArticleTypes().size() > 0) {
            for (URI uri : params.getArticleTypes()) {
                String path = uri.getPath();
                int index = path.lastIndexOf("/");
                if (index != -1) {
                    String articleType = path.substring(index + 1);
                    sb.append("\"").append(articleType).append("\"").append(" OR ");
                }
            }
            String articleTypesQuery = sb.substring(0, sb.length() - 4);

            if (articleTypesQuery.length() > 0) {
                query.addFilterQuery("article_type_facet:" + articleTypesQuery);
            }
        }

        setSort(query, params);

        query.setStart(params.getPageNum() * params.getPageSize());
        query.setRows(params.getPageSize());

        log.info("getArticlesByDate Solr Query:" + query.toString());

        try {
            QueryResponse response = this.serverFactory.getServer().query(query);
            SolrDocumentList documentList = response.getResults();
            totalSize = documentList.getNumFound();

            for (SolrDocument document : documentList) {
                SearchHit sh = createArticleBrowseDisplay(document, query.toString());
                articles.add(sh);
            }

        } catch (SolrServerException e) {
            log.error("Unable to execute a query on the Solr Server.", e);
        }

        result.setArticles(articles);
        result.setTotal(totalSize);

        return result;
    }

    /**
     * Creates a commonly used SolrQuery object
     * @return pre-populated SolrQuery object
     */
    private SolrQuery createCommonQuery(String journalKey) {
        SolrQuery query = new SolrQuery("*:*");

        query.addFilterQuery("doc_type:full");
        query.addFilterQuery("!article_type_facet:\"Issue Image\"");
        query.addFilterQuery("cross_published_journal_key:" + journalKey);

        return query;
    }

    /**
     * Populates the SearchHit object using SolrDocument object (from search result)
     * @param document one search result
     * @param query query
     * @return populated SearchHit object
     */
    private SearchHit createArticleBrowseDisplay(SolrDocument document, String query) {

        String id = SolrServiceUtil.getFieldValue(document, "id", String.class, query);
        String message = id == null ? query : id;
        String title = SolrServiceUtil.getFieldValue(document, "title_display", String.class, message);
        Date publicationDate = SolrServiceUtil.getFieldValue(document, "publication_date", Date.class, message);
        String eissn = SolrServiceUtil.getFieldValue(document, "eissn", String.class, message);
        String articleType = SolrServiceUtil.getFieldValue(document, "article_type", String.class, message);
        String abstractDisplay = SolrServiceUtil.getFieldValue(document, "abstract_primary_display", String.class,
                message);

        List<String> authorList = SolrServiceUtil.getFieldMultiValue(document, message, String.class,
                "author_display");

        SearchHit hit = new SearchHit(null, id, title, null, authorList, publicationDate, eissn, null, articleType);
        hit.setAbstractPrimary(abstractDisplay);

        return hit;
    }

    /**
     * Checks to see if solr is up or not
     * @throws org.apache.solr.client.solrj.SolrServerException
     * @throws java.io.IOException
     */
    public void pingSolr() throws SolrServerException, IOException {
        this.serverFactory.getServer().ping();
    }

    //TODO: refactor this somehow, this method (for the most part) is also in SOLRSearchService
    private Map<String, Long> facetCountsToHashMap(FacetField field) {
        List<FacetField.Count> counts = field.getValues();
        TreeMap<String, Long> result = new TreeMap<String, Long>();

        if (counts != null) {
            for (FacetField.Count count : counts) {
                result.put(count.getName(), count.getCount());
            }
            return result;
        } else {
            return null;
        }
    }
}