/*
* $HeadURL$
* $Id$
*
* Copyright (c) 2006-$today.year 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.filestore.FileStoreException;
import org.ambraproject.filestore.FileStoreService;
import org.ambraproject.models.Article;
import org.ambraproject.models.ArticleAsset;
import org.ambraproject.models.UserRole.Permission;
import org.ambraproject.models.ArticleAuthor;
import org.ambraproject.models.Journal;
import org.ambraproject.service.permission.PermissionsService;
import org.ambraproject.service.hibernate.HibernateServiceImpl;
import org.ambraproject.service.xml.XMLService;
import org.apache.poi.hslf.model.Picture;
import org.apache.poi.hslf.model.Slide;
import org.apache.poi.hslf.model.TextBox;
import org.apache.poi.hslf.model.Hyperlink;
import org.apache.poi.hslf.usermodel.RichTextRun;
import org.apache.poi.hslf.usermodel.SlideShow;
import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.hibernate.criterion.DetachedCriteria;
import org.hibernate.criterion.Projections;
import org.hibernate.criterion.Restrictions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.dao.DataAccessException;
import org.springframework.orm.hibernate3.HibernateCallback;
import org.springframework.transaction.annotation.Transactional;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.awt.Dimension;
import java.awt.Rectangle;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.sql.SQLException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @author Scott Sterling
* @author Joe Osowski
*/
public class ArticleAssetServiceImpl extends HibernateServiceImpl implements ArticleAssetService {
private static final Logger log = LoggerFactory.getLogger(ArticleAssetServiceImpl.class);
private PermissionsService permissionsService;
private ArticleService articleService;
private FileStoreService fileStoreService;
private XMLService secondaryObjectService;
private String templatesDirectory;
private String smallImageRep;
private String largeImageRep;
private String mediumImageRep;
private static final List<String> FIGURE_AND_TABLE_CONTEXT_ELEMENTS = new ArrayList<String>(2);
static {
FIGURE_AND_TABLE_CONTEXT_ELEMENTS.add("fig");
FIGURE_AND_TABLE_CONTEXT_ELEMENTS.add("table-wrap");
FIGURE_AND_TABLE_CONTEXT_ELEMENTS.add("alternatives");
}
/**
* Get the Article Asset by URI.
*
* @param assetUri uri
* @param authId the authorization ID of the current user
* @return the object-info of the object
* @throws NoSuchObjectIdException NoSuchObjectIdException
*/
@Transactional(readOnly = true)
@Override
public ArticleAsset getSuppInfoAsset(final String assetUri, final String authId) throws NoSuchObjectIdException {
// sanity check parms
if (assetUri == null)
throw new IllegalArgumentException("URI == null");
checkPermissions(assetUri, authId);
try {
return (ArticleAsset) hibernateTemplate.findByCriteria(
DetachedCriteria.forClass(ArticleAsset.class)
.add(Restrictions.eq("doi", assetUri)), 0, 1)
.get(0);
} catch (IndexOutOfBoundsException e) {
throw new NoSuchObjectIdException(assetUri);
}
}
/**
* Get the Article Representation Assets by URI
* <p/>
* This probably returns XML and PDF all the time
*
* @param articleDoi uri
* @param authId the authorization ID of the current user
* @return the object-info of the object
* @throws NoSuchObjectIdException NoSuchObjectIdException
*/
@Transactional(readOnly = true)
@SuppressWarnings("unchecked")
public List<ArticleAsset> getArticleXmlAndPdf(final String articleDoi, final String authId)
throws NoSuchObjectIdException {
checkPermissions(articleDoi, authId);
return (List<ArticleAsset>) hibernateTemplate.findByCriteria(
DetachedCriteria.forClass(ArticleAsset.class)
.add(Restrictions.eq("doi", articleDoi)));
}
/**
* Get the Article Asset by URI and type.
*
* @param assetUri uri
* @param representation the representation value (XML/PDF)
* @param authId the authorization ID of the current user
* @return the object-info of the object
* @throws NoSuchObjectIdException NoSuchObjectIdException
*/
@Transactional(readOnly = true)
@Override
public ArticleAsset getArticleAsset(final String assetUri, final String representation, final String authId)
throws NoSuchObjectIdException {
// sanity check parms
if (assetUri == null)
throw new IllegalArgumentException("URI == null");
if (representation == null) {
throw new IllegalArgumentException("representation == null");
}
checkPermissions(assetUri, authId);
try {
List<ArticleAsset> asset = (List<ArticleAsset>) hibernateTemplate.findByCriteria(
DetachedCriteria.forClass(ArticleAsset.class)
.add(Restrictions.eq("doi", assetUri))
.add(Restrictions.eq("extension", representation)), 0, 1);
if (asset != null && asset.size() > 0) {
return asset.get(0);
}
} catch (DataAccessException e) {
throw new NoSuchObjectIdException(assetUri);
}
return null;
}
@SuppressWarnings("unchecked")
private void checkPermissions(String assetDoi, String authId) throws NoSuchObjectIdException {
int state;
try {
state = (Integer) hibernateTemplate.findByCriteria(
DetachedCriteria.forClass(Article.class)
.setProjection(Projections.property("state"))
.createCriteria("assets")
.add(Restrictions.eq("doi", assetDoi)), 0, 1).get(0);
} catch (IndexOutOfBoundsException e) {
//article didn't exist
throw new NoSuchObjectIdException(assetDoi);
}
//If the article is in an unpublished state, none of the related objects should be returned
if (Article.STATE_UNPUBLISHED == state) {
try {
permissionsService.checkPermission(Permission.VIEW_UNPUBBED_ARTICLES, authId);
} catch (SecurityException se) {
throw new NoSuchObjectIdException(assetDoi);
}
}
//If the article is disabled don't return the object ever
if (Article.STATE_DISABLED == state) {
throw new NoSuchObjectIdException(assetDoi);
}
}
/**
* Return a list of Figures and Tables of a DOI.
*
* @param articleDoi DOI.
* @param authId the authorization ID of the current user
* @return Figures and Tables for the article in DOI order.
* @throws NoSuchArticleIdException NoSuchArticleIdException.
*/
@Transactional(readOnly = true)
@Override
public ArticleAssetWrapper[] listFiguresTables(final String articleDoi, final String authId) throws NoSuchArticleIdException {
//TODO:
// Can we not do a select distinct instead of getting back a large set of assets
// and then filtering the list via java code below?
articleService.checkArticleState(articleDoi, authId);
// if we get there, we are good
// get assets
List<ArticleAsset> assets = (List<ArticleAsset>) hibernateTemplate.find("select assets from Article article where article.doi = ?", articleDoi);
//keep track of dois we've added to the list so we don't duplicate assets for the same image
Map<String, ArticleAssetWrapper> dois = new HashMap<String, ArticleAssetWrapper>(assets.size());
List<ArticleAssetWrapper> results = new ArrayList<ArticleAssetWrapper>(assets.size());
for (ArticleAsset asset : assets) {
if (FIGURE_AND_TABLE_CONTEXT_ELEMENTS.contains(asset.getContextElement())) {
ArticleAssetWrapper articleAssetWrapper;
if (!dois.containsKey(asset.getDoi())) {
articleAssetWrapper = new ArticleAssetWrapper(asset, smallImageRep, mediumImageRep, largeImageRep);
results.add(articleAssetWrapper);
dois.put(asset.getDoi(), articleAssetWrapper);
} else {
articleAssetWrapper = dois.get(asset.getDoi());
}
// set the size of different representation.
String extension = asset.getExtension();
if("PNG_L".equals(extension)){
articleAssetWrapper.setSizeLarge(asset.getSize());
} else if("TIF".equals(extension)) {
articleAssetWrapper.setSizeTiff(asset.getSize());
}
}
}
return results.toArray(new ArticleAssetWrapper[results.size()]);
}
@Override
@Transactional(readOnly = true)
public Long getArticleID(ArticleAsset articleAsset) {
final Long assetID = articleAsset.getID();
Object result = hibernateTemplate.execute(new HibernateCallback() {
@Override
public Object doInHibernate(Session session) throws HibernateException, SQLException {
return session.createSQLQuery("select articleID from articleAsset where articleAssetID = ?")
.setParameter(0, assetID).list().get(0);
}
});
if (result instanceof BigInteger) {
return ((BigInteger) result).longValue();
} else {
return (Long) result;
}
}
/**
* Get the data for powerpoint
* @param assetDoi
* @param authId
* @return
* @throws NoSuchArticleIdException
*/
@Override
@Transactional(readOnly = true)
public InputStream getPowerPointSlide(String assetDoi, String authId) throws NoSuchArticleIdException, NoSuchObjectIdException, ApplicationException, IOException {
long startTime = Calendar.getInstance().getTimeInMillis();
String title = "";
//get the article
Article article = articleService.getArticle(assetDoi.substring(0, assetDoi.lastIndexOf('.')), authId);
//get the article asset for "PNG_M"
ArticleAsset articleAsset = getArticleAsset(assetDoi, "PNG_M", authId);
//get the article description
String desc = getArticleDescription(articleAsset);
//construct the title for ppt.
if(articleAsset.getTitle()!= null) {
title = articleAsset.getTitle() + ". " + desc;
} else {
title = desc;
}
//get the citation info
StringBuilder citation = getCitationInfo(article);
ByteArrayOutputStream tempOutputStream = null;
try {
byte[] image = fileStoreService.getFileByteArray(fileStoreService.objectIDMapper().doiTofsid(assetDoi, "PNG_M"));
tempOutputStream = new ByteArrayOutputStream(image.length);
//make the new slide
SlideShow slideShow = new SlideShow();
slideShow.setPageSize(new Dimension(792, 612)); // letter size: 11"x8.5", 1"=72px
//set the picture box on particular location
Picture picture = setPictureBox(image, slideShow);
//create the slide
Slide slide = slideShow.createSlide();
//add the picture to slide
slide.addShape(picture);
//add the title to slide
if(!title.isEmpty()){
TextBox pptTitle = slide.addTitle();
pptTitle.setText(title);
pptTitle.setAnchor(new Rectangle(28, 22, 737, 36));
RichTextRun rt = pptTitle.getTextRun().getRichTextRuns()[0];
rt.setFontSize(16);
rt.setBold(true);
rt.setAlignment(TextBox.AlignCenter);
}
//add the citation text to slide
TextBox pptCitationText = new TextBox();
/**
* show the journal name and logo of article in which it published
* an article can be cross published but we always want to show logo and url
* of source journal
*/
int index = assetDoi.lastIndexOf(".");
String articleDoi = assetDoi.substring(0, index);
String eIssn;
try {
eIssn = (String) hibernateTemplate.findByCriteria(
DetachedCriteria.forClass(Article.class)
.add(Restrictions.eq("doi", articleDoi))
.setProjection(Projections.property("eIssn")),0, 1).get(0);
} catch (IndexOutOfBoundsException e) {
throw new IllegalArgumentException("Doi " + articleDoi + " didn't correspond to an article");
}
String journalName = (String) hibernateTemplate.findByCriteria(
DetachedCriteria.forClass(Journal.class)
.add(Restrictions.eq("eIssn", eIssn))
.setProjection(Projections.property("journalKey")),0, 1).get(0);
String pptUrl = "http://www." + journalName.toLowerCase() + ".org/article/" + articleDoi;
pptCitationText.setText(citation.toString() + "\n" + pptUrl);
pptCitationText.setAnchor(new Rectangle(35, 513, 723, 26));
RichTextRun richTextRun = pptCitationText.getTextRun().getRichTextRuns()[0];
richTextRun.setFontSize(12);
String text = pptCitationText.getText();
Hyperlink link = new Hyperlink();
link.setAddress(pptUrl);
link.setTitle("click to visit the article page");
int linkId = slideShow.addHyperlink(link);
int startIndex = text.indexOf(pptUrl);
pptCitationText.setHyperlink(linkId, startIndex, startIndex + pptUrl.length());
slide.addShape(pptCitationText);
//add the logo to slide
String str = templatesDirectory + "/journals/" + journalName + "/webapp/images/logo.png";
File file = new File(str);
if(file.exists()) {
InputStream input = new FileInputStream(file);
Dimension dimension = getImageDimension(input);
input.close();
int logoIdx = slideShow.addPicture(file, Picture.PNG);
Picture logo = new Picture(logoIdx);
logo.setAnchor(new Rectangle(792 - 5 - dimension.width, 612 - 5 - dimension.height, dimension.width, dimension.height));
slide.addShape(logo);
} else {
log.warn("Logo for journal " + journalName + " not found at " + str);
}
slideShow.write(tempOutputStream);
return new ByteArrayInputStream(tempOutputStream.toByteArray());
} catch (FileStoreException e) {
log.error("Error fetching image from file store for doi: " + assetDoi, e);
return null;
} catch (IOException e) {
log.error("Error creating powerpoint slide for doi: " + assetDoi, e);
return null;
} finally {
long totalTime = Calendar.getInstance().getTimeInMillis() - startTime;
log.info("processing power point slide for " + assetDoi + " took " + totalTime + " milliseconds");
if (tempOutputStream != null) {
try {
tempOutputStream.close();
} catch (IOException e) {
log.warn("Error closing temporary output stream when creating power point slide for " + assetDoi, e);
}
}
}
}
/**
* get the image dimension
* @param input
* @return
*/
private Dimension getImageDimension(InputStream input) {
try {
ImageInputStream in = ImageIO.createImageInputStream(input) ;
try {
Iterator readers = ImageIO.getImageReaders(in);
if (readers.hasNext()) {
ImageReader reader = (ImageReader) readers.next();
try {
reader.setInput(in);
return new Dimension(reader.getWidth(0), reader.getHeight(0));
} finally {
reader.dispose();
}
}
} finally {
if (in != null)
in.close();
}
}
catch (Exception ex) {
log.error("cannot get image dimension", ex);
}
return new Dimension(0, 0);
}
/**
* set the dimension of picture box
* @param image
* @param slideShow
* @return
* @throws IOException
*/
private Picture setPictureBox(byte[] image, SlideShow slideShow) throws IOException {
int index = slideShow.addPicture(image, Picture.PNG);
InputStream input = new ByteArrayInputStream(image);
Dimension dimension = getImageDimension(input);
input.close();
//get the image size
int imW = dimension.width;
int imH = dimension.height;
//add the image to picture and add picture to shape
Picture picture = new Picture(index);
// Image box size 750x432 at xy=21,68
if (imW > 0 && imH > 0) {
double pgRatio = 750.0/432.0;
double imRatio = (double) imW / (double) imH;
if (pgRatio >= imRatio) {
// horizontal center
int mw = (int)((double) imW * 432.0 / (double) imH);
int mx = 21 + (750 - mw) / 2;
picture.setAnchor(new Rectangle(mx, 68, mw, 432));
}
else {
// vertical center
int mh = (int)((double) imH * 750.0 / (double) imW);
int my = 68 + (432 - mh) / 2;
picture.setAnchor(new Rectangle(21, my, 750, mh));
}
}
return picture;
}
/**
* get the article description
*
* @param articleAsset
* @return
* @throws ApplicationException
*/
private String getArticleDescription(ArticleAsset articleAsset) throws ApplicationException {
Pattern p = Pattern.compile("<title>(.*?)</title>");
String description = "";
if(articleAsset.getDescription() != null) {
Matcher m = p.matcher(articleAsset.getDescription());
if (m.find()) {
description = m.group(1);
description = description.replaceAll("<.*?>", "");
}
}
return description;
}
/**
* get the citation information for powerpoint
* @param article
* @return
*/
private StringBuilder getCitationInfo(Article article) {
List<ArticleAuthor> articleAuthors = article.getAuthors();
List<String> collabAuthors = article.getCollaborativeAuthors();
List<String> authors = new ArrayList<String>(articleAuthors.size() + collabAuthors.size());
for (Iterator<ArticleAuthor> it = articleAuthors.iterator(); it.hasNext();) {
ArticleAuthor author = it.next();
authors.add(author.getSurnames() + " " + toShortFormat(author.getGivenNames()));
}
authors.addAll(collabAuthors);
StringBuilder citation = new StringBuilder();
int maxIndex = Math.min(authors.size(), 4);
for (int i = 0; i < maxIndex - 1; i++) {
String author = authors.get(i);
citation.append(author).append(", ");
}
if(maxIndex > 0 ) {
String lastAuthor = authors.get(maxIndex - 1);
citation.append(lastAuthor);
if (authors.size() > 4) {
citation.append(", et al.");
}
}
//append the year, title, journal, volume, issue and eLocationId information
citation.append(" (").append(new SimpleDateFormat("yyyy").format(article.getDate())).append(") ")
.append(article.getTitle().replaceAll("<.*?>", "")).append(". ")
.append(article.getJournal()).append(" ")
.append(article.getVolume())
.append("(").append(article.getIssue()).append("): ")
.append(article.geteLocationId()).append(". ")
.append("doi:").append(article.getDoi().replaceFirst("info:doi/", ""));
return citation;
}
/**
* Function to format the author names
* @param name
* @return
*/
private String toShortFormat(String name) {
if (name == null)
return null;
String[] givenNames = name.split(" ");
StringBuilder sb = new StringBuilder();
for(String givenName :givenNames) {
if (givenName.length() > 0) {
if(givenName.matches(".*\\p{Pd}\\p{L}.*")) {
// Handle names with dash
String[] sarr = givenName.split("\\p{Pd}");
for (int i = 0; i < sarr.length; i++) {
if (i > 0) {
sb.append('-');
}
if(sarr[i].length() > 0) {
sb.append(sarr[i].charAt(0));
}
}
}
else {
sb.append(givenName.charAt(0));
}
}
}
return sb.toString();
}
@Required
public void setTemplatesDirectory(String templatesDirectory) {
this.templatesDirectory = templatesDirectory;
}
/**
* @param permissionsService the permissions service to use
*/
@Required
public void setPermissionsService(PermissionsService permissionsService) {
this.permissionsService = permissionsService;
}
/**
* @param articleService the article service to use
*/
@Required
public void setArticleService(ArticleService articleService) {
this.articleService = articleService;
}
/**
* Set the small image representation
*
* @param smallImageRep smallImageRep
*/
public void setSmallImageRep(final String smallImageRep) {
this.smallImageRep = smallImageRep;
}
/**
* Set the medium image representation
*
* @param mediumImageRep mediumImageRep
*/
public void setMediumImageRep(final String mediumImageRep) {
this.mediumImageRep = mediumImageRep;
}
/**
* Set the large image representation
*
* @param largeImageRep largeImageRep
*/
public void setLargeImageRep(final String largeImageRep) {
this.largeImageRep = largeImageRep;
}
@Required
public void setFileStoreService(FileStoreService fileStoreService) {
this.fileStoreService = fileStoreService;
}
public void setSecondaryObjectService(XMLService secondaryObjectService) {
this.secondaryObjectService = secondaryObjectService;
}
}