Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/release-notes/2026.10.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

- Add a Markdown preview tab to the log entry editor.
- Add the ability to maximize the log entry editor.
- Add PDF downloads for individual log entries and complete log documents, including metadata, sections, replies, Markdown formatting, referenced images, and attachment links.
- Add an All Logbooks page for viewing and filtering log documents across every logbook type.
- Add a default owner group setting for each logbook type. New log documents inherit the configured owner group.
- Improve logbook selection and document lists with hierarchy-aware names and filtering.
Expand Down
Binary file not shown.
Binary file added src/java/LogrPortal/lib/graphics2d-0.32.jar
Binary file not shown.
Binary file added src/java/LogrPortal/lib/jsoup-1.15.4.jar
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file added src/java/LogrPortal/lib/xmpbox-2.0.24.jar
Binary file not shown.
12 changes: 12 additions & 0 deletions src/java/LogrPortal/nbproject/build-impl.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1044,6 +1044,12 @@ exists or setup the property manually. For example like this:
<copyfiles files="${file.reference.flexmark-ext-tables-0.64.8.jar}" iftldtodir="${build.web.dir}/WEB-INF" todir="${dist.ear.dir}/lib"/>
<copyfiles files="${file.reference.jakarta.resource-api-2.1.0.jar}" iftldtodir="${build.web.dir}/WEB-INF" todir="${dist.ear.dir}/lib"/>
<copyfiles files="${file.reference.mqtt-jca-api-1.0.0.jar}" iftldtodir="${build.web.dir}/WEB-INF" todir="${dist.ear.dir}/lib"/>
<copyfiles files="${file.reference.flexmark-pdf-converter-0.64.8.jar}" iftldtodir="${build.web.dir}/WEB-INF" todir="${dist.ear.dir}/lib"/>
<copyfiles files="${file.reference.graphics2d-0.32.jar}" iftldtodir="${build.web.dir}/WEB-INF" todir="${dist.ear.dir}/lib"/>
<copyfiles files="${file.reference.jsoup-1.15.4.jar}" iftldtodir="${build.web.dir}/WEB-INF" todir="${dist.ear.dir}/lib"/>
<copyfiles files="${file.reference.openhtmltopdf-core-1.0.10.jar}" iftldtodir="${build.web.dir}/WEB-INF" todir="${dist.ear.dir}/lib"/>
<copyfiles files="${file.reference.xmpbox-2.0.24.jar}" iftldtodir="${build.web.dir}/WEB-INF" todir="${dist.ear.dir}/lib"/>
<copyfiles files="${file.reference.openhtmltopdf-pdfbox-1.0.10.jar}" iftldtodir="${build.web.dir}/WEB-INF" todir="${dist.ear.dir}/lib"/>
<mkdir dir="${build.web.dir}/META-INF"/>
<manifest file="${build.web.dir}/META-INF/MANIFEST.MF" mode="update"/>
</target>
Expand Down Expand Up @@ -1110,6 +1116,12 @@ exists or setup the property manually. For example like this:
<copyfiles files="${file.reference.flexmark-ext-tables-0.64.8.jar}" todir="${build.web.dir}/WEB-INF/lib"/>
<copyfiles files="${file.reference.jakarta.resource-api-2.1.0.jar}" todir="${build.web.dir}/WEB-INF/lib"/>
<copyfiles files="${file.reference.mqtt-jca-api-1.0.0.jar}" todir="${build.web.dir}/WEB-INF/lib"/>
<copyfiles files="${file.reference.flexmark-pdf-converter-0.64.8.jar}" todir="${build.web.dir}/WEB-INF/lib"/>
<copyfiles files="${file.reference.graphics2d-0.32.jar}" todir="${build.web.dir}/WEB-INF/lib"/>
<copyfiles files="${file.reference.jsoup-1.15.4.jar}" todir="${build.web.dir}/WEB-INF/lib"/>
<copyfiles files="${file.reference.openhtmltopdf-core-1.0.10.jar}" todir="${build.web.dir}/WEB-INF/lib"/>
<copyfiles files="${file.reference.xmpbox-2.0.24.jar}" todir="${build.web.dir}/WEB-INF/lib"/>
<copyfiles files="${file.reference.openhtmltopdf-pdfbox-1.0.10.jar}" todir="${build.web.dir}/WEB-INF/lib"/>
</target>
<target depends="init" if="dist.ear.dir" name="-clean-webinf-lib">
<delete dir="${build.web.dir}/WEB-INF/lib"/>
Expand Down
4 changes: 2 additions & 2 deletions src/java/LogrPortal/nbproject/genfiles.properties
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ build.xml.script.CRC32=67492cbd
[email protected]
# This file is used by a NetBeans-based IDE to track changes in generated files such as build-impl.xml.
# Do not edit this file. You may delete it but then the IDE will never regenerate such files for you.
nbproject/build-impl.xml.data.CRC32=c3c1315b
nbproject/build-impl.xml.script.CRC32=f37f2b81
nbproject/build-impl.xml.data.CRC32=d6be2672
nbproject/build-impl.xml.script.CRC32=23affe2b
nbproject/[email protected]
nbproject/rest-build.xml.data.CRC32=83e13ee9
nbproject/rest-build.xml.script.CRC32=0d1fb1b4
Expand Down
14 changes: 13 additions & 1 deletion src/java/LogrPortal/nbproject/project.properties
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ file.reference.dm-base-3.3.1.jar=lib/dm-base-3.3.1.jar
file.reference.ejb-api-3.0.jar=lib/ejb-api-3.0.jar
file.reference.flexmark-0.64.8.jar=lib/flexmark-0.64.8.jar
file.reference.flexmark-ext-tables-0.64.8.jar=lib/flexmark-ext-tables-0.64.8.jar
file.reference.flexmark-pdf-converter-0.64.8.jar=lib/flexmark-pdf-converter-0.64.8.jar
file.reference.flexmark-util-ast-0.64.8.jar=lib/flexmark-util-ast-0.64.8.jar
file.reference.flexmark-util-builder-0.64.8.jar=lib/flexmark-util-builder-0.64.8.jar
file.reference.flexmark-util-collection-0.64.8.jar=lib/flexmark-util-collection-0.64.8.jar
Expand All @@ -60,6 +61,7 @@ file.reference.flexmark-util-sequence-0.64.8.jar=lib/flexmark-util-sequence-0.64
file.reference.flexmark-util-visitor-0.64.8.jar=lib/flexmark-util-visitor-0.64.8.jar
file.reference.font-awesome-4.7.0.jar=lib/font-awesome-4.7.0.jar
file.reference.fontbox-2.0.24.jar=lib/fontbox-2.0.24.jar
file.reference.graphics2d-0.32.jar=lib/graphics2d-0.32.jar
file.reference.gson-2.10.1.jar=lib/gson-2.10.1.jar
file.reference.itextpdf-5.5.13.1.jar=lib/itextpdf-5.5.13.1.jar
file.reference.jackson-annotations-2.11.2.jar=lib/jackson-annotations-2.11.2.jar
Expand All @@ -76,12 +78,15 @@ file.reference.javax.inject-1.jar=lib/javax.inject-1.jar
file.reference.javax.servlet-api-4.0.1.jar=lib/javax.servlet-api-4.0.1.jar
file.reference.javax.xml.soap-api-1.4.0.jar=lib/javax.xml.soap-api-1.4.0.jar
file.reference.jersey-media-json-jackson-2.28.jar=lib/jersey-media-json-jackson-2.28.jar
file.reference.jsoup-1.15.4.jar=lib/jsoup-1.15.4.jar
file.reference.libphonenumber-8.12.3.jar=lib/libphonenumber-8.12.3.jar
file.reference.log4j-api-2.25.3.jar=lib/log4j-api-2.25.3.jar
file.reference.log4j-core-2.25.3.jar=lib/log4j-core-2.25.3.jar
file.reference.metadata-extractor-2.17.0.jar=lib/metadata-extractor-2.17.0.jar
file.reference.mqtt-jca-api-1.0.0.jar=lib/mqtt-jca-api-1.0.0.jar
file.reference.omnifaces-3.10.1.jar-1=lib/omnifaces-3.10.1.jar
file.reference.openhtmltopdf-core-1.0.10.jar=lib/openhtmltopdf-core-1.0.10.jar
file.reference.openhtmltopdf-pdfbox-1.0.10.jar=lib/openhtmltopdf-pdfbox-1.0.10.jar
file.reference.pdfbox-2.0.24.jar=lib/pdfbox-2.0.24.jar
file.reference.poi-4.0.1.jar=lib/poi-4.0.1.jar
file.reference.poi-ooxml-4.0.1.jar=lib/poi-ooxml-4.0.1.jar
Expand All @@ -98,6 +103,7 @@ file.reference.commons-collections4-4.2.jar=lib/commons-collections4-4.2.jar
file.reference.commons-compress-1.18.jar=lib/commons-compress-1.18.jar
file.reference.slf4j-api-1.7.26.jar=lib/slf4j-api-1.7.26.jar
file.reference.snakeyaml-1.24.jar=lib/snakeyaml-1.24.jar
file.reference.xmpbox-2.0.24.jar=lib/xmpbox-2.0.24.jar
file.reference.xmpcore-6.1.10.jar=lib/xmpcore-6.1.10.jar
includes=**
j2ee.compile.on.save=false
Expand Down Expand Up @@ -174,7 +180,13 @@ javac.classpath=\
${file.reference.flexmark-util-html-0.64.8.jar}:\
${file.reference.flexmark-ext-tables-0.64.8.jar}:\
${file.reference.jakarta.resource-api-2.1.0.jar}:\
${file.reference.mqtt-jca-api-1.0.0.jar}
${file.reference.mqtt-jca-api-1.0.0.jar}:\
${file.reference.flexmark-pdf-converter-0.64.8.jar}:\
${file.reference.graphics2d-0.32.jar}:\
${file.reference.jsoup-1.15.4.jar}:\
${file.reference.openhtmltopdf-core-1.0.10.jar}:\
${file.reference.xmpbox-2.0.24.jar}:\
${file.reference.openhtmltopdf-pdfbox-1.0.10.jar}
# Space-separated list of extra javac options
javac.compilerargs=
javac.debug=true
Expand Down
24 changes: 24 additions & 0 deletions src/java/LogrPortal/nbproject/project.xml
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,30 @@
<file>${file.reference.mqtt-jca-api-1.0.0.jar}</file>
<path-in-war>WEB-INF/lib</path-in-war>
</library>
<library dirs="200">
<file>${file.reference.flexmark-pdf-converter-0.64.8.jar}</file>
<path-in-war>WEB-INF/lib</path-in-war>
</library>
<library dirs="200">
<file>${file.reference.graphics2d-0.32.jar}</file>
<path-in-war>WEB-INF/lib</path-in-war>
</library>
<library dirs="200">
<file>${file.reference.jsoup-1.15.4.jar}</file>
<path-in-war>WEB-INF/lib</path-in-war>
</library>
<library dirs="200">
<file>${file.reference.openhtmltopdf-core-1.0.10.jar}</file>
<path-in-war>WEB-INF/lib</path-in-war>
</library>
<library dirs="200">
<file>${file.reference.xmpbox-2.0.24.jar}</file>
<path-in-war>WEB-INF/lib</path-in-war>
</library>
<library dirs="200">
<file>${file.reference.openhtmltopdf-pdfbox-1.0.10.jar}</file>
<path-in-war>WEB-INF/lib</path-in-war>
</library>
</web-module-libraries>
<web-module-additional-libraries/>
<source-roots>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import gov.anl.aps.logr.portal.model.db.entities.SettingType;
import gov.anl.aps.logr.portal.model.db.entities.UserInfo;
import gov.anl.aps.logr.portal.model.db.utilities.EntityInfoUtility;
import gov.anl.aps.logr.portal.utilities.LogbookPdfExportUtility;
import gov.anl.aps.logr.portal.utilities.MarkdownParser;
import gov.anl.aps.logr.portal.utilities.SearchResult;
import gov.anl.aps.logr.portal.utilities.SessionUtility;
Expand Down Expand Up @@ -74,6 +75,7 @@
import javax.inject.Named;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.primefaces.model.StreamedContent;

