/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.disseminate;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageTree;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.font.PDFont;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.dspace.authorize.AuthorizeException;
import org.dspace.authorize.service.AuthorizeService;
import org.dspace.content.*;
import org.dspace.content.Collection;
import org.dspace.content.service.BitstreamService;
import org.dspace.content.service.CommunityService;
import org.dspace.content.service.ItemService;
import org.dspace.core.Context;
import org.dspace.disseminate.service.CitationDocumentService;
import org.dspace.handle.service.HandleService;
import org.dspace.services.ConfigurationService;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import java.awt.*;
import java.io.*;
import java.sql.SQLException;
import java.util.*;
import java.util.List;
/**
* The Citation Document produces a dissemination package (DIP) that is different that the archival package (AIP).
* In this case we append the descriptive metadata to the end (configurable) of the document. i.e. last page of PDF.
* So instead of getting the original PDF, you get a cPDF (with citation information added).
*
* @author Peter Dietz (peter@longsight.com)
*/
public class CitationDocumentServiceImpl implements CitationDocumentService, InitializingBean {
/**
* Class Logger
*/
private static Logger log = Logger.getLogger(CitationDocumentServiceImpl.class);
/**
* A set of MIME types that can have a citation page added to them. That is,
* MIME types in this set can be converted to a PDF which is then prepended
* with a citation page.
*/
protected final Set<String> VALID_TYPES = new HashSet<String>(2);
/**
* A set of MIME types that refer to a PDF
*/
protected final Set<String> PDF_MIMES = new HashSet<String>(2);
/**
* A set of MIME types that refer to a JPEG, PNG, or GIF
*/
protected final Set<String> RASTER_MIMES = new HashSet<String>();
/**
* A set of MIME types that refer to a SVG
*/
protected final Set<String> SVG_MIMES = new HashSet<String>();
/**
* List of all enabled collections, inherited/determined for those under communities.
*/
protected List<String> citationEnabledCollectionsList;
protected File tempDir;
protected String[] header1;
protected String[] header2;
protected String[] fields;
protected String footer;
@Autowired(required = true)
protected AuthorizeService authorizeService;
@Autowired(required = true)
protected BitstreamService bitstreamService;
@Autowired(required = true)
protected CommunityService communityService;
@Autowired(required = true)
protected ItemService itemService;
@Autowired(required = true)
protected ConfigurationService configurationService;
@Autowired(required = true)
protected HandleService handleService;
@Override
public void afterPropertiesSet() throws Exception {
// Add valid format MIME types to set. This could be put in the Schema
// instead.
//Populate RASTER_MIMES
SVG_MIMES.add("image/jpeg");
SVG_MIMES.add("image/pjpeg");
SVG_MIMES.add("image/png");
SVG_MIMES.add("image/gif");
//Populate SVG_MIMES
SVG_MIMES.add("image/svg");
SVG_MIMES.add("image/svg+xml");
//Populate PDF_MIMES
PDF_MIMES.add("application/pdf");
PDF_MIMES.add("application/x-pdf");
//Populate VALID_TYPES
VALID_TYPES.addAll(PDF_MIMES);
//Load enabled collections
String[] citationEnabledCollections = configurationService.getArrayProperty("citation-page.enabled_collections");
citationEnabledCollectionsList = Arrays.asList(citationEnabledCollections);
//Load enabled communities, and add to collection-list
String[] citationEnabledCommunities = configurationService.getArrayProperty("citation-page.enabled_communities");
if(citationEnabledCollectionsList == null) {
citationEnabledCollectionsList = new ArrayList<String>();
}
if(citationEnabledCommunities != null && citationEnabledCommunities.length > 0)
{
Context context = null;
try
{
context = new Context();
for(String communityString : citationEnabledCommunities) {
DSpaceObject dsoCommunity = handleService.resolveToObject(context, communityString.trim());
if(dsoCommunity instanceof Community) {
Community community = (Community)dsoCommunity;
List<Collection> collections = communityService.getAllCollections(context, community);
for(Collection collection : collections) {
citationEnabledCollectionsList.add(collection.getHandle());
}
} else {
log.error("Invalid community for citation.enabled_communities, value:" + communityString.trim());
}
}
} catch (SQLException e) {
log.error(e.getMessage());
}
finally {
if (context!=null)
context.abort();
}
}
// Configurable text/fields, we'll set sane defaults
header1 = configurationService.getArrayProperty("citation-page.header1");
if (header1==null || header1.length==0)
{
header1 = new String[]{"DSpace Institution", ""};
}
header2 = configurationService.getArrayProperty("citation-page.header2");
if (header2==null || header2.length==0)
{
header2 = new String[]{"DSpace Repository", "http://dspace.org"};
}
fields = configurationService.getArrayProperty("citation-page.fields");
if (fields==null || fields.length==0)
{
fields = new String[]{"dc.date.issued", "dc.title", "dc.creator", "dc.contributor.author", "dc.publisher", "_line_", "dc.identifier.citation", "dc.identifier.uri"};
}
String footerConfig = configurationService.getProperty("citation-page.footer");
if(StringUtils.isNotBlank(footerConfig)) {
footer = footerConfig;
} else {
footer = "Downloaded from DSpace Repository, DSpace Institution's institutional repository";
}
//Ensure a temp directory is available
String tempDirString = configurationService.getProperty("dspace.dir") + File.separator + "temp";
tempDir = new File(tempDirString);
if(!tempDir.exists()) {
boolean success = tempDir.mkdir();
if(success) {
log.info("Created temp directory at: " + tempDirString);
} else {
log.info("Unable to create temp directory at: " + tempDirString);
}
}
}
protected CitationDocumentServiceImpl() {}
/**
* Boolean to determine is citation-functionality is enabled globally for entire site.
* config/module/citation-page: enable_globally, default false. true=on, false=off
*/
protected Boolean citationEnabledGlobally = null;
protected boolean isCitationEnabledGlobally() {
if(citationEnabledGlobally == null) {
citationEnabledGlobally = configurationService.getBooleanProperty("citation-page.enable_globally", false);
}
return citationEnabledGlobally;
}
protected boolean isCitationEnabledThroughCollection(Context context, Bitstream bitstream) throws SQLException {
//Reject quickly if no-enabled collections
if(citationEnabledCollectionsList.size() == 0) {
return false;
}
DSpaceObject owningDSO = bitstreamService.getParentObject(context, bitstream);
if(owningDSO instanceof Item) {
Item item = (Item)owningDSO;
List<Collection> collections = item.getCollections();
for(Collection collection : collections) {
if(citationEnabledCollectionsList.contains(collection.getHandle())) {
return true;
}
}
}
// If previous logic didn't return true, then we're false
return false;
}
@Override
public Boolean isCitationEnabledForBitstream(Bitstream bitstream, Context context) throws SQLException {
if(isCitationEnabledGlobally() || isCitationEnabledThroughCollection(context, bitstream)) {
boolean adminUser = authorizeService.isAdmin(context);
if(!adminUser && canGenerateCitationVersion(context, bitstream)) {
return true;
}
}
// If previous logic didn't return true, then we're false.
return false;
}
/**
* Should the citation page be the first page of the document, or the last page?
* default = true. true = first page, false = last page
* citation_as_first_page=true
*/
protected Boolean citationAsFirstPage = null;
protected Boolean isCitationFirstPage() {
if(citationAsFirstPage == null) {
citationAsFirstPage = configurationService.getBooleanProperty("citation-page.citation_as_first_page", true);
}
return citationAsFirstPage;
}
@Override
public boolean canGenerateCitationVersion(Context context, Bitstream bitstream) throws SQLException
{
return VALID_TYPES.contains(bitstream.getFormat(context).getMIMEType());
}
@Override
public File makeCitedDocument(Context context, Bitstream bitstream)
throws IOException, SQLException, AuthorizeException {
PDDocument document = new PDDocument();
PDDocument sourceDocument = new PDDocument();
try {
Item item = (Item) bitstreamService.getParentObject(context, bitstream);
sourceDocument = sourceDocument.load(bitstreamService.retrieve(context, bitstream));
PDPage coverPage = new PDPage(PDRectangle.LETTER); // TODO: needs to be configurable
generateCoverPage(context, document, coverPage, item);
addCoverPageToDocument(document, sourceDocument, coverPage);
document.save(tempDir.getAbsolutePath() + "/bitstream.cover.pdf");
return new File(tempDir.getAbsolutePath() + "/bitstream.cover.pdf");
} finally {
sourceDocument.close();
document.close();
}
}
protected void generateCoverPage(Context context, PDDocument document, PDPage coverPage, Item item) throws IOException {
PDPageContentStream contentStream = new PDPageContentStream(document, coverPage);
try {
int ypos = 760;
int xpos = 30;
int xwidth = 550;
int ygap = 20;
PDFont fontHelvetica = PDType1Font.HELVETICA;
PDFont fontHelveticaBold = PDType1Font.HELVETICA_BOLD;
PDFont fontHelveticaOblique = PDType1Font.HELVETICA_OBLIQUE;
contentStream.setNonStrokingColor(Color.BLACK);
String[][] content = {header1};
drawTable(coverPage, contentStream, ypos, xpos, content, fontHelveticaBold, 11, false);
ypos -=(ygap);
String[][] content2 = {header2};
drawTable(coverPage, contentStream, ypos, xpos, content2, fontHelveticaBold, 11, false);
ypos -=ygap;
contentStream.fillRect(xpos, ypos, xwidth, 1);
contentStream.closeAndStroke();
String[][] content3 = {{getOwningCommunity(context, item), getOwningCollection(item)}};
drawTable(coverPage, contentStream, ypos, xpos, content3, fontHelvetica, 9, false);
ypos -=ygap;
contentStream.fillRect(xpos, ypos, xwidth, 1);
contentStream.closeAndStroke();
ypos -=(ygap*2);
for(String field : fields) {
field = field.trim();
PDFont font = fontHelvetica;
int fontSize = 11;
if(field.contains("title")) {
fontSize = 26;
ypos -= ygap;
} else if(field.contains("creator") || field.contains("contributor")) {
fontSize = 16;
}
if(field.equals("_line_")) {
contentStream.fillRect(xpos, ypos, xwidth, 1);
contentStream.closeAndStroke();
ypos -=(ygap);
} else if(StringUtils.isNotEmpty(itemService.getMetadata(item, field))) {
ypos = drawStringWordWrap(coverPage, contentStream, itemService.getMetadata(item, field), xpos, ypos, font, fontSize);
}
if(field.contains("title")) {
ypos -=ygap;
}
}
contentStream.beginText();
contentStream.setFont(fontHelveticaOblique, 11);
contentStream.moveTextPositionByAmount(xpos, ypos);
contentStream.drawString(footer);
contentStream.endText();
} finally {
contentStream.close();
}
}
protected void addCoverPageToDocument(PDDocument document, PDDocument sourceDocument, PDPage coverPage) {
PDPageTree sourcePageList = sourceDocument.getDocumentCatalog().getPages();
if (isCitationFirstPage()) {
//citation as cover page
document.addPage(coverPage);
for (PDPage sourcePage : sourcePageList) {
document.addPage(sourcePage);
}
} else {
//citation as tail page
for (PDPage sourcePage : sourcePageList) {
document.addPage(sourcePage);
}
document.addPage(coverPage);
}
}
@Override
public int drawStringWordWrap(PDPage page, PDPageContentStream contentStream, String text,
int startX, int startY, PDFont pdfFont, float fontSize) throws IOException {
float leading = 1.5f * fontSize;
PDRectangle mediabox = page.getMediaBox();
float margin = 72;
float width = mediabox.getWidth() - 2*margin;
List<String> lines = new ArrayList<>();
int lastSpace = -1;
while (text.length() > 0)
{
int spaceIndex = text.indexOf(' ', lastSpace + 1);
if (spaceIndex < 0)
{
lines.add(text);
text = "";
}
else
{
String subString = text.substring(0, spaceIndex);
float size = fontSize * pdfFont.getStringWidth(subString) / 1000;
if (size > width)
{
if (lastSpace < 0) // So we have a word longer than the line... draw it anyways
lastSpace = spaceIndex;
subString = text.substring(0, lastSpace);
lines.add(subString);
text = text.substring(lastSpace).trim();
lastSpace = -1;
}
else
{
lastSpace = spaceIndex;
}
}
}
contentStream.beginText();
contentStream.setFont(pdfFont, fontSize);
contentStream.moveTextPositionByAmount(startX, startY);
int currentY = startY;
for (String line: lines)
{
contentStream.drawString(line);
currentY -= leading;
contentStream.moveTextPositionByAmount(0, -leading);
}
contentStream.endText();
return currentY;
}
@Override
public String getOwningCommunity(Context context, Item item) {
try {
List<Community> comms = itemService.getCommunities(context, item);
if(comms.size() > 0) {
return comms.get(0).getName();
} else {
return " ";
}
} catch (SQLException e) {
log.error(e.getMessage());
return e.getMessage();
}
}
@Override
public String getOwningCollection(Item item) {
return item.getOwningCollection().getName();
}
@Override
public String getAllMetadataSeparated(Item item, String metadataKey) {
List<MetadataValue> dcValues = itemService.getMetadataByMetadataString(item, metadataKey);
ArrayList<String> valueArray = new ArrayList<String>();
for(MetadataValue dcValue : dcValues) {
if(StringUtils.isNotBlank(dcValue.getValue())) {
valueArray.add(dcValue.getValue());
}
}
return StringUtils.join(valueArray.toArray(), "; ");
}
@Override
public void drawTable(PDPage page, PDPageContentStream contentStream,
float y, float margin,
String[][] content, PDFont font, int fontSize, boolean cellBorders) throws IOException {
final int rows = content.length;
final int cols = content[0].length;
final float rowHeight = 20f;
final float tableWidth = page.getMediaBox().getWidth()-(2*margin);
final float tableHeight = rowHeight * rows;
final float colWidth = tableWidth/(float)cols;
final float cellMargin=5f;
if(cellBorders) {
//draw the rows
float nexty = y ;
for (int i = 0; i <= rows; i++) {
contentStream.drawLine(margin,nexty,margin+tableWidth,nexty);
nexty-= rowHeight;
}
//draw the columns
float nextx = margin;
for (int i = 0; i <= cols; i++) {
contentStream.drawLine(nextx,y,nextx,y-tableHeight);
nextx += colWidth;
}
}
//now add the text
contentStream.setFont(font, fontSize);
float textx = margin+cellMargin;
float texty = y-15;
for(int i = 0; i < content.length; i++){
for(int j = 0 ; j < content[i].length; j++){
String text = content[i][j];
contentStream.beginText();
contentStream.moveTextPositionByAmount(textx,texty);
contentStream.drawString(text);
contentStream.endText();
textx += colWidth;
}
texty-=rowHeight;
textx = margin+cellMargin;
}
}
}