Introduction
Document annotation is a critical feature in many enterprise content management systems. Business users often need to review, mark up, redact, approve, or collaborate on documents without changing the original source file. In IBM Content Navigator and Daeja ViewONE Virtual Viewer environments, annotations provide a powerful way to support these workflows while preserving document integrity.
This blog post walks through the implementation of a custom ViewONE Plugin. The plugin demonstrates how to load annotations, save annotations, burn annotations permanently into documents, and support export workflows for PDF and TIFF outputs.
The goal of this implementation is to provide a practical, production-ready foundation for teams that need deeper control over annotation storage, formatting, persistence, and export behavior in a Daeja ViewONE-based document viewing solution.
What the Plugin Does
The ViewONE Plugin provides a complete annotation lifecycle around documents displayed in the ViewONE Virtual Viewer and exporting document.
At a high level, it supports:
- Loading existing annotations for a document
- Creating and editing annotations in the viewer
- Saving annotations to the server
- Preserving ViewONE annotation file formatting requirements
- Burning annotations permanently into the document output
- Exporting annotated documents as PDF or TIFF
- Tracking viewer events for debugging and diagnostics
- Providing a configurable foundation for enterprise deployment
This makes the plugin useful for different processes.
Architecture Overview
The implementation is structured around a simple web application pattern. The browser hosts the ViewONE Virtual Viewer, while JSP handlers manage annotation retrieval, persistence, and burn/export operations on the server side.
The design intentionally separates the viewer interface from the server-side annotation operations. This makes the plugin easier to maintain, debug, and extend.
Technology Stack
The plugin uses a lightweight stack that fits naturally into existing Java-based enterprise environments.
Although the sample implementation uses JSP files, the same pattern can be adapted to servlets, REST APIs, Spring Boot services, or repository-backed persistence layers.
Components:
Virtual-plugin.html is the main viewer interface. It is responsible for launching the ViewONE Virtual Viewer and passing the correct parameters to enable annotation behavior.
This page typically handles:
- Viewer initialization
- Document URL configuration
- Annotation URL configuration
- Save handler configuration
- Burn/export handler configuration
- Viewer event tracking
- Debug console output
A simplified conceptual structure looks like this:
The most important responsibility of this file is to connect the viewer to the correct server-side handlers. In a production environment, this page would usually receive document identifiers, repository identifiers, or case identifiers through request parameters or application context.
load.jsp retrieves the annotation file associated with the document being viewed.
When the viewer opens a document, it needs to know whether annotations already exist. The load handler receives the document reference, locates the matching annotation content, and returns it in the format expected by ViewONE.
Typical responsibilities include:
- Reading the document identifier from the request
- Resolving the annotation file path or repository object
- Validating access to the annotation resource
- Returning annotation content to the viewer
- Handling the case where no annotation exists yet
A simplified flow is shown below:
This component should be kept simple and predictable. The viewer depends on a valid response, even when no annotations exist.
save.jsp persists annotation updates from the viewer.
This is one of the most important parts of the plugin because ViewONE annotation files have formatting requirements that must be preserved. In particular, blank-line formatting can matter. If annotation content is saved incorrectly, the viewer may fail to reload annotations or may display them incorrectly.
The save handler is responsible for:
- Receiving annotation content from the viewer
- Validating the document identifier
- Validating annotation payload size and structure
- Preserving required line breaks and blank lines
- Writing the annotation content to storage
- Returning a success or failure response
A conceptual save flow looks like this:
The formatting behavior is especially important. Annotation persistence is not just a matter of writing raw text to a file. The handler must preserve the annotation file structure expected by the viewer.
burn.jsp handles permanent annotation burning.
Normal annotations are separate from the original document. This is useful because users can edit, hide, or remove them later. However, some business workflows require annotations or redactions to become part of the final exported document.
That is where annotation burning is used.
The burn handler typically supports:
- Taking a source document
- Taking its associated annotation file
- Invoking ViewONE export or burn behavior
- Producing an output document with annotations applied
- Returning the generated PDF or TIFF result
A simplified burn flow looks like this:
This is especially useful for redaction workflows where the final document must permanently include redaction marks before being shared, archived, or sent downstream.
Understanding the ViewONE Annotation File Format
ViewONE uses a proprietary annotation format. The exact content depends on annotation type, page number, coordinates, color, author, timestamp, and other metadata.
A simplified annotation file may contain information such as:
The real format can be more detailed, but the key point is that ViewONE expects the annotation content to follow a specific structure. Line breaks, section boundaries, and blank lines may affect how the viewer parses the file.
For this reason, the save operation should avoid unnecessary transformations such as trimming all whitespace, collapsing blank lines, changing encodings incorrectly, or rewriting content in a way that breaks the expected structure.
Exporting Annotated Documents
A major feature of the plugin is support for exporting documents with annotations.
Depending on business requirements, export behavior may include:
- Exporting the original document without annotations
- Exporting the document with editable annotations
- Exporting the document with annotations burned in
- Exporting PDF output
- Exporting TIFF output
- Applying special behavior for redactions
For example, a document review workflow may allow users to save editable annotations during review. After approval, the same document may be exported with annotations burned into the final PDF.
In another workflow, TIFF output may be needed for compatibility with downstream imaging systems.
The plugin structure makes this behavior easier to manage because export and burn logic is isolated in burn.jsp rather than mixed directly into the viewer page.
Debug Console Integration
A practical annotation plugin should be easy to troubleshoot. Viewer integrations can fail for several reasons, including incorrect document URLs, missing annotation files, invalid server responses, CORS issues, permissions problems, or malformed annotation content.
The implementation includes debug console integration to help diagnose these problems.
Useful debug information may include:
- Document URL being loaded
- Annotation load URL
- Annotation save URL
- Burn/export URL
- Viewer initialization status
- Save response status
- Viewer events
- Error messages returned from server-side handlers
A debug console is especially useful during development and customer issue reproduction because it makes viewer behavior visible without requiring full server-side tracing for every issue.
Viewer Event Tracking
Viewer event tracking provides additional visibility into user and viewer behavior.
Events that may be useful to track include:
- Viewer loaded
- Document opened
- Page changed
- Annotation created
- Annotation modified
- Annotation deleted
- Annotation save started
- Annotation save completed
- Export requested
- Burn operation completed
- Viewer error occurred
This event information can be written to the browser console, displayed in a debug panel, or sent to a server-side logging endpoint.
For enterprise environments, event tracking can also help support audit requirements, performance analysis, and user behavior diagnostics.
Deployment Guide
A typical deployment process may look like this:
Step 1: Package the plugin files
Include the main viewer page and server-side handlers:
Step 2: Deploy to the application server
Place the files in the appropriate web application directory or package them into a deployable application archive, depending on your environment.
Step 3: Configure document storage
Make sure the plugin can resolve document URLs or repository document references correctly.
Step 4: Configure annotation storage
Choose where annotation files will be stored. This may be a file system, database, object store, or enterprise content repository.
Step 5: Configure viewer parameters
Set the ViewONE parameters required for document loading, annotation loading, annotation saving, and export behavior.
Step 6: Validate permissions
Confirm that users can only access documents and annotations they are authorized to view or modify.
Step 7: Test
Test the full lifecycle:
Step 8: Enable logging and debugging
Enable enough logging to troubleshoot issues during rollout. Debug logging can be reduced after the implementation is stable.
Troubleshooting Common Issues
Issue 1: Annotations do not load
Possible causes:
- Incorrect annotation load URL
- Missing annotation file
- Invalid document identifier
- User does not have permission
- Server returns an unexpected response format
Recommended checks:
- Open browser developer tools
- Confirm the request to
load.jsp succeeds
- Check the response body
- Validate the annotation file exists
- Review server logs for access or path errors
Issue 2: Annotations save but do not reload
Possible causes:
- Annotation file formatting was changed during save
- Blank lines were removed
- Incorrect encoding was used
- Annotation was saved under the wrong document identifier
Recommended checks:
- Compare the saved annotation file with a known working file
- Confirm blank-line formatting is preserved
- Check whether the document ID used during save matches the document ID used during load
Issue 3: Burned output does not include annotations
Possible causes:
- Burn handler is not receiving the annotation reference
- Annotation file is empty or inaccessible
- Export parameters are incorrect
- Burn operation is using the source document only
Recommended checks:
- Confirm
burn.jsp receives both document and annotation inputs
- Check server logs during export
- Validate the annotation file content before burn
- Test with a simple annotation first
Issue 4: Export format is incorrect
Possible causes:
- Output format parameter is missing
- Viewer or server defaults to PDF
- TIFF-specific behavior is not enabled
- PDF input is handled differently from image input
Recommended checks:
- Confirm the requested output format
- Validate export parameters
- Test PDF and TIFF inputs separately
- Document expected behavior for each input/output combination
Issue 5: Viewer works locally but fails in production
Possible causes:
- Different context root
- Reverse proxy or load balancer path changes
- Authentication differences
- CORS or CSP restrictions
- Missing application server permissions
Recommended checks:
- Compare local and production URLs
- Check browser console errors
- Review network requests
- Validate proxy headers and context paths
- Confirm production security policies allow viewer resources to load
Conclusion
The ViewONE Annotation Plugin provides a complete foundation for managing annotations in a Daeja ViewONE Virtual Viewer environment. It supports the core annotation lifecycle: loading, editing, saving, exporting, and permanently burning annotations into output documents.
By separating the viewer interface from server-side handlers, the implementation remains understandable and extensible. The use of Virtual-plugin.html, load.jsp, save.jsp, and burn.jsp creates a clean structure that can be adapted for file-based storage, repository-backed storage, or larger enterprise content management systems.
For teams working with IBM Content Navigator, Daeja ViewONE, or document-heavy enterprise workflows, this plugin is a practical starting point for building controlled, secure, and auditable annotation capabilities.
With additional enhancements such as audit logging, repository persistence, versioning, and role-based permissions, the same foundation can evolve into a robust production annotation service for enterprise document processing.
Virtual-plugin.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Daeja Virtual Viewer</title>
<script type="text/javascript" src="v1files/viewone.js"></script>
<style>
body {
margin: 0;
font-family: Arial, sans-serif;
}
.topbar {
display: none;
}
#status {
padding: 8px 10px;
font-size: 14px;
color: #333;
border-bottom: 1px solid #ccc;
background: #f8f8f8;
}
.viewer-save-button {
position: absolute;
top: 8px;
right: 470px;
z-index: 15;
height: 28px;
min-width: 120px;
padding: 0 10px;
border: 1px solid #bfbfbf;
border-radius: 3px;
background: rgba(255, 255, 255, 0.96);
color: #333;
font-size: 12px;
cursor: pointer;
box-shadow: 0 1px 3px rgba(0,0,0,0.12);
}
.viewer-save-button:hover {
background: #f3f3f3;
}
.viewer-shell {
position: relative;
background: white;
overflow: hidden;
}
.viewer-export-button {
position: absolute;
top: 8px;
right: 248px;
z-index: 15;
height: 28px;
min-width: 96px;
padding: 0 10px;
border: 1px solid #bfbfbf;
border-radius: 3px;
background: rgba(255, 255, 255, 0.96);
color: #333;
font-size: 12px;
cursor: pointer;
box-shadow: 0 1px 3px rgba(0,0,0,0.12);
}
.viewer-export-button:hover {
background: #f3f3f3;
}
.viewer-export-format {
position: absolute;
top: 8px;
right: 350px;
z-index: 15;
height: 28px;
border: 1px solid #bfbfbf;
border-radius: 3px;
background: rgba(255, 255, 255, 0.96);
color: #333;
font-size: 12px;
padding: 0 6px;
}
#debugPanel {
height: 180px;
overflow: auto;
background: #111;
color: #9fef9f;
font-family: Consolas, monospace;
font-size: 12px;
line-height: 1.4;
padding: 8px 10px;
border-top: 1px solid #333;
white-space: pre-wrap;
}
</style>
<script type="text/javascript">
function getViewer() {
return document.getElementById('viewone');
}
function appendDebug(message) {
var debugPanel = document.getElementById('debugPanel');
var timestamp = new Date().toISOString();
var line = '[' + timestamp + '] ' + message;
if (debugPanel) {
debugPanel.textContent += line + '\n';
debugPanel.scrollTop = debugPanel.scrollHeight;
}
}
function setStatus(message) {
document.getElementById('status').innerHTML = message;
appendDebug(message);
}
function mirrorConsoleToDebugPanel() {
if (window.__virtualAntConsoleMirrored) {
return;
}
window.__virtualAntConsoleMirrored = true;
var originalLog = console.log;
var originalWarn = console.warn;
var originalError = console.error;
var originalClear = console.clear;
function serializeConsoleArgs(args) {
return Array.prototype.slice.call(args).map(function(arg) {
if (typeof arg === 'string') {
return arg;
}
try {
return JSON.stringify(arg, null, 2);
} catch (e) {
return String(arg);
}
}).join(' ');
}
console.log = function() {
var text = serializeConsoleArgs(arguments);
appendDebug(text);
originalLog.apply(console, arguments);
};
console.warn = function() {
var text = serializeConsoleArgs(arguments);
appendDebug('WARN: ' + text);
originalWarn.apply(console, arguments);
};
console.error = function() {
var text = serializeConsoleArgs(arguments);
appendDebug('ERROR: ' + text);
originalError.apply(console, arguments);
};
console.clear = function() {
var debugPanel = document.getElementById('debugPanel');
if (debugPanel) {
debugPanel.textContent = '';
}
appendDebug('Console was cleared');
if (originalClear) {
originalClear.apply(console, arguments);
}
};
}
function triggerLocalDownload(downloadLocation, exportFormat) {
if (!downloadLocation) {
console.warn('No download location returned from exportDocument.');
return;
}
try {
var normalizedFormat = (exportFormat || 'bin').toLowerCase();
var separator = downloadLocation.indexOf('?') === -1 ? '?' : '&';
var forcedDownloadUrl = downloadLocation + separator + 'downloadAs=' + encodeURIComponent(normalizedFormat);
var link = document.createElement('a');
link.href = forcedDownloadUrl;
link.target = '_blank';
link.rel = 'noopener';
link.download = 'viewone-export.' + normalizedFormat;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
console.log('Triggered local download using returned URL:', forcedDownloadUrl);
} catch (downloadError) {
console.error('Failed to trigger local download:', downloadError);
window.open(downloadLocation, '_blank');
}
}
function normalizeAnnotationText(annotationText) {
if (!annotationText) {
return '';
}
var text = String(annotationText).replace(/\r\n/g, '\n').replace(/\r/g, '\n').trim();
if (!text || text === '[EMPTY]') {
return '';
}
return text + '\n';
}
function attachViewerDebugHooks() {
var viewer = getViewer();
if (!viewer) {
appendDebug('Viewer debug hooks: viewer element not found.');
return;
}
appendDebug('Viewer debug hooks attached.');
appendDebug('Viewer click tracing enabled for toolbar/document interactions.');
viewer.addEventListener('click', function(event) {
var target = event.target;
var label = '';
if (target) {
label = target.getAttribute('aria-label') ||
target.getAttribute('title') ||
target.getAttribute('alt') ||
target.textContent ||
target.tagName;
}
label = String(label || '').replace(/\s+/g, ' ').trim();
appendDebug('Viewer click: ' + (label || 'unknown viewer target'));
}, true);
viewer.addEventListener('mousedown', function(event) {
appendDebug('Viewer mouse action: mousedown at x=' + event.clientX + ', y=' + event.clientY);
}, true);
viewer.addEventListener('mouseup', function(event) {
appendDebug('Viewer mouse action: mouseup at x=' + event.clientX + ', y=' + event.clientY);
}, true);
viewer.addEventListener('keydown', function(event) {
appendDebug('Viewer key action: key=' + event.key + ', code=' + event.code);
}, true);
}
function saveAnnotationData() {
try {
var viewer = getViewer();
if (!viewer) {
setStatus('Viewer not found');
return;
}
var annotationText = '';
if (typeof viewer.getAnnotations === 'function') {
annotationText = viewer.getAnnotations();
console.log('Using viewer.getAnnotations()');
} else if (typeof ViewONE !== 'undefined' && typeof ViewONE.getAnnotations === 'function') {
annotationText = ViewONE.getAnnotations();
console.log('Using ViewONE.getAnnotations()');
} else if (typeof viewer.getAnnotationText === 'function') {
annotationText = viewer.getAnnotationText();
console.log('Using viewer.getAnnotationText()');
} else if (typeof ViewONE !== 'undefined' && typeof ViewONE.getAnnotationText === 'function') {
annotationText = ViewONE.getAnnotationText();
console.log('Using ViewONE.getAnnotationText()');
}
annotationText = normalizeAnnotationText(annotationText);
if (!annotationText) {
setStatus('No annotation data returned by viewer');
return;
}
var xhr = new XMLHttpRequest();
xhr.open('POST', 'http://localhost:8080/p2s/annotations/save.jsp?filename=Test.ant', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
setStatus('Annotations saved to Test.ant');
} else {
setStatus('Annotation save failed: HTTP ' + xhr.status + ' ' + xhr.responseText);
}
};
xhr.onerror = function() {
setStatus('Annotation save failed due to network error');
};
xhr.send('annotationData=' + encodeURIComponent(annotationText));
} catch (e) {
setStatus('Annotation save failed: ' + e.message);
}
}
function runExportDocumentDemo() {
console.clear();
console.log('=== exportDocument demo ===');
var viewer = getViewer();
var viewOneGlobal = (typeof ViewONE !== 'undefined') ? ViewONE : null;
var exportTarget = null;
console.log('Viewer object:', viewer);
console.log('window.ViewONE type:', typeof ViewONE);
if (viewer && typeof viewer.exportDocument === 'function') {
exportTarget = viewer;
console.log('Using viewer.exportDocument()');
} else if (viewOneGlobal && typeof viewOneGlobal.exportDocument === 'function') {
exportTarget = viewOneGlobal;
console.log('Using window.ViewONE.exportDocument()');
} else {
setStatus('Export demo failed: exportDocument API unavailable. Check F12 console.');
console.warn('No exportDocument method found on viewer element or window.ViewONE.');
if (viewer) {
try {
console.log('Viewer keys sample:', Object.keys(viewer));
} catch (viewerKeysError) {
console.warn('Unable to inspect viewer keys:', viewerKeysError);
}
}
if (viewOneGlobal) {
console.log('Available ViewONE keys:', Object.keys(viewOneGlobal));
}
return;
}
var exportFormatElement = document.getElementById('exportFormat');
var exportFormat = exportFormatElement ? exportFormatElement.value : 'tif';
var baseDocumentUrl = 'http://localhost:8080/TestDocs/Tiff/001.tif';
var exportDocumentUrl = baseDocumentUrl;
var annotationUrl = baseDocumentUrl;
if (exportFormat === 'tif') {
appendDebug('TIFF is selected in dropdown.');
appendDebug('Using exportDocument for TIFF flow.');
exportDocumentUrl = baseDocumentUrl + '#export=tif';
annotationUrl = baseDocumentUrl + '#annotation=tif';
} else if (exportFormat === 'pdf') {
appendDebug('PDF is selected in dropdown.');
appendDebug('Using exportDocument for PDF flow.');
exportDocumentUrl = baseDocumentUrl + '#export=pdf';
annotationUrl = baseDocumentUrl + '#annotation=pdf';
} else {
appendDebug('Unknown dropdown value, defaulting to TIFF.');
exportFormat = 'tif';
exportDocumentUrl = baseDocumentUrl + '#export=tif';
annotationUrl = baseDocumentUrl + '#annotation=tif';
}
appendDebug('Requested export format from dropdown: ' + exportFormat);
appendDebug('Calling exportDocument(documentUrl, annotationUrl, callback) with documentUrl=' + exportDocumentUrl + ', annotationUrl=' + annotationUrl);
var callback = {
onSuccess: function(downloadLocation) {
appendDebug('exportDocument onSuccess downloadLocation: ' + downloadLocation);
setStatus('Export completed as ' + exportFormat.toUpperCase() + '. Browser download triggered; see debug panel.');
triggerLocalDownload(downloadLocation, exportFormat);
},
onError: function(errorMsg) {
appendDebug('exportDocument onError: ' + errorMsg);
setStatus('Export failed. See debug panel.');
},
onStatusUpdate: function(message, error, pagesProcessed) {
appendDebug('exportDocument onStatusUpdate: message=' + message + ', error=' + error + ', pagesProcessed=' + pagesProcessed);
}
};
try {
exportTarget.exportDocument(exportDocumentUrl, annotationUrl, callback);
setStatus('Export request submitted for ' + exportFormat.toUpperCase() + ' using dropdown selection. Check F12 console for generated URL.');
} catch (e) {
console.error('Exception calling exportDocument:', e);
setStatus('Export demo exception. Check F12 console.');
}
}
window.addEventListener('load', function() {
mirrorConsoleToDebugPanel();
appendDebug('VirtualAnt page loaded.');
appendDebug('Viewer initialization tracing enabled.');
setTimeout(function() {
attachViewerDebugHooks();
}, 1500);
});
</script>
</head>
<body>
<div id="status">Viewer ready. Debug panel is shown below for export/save tracing.</div>
<div class="viewer-shell">
<button type="button" class="viewer-save-button" onclick="saveAnnotationData()">Save Annotations</button>
<select id="exportFormat" class="viewer-export-format">
<option value="tif">TIFF</option>
<option value="pdf">PDF</option>
</select>
<button type="button" class="viewer-export-button" onclick="runExportDocumentDemo()">Export Demo</button>
<OBJECT CLASS="com.ibm.dv.client.Viewer"
ID="viewone"
WIDTH="100%"
HEIGHT="700px">
<param name="filename" value="http://localhost:8080/TestDocs/Tiff/001.tif">
<param name="designPackage" value="carbon">
<param name="viewmode" value="thumbsleft">
<param name="annotate" value="true">
<param name="annotateEdit" value="true">
<!-- Annotation Load/Save Configuration -->
<param name="annotationFile" value="http://localhost:8080/p2s/annotations/load.jsp?filename=Test.ant">
<param name="annotationSaveServlet" value="http://localhost:8080/p2s/annotations/save.jsp?filename=Test.ant">
<param name="annotationDefaults" value="all {burnable=true}">
<param name="annotationsToggleBurn" value="mixed">
<param name="annotationtrace" value="true">
<param name="annotationDefaultLineColor1" value="line: 255,0,0">
<param name="annotationDefaultLineColor2" value="rectangle: 0,0,255">
<param name="annotationEncoding" value="UTF8">
<param name="annotationJavascriptExtensions" value="true">
<param name="JavascriptExtensions" value="true">
<param name="mayscript" value="true">
<param name="trace" value="true">
<param name="traceNet" value="true">
<param name="traceFilter" value="true">
<param name="traceRender" value="true">
<param name="traceFont" value="true">
<param name="traceException" value="true">
<param name="fileButtonSave" value="true">
<param name="fileButtonOpen" value="true">
<param name="initialFocus" value="false">
<param name="focusBorder" value="false">
</OBJECT>
</div>
<div id="debugPanel"></div>
</body>
</html>
load.jsp
<%@ page language="java" contentType="text/xml; charset=UTF-8" pageEncoding="UTF-8"%><%@ page import="java.io.*,java.nio.file.*" %><%
// Annotation Load Handler for ViewONE
// Loads annotation data from a file on the server
response.setContentType("text/xml");
response.setCharacterEncoding("UTF-8");
try {
// Get filename parameter from URL
String filename = request.getParameter("filename");
if (filename == null || filename.trim().isEmpty()) {
filename = "annotations.ant";
}
// Build file path
String webappPath = application.getRealPath("/");
String annotationsDir = webappPath + "annotations";
String filePath = annotationsDir + File.separator + filename;
File annotationFile = new File(filePath);
if (annotationFile.exists() && annotationFile.isFile()) {
// Read and return the annotation data
BufferedReader reader = new BufferedReader(new FileReader(annotationFile));
String line;
StringBuilder content = new StringBuilder();
while ((line = reader.readLine()) != null) {
content.append(line).append("\n");
}
reader.close();
// Log the load operation
System.out.println("========================================");
System.out.println("[ANNOTATION LOAD] SUCCESS");
System.out.println("[ANNOTATION LOAD] Filename: " + filename);
System.out.println("[ANNOTATION LOAD] Path: " + filePath);
System.out.println("[ANNOTATION LOAD] Size: " + content.length() + " bytes");
System.out.println("[ANNOTATION LOAD] Timestamp: " + new java.util.Date());
System.out.println("========================================");
// Return the annotation data
out.print(content.toString());
} else {
// No annotations found - return empty response
System.out.println("========================================");
System.out.println("[ANNOTATION LOAD] No file found");
System.out.println("[ANNOTATION LOAD] Filename: " + filename);
System.out.println("[ANNOTATION LOAD] Path: " + filePath);
System.out.println("[ANNOTATION LOAD] Returning empty annotations");
System.out.println("========================================");
out.print("<?xml version=\"1.0\" encoding=\"UTF-8\"?><annotations></annotations>");
}
} catch (Exception e) {
System.err.println("========================================");
System.err.println("[ANNOTATION LOAD ERROR] " + e.getMessage());
e.printStackTrace();
System.err.println("========================================");
// Return empty annotations on error
out.print("<?xml version=\"1.0\" encoding=\"UTF-8\"?><annotations></annotations>");
}
%>
save.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%><%@ page import="java.io.*,java.nio.file.*,java.util.*" %><%
// Annotation Save Handler for ViewONE
// Saves annotation data to a file on the server
response.setContentType("text/html");
response.setCharacterEncoding("UTF-8");
response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
response.setHeader("Pragma", "no-cache");
response.setDateHeader("Expires", 0);
try {
// Get filename parameter from URL
String filename = request.getParameter("filename");
if (filename == null || filename.trim().isEmpty()) {
filename = "annotations.ant";
}
// Read annotation data from the request.
// Match the working WAR behavior more closely:
// 1) save raw posted body when present
// 2) support direct annotationData posts from VirtualAnt.html
// 3) support segmented form posts such as annotationData, annotationData01..NN + annotationDataCount
StringBuilder annotationData = new StringBuilder();
String contentType = request.getContentType();
if (contentType == null) {
contentType = "";
}
String dataSource = "none";
String rawPayload = null;
// Direct parameter used by custom page save
String directAnnotationData = request.getParameter("annotationData");
if (directAnnotationData != null && !directAnnotationData.trim().isEmpty()) {
annotationData.append(directAnnotationData);
dataSource = "request parameter 'annotationData'";
}
// Segmented payload similar to TestingWebApp save.servlet handling
if (annotationData.length() == 0) {
String[] countCandidates = { "annotationDataCount", "dataCount", "annotationCount", "numannotations", "numdata" };
String countValue = null;
for (String countCandidate : countCandidates) {
countValue = request.getParameter(countCandidate);
if (countValue != null && !countValue.trim().isEmpty()) {
break;
}
}
if (countValue != null && !countValue.trim().isEmpty()) {
try {
int count = Integer.parseInt(countValue.trim());
String[] baseCandidates = { "annotationData", "data", "annotations" };
for (String baseName : baseCandidates) {
StringBuilder combined = new StringBuilder();
boolean foundAny = false;
boolean missingPart = false;
for (int i = 1; i <= count; i++) {
String indexedName = (i < 10) ? (baseName + "0" + i) : (baseName + i);
String part = request.getParameter(indexedName);
if (part == null) {
missingPart = true;
break;
}
combined.append(part);
foundAny = true;
}
if (foundAny && !missingPart) {
String encoding = request.getParameter("encoding");
if (encoding == null || encoding.trim().isEmpty()) {
encoding = "UTF-8";
}
try {
annotationData.append(java.net.URLDecoder.decode(combined.toString(), encoding));
} catch (Exception decodeException) {
annotationData.append(combined.toString());
}
dataSource = "segmented request parameters base '" + baseName + "' count " + count;
break;
}
}
} catch (Exception parseException) {
System.out.println("[ANNOTATION SAVE] Unable to parse segmented annotation count: " + countValue);
}
}
}
// Fallback to common single parameters
if (annotationData.length() == 0) {
String[] parameterCandidates = {
"annotations", "data", "xml", "content"
};
for (String paramName : parameterCandidates) {
String value = request.getParameter(paramName);
if (value != null && !value.trim().isEmpty()) {
annotationData.append(value);
dataSource = "request parameter '" + paramName + "'";
break;
}
}
}
// Final fallback to raw request body
if (annotationData.length() == 0) {
BufferedReader reader = request.getReader();
String line;
while ((line = reader.readLine()) != null) {
annotationData.append(line).append("\n");
}
if (annotationData.length() > 0) {
dataSource = "raw request body";
}
}
// Preserve full parameter dump for diagnostics when save is empty
Map<String, String[]> paramMap = request.getParameterMap();
if (paramMap != null && !paramMap.isEmpty()) {
StringBuilder formDump = new StringBuilder();
for (Map.Entry<String, String[]> entry : paramMap.entrySet()) {
formDump.append(entry.getKey()).append("=");
String[] values = entry.getValue();
if (values != null) {
for (int i = 0; i < values.length; i++) {
if (i > 0) formDump.append(",");
formDump.append(values[i]);
}
}
formDump.append("\n");
}
rawPayload = formDump.toString();
}
String normalizedAnnotationText = annotationData.toString();
if (normalizedAnnotationText != null && !normalizedAnnotationText.trim().isEmpty()) {
normalizedAnnotationText = normalizedAnnotationText
.replace("\r\n", "\n")
.replace("\r", "\n")
.replace("<P>", "\n")
.trim();
// Add blank line after each annotation block
// ViewONE annotations end with MODIFIEDDATE line
normalizedAnnotationText = normalizedAnnotationText
.replaceAll("(MODIFIEDDATE = [^\n]+)", "$1\n");
// Clean up excessive blank lines (more than 2 consecutive)
normalizedAnnotationText = normalizedAnnotationText.replaceAll("\n{3,}", "\n\n");
if (!normalizedAnnotationText.endsWith("\n")) {
normalizedAnnotationText = normalizedAnnotationText + "\n";
}
}
// Create annotations directory if it doesn't exist
String webappPath = application.getRealPath("/");
String annotationsDir = webappPath + "annotations";
File dir = new File(annotationsDir);
if (!dir.exists()) {
dir.mkdirs();
}
// Save annotation file
String filePath = annotationsDir + File.separator + filename;
FileWriter writer = new FileWriter(filePath);
writer.write(normalizedAnnotationText);
writer.close();
annotationData.setLength(0);
annotationData.append(normalizedAnnotationText);
// Log the save operation
System.out.println("========================================");
System.out.println("[ANNOTATION SAVE] SUCCESS");
System.out.println("[ANNOTATION SAVE] Filename: " + filename);
System.out.println("[ANNOTATION SAVE] Path: " + filePath);
System.out.println("[ANNOTATION SAVE] Content-Type: " + contentType);
System.out.println("[ANNOTATION SAVE] Data source: " + dataSource);
System.out.println("[ANNOTATION SAVE] Size: " + annotationData.length() + " bytes");
if (annotationData.length() > 0) {
int previewLength = Math.min(annotationData.length(), 500);
System.out.println("[ANNOTATION SAVE] Preview: " + annotationData.substring(0, previewLength));
} else {
System.out.println("[ANNOTATION SAVE] WARNING: No annotation data received");
if (rawPayload != null && !rawPayload.isEmpty()) {
System.out.println("[ANNOTATION SAVE] Form parameters received:");
System.out.println(rawPayload);
}
}
System.out.println("[ANNOTATION SAVE] Timestamp: " + new java.util.Date());
System.out.println("========================================");
// Return success response with HTML format for ViewONE to display message
out.println("<html>");
out.println("<head><title>Annotation Save</title></head>");
out.println("<body>");
out.println("<h3>Annotations saved successfully</h3>");
out.println("<p>File: " + filename + "</p>");
out.println("<p>Size: " + annotationData.length() + " bytes</p>");
out.println("<p>Status: OK</p>");
out.println("</body>");
out.println("</html>");
out.flush();
} catch (Exception e) {
System.err.println("========================================");
System.err.println("[ANNOTATION SAVE ERROR] " + e.getMessage());
e.printStackTrace();
System.err.println("========================================");
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
out.print("ERROR: " + e.getMessage());
}
%>
burn.jsp
<%@ page language="java" contentType="text/plain; charset=UTF-8" pageEncoding="UTF-8"%><%@ page import="java.io.*,java.nio.file.*" %><%
// Annotation Burn Handler
// Receives the burned (annotated) document and saves it back to disk
response.setContentType("text/plain");
response.setCharacterEncoding("UTF-8");
try {
// Get the document filename from request
String filename = request.getParameter("filename");
if (filename == null || filename.trim().isEmpty()) {
filename = "annotated-document.tif";
}
// Extract just the filename without path
if (filename.contains("/")) {
filename = filename.substring(filename.lastIndexOf("/") + 1);
}
if (filename.contains("\\")) {
filename = filename.substring(filename.lastIndexOf("\\") + 1);
}
// Create output directory if it doesn't exist
String outputDir = "C:/tmp/daeja_annotated";
File dir = new File(outputDir);
if (!dir.exists()) {
dir.mkdirs();
}
// Generate output filename with timestamp
String timestamp = String.valueOf(System.currentTimeMillis());
String baseName = filename.substring(0, filename.lastIndexOf('.'));
String extension = filename.substring(filename.lastIndexOf('.'));
String outputFilename = baseName + "_annotated_" + timestamp + extension;
String outputPath = outputDir + "/" + outputFilename;
// Read the burned document from request input stream
InputStream inputStream = request.getInputStream();
FileOutputStream outputStream = new FileOutputStream(outputPath);
byte[] buffer = new byte[8192];
int bytesRead;
long totalBytes = 0;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
totalBytes += bytesRead;
}
outputStream.close();
inputStream.close();
// Log the burn operation
System.out.println("[ANNOTATION BURN] Saved annotated document");
System.out.println("[ANNOTATION BURN] Original: " + filename);
System.out.println("[ANNOTATION BURN] Output: " + outputPath);
System.out.println("[ANNOTATION BURN] Size: " + totalBytes + " bytes");
// Return the path to the saved file
out.print("OK:" + outputPath);
} catch (Exception e) {
System.err.println("[ANNOTATION BURN ERROR] " + e.getMessage());
e.printStackTrace();
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
out.print("ERROR: " + e.getMessage());
}
%>