/**
* Controller that provides functionality to create, edit, delete, and view
Expand Down Expand Up @@ -189,6 +191,26 @@ public static ItemDomainLogbookController getInstance() {
return (ItemDomainLogbookController) SessionUtility.findBean(controllerNamed);
}

public StreamedContent downloadCurrentDocumentPdf() {
try {
return LogbookPdfExportUtility.exportDocument(getCurrent());
} catch (Exception ex) {
logger.error("Could not export log document PDF", ex);
SessionUtility.addErrorMessage("PDF Export Error", "Could not create the log document PDF.");
return null;
}
}

public StreamedContent downloadLogEntryPdf(Log log) {
try {
return LogbookPdfExportUtility.exportEntry(getCurrent(), log);
} catch (Exception ex) {
logger.error("Could not export log entry PDF", ex);
SessionUtility.addErrorMessage("PDF Export Error", "Could not create the log entry PDF.");
return null;
}
}

@Override
public ItemDomainLogbookLazyDataModel createItemLazyDataModel() {
return new ItemDomainLogbookLazyDataModel(itemDomainLogbookFacade, getDefaultDomain(), settingObject);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
/*
* Copyright (c) UChicago Argonne, LLC. All rights reserved.
* See LICENSE file.
*/
package gov.anl.aps.logr.portal.utilities;

