Java tutorial
/* * 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; } }