Java tutorial
/* * $HeadURL$ * $Id$ * * Copyright (c) 2006-2011 by Public Library of Science * http://plos.org * http://ambraproject.org * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. | */ package org.ambraproject.service.annotation; import org.ambraproject.models.Annotation; import org.ambraproject.models.AnnotationType; import org.ambraproject.models.Article; import org.ambraproject.models.Flag; import org.ambraproject.models.FlagReasonCode; import org.ambraproject.models.UserProfile; import org.ambraproject.service.hibernate.HibernateServiceImpl; import org.ambraproject.util.URIGenerator; import org.ambraproject.views.AnnotationView; import org.hibernate.Criteria; import org.hibernate.HibernateException; import org.hibernate.SQLQuery; 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.orm.hibernate3.HibernateCallback; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Nullable; import java.net.URISyntaxException; import java.sql.SQLException; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; /** * @author Alex Kudlick Date: 4/29/11 * <p/> * org.ambraproject.annotation.service */ public class AnnotationServiceImpl extends HibernateServiceImpl implements AnnotationService { private static final Logger log = LoggerFactory.getLogger(AnnotationServiceImpl.class); private static final SimpleDateFormat yearFormat = new SimpleDateFormat("yyyy"); /** * Get a list of all annotations satisfying the given criteria. * * @param startDate search for annotation after start date. * @param endDate is the date to search until. If null, search until present date * @param annotTypes List of annotation types * @param maxResults the maximum number of results to return, or 0 for no limit * @param journal journalName * @return the (possibly empty) list of article annotations. * @throws ParseException if any of the dates or query could not be parsed * @throws URISyntaxException if an element of annotType cannot be parsed as a URI */ @Override @Transactional(readOnly = true) @SuppressWarnings("unchecked") public List<AnnotationView> getAnnotations(final Date startDate, final Date endDate, final Set<String> annotTypes, final int maxResults, final String journal) throws ParseException, URISyntaxException { /*** * There may be a more efficient way to do this other than querying the database twice, at some point in time * we might improve how hibernate does the object mappings * * This execute returns annotationIDs, article DOIs and titles, which are needed to construction the annotionView * object */ Map<Long, String[]> results = (Map<Long, String[]>) hibernateTemplate.execute(new HibernateCallback() { @Override public Object doInHibernate(Session session) throws HibernateException, SQLException { /** URIGen * We have to do this with SQL because of how the mappings are currently defined * And hence, there is no way to unit test this */ StringBuilder sqlQuery = new StringBuilder(); Map<String, Object> params = new HashMap<String, Object>(3); sqlQuery.append("select ann.annotationID, art.doi, art.title "); sqlQuery.append("from annotation ann "); sqlQuery.append("join article art on art.articleID = ann.articleID "); sqlQuery.append("join journal j on art.eIssn = j.eIssn "); sqlQuery.append("where j.journalKey = :journal "); params.put("journal", journal); if (startDate != null) { sqlQuery.append(" and ann.created > :startDate"); params.put("startDate", startDate); } if (endDate != null) { sqlQuery.append(" and ann.created < :endDate"); params.put("endDate", endDate); } if (annotTypes != null) { sqlQuery.append(" and ann.type in (:annotTypes)"); params.put("annotTypes", annotTypes); } sqlQuery.append(" order by ann.created desc"); SQLQuery query = session.createSQLQuery(sqlQuery.toString()); query.setProperties(params); if (maxResults > 0) { query.setMaxResults(maxResults); } List<Object[]> tempResults = query.list(); Map<Long, String[]> results = new HashMap<Long, String[]>(tempResults.size()); for (Object[] obj : tempResults) { //This forces this method to return Long values and not BigInteger results.put((((Number) obj[0]).longValue()), new String[] { (String) obj[1], (String) obj[2] }); } return results; } }); //The previous query puts annotationID and doi into the map. annotationID is key //I do this to avoid extra doi lookups later in the code. if (results.size() > 0) { DetachedCriteria criteria = DetachedCriteria.forClass(Annotation.class) .add(Restrictions.in("ID", results.keySet())).addOrder(Order.desc("created")) .setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY); List<Annotation> annotations = (List<Annotation>) hibernateTemplate.findByCriteria(criteria); List<AnnotationView> views = new ArrayList<AnnotationView>(annotations.size()); for (Annotation ann : annotations) { String articleDoi = results.get(ann.getID())[0]; String articleTitle = results.get(ann.getID())[1]; views.add(buildAnnotationView(ann, articleDoi, articleTitle, false)); } return views; } else { return new ArrayList<AnnotationView>(); } } @Override @Transactional(readOnly = true) public AnnotationView[] listAnnotations(final Long articleID, final Set<AnnotationType> annotationTypes, final AnnotationOrder order) { //Basic criteria DetachedCriteria criteria = DetachedCriteria.forClass(Annotation.class) .add(Restrictions.eq("articleID", articleID)).setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY); //restrict by type if (annotationTypes != null && !annotationTypes.isEmpty()) { criteria.add(Restrictions.in("type", annotationTypes)); } switch (order) { case OLDEST_TO_NEWEST: criteria.addOrder(Order.asc("created")); break; case MOST_RECENT_REPLY: //Still going to have to sort the results after creating views, because 'Most Recent Reply' isn't something that's stored on the database level //but ordering newest to oldest makes it more likely that the annotations will be in close to the correct order by the time we sort criteria.addOrder(Order.desc("created")); break; } List annotationResults = hibernateTemplate.findByCriteria(criteria); //Don't want to call buildAnnotationView() here because that would involve loading up the reply map for each annotation, // when we only need to do it once. So load up the info we need to build annotation views here Object[] articleTitleAndDoi; try { articleTitleAndDoi = (Object[]) hibernateTemplate .findByCriteria(DetachedCriteria.forClass(Article.class).add(Restrictions.eq("ID", articleID)) .setProjection(Projections.projectionList().add(Projections.property("doi")) .add(Projections.property("title"))), 0, 1) .get(0); } catch (IndexOutOfBoundsException e) { throw new IllegalArgumentException("article " + articleID + " didn't exist"); } String articleDoi = (String) articleTitleAndDoi[0]; String articleTitle = (String) articleTitleAndDoi[1]; Map<Long, List<Annotation>> replyMap = buildReplyMap(articleID); List<AnnotationView> viewResults = new ArrayList<AnnotationView>(annotationResults.size()); for (Object annotation : annotationResults) { viewResults.add(new AnnotationView((Annotation) annotation, articleDoi, articleTitle, replyMap)); } if (order == AnnotationOrder.MOST_RECENT_REPLY) { //Order the results by the most recent reply date Collections.sort(viewResults, new Comparator<AnnotationView>() { @Override public int compare(AnnotationView view1, AnnotationView view2) { return -1 * view1.getLastReplyDate().compareTo(view2.getLastReplyDate()); } }); } return viewResults.toArray(new AnnotationView[viewResults.size()]); } @Override @Transactional(readOnly = true) public AnnotationView[] listAnnotationsNoReplies(final Long articleID, final Set<AnnotationType> annotationTypes, final AnnotationOrder order) { if (order == AnnotationOrder.MOST_RECENT_REPLY) { throw new IllegalArgumentException( "Cannot specify Most Recent Reply order type when replies are not being loaded up"); } //Basic criteria DetachedCriteria criteria = DetachedCriteria.forClass(Annotation.class) .add(Restrictions.eq("articleID", articleID)).setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY); //restrict by type if (annotationTypes != null && !annotationTypes.isEmpty()) { criteria.add(Restrictions.in("type", annotationTypes)); } switch (order) { case OLDEST_TO_NEWEST: criteria.addOrder(Order.asc("created")); break; case NEWEST_TO_OLDEST: criteria.addOrder(Order.desc("created")); break; } List annotationResults = hibernateTemplate.findByCriteria(criteria); //Don't want to call buildAnnotationView() here because that would involve finding the article title and doi for each annotation, // when we only need to do it once. So load up the info we need to build annotation views here Object[] articleTitleAndDoi; try { articleTitleAndDoi = (Object[]) hibernateTemplate .findByCriteria(DetachedCriteria.forClass(Article.class).add(Restrictions.eq("ID", articleID)) .setProjection(Projections.projectionList().add(Projections.property("doi")) .add(Projections.property("title"))), 0, 1) .get(0); } catch (IndexOutOfBoundsException e) { throw new IllegalArgumentException("article " + articleID + " didn't exist"); } String articleDoi = (String) articleTitleAndDoi[0]; String articleTitle = (String) articleTitleAndDoi[1]; List<AnnotationView> viewResults = new ArrayList<AnnotationView>(annotationResults.size()); for (Object annotation : annotationResults) { viewResults.add(new AnnotationView((Annotation) annotation, articleDoi, articleTitle, null)); } return viewResults.toArray(new AnnotationView[viewResults.size()]); } @Override @Transactional(readOnly = true) public int countAnnotations(Long articleID, Set<AnnotationType> annotationTypes) { if (annotationTypes != null && !annotationTypes.isEmpty()) { return ((Number) hibernateTemplate .findByCriteria(DetachedCriteria.forClass(Annotation.class) .add(Restrictions.eq("articleID", articleID)) .add(Restrictions.in("type", annotationTypes)).setProjection(Projections.rowCount())) .get(0)).intValue(); } else { return ((Number) hibernateTemplate .findByCriteria(DetachedCriteria.forClass(Annotation.class) .add(Restrictions.eq("articleID", articleID)).setProjection(Projections.rowCount())) .get(0)).intValue(); } } @Override @Transactional public Long createComment(UserProfile user, String articleDoi, String title, String body, String ciStatement) { if (articleDoi == null) { throw new IllegalArgumentException("Attempted to create comment with null article id"); } else if (user == null || user.getID() == null) { throw new IllegalArgumentException("Attempted to create comment without a creator"); } else if (body == null || body.isEmpty()) { throw new IllegalArgumentException("Attempted to create comment with no body"); } log.debug("Creating comment on article: {}; title: {}; body: {}", new Object[] { articleDoi, title, body }); Long articleID; try { articleID = (Long) hibernateTemplate.findByCriteria(DetachedCriteria.forClass(Article.class) .add(Restrictions.eq("doi", articleDoi)).setProjection(Projections.id())).get(0); } catch (IndexOutOfBoundsException e) { throw new IllegalArgumentException("Invalid doi: " + articleDoi); } //generate an annotation uri Annotation comment = new Annotation(user, AnnotationType.COMMENT, articleID); comment.setAnnotationUri(URIGenerator.generate(comment)); comment.setTitle(title); comment.setBody(body); comment.setCompetingInterestBody(ciStatement); Long id = (Long) hibernateTemplate.save(comment); return id; } @Override @Transactional public Long createReply(UserProfile user, Long parentId, String title, String body, @Nullable String ciStatement) { if (parentId == null) { throw new IllegalArgumentException("Attempting to create reply with null parent id"); } log.debug("Creating reply to {}; title: {}; body: {}", new Object[] { parentId, title, body }); Long articleID; try { articleID = (Long) hibernateTemplate.findByCriteria(DetachedCriteria.forClass(Annotation.class) .add(Restrictions.eq("ID", parentId)).setProjection(Projections.property("articleID")), 0, 1) .get(0); } catch (IndexOutOfBoundsException e) { throw new IllegalArgumentException("Invalid annotation id: " + parentId); } Annotation reply = new Annotation(user, AnnotationType.REPLY, articleID); reply.setParentID(parentId); reply.setTitle(title); reply.setBody(body); reply.setCompetingInterestBody(ciStatement); reply.setAnnotationUri(URIGenerator.generate(reply)); return (Long) hibernateTemplate.save(reply); } @Override @SuppressWarnings("unchecked") @Transactional(readOnly = true) public AnnotationView getFullAnnotationView(Long annotationId) { if (annotationId == null) { throw new IllegalArgumentException("No annotation id specified"); } log.debug("populating view object for annotation {}", annotationId); Annotation annotation = (Annotation) hibernateTemplate.get(Annotation.class, annotationId); if (annotation == null) { throw new IllegalArgumentException( "Specified id that does not correspond to an annotation; " + annotationId); } return buildAnnotationView(annotation, true); } @Override @Transactional(readOnly = true) public AnnotationView getBasicAnnotationView(Long annotationId) { if (annotationId == null) { throw new IllegalArgumentException("No annotation id specified"); } log.debug("populating view object for annotation {}", annotationId); Annotation annotation = (Annotation) hibernateTemplate.get(Annotation.class, annotationId); if (annotation == null) { throw new IllegalArgumentException( "Specified id that does not correspond to an annotation; " + annotationId); } return buildAnnotationView(annotation, false); } @Override @Transactional(readOnly = true) public AnnotationView getBasicAnnotationViewByUri(String annotationUri) { if (annotationUri == null) { throw new IllegalArgumentException("No annotation URI specified"); } log.debug("populating view object for annotation {}", annotationUri); Annotation annotation; try { annotation = (Annotation) hibernateTemplate.findByCriteria( DetachedCriteria.forClass(Annotation.class).add(Restrictions.eq("annotationUri", annotationUri)) .setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY)) .get(0); } catch (IndexOutOfBoundsException e) { throw new IllegalArgumentException( "Specified URI that does not correspond to an annotation; " + annotationUri); } return buildAnnotationView(annotation, false); } @SuppressWarnings("unchecked") private AnnotationView buildAnnotationView(Annotation annotation, String articleDoi, String articleTitle, boolean loadAllReplies) { Map<Long, List<Annotation>> fulReplyMap = null; if (loadAllReplies) { fulReplyMap = buildReplyMap(annotation.getArticleID()); } return new AnnotationView(annotation, articleDoi, articleTitle, fulReplyMap); } /** * Build up a map of id, and replies to that id so we can initialize the reply tree * * @param articleId * @return */ @SuppressWarnings("unchecked") private Map<Long, List<Annotation>> buildReplyMap(Long articleId) { Map<Long, List<Annotation>> fullReplyMap = new HashMap<Long, List<Annotation>>(); List<Annotation> allReplies = (List<Annotation>) hibernateTemplate.findByCriteria( DetachedCriteria.forClass(Annotation.class).add(Restrictions.eq("articleID", articleId)) .add(Restrictions.eq("type", AnnotationType.REPLY))); for (Annotation reply : allReplies) { //parent id should never be null on a reply if (reply.getParentID() == null) { log.warn("Found a reply with null parent id. Reply id: " + reply.getID()); } else { if (!fullReplyMap.containsKey(reply.getParentID())) { fullReplyMap.put(reply.getParentID(), new ArrayList<Annotation>()); } fullReplyMap.get(reply.getParentID()).add(reply); } } return Collections.unmodifiableMap(fullReplyMap); } @SuppressWarnings("unchecked") private AnnotationView buildAnnotationView(Annotation annotation, boolean loadAllReplies) { Object values[]; try { values = (Object[]) hibernateTemplate.findByCriteria(DetachedCriteria.forClass(Article.class) .add(Restrictions.eq("ID", annotation.getArticleID())).setProjection(Projections .projectionList().add(Projections.property("doi")).add(Projections.property("title"))), 0, 1).get(0); } catch (IndexOutOfBoundsException e) { //this should never happen throw new IllegalStateException("Annotation " + annotation.getID() + " pointed to an article that didn't exist;" + " articleID: " + annotation.getArticleID()); } String articleDoi = (String) values[0]; String articleTitle = (String) values[1]; return buildAnnotationView(annotation, articleDoi, articleTitle, loadAllReplies); } @Override @Transactional(rollbackFor = Throwable.class) public Long createFlag(UserProfile user, Long annotationId, FlagReasonCode reasonCode, String body) { if (annotationId == null) { throw new IllegalArgumentException("No annotation id specified"); } Annotation flaggedAnnotation = (Annotation) hibernateTemplate.get(Annotation.class, annotationId); if (flaggedAnnotation == null) { throw new IllegalArgumentException("Id " + annotationId + " didn't correspond to an annotation"); } log.debug("Creating flag on annotation: {} with reason code: {}", annotationId, reasonCode); Flag flag = new Flag(user, reasonCode, flaggedAnnotation); flag.setComment(body); return (Long) hibernateTemplate.save(flag); } }