/**
* <a href="http://www.openolat.org">
* OpenOLAT - Online Learning and Training</a><br>
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); <br>
* you may not use this file except in compliance with the License.<br>
* You may obtain a copy of the License at the
* <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
* <p>
* Unless required by applicable law or agreed to in writing,<br>
* software distributed under the License is distributed on an "AS IS" BASIS, <br>
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
* See the License for the specific language governing permissions and <br>
* limitations under the License.
* <p>
* Initial code contributed and copyrighted by<br>
* frentix GmbH, http://www.frentix.com
* <p>
*/
package org.olat.course.nodes.cl.ui;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import javax.xml.transform.TransformerException;
import org.apache.pdfbox.exceptions.COSVisitorException;
import org.olat.core.gui.media.MediaResource;
import org.olat.core.gui.translator.Translator;
import org.olat.core.id.UserConstants;
import org.olat.core.logging.OLog;
import org.olat.core.logging.Tracing;
import org.olat.core.util.StringHelper;
import org.olat.core.util.pdf.PdfDocument;
import org.olat.course.nodes.cl.model.Checkbox;
import org.olat.course.nodes.cl.model.CheckboxList;
import org.olat.user.propertyhandlers.UserPropertyHandler;
/**
*
* Initial date: 12.02.2014<br>
* @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
*
*/
public class CheckboxPDFExport extends PdfDocument implements MediaResource {
private static final OLog log = Tracing.createLoggerFor(CheckboxPDFExport.class);
private final String filename;
private String courseTitle;
private String courseNodeTitle;
private String groupName;
private String author;
private final Translator translator;
private int firstNameIndex, lastNameIndex, institutionalUserIdentifierIndex;
public CheckboxPDFExport(String filename, Translator translator, List<UserPropertyHandler> userPropertyHandlers)
throws IOException {
super(translator.getLocale());
marginTopBottom = 62.0f;
marginLeftRight = 62.0f;
this.filename = filename;
this.translator = translator;
lastNameIndex = findPropertyIndex(UserConstants.LASTNAME, userPropertyHandlers);
firstNameIndex = findPropertyIndex(UserConstants.FIRSTNAME, userPropertyHandlers);
institutionalUserIdentifierIndex = findPropertyIndex(UserConstants.INSTITUTIONALUSERIDENTIFIER, userPropertyHandlers);
}
private int findPropertyIndex(String propertyName, List<UserPropertyHandler> userPropertyHandlers) {
int i=0;
int index = -1;
for(UserPropertyHandler userPropertyHandler:userPropertyHandlers) {
if(propertyName.equals(userPropertyHandler.getName())) {
index = i;
}
i++;
}
return index;
}
public String getCourseTitle() {
return courseTitle;
}
public void setCourseTitle(String courseTitle) {
this.courseTitle = courseTitle;
}
public String getCourseNodeTitle() {
return courseNodeTitle;
}
public void setCourseNodeTitle(String courseNodeTitle) {
this.courseNodeTitle = courseNodeTitle;
}
public String getGroupName() {
return groupName;
}
public void setGroupName(String groupName) {
this.groupName = groupName;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
@Override
public boolean acceptRanges() {
return false;
}
@Override
public String getContentType() {
return "application/pdf";
}
@Override
public Long getSize() {
return null;
}
@Override
public InputStream getInputStream() {
return null;
}
@Override
public Long getLastModified() {
return null;
}
@Override
public void prepare(HttpServletResponse hres) {
try {
hres.setHeader("Content-Disposition","attachment; filename*=UTF-8''" + StringHelper.urlEncodeUTF8(filename));
hres.setHeader("Content-Description",StringHelper.urlEncodeUTF8(filename));
document.save(hres.getOutputStream());
} catch (COSVisitorException | IOException e) {
log.error("", e);
}
}
@Override
public void release() {
try {
close();
} catch (IOException e) {
log.error("", e);
}
}
public void create(CheckboxList checkboxList, List<CheckListAssessmentRow> rows)
throws IOException, COSVisitorException, TransformerException {
addPage();
addMetadata(courseNodeTitle, courseTitle, author);
if(StringHelper.containsNonWhitespace(courseTitle)) {
addParagraph(courseTitle, 16, true, width);
}
if(StringHelper.containsNonWhitespace(courseNodeTitle)) {
addParagraph(courseNodeTitle, 14, true, width);
}
if(StringHelper.containsNonWhitespace(groupName)) {
String prefix = translator.translate("participants");
addParagraph(prefix + ": " + groupName, 14, true, width);
}
float cellMargin = 5.0f;
float headerMaxSize = 0.0f;
float fontSize = 10.0f;
for(Checkbox box:checkboxList.getList()) {
headerMaxSize = Math.max(headerMaxSize, getStringWidth(box.getTitle(), fontSize));
}
String[] headers = getHeaders(checkboxList);
String[][] content = getRows(checkboxList, rows);
float nameMaxSize = 0.0f;
for(String[] row:content) {
float nameWidth = getStringWidth(row[0], fontSize);
nameMaxSize = Math.max(nameMaxSize, nameWidth);
}
nameMaxSize = Math.min(nameMaxSize, 150f);
int numOfRows = content.length;
for(int offset=0; offset<numOfRows; ) {
offset += drawTable(headers, content, offset, headerMaxSize, nameMaxSize, fontSize, cellMargin);
closePage();
if(offset<numOfRows) {
addPage();
}
}
addPageNumbers();
}
private String[][] getRows(CheckboxList checkboxList, List<CheckListAssessmentRow> rows) {
int numOfRows = rows.size();
List<Checkbox> boxList = checkboxList.getList();
int numOfCheckbox = boxList.size();
String[][] content = new String[numOfRows][];
for(int i=0; i<numOfRows; i++) {
CheckListAssessmentRow row = rows.get(i);
content[i] = new String[numOfCheckbox + 2];
content[i][0] = getName(row);
for(int j=0; j<numOfCheckbox; j++) {
Boolean[] checked = row.getChecked();
if(checked != null && j >= 0 && j < checked.length) {
Boolean check = checked[j];
if(check != null && check.booleanValue()) {
content[i][j+1] = "x";
}
}
}
}
return content;
}
private String getName(CheckListAssessmentRow view) {
StringBuilder sb = new StringBuilder();
if(lastNameIndex >= 0) {
sb.append(view.getIdentityProp(lastNameIndex));
}
if(firstNameIndex >= 0) {
if(sb.length() > 0) sb.append(", ");
sb.append(view.getIdentityProp(firstNameIndex));
}
if(institutionalUserIdentifierIndex >= 0) {
String val = view.getIdentityProp(institutionalUserIdentifierIndex);
if(StringHelper.containsNonWhitespace(val)) {
if(sb.length() > 0) sb.append(", ");
sb.append(val);
}
}
return sb.toString();
}
private String[] getHeaders(CheckboxList checkboxList) {
int numOfCheckbox = checkboxList.getList().size();
String[] headers = new String[numOfCheckbox + 2];
headers[0] = translator.translate("participants");
int pos = 1;
for(Checkbox box:checkboxList.getList()) {
headers[pos++] = box.getTitle();
}
headers[numOfCheckbox + 1] = translator.translate("signature");
return headers;
}
public int drawTable(String[] headers, String[][] content, int offset,
float maxHeaderSize, float nameMaxSize, float fontSize, float cellMargin)
throws IOException {
float tableWidth = width;
int cols = content[0].length;
float headerHeight = maxHeaderSize + (2*cellMargin);
float rowHeight = (lineHeightFactory * fontSize) + (2 * cellMargin);
float nameMaxSizeWithMargin = nameMaxSize + (2 * cellMargin);
float availableHeight = currentY - marginTopBottom - headerHeight;
float[] rowHeights = new float[content.length];
float usedHeight = 0.0f;
int possibleRows = 0;
for(int i = offset; i < content.length; i++) {
String[] names = content[i];
String name = names[0];
float nameWidth = getStringWidth(name, fontSize);
float nameHeight;
if(nameWidth > nameMaxSize) {
nameHeight = rowHeight + (lineHeightFactory * fontSize);
} else {
nameHeight = rowHeight;
}
if((usedHeight + nameHeight) > availableHeight) {
break;
}
usedHeight += nameHeight;
rowHeights[i] = nameHeight;
possibleRows++;
}
int end = Math.min(offset + possibleRows, content.length);
int rows = end - offset;
float tableHeight = usedHeight + headerHeight;
float colWidth = (tableWidth - (100 + nameMaxSizeWithMargin)) / (cols - 2.0f);
// draw the horizontal line of the rows
float y = currentY;
float nexty = currentY;
drawLine(marginLeftRight, nexty, marginLeftRight + tableWidth, nexty, 0.5f);
nexty -= headerHeight;
for (int i =offset; i < end; i++) {
drawLine(marginLeftRight, nexty, marginLeftRight + tableWidth, nexty, 0.5f);
nexty -= rowHeights[i];
}
drawLine(marginLeftRight, nexty, marginLeftRight + tableWidth, nexty, 0.5f);
// draw the vertical line of the columns
float nextx = marginLeftRight;
drawLine(nextx, y, nextx, y - tableHeight, 0.5f);
nextx += nameMaxSizeWithMargin;
for (int i=1; i<=cols-2; i++) {
drawLine(nextx, y, nextx, y - tableHeight, 0.5f);
nextx += colWidth;
}
drawLine(nextx, y, nextx, y - tableHeight, 0.5f);
nextx += 100; // signature
drawLine(nextx, y, nextx, y - tableHeight, 0.5f);
// now add the text
// draw the headers
float textx = marginLeftRight + cellMargin;
float texty = currentY;
int lastColIndex = cols -1;
for (int h=0; h<cols; h++) {
String text = headers[h];
if(text == null) {
text = "";
}
currentContentStream.beginText();
currentContentStream.setFont(font, fontSize);
if (h == 0 || (h == lastColIndex)) {
currentContentStream.moveTextPositionByAmount(textx, texty - headerHeight + cellMargin);
textx += nameMaxSizeWithMargin;
} else {
currentContentStream.setTextRotation(3 * (Math.PI / 2), textx + cellMargin, texty - cellMargin);
textx += colWidth;
}
currentContentStream.drawString(text);
currentContentStream.endText();
}
currentY -= headerHeight;
//draw the content
textx = marginLeftRight + cellMargin;
texty = currentY - 15;
for (int i=offset; i<end; i++) {
String[] rowContent = content[i];
if(rowContent == null) continue;
for (int j = 0; j < cols; j++) {
String text = rowContent[j];
float cellWidth = (j==0 ? nameMaxSizeWithMargin : colWidth);
float textWidth = (j==0 ? nameMaxSize : colWidth);
if(text != null) {
if(rowHeights[i] > rowHeight + 1) {
//can do 2 lines
String[] texts = splitText(text, textWidth, fontSize);
float lineTexty = texty;
for(int k=0; k<2 && k<texts.length; k++) {
String textLine = texts[k];
currentContentStream.beginText();
currentContentStream.setFont(font, fontSize);
currentContentStream.moveTextPositionByAmount(textx, lineTexty);
currentContentStream.drawString(textLine);
currentContentStream.endText();
lineTexty -= (lineHeightFactory * fontSize);
}
} else {
currentContentStream.beginText();
currentContentStream.setFont(font, fontSize);
currentContentStream.moveTextPositionByAmount(textx, texty);
currentContentStream.drawString(text);
currentContentStream.endText();
}
}
textx += cellWidth;
}
texty -= rowHeights[i];
textx = marginLeftRight + cellMargin;
}
return rows;
}
/**
* The number 6 was chosen after some trial and errors. It's a good compromise as
* the width of the letter is not fixed. Don't replace ... with ellipsis, it break
* the PDF.
*
* @param text
* @param maxWidth
* @param fontSize
* @return
* @throws IOException
*/
private String[] splitText(String text, float maxWidth, float fontSize) throws IOException {
float textWidth = getStringWidth(text, fontSize);
if(maxWidth < textWidth) {
float letterWidth = textWidth / text.length();
int maxNumOfLetter = Math.round(maxWidth / letterWidth) - 1;
//use space and comma as separator to gentle split the text
int indexBefore = findBreakBefore(text, maxNumOfLetter);
if(indexBefore < (maxNumOfLetter / 2)) {
indexBefore = -1;//use more place
}
String one, two;
if(indexBefore <= 0) {
//one word
indexBefore = Math.min(text.length(), maxNumOfLetter - 6);
one = text.substring(0, indexBefore) + "...";
int indexAfter = findBreakAfter(text, maxNumOfLetter);
if(indexAfter <= 0) {
two = text.substring(indexBefore);
} else {
two = text.substring(indexAfter);
}
} else {
one = text.substring(0, indexBefore + 1);
two = text.substring(indexBefore + 1);
}
if(two.length() > maxNumOfLetter) {
two = two.substring(0, maxNumOfLetter - 6) + "...";
}
return new String[] { one.trim(), two.trim() };
}
return new String[]{ text };
}
public static int findBreakBefore(String line, int start) {
start = Math.min(line.length(), start);
for (int i = start; i >= 0; --i) {
char c = line.charAt(i);
if (Character.isWhitespace(c) || c == '-' || c == ',') {
return i;
}
}
return -1;
}
public static int findBreakAfter(String line, int start) {
int len = line.length();
for (int i = start; i < len; ++i) {
char c = line.charAt(i);
if (Character.isWhitespace(c) || c == ',') {
if(i + 1 < line.length()) {
return i + 1;
}
return i;
}
}
return -1;
}
}