import com.vladsch.flexmark.pdf.converter.PdfConverterExtension;
import com.vladsch.flexmark.util.data.MutableDataSet;
import gov.anl.aps.logr.portal.model.db.entities.EntityInfo;
import gov.anl.aps.logr.portal.model.db.entities.ItemDomainLogbook;
import gov.anl.aps.logr.portal.model.db.entities.Log;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import org.primefaces.model.DefaultStreamedContent;
import org.primefaces.model.StreamedContent;

/** Builds PDF downloads for log entries and log documents. */
public final class LogbookPdfExportUtility {

private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("MM/dd/yyyy hh:mm a");

private LogbookPdfExportUtility() {
}

public static StreamedContent exportEntry(ItemDomainLogbook document, Log log) throws IOException {
byte[] pdf = renderPdf(buildEntryHtml(document, log));
return stream(pdf, getEntryFilename(log));
}

public static StreamedContent exportDocument(ItemDomainLogbook document) throws IOException {
byte[] pdf = renderPdf(buildDocumentHtml(document));
return stream(pdf, getDocumentFilename(document));
}

static String buildEntryHtml(ItemDomainLogbook document, Log log) {
StringBuilder html = startHtml("Log Entry " + log.getId());
html.append("<div class=\"document-metadata\">");
appendMetadata(html, "Document ID", document.getId());
html.append("</div>");
appendEntry(html, log, false);
return finishHtml(html);
}

static String buildDocumentHtml(ItemDomainLogbook document) {
StringBuilder html = startHtml(document.getName());
html.append("<h1>").append(escape(document.getName())).append("</h1>");
appendDocumentMetadata(html, document);

List<ItemDomainLogbook> sections = document.getLogbookSections();
if (sections == null || sections.isEmpty()) {
appendEntries(html, document.getLogList());
} else {
for (ItemDomainLogbook section : sections) {
html.append("<section class=\"logbook-section\"><h2>")
.append(escape(section.getName())).append("</h2>");
appendEntries(html, section.getLogList());
html.append("</section>");
}
}
return finishHtml(html);
}

private static void appendDocumentMetadata(StringBuilder html, ItemDomainLogbook document) {
EntityInfo info = document.getEntityInfo();
html.append("<div class=\"document-metadata\">");
appendMetadata(html, "Document ID", document.getId());
appendMetadata(html, "Type", document.getEntityTypeString());
if (info != null) {
appendMetadata(html, "Created", format(info.getCreatedOnDateTime()));
appendMetadata(html, "Created by", info.getCreatedByDisplayName());
appendMetadata(html, "Last modified", format(info.getLastModifiedOnDateTime()));
appendMetadata(html, "Last modified by", info.getLastModifiedByDisplayName());
}
html.append("</div>");
}

private static void appendEntries(StringBuilder html, List<Log> entries) {
if (entries == null) {
return;
}
for (Log entry : entries) {
appendEntry(html, entry, false);
List<Log> replies = entry.getChildLogList();
if (replies != null) {
for (Log reply : replies) {
appendEntry(html, reply, true);
}
}
}
}

private static void appendEntry(StringBuilder html, Log log, boolean reply) {
html.append("<article class=\"").append(reply ? "log-entry reply" : "log-entry")
.append("\"><div class=\"entry-metadata\">");
appendMetadata(html, "Entry ID", log.getId());
appendMetadata(html, "Added", format(log.getEnteredOnDateTime()));
appendMetadata(html, "Author", log.getEnteredByUser() == null ? "" : log.getEnteredByUser().getUsername());
if (log.getLogTopic() != null) {
appendMetadata(html, "Topic", log.getLogTopic().getName());
}
if (log.isModifiedEntry()) {
appendMetadata(html, "Edited", format(log.getLastModifiedOnDateTime()));
appendMetadata(html, "Edited by", log.getLastModifiedByUser() == null ? "" : log.getLastModifiedByUser().getUsername());
}
html.append("</div><div class=\"entry-body\">")
.append(MarkdownParser.parseMarkdownAsPdfHTML(log.getText()))
.append("</div></article>");
}

private static void appendMetadata(StringBuilder html, String label, Object value) {
if (value != null && !value.toString().isEmpty()) {
html.append("<span><strong>").append(escape(label)).append(":</strong> ")
.append(escape(value.toString())).append("</span> ");
}
}

private static StringBuilder startHtml(String title) {
return new StringBuilder("<!DOCTYPE html><html><head><meta charset=\"UTF-8\"/><title>")
.append(escape(title)).append("</title><style>")
.append(".document-metadata,.entry-metadata{color:#555;font-size:9pt;margin-bottom:8px}")
.append(".entry-metadata{background:#e9ecef;border-left:5px solid #555;padding:7px 9px}")
.append(".document-metadata span,.entry-metadata span{margin-right:12px}")
.append(".log-entry{padding:10px 0;page-break-inside:avoid}")
.append(".reply{margin-left:24px;border-left:3px solid #ddd;padding-left:12px}")
.append("img{max-width:450px;max-height:450px;width:auto;height:auto}")
.append("</style></head><body>");
}

private static String finishHtml(StringBuilder html) {
return html.append("</body></html>").toString();
}

static byte[] renderPdf(String html) throws IOException {
ByteArrayOutputStream output = new ByteArrayOutputStream();
PdfConverterExtension.exportToPdf(output, html, "", new MutableDataSet());
byte[] pdf = output.toByteArray();
if (pdf.length < 5 || pdf[0] != '%' || pdf[1] != 'P' || pdf[2] != 'D' || pdf[3] != 'F') {
throw new IOException("PDF conversion failed");
}
return pdf;
}

private static StreamedContent stream(byte[] pdf, String filename) {
return DefaultStreamedContent.builder()
.name(filename)
.contentType("application/pdf")
.stream(() -> new java.io.ByteArrayInputStream(pdf))
.build();
}

static String getEntryFilename(Log log) {
return "log-entry-" + (log.getId() == null ? "new" : log.getId()) + ".pdf";
}

static String getDocumentFilename(ItemDomainLogbook document) {
String name = sanitizeFilename(document.getName());
if (name.isEmpty()) {
name = "log-document";
}
return name + "-" + (document.getId() == null ? "new" : document.getId()) + ".pdf";
}

private static String sanitizeFilename(String value) {
return value == null ? "" : value.trim().replaceAll("[^A-Za-z0-9._-]+", "-")
.replaceAll("^-+|-+$", "");
}

private static synchronized String format(Date date) {
return date == null ? "" : DATE_FORMAT.format(date);
}

private static String escape(String value) {
if (value == null) {
return "";
}
return value.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
.replace("\"", "&quot;").replace("'", "&#39;");
}
}
Loading
Loading