/*
* Copyright (c) 2006-2014 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.service.article;
import org.ambraproject.ApplicationException;
import org.ambraproject.views.CitedArticleView;
import org.ambraproject.views.SearchHit;
import org.ambraproject.views.TOCArticle;
import org.ambraproject.views.TOCRelatedArticle;
import org.ambraproject.views.UserProfileInfo;
import org.ambraproject.views.article.ArticleInfo;
import org.ambraproject.views.article.ArticleType;
import org.ambraproject.views.article.CitationInfo;
import org.ambraproject.views.article.RelatedArticleInfo;
import org.ambraproject.models.Article;
import org.ambraproject.models.ArticleAsset;
import org.ambraproject.models.ArticleAuthor;
import org.ambraproject.models.ArticleRelationship;
import org.ambraproject.models.Category;
import org.ambraproject.models.CitedArticle;
import org.ambraproject.models.Issue;
import org.ambraproject.models.Journal;
import org.ambraproject.models.UserProfile;
import org.ambraproject.models.UserRole.Permission;
import org.ambraproject.models.Volume;
import org.ambraproject.service.hibernate.HibernateServiceImpl;
import org.ambraproject.service.permission.PermissionsService;
import org.ambraproject.views.ArticleCategory;
import org.ambraproject.views.AssetView;
import org.ambraproject.views.JournalView;
import org.ambraproject.views.article.BaseArticleInfo;
import org.hibernate.Criteria;
import org.hibernate.HibernateException;
import org.hibernate.Query;
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.orm.hibernate3.HibernateAccessor;
import org.springframework.orm.hibernate3.HibernateCallback;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigInteger;
import java.net.URI;
import java.sql.SQLException;
import java.text.ParseException;
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.GregorianCalendar;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
/**
* @author Joe Osowski
*/
public class ArticleServiceImpl extends HibernateServiceImpl implements ArticleService {
private static final Logger log = LoggerFactory.getLogger(ArticleServiceImpl.class);
private PermissionsService permissionsService;
@Override
public boolean containsResearchType(final Set<String> types) throws ApplicationException {
ArticleType articleType = ArticleType.getDefaultArticleType();
if(types != null) {
for (String artTypeUri : types) {
if (ArticleType.getKnownArticleTypeForURI(URI.create(artTypeUri)) != null) {
articleType = ArticleType.getKnownArticleTypeForURI(URI.create(artTypeUri));
if(articleType != null && ArticleType.isResearchArticle(articleType)) {
return true;
}
}
}
}
if (articleType == null) {
throw new ApplicationException("Unable to resolve article type");
}
return false;
}
@Override
public boolean isResearchArticle(final Article article)
throws ApplicationException, NoSuchArticleIdException {
return containsResearchType(article.getTypes());
}
/**
* Determines if the articleURI is of type researchArticle
*
* @param articleInfo The articleInfo Object
* @return True if the article is a research article
* @throws org.ambraproject.ApplicationException
* if there was a problem talking to the OTM
* @throws NoSuchArticleIdException When the article does not exist
*/
public boolean isResearchArticle(final ArticleInfo articleInfo)
throws ApplicationException, NoSuchArticleIdException {
return containsResearchType(articleInfo.getTypes());
}
public boolean containsRetractionType(final Set<String> types) throws ApplicationException {
ArticleType articleType = ArticleType.getDefaultArticleType();
if(types != null) {
for (String artTypeUri : types) {
if (ArticleType.getKnownArticleTypeForURI(URI.create(artTypeUri)) != null) {
articleType = ArticleType.getKnownArticleTypeForURI(URI.create(artTypeUri));
if(articleType != null && ArticleType.isRetractionArticle(articleType)) {
return true;
}
}
}
}
if (articleType == null) {
throw new ApplicationException("Unable to resolve article type");
}
return false;
}
/**
*
* @param articleInfo The ArticleInfo object
* @return
* @throws ApplicationException
* @throws NoSuchArticleIdException
*/
public boolean isRetractionArticle(final BaseArticleInfo articleInfo)
throws ApplicationException, NoSuchArticleIdException {
return containsRetractionType(articleInfo.getTypes());
}
public boolean containsEocType(final Set<String> types) throws ApplicationException {
ArticleType articleType = ArticleType.getDefaultArticleType();
if(types != null) {
for (String artTypeUri : types) {
if (ArticleType.getKnownArticleTypeForURI(URI.create(artTypeUri)) != null) {
articleType = ArticleType.getKnownArticleTypeForURI(URI.create(artTypeUri));
if(articleType != null && ArticleType.isEocArticle(articleType)) {
return true;
}
}
}
}
if (articleType == null) {
throw new ApplicationException("Unable to resolve article type");
}
return false;
}
/**
* Determines if the articleURI is of type Expression of Concern
*
* @param articleInfo The articleInfo Object
* @return True if the article is a Expression of Concern article
* @throws org.ambraproject.ApplicationException
* @throws NoSuchArticleIdException When the article does not exist
*/
public boolean isEocArticle(final BaseArticleInfo articleInfo)
throws ApplicationException, NoSuchArticleIdException {
return containsEocType(articleInfo.getTypes());
}
public boolean containsCorrectionType(final Set<String> types) throws ApplicationException {
ArticleType articleType = ArticleType.getDefaultArticleType();
if(types != null) {
for (String artTypeUri : types) {
if (ArticleType.getKnownArticleTypeForURI(URI.create(artTypeUri)) != null) {
articleType = ArticleType.getKnownArticleTypeForURI(URI.create(artTypeUri));
if(articleType != null && ArticleType.isCorrectionArticle(articleType)) {
return true;
}
}
}
}
if (articleType == null) {
throw new ApplicationException("Unable to resolve article type");
}
return false;
}
public boolean isCorrectionArticle(final BaseArticleInfo articleInfo)
throws ApplicationException, NoSuchArticleIdException {
return containsCorrectionType(articleInfo.getTypes());
}
public boolean isAmendment(Article article) throws ApplicationException, NoSuchArticleIdException {
return containsCorrectionType(article.getTypes()) ||
containsEocType(article.getTypes()) ||
containsRetractionType(article.getTypes());
}
/**
* Change an articles state.
*
* @param articleDoi uri
* @param authId the authorization ID of the current user
* @param state state
*
* @throws NoSuchArticleIdException NoSuchArticleIdException
*/
@Transactional(rollbackFor = {Throwable.class})
public void setState(final String articleDoi, final String authId, final int state) throws NoSuchArticleIdException {
permissionsService.checkPermission(Permission.INGEST_ARTICLE, authId);
List articles = hibernateTemplate.findByCriteria(DetachedCriteria.forClass(Article.class)
.add(Restrictions.eq("doi", articleDoi)));
if (articles.size() == 0) {
throw new NoSuchArticleIdException(articleDoi);
}
Article a = (Article)articles.get(0);
a.setState(state);
//Remove relationships if this article is being disabled
//they will be created on re-ingest if necessary, but if both articles in a reciprocal relationship are
//disabled and have the relationships removed from xml, we want to the relationships to be gone when both are reingested
if (state == Article.STATE_DISABLED) {
a.getRelatedArticles().clear();
}
hibernateTemplate.update(a);
//log whenever someone disables or unpublishes an article
if ((state == Article.STATE_UNPUBLISHED ||
state == Article.STATE_DISABLED)
&& log.isInfoEnabled()) {
DetachedCriteria criteria = DetachedCriteria.forClass(UserProfile.class)
.setProjection(Projections.property("displayName"))
.add(Restrictions.eq("authId",authId));
String userName = (String) hibernateTemplate.findByCriteria(criteria, 0, 1).get(0);
userName = userName == null ? "UNKNOWN" : userName;
log.info("User '{}' {} the article {}",
new String[] {userName, state == Article.STATE_DISABLED ?
"disabled" : "unpublished", articleDoi});
}
}
/**
* Get the ids of all articles satisfying the given criteria.
* <p/>
* This method calls <code>getArticles(...)</code> then parses the Article IDs from that List.
* <p/>
*
* @param params
* @return the (possibly empty) list of article ids.
* @throws java.text.ParseException if any of the dates could not be parsed
*/
@Transactional(readOnly = true)
@SuppressWarnings("unchecked")
public List<String> getArticleDOIs(final ArticleServiceSearchParameters params) throws ParseException {
List<Article> articles = getArticles(params);
List<String> articleIds = new ArrayList<String>(articles.size());
for (Article article : articles) {
articleIds.add(article.getDoi());
}
return articleIds;
}
/**
* @inheritDoc
*/
@Transactional (readOnly = true)
public List<SearchHit> getRandomRecentArticles(String journal_eIssn, List<URI>articleTypesToShow,
int numDaysInPast, int articleCount)
{
//end date is most recent midnight
Calendar endDate = GregorianCalendar.getInstance();
endDate.set(Calendar.HOUR_OF_DAY, 23);
endDate.set(Calendar.MINUTE, 59);
endDate.set(Calendar.SECOND, 59);
Calendar startDate = (Calendar)endDate.clone();
//This may look a bit off, but we want a larger value here without making it too large.
//The initial query should return results outside of the defined window. But not too large a result set
//We may need to tune this a bit.
int numDaysTemp = -(numDaysInPast * 2) - 30;
startDate.add(Calendar.DAY_OF_YEAR, numDaysTemp);
//Get 30 days worth of articles first
List<SearchHit> recentArticles = getArticles(startDate, endDate, articleTypesToShow, journal_eIssn);
List<SearchHit> results = new ArrayList<SearchHit>();
startDate = (Calendar)endDate.clone();
startDate.add(Calendar.DAY_OF_YEAR, -numDaysInPast);
//First grab all the articles that fall into the defined window
//Regardless of articleCount. We want to randomize before we limit
//So each time the method returns, it returns a different list
for(SearchHit searchHit : recentArticles) {
if(searchHit.getDate().after(startDate.getTime())) {
results.add(searchHit);
}
}
//We assume the list is sorted by date desc (Most recent first)
//So we can reduce the list by the count of the results so far for a minor
//performance boost
recentArticles = recentArticles.subList(results.size(), recentArticles.size());
//If we still don't have enough, decrement the start date and try again
//But let's not go on forever, only back 30 days. (in this case 10 loops, each iteration is 3 days)
int loop = 0;
while(results.size() < articleCount && loop < 10) {
startDate.add(Calendar.DAY_OF_YEAR, -3);
for(SearchHit searchHit : recentArticles) {
if(searchHit.getDate().after(startDate.getTime())) {
results.add(searchHit);
}
}
loop++;
}
//Shuffle results
Collections.shuffle(results);
//pare down the actual number of recent articles to match articleCount
if (results.size() > articleCount) {
results = results.subList(0, articleCount);
}
return results;
}
@SuppressWarnings("unchecked")
private List<SearchHit> getArticles(final Calendar startDate, final Calendar endDate,
final List<URI> articleTypesToShow,
final String journal_eIssn)
{
// if articleTypesToShow is empty, then all types of articles should be returned
return (List<SearchHit>) hibernateTemplate.execute(new HibernateCallback() {
public Object doInHibernate(Session session) throws HibernateException, SQLException {
//Expected SQL Query:
//select distinct a.doi, a.title, a.date from article a join articleTypes at on a.articleID = at.articleID
//where a.eIssn = '%journal_eIssn%' and a.date between '%date%' and '%date%'
//at.type in ('%article type%', '%article type%')
String[] types = new String[articleTypesToShow.size()];
for(int a = 0; a < articleTypesToShow.size(); a++) {
types[a] = articleTypesToShow.get(a).toString();
}
String sql = "select distinct a.doi, a.title, a.date " +
"from article a join articleType atype on a.articleID = atype.articleID " +
"where a.eIssn = :eIssn " +
"and a.date between :startDate and :endDate ";
if (articleTypesToShow.size() > 0) {
sql = sql + "and atype.type in :types ";
}
sql = sql + "order by a.date desc";
Query query = session.createSQLQuery(sql)
.setString("eIssn", journal_eIssn)
.setCalendar("startDate", startDate)
.setCalendar("endDate", endDate);
if (articleTypesToShow.size() > 0) {
query.setParameterList("types", types);
}
List<Object[]> articleResults = query.list();
List<SearchHit> searchResults = new ArrayList<SearchHit>();
for (int i = 0; i < articleResults.size(); i++) {
Object[] res = articleResults.get(i);
String doi = (String) res[0];
String title = (String) res[1];
Date pubDate = (Date) res[2];
searchResults.add(SearchHit.builder()
.setUri(doi)
.setTitle(title)
.setDate(pubDate)
.build());
}
return searchResults;
}
});
}
/**
* Get all of the articles satisfying the given criteria.
* @param params
*
* @return all of the articles satisfying the given criteria (possibly null) Key is the Article DOI. Value is the
* Article itself.
*/
@Transactional(readOnly = true)
@SuppressWarnings("unchecked")
public List<Article> getArticles(final ArticleServiceSearchParameters params) {
return (List<Article>) this.hibernateTemplate.execute(new HibernateCallback() {
public Object doInHibernate(Session session) throws HibernateException, SQLException {
Criteria query = session.createCriteria(Article.class);
if (params.getStates() != null && params.getStates().length > 0) {
List<Integer> statesList = new ArrayList<Integer>(params.getStates().length);
for (int state : params.getStates()) {
statesList.add(state);
}
query.add(Restrictions.in("state", statesList));
}
if (params.geteIssn() != null) {
query.add(Restrictions.eq("eIssn", params.geteIssn()));
}
if (params.getOrderField() != null) {
if (params.isOrderAscending()) {
query.addOrder(Order.asc(params.getOrderField()));
} else {
query.addOrder(Order.desc(params.getOrderField()));
}
}
if (params.getStartDate() != null) {
query.add(Restrictions.ge("date", params.getStartDate()));
}
if (params.getEndDate() != null) {
query.add(Restrictions.le("date", params.getEndDate()));
}
if (params.getMaxResults() > 0) {
query.setMaxResults(params.getMaxResults());
}
//Filter the results post-db access since the kind of restriction we want isn't easily representable in sql -
//we want only articles such that the list of full names of the author list contains ALL the names in the given
//author array. This is NOT an 'in' restriction or an 'equals' restriction
List<Article> queryResults = query.list();
List<Article> filteredResults = new ArrayList<Article>(queryResults.size());
for (Article article : queryResults) {
if (matchesAuthorFilter(params.getAuthors(), article.getAuthors())
&& matchesCategoriesFilter(params.getCategories(), article.getCategories().keySet())) {
filteredResults.add(article);
}
}
return filteredResults;
}
});
}
/**
* Get an Article by ID.
*
* @param articleID ID of Article to get.
* @param authId the authorization ID of the current user
* @return Article with specified URI or null if not found.
* @throws NoSuchArticleIdException NoSuchArticleIdException
*/
@Transactional(readOnly = true, noRollbackFor = {SecurityException.class})
public Article getArticle(final Long articleID, final String authId) throws NoSuchArticleIdException
{
// sanity check parms
if (articleID == null)
throw new IllegalArgumentException("articleID == null");
Article article = (Article)hibernateTemplate.load(Article.class, articleID);
if (article == null) {
throw new NoSuchArticleIdException(String.valueOf(articleID));
}
checkArticleState(article, authId);
return article;
}
/**
* Get an Article by URI.
*
* @param articleDoi URI of Article to get.
* @param authId the authorization ID of the current user
* @return Article with specified URI or null if not found.
* @throws NoSuchArticleIdException NoSuchArticleIdException
*/
@Override
@Transactional(readOnly = true, noRollbackFor = {SecurityException.class})
@SuppressWarnings("unchecked")
public Article getArticle(final String articleDoi, final String authId) throws NoSuchArticleIdException {
// sanity check parms
if (articleDoi == null)
throw new IllegalArgumentException("articleDoi == null");
List<Article> articles = (List<Article>) hibernateTemplate.findByCriteria(
DetachedCriteria.forClass(Article.class)
.add(Restrictions.eq("doi", articleDoi)));
if (articles.size() == 0) {
throw new NoSuchArticleIdException(articleDoi);
}
checkArticleState(articles.get(0), authId);
return articles.get(0);
}
private void checkArticleState(Article article, String authId) throws NoSuchArticleIdException {
//If the article is unpublished, it should not be returned if the user is not an admin
if (article.getState() == Article.STATE_UNPUBLISHED) {
try {
permissionsService.checkPermission(Permission.VIEW_UNPUBBED_ARTICLES, authId);
} catch(SecurityException se) {
throw new NoSuchArticleIdException(article.getDoi());
}
}
//If the article is disabled, don't display it ever
if (article.getState() == Article.STATE_DISABLED) {
throw new NoSuchArticleIdException(article.getDoi());
}
}
/**
* {@inheritDoc}
*/
@Transactional(readOnly = true)
public void checkArticleState(final String articleDoi, final String authId) throws NoSuchArticleIdException {
//If the article is unpublished, it should not be returned if the user is not an admin
List<Integer> results = (List<Integer>) hibernateTemplate.findByCriteria(DetachedCriteria.forClass(Article.class)
.add(Restrictions.eq("doi", articleDoi))
.setProjection(Projections.projectionList()
.add(Projections.property("state")))
,0,1);
if (results.size() == 0) {
throw new NoSuchArticleIdException(articleDoi);
}
Integer articleState = results.get(0);
if (articleState == Article.STATE_UNPUBLISHED) {
try {
permissionsService.checkPermission(Permission.VIEW_UNPUBBED_ARTICLES, authId);
} catch(SecurityException se) {
throw new NoSuchArticleIdException(articleDoi);
}
}
//If the article is disabled, don't display it ever
if (articleState == Article.STATE_DISABLED) {
throw new NoSuchArticleIdException(articleDoi);
}
}
/**
* Get articles based on a list of Article id's.
*
* If an article is requested that the user does not have access to, it will not be returned
*
* @param articleDois list of article doi's
* @param authId the authorization ID of the current user
* @return <code>List<Article></code> of articles requested
* @throws NoSuchArticleIdException NoSuchArticleIdException
*/
@Transactional(readOnly = true)
@SuppressWarnings("unchecked")
public List<Article> getArticles(List<String> articleDois, final String authId) {
// sanity check parms
if (articleDois == null)
throw new IllegalArgumentException("articleDois == null");
List<Article> articles = new ArrayList<Article>();
if(!articleDois.isEmpty()) {
articles = (List<Article>) hibernateTemplate.findByCriteria(
DetachedCriteria.forClass(Article.class)
.add(Restrictions.in("doi", articleDois)));
}
for(int a = 0; a < articles.size(); a++) {
try {
checkArticleState(articles.get(a), authId);
} catch(NoSuchArticleIdException ex) {
articles.remove(a);
}
}
//Make sure the list of returned articles is in the same order as the requesting list.
List<Article> articlesSorted = new ArrayList<Article>();
for(String doi : articleDois) {
for(Article article : articles) {
if(article.getDoi().equals(doi)) {
articlesSorted.add(article);
}
}
}
return articlesSorted;
}
/**
* Get a List of all of the Journal/Volume/Issue combinations that contain the <code>articleURI</code> which was
* passed in. Each primary List element contains a secondary List of six Strings which are, in order: <ul>
* <li><strong>Element 0: </strong> Journal URI</li> <li><strong>Element 1: </strong> Journal key</li>
* <li><strong>Element 2: </strong> Volume URI</li> <li><strong>Element 3: </strong> Volume name</li>
* <li><strong>Element 4: </strong> Issue URI</li> <li><strong>Element 5: </strong> Issue name</li> </ul> A Journal
* might have multiple Volumes, any of which might have multiple Issues that contain the <code>articleURI</code>. The
* primary List will always contain one element for each Issue that contains the <code>articleURI</code>.
*
* @param articleDoi Article DOI that is contained in the Journal/Volume/Issue combinations which will be returned
* @return All of the Journal/Volume/Issue combinations which contain the articleURI passed in
*/
@Transactional(readOnly = true)
@SuppressWarnings("unchecked")
public List<List<String>> getArticleIssues(final String articleDoi) {
return (List<List<String>>) hibernateTemplate.execute(new HibernateCallback() {
public Object doInHibernate(Session session) throws HibernateException, SQLException {
List<Object[]> articleIssues = session.createSQLQuery(
"select {j.*}, {v.*}, {i.*} " +
"from issueArticleList ial " +
"join issue i on ial.issueID = i.issueID " +
"join volume v on i.volumeID = v.volumeID " +
"join journal j on v.journalID = j.journalID " +
"where ial.doi = :articleURI " +
"order by i.created desc ")
.addEntity("j", Journal.class)
.addEntity("v", Volume.class)
.addEntity("i", Issue.class)
.setString("articleURI", articleDoi)
.list();
List<List<String>> finalResults = new ArrayList<List<String>>(articleIssues.size());
for (Object[] row : articleIssues) {
Journal journal = (Journal) row[0];
Volume volume = (Volume) row[1];
Issue issue = (Issue) row[2];
List<String> secondaryList = new ArrayList<String>();
secondaryList.add(journal.getID().toString()); // Journal ID
secondaryList.add(journal.getJournalKey()); // Journal Key
secondaryList.add(volume.getVolumeUri()); // Volume URI
secondaryList.add(volume.getDisplayName()); // Volume name
secondaryList.add(issue.getIssueUri()); // Issue URI
secondaryList.add(issue.getDisplayName()); // Issue name
finalResults.add(secondaryList);
}
return finalResults;
}
});
}
/**
* Get the articleInfo object for an article
* @param articleDoi the ID of the article
* @param authId the authorization ID of the current user
* @return articleInfo
*/
@Transactional(readOnly = true)
@Override
@SuppressWarnings("unchecked")
public ArticleInfo getArticleInfo(final String articleDoi, final String authId) throws NoSuchArticleIdException {
final Article article;
article = getArticle(articleDoi, authId);
return createArticleInfo(article, authId);
}
/**
* {@inheritDoc}
*/
@Transactional(readOnly = true)
@Override
public ArticleInfo getArticleInfo(Long articleID, String authId) throws NoSuchArticleIdException {
Article article = hibernateTemplate.get(Article.class, articleID);
return createArticleInfo(article, authId);
}
private ArticleInfo createArticleInfo(Article article, final String authId) {
final ArticleInfo articleInfo = new ArticleInfo();
articleInfo.setId(article.getID());
//Set properties from the dublin core
articleInfo.setDoi(article.getDoi());
articleInfo.setDate(article.getDate());
articleInfo.setTitle(article.getTitle());
articleInfo.setVolume(article.getVolume());
articleInfo.setIssue(article.getIssue());
articleInfo.setJournal(article.getJournal());
articleInfo.setDescription(article.getDescription());
articleInfo.setRights(article.getRights());
articleInfo.setPublisher(article.getPublisherName());
articleInfo.seteIssn(article.geteIssn());
articleInfo.setTypes(article.getTypes());
articleInfo.setPages(article.getPages());
articleInfo.setIssue(article.getIssue());
articleInfo.setVolume(article.getVolume());
articleInfo.seteLocationId(article.geteLocationId());
articleInfo.setCitedArticles(article.getCitedArticles());
articleInfo.setStrkImgURI(article.getStrkImgURI());
//Set the citation info
CitationInfo citationInfo = new CitationInfo();
citationInfo.setId(URI.create(article.getDoi()));
citationInfo.setCollaborativeAuthors(article.getCollaborativeAuthors());
//set article asset views
List<AssetView> aViews = new ArrayList<AssetView>();
for (ArticleAsset asset : article.getAssets()) {
aViews.add(new AssetView(asset.getDoi(), asset.getSize(), asset.getExtension()));
}
articleInfo.setArticleAssets(aViews);
Map<Category, Integer> categories = article.getCategories();
Set<ArticleCategory> catViews = new HashSet<ArticleCategory>(categories.size());
//See if the user flagged any of the existing categories
List<Long> flaggedCategories = getFlaggedCategories(article.getID(), authId);
for(Category cat : categories.keySet()) {
catViews.add(
ArticleCategory.builder()
.setCategoryID(cat.getID())
.setMainCategory(cat.getMainCategory())
.setSubCategory(cat.getSubCategory())
.setPath(cat.getPath())
.setFlagged(flaggedCategories.contains(cat.getID()))
.build()
);
}
articleInfo.setCategories(catViews);
List<ArticleCategory> orderedCategories = sortCategories(catViews);
articleInfo.setOrderedCategories(orderedCategories);
//authors (list of UserProfileInfo)
//TODO: Refactor ArticleInfo and CitationInfo objects
//there's no reason why authors need to be attached to the citation
List<UserProfileInfo> authors = new ArrayList<UserProfileInfo>();
for (ArticleAuthor ac : article.getAuthors()) {
UserProfileInfo author = new UserProfileInfo();
author.setRealName(ac.getFullName());
authors.add(author);
}
citationInfo.setAuthors(authors);
articleInfo.setCi(citationInfo);
//set article type
if (article.getTypes() != null) {
articleInfo.setAt(article.getTypes());
}
Set<org.ambraproject.models.Journal> journals = article.getJournals();
Set<JournalView> journalViews = new HashSet<JournalView>(journals.size());
for(org.ambraproject.models.Journal journal : journals) {
journalViews.add(new JournalView(journal));
}
articleInfo.setJournals(journalViews);
//get related articles
//this results in more queries than doing a join, but getArticle() already has security logic built in to it
//and a very small percentage of articles even have related articles
List<RelatedArticleInfo> articleInfos = getRelatedArticleInfos(articleInfo.getDoi(), articleInfo.getTypes(),
article.getRelatedArticles(), authId);
articleInfo.setRelatedArticles(articleInfos);
log.debug("loaded ArticleInfo: id={}, articleTypes={}, " +
"date={}, title={}, authors={}, related-articles={}",
new Object[] {articleInfo.getDoi(), articleInfo.getArticleTypeForDisplay(), articleInfo.getDate(),
articleInfo.getTitle(), Arrays.toString(articleInfo.getAuthors().toArray()),
Arrays.toString(articleInfo.getRelatedArticles().toArray())});
return articleInfo;
}
private List<RelatedArticleInfo> getRelatedArticleInfos(final String doi, final Set<String> types,
final List<ArticleRelationship> articleRelationships, String authId) {
List<RelatedArticleInfo> results = new ArrayList<RelatedArticleInfo>(articleRelationships.size());
for (ArticleRelationship relationship : articleRelationships) {
if (relationship.getOtherArticleDoi() != null) {
try {
// related articles of the article itself
//Just fetch the related articles for the other article
Article otherArticle = getArticle(relationship.getOtherArticleDoi(), authId);
RelatedArticleInfo relatedArticleInfo = getRelatedArticleInfo(relationship, otherArticle);
if (!results.contains(relatedArticleInfo)) {
results.add(relatedArticleInfo);
}
if(isDisplayableRelationship(types, relationship.getType())) {
for (ArticleRelationship otherArticleRelationship : otherArticle.getRelatedArticles()) {
if (isEqualOrAmmendment(doi, otherArticleRelationship.getOtherArticleDoi(),
otherArticleRelationship.getType()))
{
Article otherRelatedArticle = getArticle(relationship.getOtherArticleDoi(), authId);
RelatedArticleInfo otherArticleRelatedArticleInfo = getRelatedArticleInfo(otherArticleRelationship, otherRelatedArticle);
if (!results.contains(otherArticleRelatedArticleInfo)) {
results.add(otherArticleRelatedArticleInfo);
}
}
}
}
} catch (NoSuchArticleIdException e) {
//exclude this article
} catch (ApplicationException e) {
//exclude this article
}
}
}
return results;
}
@SuppressWarnings("unchecked")
private List<TOCRelatedArticle> getRelatedArticlesForTOC(final String doi, final Set<String> types, String authId) {
List<Object[]> relatedArticles = getRelatedArticles(doi, authId);
List<TOCRelatedArticle> results = new ArrayList<TOCRelatedArticle>();
for (Object relationship[] : relatedArticles) {
final String relatedDoi = (String)relationship[0];
final String relatedTitle = (String)relationship[1];
final String relatedType = (String)relationship[2];
final Date relatedDate = (Date)relationship[3];
if (relatedDoi != null) {
try {
TOCRelatedArticle tocRelatedArticle = new TOCRelatedArticle(relatedDoi, relatedTitle, relatedType, relatedDate);
if (!results.contains(tocRelatedArticle)) {
results.add(tocRelatedArticle);
}
if(isDisplayableRelationship(types, relatedType)) {
List<Object[]> otherArticleRelationships = getRelatedArticles(relatedDoi, authId);
for (Object[] otherArticleRelationship : otherArticleRelationships) {
String otherArticleDoi = (String)otherArticleRelationship[0];
String otherArticleTitle = (String)otherArticleRelationship[1];
String otherArticleRelationshipType = (String)otherArticleRelationship[2];
Date otherArticleDate = (Date)otherArticleRelationship[3];
if (isEqualOrAmmendment(doi, otherArticleDoi, otherArticleRelationshipType)) {
TOCRelatedArticle tocOtherRelatedArticle = new TOCRelatedArticle(otherArticleDoi,
otherArticleTitle, relatedType, otherArticleDate);
if (!results.contains(tocOtherRelatedArticle)) {
results.add(tocOtherRelatedArticle);
}
}
}
}
} catch (ApplicationException e) {
//exclude this article
}
}
}
return results;
}
private boolean isEqualOrAmmendment(final String doi, final String otherDoi, final String otherRelationshipType) {
// exclude the current amendment article, non-amendment articles, and other articles th
return !doi.equals(otherDoi) && ArticleRelationship.isAmendmentRelationship(otherRelationshipType);
}
private boolean isDisplayableRelationship(final Set<String> types, final String type) throws ApplicationException {
/* Logic for the amendments Related Article Sidebar to include the link to its original article's other amendments */
if (containsRetractionType(types) || containsEocType(types) || containsCorrectionType(types)) {
// make sure that the related article is the amendment's original article
if (ArticleRelationship.isOriginalArticleOfAmendment(type)) {
return true;
}
}
return false;
}
@Transactional(readOnly = true)
@SuppressWarnings("unchecked")
public List<TOCArticle> getArticleTOCEntries(final List<String> articleDois, final String authId) {
for(int a = 0; a < articleDois.size(); a++) {
try {
checkArticleState(articleDois.get(a), authId);
} catch(NoSuchArticleIdException ex) {
articleDois.remove(a);
}
}
return hibernateTemplate.execute(new HibernateCallback<List<TOCArticle>>() {
@Override
public List<TOCArticle> doInHibernate(Session session) throws HibernateException, SQLException {
List<TOCArticle> results = new ArrayList<TOCArticle>(articleDois.size());
for(String doi : articleDois) {
List<String> articleStringTypes = session.createSQLQuery("select articleType.type from articleType " +
"join article on article.articleID = articleType.articleID where article.doi = :doi")
.setParameter("doi", doi).list();
assert(articleStringTypes != null);
Set<ArticleType> articleTypes = new HashSet<ArticleType>(articleStringTypes.size());
for (String artType : articleStringTypes) {
articleTypes.add(ArticleType.getArticleTypeForURI(URI.create(artType), true));
}
//Can assume here it's a valid doi from the checkArticleState query from above
Object[] article = ((List<Object[]>)session.createSQLQuery("select article.doi, article.title, journal.title as journal, " +
"article.date, count(*) from article left outer join articleAsset on article.articleID = articleAsset.articleID " +
"left outer join journal on article.eIssn = journal.eIssn " +
"where article.doi = :doi group by article.doi, article.title, journal.title, article.date")
.setParameter("doi", doi)
.list()).get(0);
List<String> collaborativeAuthors = session.createSQLQuery("select ca.name from " +
"articleCollaborativeAuthors ca join article a on ca.articleID = a.articleID " +
"where a.doi = :doi order by ca.sortOrder asc")
.setParameter("doi", doi)
.list();
List<String> authors = session.createSQLQuery("select ap.fullName from " +
"articlePerson ap join article a on ap.articleID = a.articleID " +
"where a.doi = :doi and ap.type = 'author' order by ap.sortOrder asc")
.setParameter("doi", doi)
.list();
List<TOCRelatedArticle> relatedArticleInfos = getRelatedArticlesForTOC(doi,
new HashSet<String>(articleStringTypes), authId);
TOCArticle tocArticle = TOCArticle.builder()
.setDoi((String)article[0])
.setTitle((String)article[1])
.setAuthors(authors)
.setCollaborativeAuthors(collaborativeAuthors)
.setArticleTypes(articleTypes)
.setRelatedArticles(relatedArticleInfos)
.setPublishedJournal((String)article[2])
.setDate((Date)article[3])
//ignore article xml and pdf
.setHasFigures(((BigInteger)article[4]).intValue() > 2)
.build();
results.add(tocArticle);
}
return results;
}
});
}
@SuppressWarnings("unchecked")
@Transactional(readOnly = true)
private List<Object[]> getRelatedArticles(final String doi, final String authId) {
//Query for rows, take authId into account
List<Object[]> tempRes = (List<Object[]>)hibernateTemplate.execute(new HibernateCallback() {
@Override
public Object doInHibernate(Session session) throws HibernateException, SQLException {
String sqlQuery = "select ar.otherArticleDoi, a1.title, ar.type, a1.date " +
"from article a " +
"join articleRelationship ar on a.articleID = ar.parentArticleID " +
"join article a1 on ar.otherArticleID = a1.articleID " +
"where a.doi = :doi";
return session.createSQLQuery(sqlQuery)
.setParameter("doi", doi)
.list();
}
});
List<Object[]> results = new ArrayList<Object[]>();
for(Object[] row : tempRes) {
String otherArticleDoi = (String)row[0];
String otherArticleTitle = (String)row[1];
String otherArticleType = (String)row[2];
Date otherArticleDate = (Date)row[3];
try {
checkArticleState(doi, authId);
results.add(new Object[] { otherArticleDoi, otherArticleTitle, otherArticleType, otherArticleDate } );
} catch(NoSuchArticleIdException ex) {
//Do nothing
}
}
return results;
}
@Override
@Transactional(readOnly = true)
public ArticleInfo getBasicArticleView(Long articleID) throws NoSuchArticleIdException {
if (articleID == null) {
throw new NoSuchArticleIdException("Null id");
}
log.debug("loading up title and doi for article: {}", articleID);
ArticleInfo articleInfo = getBasicArticleViewArticleInfo(articleID);
return articleInfo;
}
@Override
@Transactional(readOnly = true)
public ArticleInfo getBasicArticleView(String articleDoi) throws NoSuchArticleIdException {
if (articleDoi == null) {
throw new NoSuchArticleIdException("Null doi");
}
log.debug("loading up title and doi for article: {}", articleDoi);
ArticleInfo articleInfo = getBasicArticleViewArticleInfo(articleDoi);
return articleInfo;
}
/**
* Return a list of categories this user flagged for this article
*
* @param articleID an articleID
* @param authID the user's authorization ID
*
* @return list of category IDs this user flagged for this article
*/
@SuppressWarnings("unchecked")
@Transactional(readOnly = true)
private List<Long> getFlaggedCategories(final long articleID, final String authID) {
if(authID != null && authID.length() > 0) {
return hibernateTemplate.execute(new HibernateCallback<List<Long>>() {
public List<Long> doInHibernate(Session session) throws HibernateException, SQLException {
List<BigInteger> categories = session.createSQLQuery(
"select acf.categoryID from articleCategoryFlagged acf " +
"join userProfile up on up.userProfileID = acf.userProfileID " +
"where up.authId = :authID and acf.articleID = :articleID")
.setString("authID", authID)
.setLong("articleID", articleID)
.list();
List<Long> results = new ArrayList<Long>();
for(BigInteger row : categories) {
results.add(row.longValue());
}
return results;
}
});
} else {
return new ArrayList<Long>();
}
}
/**
* Returns ArticleInfo object with articleID, doi, title, authors, collaborativeAuthors and article type populated
* @param articleIdentifier articleID or articleDoi
* @return ArticleInfo object with articleID, doi, title, authors, collaborativeAuthors and article type populated
* @throws NoSuchArticleIdException
*/
private ArticleInfo getBasicArticleViewArticleInfo (Object articleIdentifier) throws NoSuchArticleIdException {
Object[] results = new Object[0];
List<ArticleAuthor> authors;
List<String> collabAuthors;
List<String> articleTypes;
try {
DetachedCriteria dc = DetachedCriteria.forClass(Article.class)
.setProjection(Projections.projectionList()
.add(Projections.id())
.add(Projections.property("doi"))
.add(Projections.property("title"))
);
if (articleIdentifier instanceof Long) {
dc.add(Restrictions.eq("ID", articleIdentifier));
} else if (articleIdentifier instanceof String) {
dc.add(Restrictions.eq("doi", articleIdentifier));
}
results = (Object[]) hibernateTemplate.findByCriteria(dc,0,1).get(0);
authors = (List<ArticleAuthor>) hibernateTemplate.find("from ArticleAuthor where articleID = ?", results[0]);
collabAuthors = (List<String>) hibernateTemplate.find("select elements(article.collaborativeAuthors) from Article as article where id = ?", results[0]);
articleTypes = (List<String>) hibernateTemplate.find("select elements(article.types) from Article as article where id = ?", results[0]);
} catch (IndexOutOfBoundsException e) {
throw new NoSuchArticleIdException(articleIdentifier.toString());
}
ArticleInfo articleInfo = new ArticleInfo();
articleInfo.setId((Long) results[0]);
articleInfo.setDoi((String) results[1]);
articleInfo.setTitle((String) results[2]);
List<String> authors2 = new ArrayList<String>(authors.size());
for (ArticleAuthor ac : authors) {
authors2.add(ac.getFullName());
}
articleInfo.setAuthors(authors2);
articleInfo.setCollaborativeAuthors(collabAuthors);
articleInfo.setAt(new HashSet<String>(articleTypes));
return articleInfo;
}
/**
* {@inheritDoc}
*
*/
@Override
@SuppressWarnings("unchecked")
@Transactional(readOnly = true)
public CitedArticleView getCitedArticle(long citedArticleID) {
//TODO, unit test needed SE-133
List<String> results = (List<String>)hibernateTemplate.findByCriteria(
DetachedCriteria.forClass(Article.class)
.setProjection(Projections.property("doi"))
.createCriteria("citedArticles", "ca")
.add(Restrictions.eq("ca.ID", citedArticleID)));
if(results.size() == 0) {
return null;
}
CitedArticle citedArticle = hibernateTemplate.get(CitedArticle.class, citedArticleID);
String doi = results.get(0);
return new CitedArticleView(doi, citedArticle);
}
/**
* Create a sorted list sorted by the integer value, largest first, smallest last
*
* @param values the Map to sort
*
* @return a List of the map entries of the map passed in
*/
protected static List<Map.Entry<String, Integer>> sortCategoriesByValue(Map<String, Integer> values) {
List<Map.Entry<String, Integer>> categoryStringsSorted = new ArrayList<Map.Entry<String, Integer>>();
categoryStringsSorted.addAll(values.entrySet());
Collections.sort(categoryStringsSorted, new Comparator<Map.Entry<String, Integer>>() {
@Override
public int compare(Map.Entry<String, Integer> e1, Map.Entry<String, Integer> e2) {
return e2.getValue().compareTo(e1.getValue());
}
});
return categoryStringsSorted;
}
/**
* {@inheritDoc}
*/
@Override
@Transactional
public Map<Category, Integer> setArticleCategories(Article article, Map<String, Integer> categoryMap) {
List<Map.Entry<String, Integer>> sortedCategories = sortCategoriesByValue(categoryMap);
//LinkedHashMap keeps things ordered by insertion order
Map<Category, Integer> results = new LinkedHashMap<Category, Integer>(categoryMap.size());
Set<String> uniqueLeafs = new HashSet<String>();
for (Map.Entry<String, Integer> s : sortedCategories) {
if (s.getKey().charAt(0) != '/') {
throw new IllegalArgumentException("Bad category: " + s);
}
Category category = new Category();
category.setPath(s.getKey());
//We want a count of distinct lead nodes. When this
//Reaches eight stop. Note the second check, we can be at
//eight uniqueLeafs, but still finding different paths. Stop
//Adding when a new unique leaf is found. Yes, a little confusing
if (uniqueLeafs.size() == 8 &&
//getSubCategory returns leaf node of the path
!uniqueLeafs.contains(category.getSubCategory())
) {
break;
} else {
//getSubCategory returns leaf node of the path
uniqueLeafs.add(category.getSubCategory());
results.put(category, s.getValue());
}
}
article.setCategories(results);
updateWithExistingCategories(article);
return results;
}
/**
* Update the article to reference any already existing categories in the database.
*
* @param article the article to update
*/
private void updateWithExistingCategories(Article article) {
// I was having an issue where the first call to hibernateTemplate.findByCriteria below
// was triggering a "flush"... saving a dirty but uncommitted object to the DB. The
// object being saved was a newly-created category that had a duplicate in the DB, which
// triggered a duplicate key exception. Of course, this is the whole point of this
// method... to prevent this from happening. The following two lines fix this, but it
// seems kind of wrong. This happened from a standalone app not running in a servlet
// container, and I suspect that I was somehow misconfiguring my session factory
// or transaction manager or something (but this was the only solution I found).
int oldFlushMode = hibernateTemplate.getFlushMode();
hibernateTemplate.setFlushMode(HibernateAccessor.FLUSH_COMMIT);
try {
Map<Category, Integer> existingCategories = article.getCategories();
if (existingCategories != null && !existingCategories.isEmpty()) {
Map<Category, Integer> correctCategories = new LinkedHashMap<Category, Integer>(existingCategories.size());
for (Map.Entry<Category, Integer> entry : existingCategories.entrySet()) {
try {
Category existingCategory;
if (entry.getKey().getSubCategory() != null) {
existingCategory = (Category) hibernateTemplate.findByCriteria(
DetachedCriteria.forClass(Category.class)
.add(Restrictions.eq("path", entry.getKey().getPath())), 0, 1).get(0);
} else {
existingCategory = (Category) hibernateTemplate.findByCriteria(
DetachedCriteria.forClass(Category.class)
.add(Restrictions.eq("path", entry.getKey().getPath())), 0, 1).get(0);
}
correctCategories.put(existingCategory, entry.getValue());
} catch (IndexOutOfBoundsException e) {
//category must not have existed, save it now
hibernateTemplate.save(entry.getKey());
correctCategories.put(entry.getKey(), entry.getValue());
}
}
article.setCategories(correctCategories);
}
} finally {
hibernateTemplate.setFlushMode(oldFlushMode);
}
}
/**
* Help0er method for getArticleIds() since the restriction we want doesn't appear
*
* @param authorFilter
* @param authors
* @return
*/
private boolean matchesAuthorFilter(String[] authorFilter, List<ArticleAuthor> authors) {
if (authorFilter != null && authorFilter.length > 0) {
List<String> authorNames = new ArrayList<String>(authors.size());
for (ArticleAuthor author : authors) {
authorNames.add(author.getFullName());
}
for (String author : authorFilter) {
if (!authorNames.contains(author)) {
return false;
}
}
}
return true;
}
/**
* Helper method for getArticleIds(), since the type of restriction needed doesn't seem to be do-able in HQL or with
* criteria - we want articles where each of the Strings in the category array is the main category of one of the
* article's categories
*
* @param filterCategories - the array of main categories that was passed in to getArticleIds() to use to filter
* results
* @param categorySet - the 'categories' property of an article
* @return - true if the article passes the category filter, false otherwise
*/
private boolean matchesCategoriesFilter(String[] filterCategories, Set<Category> categorySet) {
if (filterCategories != null && filterCategories.length > 0) {
if (filterCategories.length > categorySet.size()) {
return false; //can't possibly contain all the categories if there's more of them than you have
}
//Just get the main category
Set<String> mainCategories = new HashSet<String>(categorySet.size());
for (Category category : categorySet) {
mainCategories.add(category.getMainCategory());
}
//check that all the filter categories are in their
for (String cat : filterCategories) {
if (!mainCategories.contains(cat)) {
return false;
}
}
}
return true;
}
/**
* @param permissionsService the permissions service to use
*/
@Required
public void setPermissionsService(PermissionsService permissionsService) {
this.permissionsService = permissionsService;
}
/**
* This method sorts the categories in alphabetical order. It uses the overridden
* compareTo() method in the ArticleCategory to compare the subcategories for sorting;
* if the subcategory does not exist (in case of one-level deep categories)
* this method uses the main category.
*
* @param categoryViews the 'categories' property of an article as a list
* @return the alphabetically ordered categories
*/
public List<ArticleCategory> sortCategories (Set<ArticleCategory> categoryViews) {
List<ArticleCategory> orderedCategories = new ArrayList<ArticleCategory>();
orderedCategories.addAll(categoryViews);
Collections.sort(orderedCategories);
return orderedCategories;
}
/**
* This method returns an instance of the RelatedArticleInfo based on the article's related article and
* their relationship.
*
* @param relationship the relationship between the parent article and its related article
* @param otherArticle the article's related article
* @return an instance of the RelatedArticleInfo
*/
private RelatedArticleInfo getRelatedArticleInfo(ArticleRelationship relationship, Article otherArticle) {
RelatedArticleInfo relatedArticleInfo = new RelatedArticleInfo();
relatedArticleInfo.setUri(URI.create(otherArticle.getDoi()));
relatedArticleInfo.setTitle(otherArticle.getTitle());
relatedArticleInfo.setDoi(otherArticle.getDoi());
relatedArticleInfo.setDate(otherArticle.getDate());
relatedArticleInfo.seteIssn(otherArticle.geteIssn());
relatedArticleInfo.setRelationType(relationship.getType());
relatedArticleInfo.setTypes(otherArticle.getTypes());
Set<org.ambraproject.models.Journal> journals = otherArticle.getJournals();
Set<JournalView> journalViews = new HashSet<JournalView>(journals.size());
for(org.ambraproject.models.Journal journal : journals) {
journalViews.add(new JournalView(journal));
}
relatedArticleInfo.setJournals(journalViews);
List<String> relatedArticleAuthors = new ArrayList<String>(otherArticle.getAuthors().size());
for (ArticleAuthor ac : otherArticle.getAuthors()) {
relatedArticleAuthors.add(ac.getFullName());
}
relatedArticleInfo.setAuthors(relatedArticleAuthors);
//set article type
if (otherArticle.getTypes() != null) {
relatedArticleInfo.setAt(otherArticle.getTypes());
}
return relatedArticleInfo;
}
public List<ArticleRelationship> getArticleAmendments(final String articleDoi) {
if (articleDoi == null)
throw new IllegalArgumentException("articleDoi = null");
// TODO: order the amendments by date
List<ArticleRelationship> result = (List<ArticleRelationship>) hibernateTemplate.find("select distinct relatedArticles from Article as art " +
"inner join art.relatedArticles as relatedArticles where art.doi = ? " +
"and relatedArticles.type in ('expressed-concern' , 'retraction', 'correction-forward')" , articleDoi);
return result;
}
}