/**
* 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.exceptions.COSVisitorException;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.edit.PDPageContentStream;
import org.apache.pdfbox.pdmodel.font.PDFont;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.dspace.authorize.AuthorizeException;
import org.dspace.authorize.AuthorizeManager;
import org.dspace.content.*;
import org.dspace.content.Collection;
import org.dspace.core.ConfigurationManager;
import org.dspace.core.Context;
import org.dspace.handle.HandleManager;
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 CitationDocument {
/**
* Class Logger
*/
private static Logger log = Logger.getLogger(CitationDocument.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.
*/
private static final Set<String> VALID_TYPES = new HashSet<String>(2);
/**
* A set of MIME types that refer to a PDF
*/
private static final Set<String> PDF_MIMES = new HashSet<String>(2);
/**
* A set of MIME types that refer to a JPEG, PNG, or GIF
*/
private static final Set<String> RASTER_MIMES = new HashSet<String>();
/**
* A set of MIME types that refer to a SVG
*/
private static final Set<String> SVG_MIMES = new HashSet<String>();
/**
* Comma separated list of collections handles to enable citation for.
* webui.citation.enabled_collections, default empty/none. ex: =1811/123, 1811/345
*/
private static String citationEnabledCollections = null;
/**
* Comma separated list of community handles to enable citation for.
* webui.citation.enabled_communties, default empty/none. ex: =1811/123, 1811/345
*/
private static String citationEnabledCommunities = null;
/**
* List of all enabled collections, inherited/determined for those under communities.
*/
private static ArrayList<String> citationEnabledCollectionsList;
private static File tempDir;
private static String[] header1;
private static String[] header2;
private static String[] fields;
private static String footer;
static {
// 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
citationEnabledCollections = ConfigurationManager.getProperty("disseminate-citation", "enabled_collections");
citationEnabledCollectionsList = new ArrayList<String>();
if(citationEnabledCollections != null && citationEnabledCollections.length() > 0) {
String[] collectionChunks = citationEnabledCollections.split(",");
for(String collectionString : collectionChunks) {
citationEnabledCollectionsList.add(collectionString.trim());
}
}
//Load enabled communities, and add to collection-list
citationEnabledCommunities = ConfigurationManager.getProperty("disseminate-citation", "enabled_communities");
if(citationEnabledCollectionsList == null) {
citationEnabledCollectionsList = new ArrayList<String>();
}
if(citationEnabledCommunities != null && citationEnabledCommunities.length() > 0) {
try {
String[] communityChunks = citationEnabledCommunities.split(",");
for(String communityString : communityChunks) {
Context context = new Context();
DSpaceObject dsoCommunity = HandleManager.resolveToObject(context, communityString.trim());
if(dsoCommunity instanceof Community) {
Community community = (Community)dsoCommunity;
Collection[] collections = community.getAllCollections();
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());
}
}
// Configurable text/fields, we'll set sane defaults
String header1Config = ConfigurationManager.getProperty("disseminate-citation", "header1");
if(StringUtils.isNotBlank(header1Config)) {
header1 = header1Config.split(",");
} else {
header1 = new String[]{"DSpace Institution", ""};
}
String header2Config = ConfigurationManager.getProperty("disseminate-citation", "header2");
if(StringUtils.isNotBlank(header2Config)) {
header2 = header2Config.split(",");
} else {
header2 = new String[]{"DSpace Repository", "http://dspace.org"};
}
String fieldsConfig = ConfigurationManager.getProperty("disseminate-citation", "fields");
if(StringUtils.isNotBlank(fieldsConfig)) {
fields = fieldsConfig.split(",");
} else {
fields = new String[]{"dc.date.issued", "dc.title", "dc.creator", "dc.contributor.author", "dc.publisher", "_line_", "dc.identifier.citation", "dc.identifier.uri"};
}
String footerConfig = ConfigurationManager.getProperty("disseminate-citation", "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 = ConfigurationManager.getProperty("dspace.dir") + "/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);
}
}
}
public CitationDocument() {}
/**
* Boolean to determine is citation-functionality is enabled globally for entire site.
* config/module/disseminate-citation: enable_globally, default false. true=on, false=off
*/
private static Boolean citationEnabledGlobally = null;
private static boolean isCitationEnabledGlobally() {
if(citationEnabledGlobally == null) {
citationEnabledGlobally = ConfigurationManager.getBooleanProperty("disseminate-citation", "enable_globally", false);
}
return citationEnabledGlobally;
}
private static boolean isCitationEnabledThroughCollection(Bitstream bitstream) throws SQLException {
//Reject quickly if no-enabled collections
if(citationEnabledCollectionsList.size() == 0) {
return false;
}
DSpaceObject owningDSO = bitstream.getParentObject();
if(owningDSO instanceof Item) {
Item item = (Item)owningDSO;
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;
}
/**
* Repository policy can specify to have a custom citation cover/tail page to the document, which embeds metadata.
* We need to determine if we will intercept this bitstream download, and give out a citation dissemination rendition.
*
* What will trigger a redirect/intercept?
* Citation enabled globally (all citable bitstreams will get "watermarked") modules/disseminate-citation: enable_globally
* OR
* The container is this object is whitelist enabled.
* - community: modules/disseminate-citation: enabled_communities
* - collection: modules/disseminate-citation: enabled_collections
* AND
* This User is not an admin. (Admins need to be able to view the "raw" original instead.)
* AND
* This object is citation-able (presently, just PDF)
*
* The module must be enabled, before the permission level checks happen.
* @param bitstream
* @return
*/
public static Boolean isCitationEnabledForBitstream(Bitstream bitstream, Context context) throws SQLException {
if(isCitationEnabledGlobally() || isCitationEnabledThroughCollection(bitstream)) {
boolean adminUser = AuthorizeManager.isAdmin(context);
if(!adminUser && canGenerateCitationVersion(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
*/
private static Boolean citationAsFirstPage = null;
private static Boolean isCitationFirstPage() {
if(citationAsFirstPage == null) {
citationAsFirstPage = ConfigurationManager.getBooleanProperty("disseminate-citation", "citation_as_first_page", true);
}
return citationAsFirstPage;
}
public static boolean canGenerateCitationVersion(Bitstream bitstream) {
return VALID_TYPES.contains(bitstream.getFormat().getMIMEType());
}
/**
* Creates a
* cited document from the given bitstream of the given item. This
* requires that bitstream is contained in item.
* <p>
* The Process for adding a cover page is as follows:
* <ol>
* <li> Load source file into PdfReader and create a
* Document to put our cover page into.</li>
* <li> Create cover page and add content to it.</li>
* <li> Concatenate the coverpage and the source
* document.</li>
* </p>
*
* @param bitstream The source bitstream being cited. This must be a PDF.
* @return The temporary File that is the finished, cited document.
* @throws java.io.FileNotFoundException
* @throws SQLException
* @throws org.dspace.authorize.AuthorizeException
*/
public File makeCitedDocument(Bitstream bitstream)
throws IOException, SQLException, AuthorizeException, COSVisitorException {
PDDocument document = new PDDocument();
PDDocument sourceDocument = new PDDocument();
try {
Item item = (Item) bitstream.getParentObject();
sourceDocument = sourceDocument.load(bitstream.retrieve());
PDPage coverPage = new PDPage(PDPage.PAGE_SIZE_LETTER);
generateCoverPage(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();
}
}
private void generateCoverPage(PDDocument document, PDPage coverPage, Item item) throws IOException, COSVisitorException {
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(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(item.getMetadata(field))) {
ypos = drawStringWordWrap(coverPage, contentStream, item.getMetadata(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();
}
}
private void addCoverPageToDocument(PDDocument document, PDDocument sourceDocument, PDPage coverPage) {
List<PDPage> sourcePageList = sourceDocument.getDocumentCatalog().getAllPages();
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);
}
sourcePageList.clear();
}
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.findMediaBox();
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;
}
public String getOwningCommunity(Item item) {
try {
Community[] comms = item.getCommunities();
if(comms.length > 0) {
return comms[0].getName();
} else {
return " ";
}
} catch (SQLException e) {
log.error(e.getMessage());
return e.getMessage();
}
}
public String getOwningCollection(Item item) {
try {
return item.getOwningCollection().getName();
} catch (SQLException e) {
log.error(e.getMessage());
return e.getMessage();
}
}
public String getAllMetadataSeparated(Item item, String metadataKey) {
Metadatum[] dcValues = item.getMetadataByMetadataString(metadataKey);
ArrayList<String> valueArray = new ArrayList<String>();
for(Metadatum dcValue : dcValues) {
if(StringUtils.isNotBlank(dcValue.value)) {
valueArray.add(dcValue.value);
}
}
return StringUtils.join(valueArray.toArray(), "; ");
}
/**
* @param page
* @param contentStream
* @param y the y-coordinate of the first row
* @param margin the padding on left and right of table
* @param content a 2d array containing the table data
* @throws IOException
*/
public static 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.findMediaBox().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;
}
}
}