org.ambraproject.admin.service.impl.AdminServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.ambraproject.admin.service.impl.AdminServiceImpl.java

Source

/*
 * Copyright (c) 2006-2013 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.admin.service.impl;

import org.ambraproject.ApplicationException;
import org.ambraproject.admin.service.AdminService;
import org.ambraproject.admin.service.OnCrossPubListener;
import org.ambraproject.admin.service.OnPublishListener;
import org.ambraproject.models.Category;
import org.ambraproject.queue.MessageSender;
import org.ambraproject.service.article.ArticleClassifier;
import org.ambraproject.service.article.ArticleService;
import org.ambraproject.service.article.FetchArticleService;
import org.ambraproject.service.article.NoSuchArticleIdException;
import org.ambraproject.routes.SavedSearchEmailRoutes;
import org.ambraproject.search.SavedSearchRetriever;
import org.ambraproject.views.TOCArticleGroup;
import org.ambraproject.views.article.ArticleInfo;
import org.ambraproject.views.article.ArticleType;
import org.ambraproject.models.Article;
import org.ambraproject.models.Issue;
import org.ambraproject.models.Journal;
import org.ambraproject.models.Volume;
import org.ambraproject.service.hibernate.HibernateServiceImpl;
import org.apache.camel.CamelExecutionException;
import org.apache.commons.configuration.Configuration;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.hibernate.Criteria;
import org.hibernate.FetchMode;
import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.hibernate.criterion.DetachedCriteria;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Projections;
import org.hibernate.criterion.Restrictions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.dao.support.DataAccessUtils;
import org.springframework.orm.hibernate3.HibernateCallback;
import org.springframework.transaction.annotation.Transactional;
import org.ambraproject.routes.CrossRefLookupRoutes;
import org.w3c.dom.Document;
import java.sql.SQLException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

public class AdminServiceImpl extends HibernateServiceImpl implements AdminService, OnPublishListener {
    private static final Logger log = LoggerFactory.getLogger(AdminServiceImpl.class);

    private MessageSender messageSender;
    private FetchArticleService fetchArticleService;
    private ArticleService articleService;
    private ArticleClassifier articleClassifier;
    private Configuration configuration;
    private List<OnCrossPubListener> onCrossPubListener;

    public void setOnCrossPubListener(List<OnCrossPubListener> onCrossPubListener) {
        this.onCrossPubListener = onCrossPubListener;
    }

    @Required
    public void setConfiguration(Configuration configuration) {
        this.configuration = configuration;
    }

    @Required
    public void setMessageSender(MessageSender messageSender) {
        this.messageSender = messageSender;
    }

    @Required
    public void setFetchArticleService(FetchArticleService fetchArticleService) {
        this.fetchArticleService = fetchArticleService;
    }

    @Required
    public void setArticleClassifier(ArticleClassifier articleClassifier) {
        this.articleClassifier = articleClassifier;
    }

    @Required
    public void setArticleService(ArticleService articleService) {
        this.articleService = articleService;
    }

    @Override
    @Transactional(readOnly = true)
    @SuppressWarnings("unchecked")
    public List<ArticleInfo> getPublishableArticles(String eIssn, String orderField, boolean isOrderAscending)
            throws ApplicationException {

        List<ArticleInfo> articlesInfo = new ArrayList<ArticleInfo>();
        Order order = isOrderAscending ? Order.asc(orderField) : Order.desc(orderField);
        List<Object[]> results = hibernateTemplate.findByCriteria(DetachedCriteria.forClass(Article.class)
                .add(Restrictions.eq("eIssn", eIssn)).add(Restrictions.eq("state", Article.STATE_UNPUBLISHED))
                .addOrder(order).setProjection(Projections.projectionList().add(Projections.property("doi"))
                        .add(Projections.property("date"))));

        for (Object[] rows : results) {
            ArticleInfo articleInfo = new ArticleInfo();
            articleInfo.setDoi(rows[0].toString());
            articleInfo.setDate((Date) rows[1]);
            articlesInfo.add(articleInfo);
        }

        return articlesInfo;
    }

    @Override
    @Transactional
    public void crossPubArticle(final String articleDoi, final String journalKey) throws Exception {
        log.debug("Cross publishing {} in {}", articleDoi, journalKey);
        hibernateTemplate.execute(new HibernateCallback() {
            @Override
            public Object doInHibernate(Session session) throws HibernateException, SQLException {
                Journal journal = (Journal) session.createCriteria(Journal.class)
                        .add(Restrictions.eq("journalKey", journalKey)).uniqueResult();
                Article article = (Article) session.createCriteria(Article.class)
                        .add(Restrictions.eq("doi", articleDoi)).uniqueResult();
                if (!article.getJournals().contains(journal)) {
                    article.getJournals().add(journal);
                }
                session.update(article);
                return null;
            }
        });
        invokeOnCrossPubListeners(articleDoi);
    }

    @Override
    public void articlePublished(String articleId, String authID) throws Exception {
        refreshReferences(articleId, authID);
    }

    @Override
    public void refreshReferences(final String articleDoi, final String authID) {
        log.debug("Sending message to: {}, ({},{})",
                new Object[] { "activemq:plos.updatedCitedArticles?transacted=true", articleDoi, authID });

        String refreshCitedArticlesQueue = configuration.getString("ambra.services.queue.refreshCitedArticles",
                null);
        if (refreshCitedArticlesQueue != null) {
            try {
                messageSender.sendMessage(refreshCitedArticlesQueue, articleDoi, new HashMap() {
                    {
                        put(CrossRefLookupRoutes.HEADER_AUTH_ID, authID);
                    }
                });
            } catch (CamelExecutionException ex) {
                log.error(ex.getMessage(), ex);
                throw new RuntimeException(
                        "Failed to queue job for refreshing article references, is the queue running?");
            }
        } else {
            throw new RuntimeException("Refresh cited articles queue not defined. No route created.");
        }
    }

    /**
     * @inheritDoc
     */
    @Override
    public void sendJournalAlerts(SavedSearchRetriever.AlertType type, Date startTime, Date endTime) {
        log.debug("Sending message to send alerts for type: {}", type);

        String sendSearchAlertsQueue = configuration.getString("ambra.services.queue.sendSearchAlerts", null);
        if (sendSearchAlertsQueue != null) {
            Map<String, Object> headers = new HashMap<String, Object>();
            SimpleDateFormat formatter = new SimpleDateFormat("MM/dd/yyyy");

            //The queue expects dates to be in a specific format
            headers.put(SavedSearchEmailRoutes.HEADER_STARTTIME,
                    (startTime == null ? null : formatter.format(startTime)));
            headers.put(SavedSearchEmailRoutes.HEADER_ENDTIME,
                    (endTime == null ? null : formatter.format(endTime)));

            messageSender.sendMessage(sendSearchAlertsQueue, type.toString(), headers);
        } else {
            throw new RuntimeException("No message sent to send alerts, No route created.");
        }
    }

    @Override
    @Transactional
    public void removeArticleFromJournal(final String articleDoi, final String journalKey) throws Exception {
        hibernateTemplate.execute(new HibernateCallback() {
            @Override
            public Object doInHibernate(Session session) throws HibernateException, SQLException {
                Article article = (Article) session.createCriteria(Article.class)
                        .add(Restrictions.eq("doi", articleDoi)).uniqueResult();
                for (Iterator<Journal> iterator = article.getJournals().iterator(); iterator.hasNext();) {
                    if (journalKey.equals(iterator.next().getJournalKey())) {
                        iterator.remove();
                        break;
                    }
                }
                session.update(article);
                return null;
            }
        });
        invokeOnCrossPubListeners(articleDoi);
    }

    @Override
    @Transactional(readOnly = true)
    @SuppressWarnings("unchecked")
    public List<String> getCrossPubbedArticles(Journal journal) {
        return hibernateTemplate.findByCriteria(DetachedCriteria.forClass(Article.class)
                .add(Restrictions.ne("eIssn", journal.geteIssn())).createAlias("journals", "j")
                .add(Restrictions.eq("j.eIssn", journal.geteIssn())).setProjection(Projections.property("doi")));
    }

    @Transactional
    @Override
    public void setCurrentIssue(String journalKey, String issueUri) {
        Issue issue;
        try {
            issue = (Issue) hibernateTemplate.findByCriteria(DetachedCriteria.forClass(Issue.class)
                    .add(Restrictions.eq("issueUri", issueUri)).setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY))
                    .get(0);
        } catch (IndexOutOfBoundsException e) {
            throw new IllegalArgumentException("Issue '" + issueUri + "' didn't exist");
        }
        Journal journal;
        try {
            journal = (Journal) hibernateTemplate
                    .findByCriteria(
                            DetachedCriteria.forClass(Journal.class).add(Restrictions.eq("journalKey", journalKey)))
                    .get(0);
        } catch (IndexOutOfBoundsException e) {
            throw new IllegalArgumentException("Journal '" + journalKey + "' didn't exist");
        }
        journal.setCurrentIssue(issue);
        hibernateTemplate.update(journal);
    }

    @Override
    @Transactional(readOnly = true)
    public Volume getVolume(String volumeUri) {
        try {
            return (Volume) hibernateTemplate
                    .findByCriteria(
                            DetachedCriteria.forClass(Volume.class).add(Restrictions.eq("volumeUri", volumeUri)))
                    .get(0);
        } catch (IndexOutOfBoundsException e) {
            return null;
        }
    }

    @SuppressWarnings("unchecked")
    @Override
    @Transactional(readOnly = true)
    public List<Volume> getVolumes(final String journalKey) {
        //volumes are lazy so we need to access them in a session
        return (List<Volume>) hibernateTemplate.execute(new HibernateCallback() {
            @Override
            public Object doInHibernate(Session session) throws HibernateException, SQLException {
                Journal journal = (Journal) session.createCriteria(Journal.class)
                        .add(Restrictions.eq("journalKey", journalKey)).uniqueResult();
                if (journal == null) {
                    log.debug("No journal existed for key: " + journalKey);
                    return Collections.emptyList();
                } else {
                    //bring up all the volumes
                    for (int i = 0; i < journal.getVolumes().size(); i++) {
                        journal.getVolumes().get(i);
                    }
                    return journal.getVolumes();
                }
            }
        });
    }

    @Transactional
    @Override
    public Volume createVolume(final String journalKey, final String volumeUri, final String displyName) {
        if (StringUtils.isEmpty(journalKey)) {
            throw new IllegalArgumentException("No journal specified");
        } else if (StringUtils.isEmpty(volumeUri)) {
            throw new IllegalArgumentException("No Volume Uri specified");
        }
        return (Volume) hibernateTemplate.execute(new HibernateCallback() {
            @Override
            public Object doInHibernate(Session session) throws HibernateException, SQLException {
                Journal journal = (Journal) session.createCriteria(Journal.class)
                        .add(Restrictions.eq("journalKey", journalKey)).uniqueResult();
                //if the journal doesn't exist, return null
                if (journal == null) {
                    return null;
                } else {
                    //check if a volume with the same uri exists, and if so, return null
                    for (Volume existingVolume : journal.getVolumes()) {
                        if (existingVolume.getVolumeUri().equals(volumeUri)) {
                            return null;
                        }
                    }
                    Volume newVolume = new Volume();
                    newVolume.setVolumeUri(volumeUri);
                    newVolume.setDisplayName(displyName);
                    journal.getVolumes().add(newVolume);
                    session.update(journal);
                    return newVolume;
                }
            }
        });
    }

    @Transactional
    @Override
    public String[] deleteVolumes(final String journalKey, final String... volumeUris) {
        //volumes are lazy, so we have to access them in a session
        return (String[]) hibernateTemplate.execute(new HibernateCallback() {
            @Override
            public Object doInHibernate(Session session) throws HibernateException, SQLException {
                Journal journal = (Journal) session.createCriteria(Journal.class)
                        .add(Restrictions.eq("journalKey", journalKey)).uniqueResult();
                if (journal == null) {
                    throw new IllegalArgumentException("No such journal: " + journalKey);
                }
                List<String> deletedVolumes = new ArrayList<String>(volumeUris.length);
                Iterator<Volume> iterator = journal.getVolumes().iterator();
                while (iterator.hasNext()) {
                    Volume volume = iterator.next();
                    if (ArrayUtils.indexOf(volumeUris, volume.getVolumeUri()) != -1) {
                        iterator.remove();
                        session.delete(volume);
                        deletedVolumes.add(volume.getVolumeUri());
                    }
                }
                session.update(journal);
                return deletedVolumes.toArray(new String[deletedVolumes.size()]);
            }
        });
    }

    @Override
    @Transactional
    public void updateVolume(final String volumeUri, final String displayName, final String issueCsv)
            throws IllegalArgumentException {
        hibernateTemplate.execute(new HibernateCallback() {
            @Override
            public Object doInHibernate(Session session) throws HibernateException, SQLException {
                Volume volume = (Volume) session.createCriteria(Volume.class)
                        .add(Restrictions.eq("volumeUri", volumeUri)).setFetchMode("issues", FetchMode.JOIN)
                        .setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY).uniqueResult();

                if (volume != null) {
                    //check that we've only reordered the issue csv, not added or deleted
                    String existingIssues = AdminServiceImpl.this.formatIssueCsv(volume.getIssues());
                    for (String oldIssue : existingIssues.split(",")) {
                        if (!issueCsv.contains(oldIssue)) {
                            throw new IllegalArgumentException(
                                    "Removed issue '" + oldIssue + "' from csv when updating volume");
                        }
                    }
                    for (String newIssue : issueCsv.split(",")) {
                        if (!existingIssues.contains(newIssue)) {
                            throw new IllegalArgumentException(
                                    "Added issue '" + newIssue + "' to csv when updating volume");
                        }
                    }

                    volume.getIssues().clear();
                    for (String issueUri : issueCsv.split(",")) {
                        Issue issue = (Issue) session.createCriteria(Issue.class)
                                .add(Restrictions.eq("issueUri", issueUri))
                                .setFetchMode("articleDois", FetchMode.SELECT)
                                .setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY).uniqueResult();
                        if (issue != null) {
                            volume.getIssues().add(issue);
                        }
                    }
                    volume.setDisplayName(displayName);
                    session.update(volume);
                }
                return null;
            }
        });
    }

    @Override
    @Transactional
    public void deleteIssue(final String issueUri) {
        log.debug("Deleting issue '{}'", issueUri);
        //using hibernateTemplate.execute() instead of hibernateTemplate.findByCriteria() here
        //because for some reason adding the issue restriction causes issues to be lazy-loaded,
        //even with fetchMode = JOIN
        hibernateTemplate.execute(new HibernateCallback<Void>() {
            @Override
            public Void doInHibernate(Session session) throws HibernateException, SQLException {
                Volume volume = (Volume) DataAccessUtils
                        .uniqueResult(session.createCriteria(Volume.class).setFetchMode("issues", FetchMode.JOIN)
                                .createAlias("issues", "i").add(Restrictions.eq("i.issueUri", issueUri))
                                .setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY).list());
                if (volume != null) {
                    Iterator<Issue> iterator = volume.getIssues().iterator();
                    while (iterator.hasNext()) {
                        Issue issue = iterator.next();
                        if (issue.getIssueUri().equals(issueUri)) {
                            iterator.remove();
                            session.delete(issue);
                            break;
                        }
                    }
                    session.update(volume);
                }
                return null;
            }
        });
    }

    @Override
    @Transactional(readOnly = true)
    public Issue getIssue(String issueUri) {
        log.debug("Retrieving issue with uri '{}'", issueUri);
        try {
            return (Issue) hibernateTemplate.findByCriteria(DetachedCriteria.forClass(Issue.class)
                    .add(Restrictions.eq("issueUri", issueUri)).setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY))
                    .get(0);
        } catch (IndexOutOfBoundsException e) {
            return null;
        }
    }

    @Override
    @Transactional(readOnly = true)
    public List<Issue> getIssues(final String volumeUri) {
        log.debug("Retrieving issues for '{}'", volumeUri);
        try {
            return ((Volume) hibernateTemplate.findByCriteria(DetachedCriteria.forClass(Volume.class)
                    .add(Restrictions.eq("volumeUri", volumeUri)).setFetchMode("issues", FetchMode.JOIN)
                    .setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY)).get(0)).getIssues();
        } catch (IndexOutOfBoundsException e) {
            return Collections.emptyList();
        }
    }

    @Override
    public String formatIssueCsv(final List<Issue> issues) {
        List<String> issueUris = new ArrayList<String>(issues.size());
        for (Issue issue : issues) {
            issueUris.add(issue.getIssueUri());
        }
        return StringUtils.join(issueUris, ",");
    }

    @Override
    @Transactional
    public void addIssueToVolume(final String volumeUri, final Issue issue) {
        if (StringUtils.isEmpty(issue.getIssueUri())) {
            throw new IllegalArgumentException("Must specify an Issue URI");
        }
        log.debug("Creating an issue with uri: '{}' and adding it to volume: '{}'", issue.getIssueUri(), volumeUri);
        if (((Number) hibernateTemplate.findByCriteria(DetachedCriteria.forClass(Issue.class)
                .add(Restrictions.eq("issueUri", issue.getIssueUri())).setProjection(Projections.rowCount()))
                .get(0)).intValue() > 0) {
            throw new IllegalArgumentException("An issue with uri '" + issue.getIssueUri() + "' already exists");
        }
        Volume storedVolume;
        try {
            storedVolume = (Volume) hibernateTemplate.findByCriteria(DetachedCriteria.forClass(Volume.class)
                    .add(Restrictions.eq("volumeUri", volumeUri)).setFetchMode("issues", FetchMode.JOIN)
                    .setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY)).get(0);
        } catch (IndexOutOfBoundsException e) {
            throw new IllegalArgumentException("Volume '" + volumeUri + "' doesn't exist");
        }
        if (StringUtils.isEmpty(issue.getTitle()) || StringUtils.isEmpty(issue.getDescription())) {
            try {
                Object[] titleAndDescription = (Object[]) hibernateTemplate.findByCriteria(
                        DetachedCriteria.forClass(Article.class).add(Restrictions.eq("doi", issue.getImageUri()))
                                .setProjection(Projections.projectionList().add(Projections.property("title"))
                                        .add(Projections.property("description"))))
                        .get(0);
                issue.setTitle(
                        StringUtils.isEmpty(issue.getTitle()) ? (String) titleAndDescription[0] : issue.getTitle());
                issue.setDescription(StringUtils.isEmpty(issue.getDescription()) ? (String) titleAndDescription[1]
                        : issue.getDescription());
            } catch (IndexOutOfBoundsException e) {
                //it's fine if the image article doesn't exist
            }
        }
        storedVolume.getIssues().add(issue);
        hibernateTemplate.update(storedVolume);
    }

    @Override
    @Transactional
    public void updateIssue(String issueUri, String imageUri, String displayName, boolean respectOrder,
            List<String> articleDois) {
        log.debug("Updating issue '{}'", issueUri);
        Issue issue;
        try {
            issue = (Issue) hibernateTemplate.findByCriteria(DetachedCriteria.forClass(Issue.class)
                    .add(Restrictions.eq("issueUri", issueUri)).setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY))
                    .get(0);
        } catch (IndexOutOfBoundsException e) {
            //if the issue doesn't exist, just return
            return;
        }
        //check that we aren't adding or removing an article here
        for (String oldDoi : issue.getArticleDois()) {
            if (!articleDois.contains(oldDoi)) {
                throw new IllegalArgumentException("Removed article '" + oldDoi + "' when updating issue");
            }
        }
        for (String newDoi : articleDois) {
            if (!issue.getArticleDois().contains(newDoi)) {
                throw new IllegalArgumentException("Added article '" + newDoi + "' when updating issue");
            }
        }

        issue.getArticleDois().clear();
        issue.getArticleDois().addAll(articleDois);
        issue.setDisplayName(displayName);
        issue.setRespectOrder(respectOrder);
        issue.setImageUri(imageUri);
        //pull down title and description from the image article, if it exists
        try {
            Object[] titleAndDescription = (Object[]) hibernateTemplate
                    .findByCriteria(DetachedCriteria.forClass(Article.class).add(Restrictions.eq("doi", imageUri))
                            .setProjection(Projections.projectionList().add(Projections.property("title"))
                                    .add(Projections.property("description"))))
                    .get(0);
            issue.setTitle((String) titleAndDescription[0]);
            issue.setDescription((String) titleAndDescription[1]);
        } catch (IndexOutOfBoundsException e) {
            //it's ok if image article doesn't exist
        }
        hibernateTemplate.update(issue);
    }

    @Override
    @Transactional
    public void removeArticlesFromIssue(String issueUri, String... articleDois) {
        log.debug("Removing articles {} to issue '{}'", Arrays.toString(articleDois), issueUri);
        Issue issue;
        try {
            issue = (Issue) hibernateTemplate.findByCriteria(DetachedCriteria.forClass(Issue.class)
                    .add(Restrictions.eq("issueUri", issueUri)).setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY))
                    .get(0);
        } catch (IndexOutOfBoundsException e) {
            //it's ok if the issue doesn't exist
            return;
        }
        for (String doi : articleDois) {
            issue.getArticleDois().remove(doi);
        }
        hibernateTemplate.update(issue);
    }

    @Override
    @Transactional
    public void addArticlesToIssue(String issueUri, String... articleDois) {
        log.debug("Adding articles {} to issue '{}'", Arrays.toString(articleDois), issueUri);
        Issue issue;
        try {
            issue = (Issue) hibernateTemplate.findByCriteria(DetachedCriteria.forClass(Issue.class)
                    .add(Restrictions.eq("issueUri", issueUri)).setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY))
                    .get(0);
        } catch (IndexOutOfBoundsException e) {
            //it's ok if the issue doesn't exist
            return;
        }
        for (String doi : articleDois) {
            if (!issue.getArticleDois().contains(doi)) {
                issue.getArticleDois().add(doi);
            }
        }
        hibernateTemplate.update(issue);
    }

    @Override
    public String formatArticleCsv(List<TOCArticleGroup> issueArticleGroups) {
        if (issueArticleGroups.isEmpty()) {
            return "";
        }
        String csv = "";
        for (TOCArticleGroup group : issueArticleGroups) {
            for (ArticleInfo article : group.getArticles()) {
                csv += article.getDoi() + ",";
            }
        }
        return csv.substring(0, csv.lastIndexOf(","));
    }

    @Override
    @Transactional(readOnly = true)
    @SuppressWarnings("unchecked")
    public List<TOCArticleGroup> getArticleGroupList(final Issue issue) {
        //if the issue doesn't have any dois, return an empty list of groups
        if (issue.getArticleDois() == null || issue.getArticleDois().isEmpty()) {
            return Collections.emptyList();
        }

        //Create a comparator to sort articles in groups depending on if the issue has manual ordering enabled
        Comparator<ArticleInfo> comparator;
        if (issue.isRespectOrder()) {
            comparator = new Comparator<ArticleInfo>() {
                @Override
                public int compare(ArticleInfo left, ArticleInfo right) {
                    Integer leftIndex = issue.getArticleDois().indexOf(left.getDoi());
                    Integer rightIndex = issue.getArticleDois().indexOf(right.getDoi());
                    return leftIndex.compareTo(rightIndex);
                }
            };
        } else {
            comparator = new Comparator<ArticleInfo>() {
                @Override
                public int compare(ArticleInfo left, ArticleInfo right) {
                    if (left.getDate().after(right.getDate())) {
                        return -1;
                    }
                    if (left.getDate().before(right.getDate())) {
                        return 1;
                    }
                    return left.getDoi().compareTo(right.getDoi());
                }
            };
        }

        //keep track of dois in a separate list so we can remove them as we find articles and then keep track of the orphans at the end
        List<String> dois = new ArrayList<String>(issue.getArticleDois());

        log.debug("Loading up article groups for issue '{}'", issue.getIssueUri());
        List<TOCArticleGroup> groups = new ArrayList<TOCArticleGroup>(
                ArticleType.getOrderedListForDisplay().size());

        List<Object[]> rows = hibernateTemplate.findByNamedParam(
                "select a.doi, a.title, a.date, t from Article a inner join a.types t where a.doi in :dois",
                new String[] { "dois" }, new Object[] { issue.getArticleDois() });

        //results will be row of [doi, title, date, type] with an entry for each type of each article
        //i.e. we'll see duplicate results for articles

        for (ArticleType type : ArticleType.getOrderedListForDisplay()) {
            TOCArticleGroup group = new TOCArticleGroup(type);
            //using an explicit iterator so we can remove rows as we find matches
            Iterator<Object[]> iterator = rows.iterator();
            while (iterator.hasNext()) {
                Object[] row = iterator.next();
                //check if this row is of the correct type, and that we haven't added the article
                if (type.getUri().toString().equals(row[3]) && dois.contains(row[0])) {
                    ArticleInfo articleInfo = new ArticleInfo();
                    articleInfo.setDoi((String) row[0]);
                    articleInfo.setTitle((String) row[1]);
                    articleInfo.setDate((Date) row[2]);
                    group.addArticle(articleInfo);

                    //remove the row so we don't have to check it again later
                    iterator.remove();
                    //remove the doi so we can keep track of orphans
                    dois.remove(articleInfo.getDoi());
                }
            }
            Collections.sort(group.getArticles(), comparator);
            //only add a group if there are articles for it
            if (group.getCount() > 0) {
                groups.add(group);
                log.debug("Found {} articles of type '{}' for issue '{}",
                        new Object[] { group.getCount(), type.getHeading(), issue.getIssueUri() });
            }
        }

        //create a group for orphaned articles
        TOCArticleGroup orphans = new TOCArticleGroup(null);
        orphans.setHeading("Orphaned Article");
        orphans.setPluralHeading("Orphaned Articles");

        //anything left in the doi list is an orphan
        for (String doi : dois) {
            ArticleInfo article = new ArticleInfo();
            article.setDoi(doi);
            article.setDate(Calendar.getInstance().getTime());
            orphans.addArticle(article);
        }
        Collections.sort(orphans.getArticles(), comparator);

        groups.add(orphans);
        return groups;
    }

    @Override
    @Transactional(readOnly = true)
    public Journal getJournal(String journalKey) {
        try {
            return (Journal) hibernateTemplate
                    .findByCriteria(
                            DetachedCriteria.forClass(Journal.class).add(Restrictions.eq("journalKey", journalKey)))
                    .get(0);
        } catch (IndexOutOfBoundsException e) {
            return null;
        }
    }

    @Override
    @Transactional
    public List<Category> refreshSubjectCategories(String articleDoi, String authID)
            throws NoSuchArticleIdException {
        // Attempt to assign categories to the article based on the taxonomy server.

        Document articleXml = fetchArticleService.getArticleDocument(new ArticleInfo(articleDoi));
        List<String> terms = null;

        try {
            terms = articleClassifier.classifyArticle(articleXml);
        } catch (Exception e) {
            log.warn("Taxonomy server not responding, but ingesting article anyway", e);
        }

        if (terms != null && terms.size() > 0) {
            Article article = articleService.getArticle(articleDoi, authID);
            return articleService.setArticleCategories(article, terms);
        }

        return Collections.emptyList();
    }

    private void invokeOnCrossPubListeners(String articleDoi) throws Exception {
        if (onCrossPubListener != null) {
            for (OnCrossPubListener listener : onCrossPubListener) {
                listener.articleCrossPublished(articleDoi);
            }
        }
    }
}