Skip to content

Commit

Permalink
feat: add PowerPoint and Excel stamping functionality, CLI improvements
Browse files Browse the repository at this point in the history
Introduce `PowerPointStamper` and `ExcelContext` to enable processing and stamping of PowerPoint and Excel files. Implement CLI enhancements for dynamic stamper type selection and file context handling.

The change enables better extensibility and user interaction, while streamlining support for additional file formats in the stamping process.
  • Loading branch information
caring-coder committed Feb 11, 2025
1 parent c0c34b5 commit 8e046c8
Show file tree
Hide file tree
Showing 8 changed files with 337 additions and 64 deletions.
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package pro.verron.officestamper.preset;

import org.docx4j.openpackaging.exceptions.Docx4JException;
import org.docx4j.openpackaging.packages.PresentationMLPackage;
import org.docx4j.openpackaging.packages.SpreadsheetMLPackage;
import pro.verron.officestamper.api.OfficeStamper;
import pro.verron.officestamper.api.OfficeStamperException;
import pro.verron.officestamper.api.StreamStamper;
import pro.verron.officestamper.experimental.ExcelStamper;
import pro.verron.officestamper.experimental.PowerpointStamper;

import java.io.InputStream;

/**
* ExperimentalStampers is a class that provides static methods for obtaining instances of OfficeStamper
* implementations for stamping PowerPoint presentations and Excel templates with context and writing
Expand All @@ -29,8 +33,17 @@ private ExperimentalStampers() {
*
* @since 1.6.8
*/
public static OfficeStamper<PresentationMLPackage> pptxStamper() {
return new PowerpointStamper();
public static StreamStamper<PresentationMLPackage> pptxStamper() {
var stamper = new PowerpointStamper();
return new StreamStamper<>(ExperimentalStampers::loadPowerPoint, stamper);
}

private static PresentationMLPackage loadPowerPoint(InputStream inputStream) {
try {
return PresentationMLPackage.load(inputStream);
} catch (Docx4JException e) {
throw new OfficeStamperException(e);
}
}

/**
Expand Down
6 changes: 6 additions & 0 deletions examples/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@
</properties>

<dependencies>
<dependency>
<groupId>info.picocli</groupId>
<artifactId>picocli</artifactId>
<version>4.7.6</version>
</dependency>

<dependency>
<groupId>pro.verron.office-stamper</groupId>
<artifactId>engine</artifactId>
Expand Down
52 changes: 0 additions & 52 deletions examples/src/main/java/pro/verron/officestamper/Examples.java

This file was deleted.

114 changes: 114 additions & 0 deletions examples/src/main/java/pro/verron/officestamper/ExcelContext.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package pro.verron.officestamper;

import org.docx4j.openpackaging.exceptions.Docx4JException;
import org.docx4j.openpackaging.packages.SpreadsheetMLPackage;
import org.docx4j.openpackaging.parts.SpreadsheetML.WorkbookPart;
import org.docx4j.openpackaging.parts.SpreadsheetML.WorksheetPart;
import org.docx4j.openpackaging.parts.relationships.RelationshipsPart;
import org.xlsx4j.org.apache.poi.ss.usermodel.DataFormatter;
import org.xlsx4j.sml.Row;
import org.xlsx4j.sml.Sheet;
import org.xlsx4j.sml.Workbook;
import org.xlsx4j.sml.Worksheet;
import pro.verron.officestamper.api.OfficeStamperException;

import java.io.InputStream;
import java.util.*;

import static java.util.Collections.emptyList;

public class ExcelContext
extends AbstractMap<String, List<Map<String, String>>> {

public static final DataFormatter formatter = new DataFormatter();
private final Map<String, List<Map<String, String>>> source;

public ExcelContext(SpreadsheetMLPackage spreadsheetPackage) {
var workbookPart = spreadsheetPackage.getWorkbookPart();
var workbook = getWorkbook(workbookPart);
var allSheets = workbook.getSheets();
var sheets = allSheets.getSheet();

var relationshipsPart = workbookPart.getRelationshipsPart();

var excel = new TreeMap<String, List<Map<String, String>>>();

for (var sheet : sheets) {
var worksheetPart = extractWorksheetPart(sheet, relationshipsPart);
var worksheet = extractWorksheet(worksheetPart);
var sheetDate = worksheet.getSheetData();
var rows = sheetDate.getRow();
excel.put(sheet.getName(), extractRecords(rows));
}

source = Collections.unmodifiableMap(excel);
}

private static Workbook getWorkbook(WorkbookPart workbookPart) {
try {
return workbookPart.getContents();
} catch (Docx4JException e) {
throw new OfficeStamperException(e);
}
}

private WorksheetPart extractWorksheetPart(Sheet sheet, RelationshipsPart relationshipsPart) {
return (WorksheetPart) relationshipsPart.getPart(sheet.getId());
}

private Worksheet extractWorksheet(WorksheetPart worksheetPart) {
try {
return worksheetPart.getContents();
} catch (Docx4JException e) {
throw new OfficeStamperException(e);
}
}

private List<Map<String, String>> extractRecords(List<Row> rows) {
if (rows.isEmpty()) return emptyList();
var headers = extractHeaders(rows.getFirst());
return extractRecords(headers, rows.subList(1, rows.size()));
}

private List<String> extractHeaders(Row row) {
return row.getC()
.stream()
.map(formatter::formatCellValue)
.toList();
}

private List<Map<String, String>> extractRecords(List<String> headers, List<Row> rows) {
List<Map<String, String>> list = new ArrayList<>();
for (var row : rows) {
Map<String, String> rec = new TreeMap<>();
for (var i = 0; i < headers.size(); i++) {
rec.put(headers.get(i), formatCellValueAt(row, i));
}
list.add(rec);
}
return list;
}

private static String formatCellValueAt(Row row, int i) {
var cells = row.getC();
if (i >= cells.size()) return "";
return formatter.formatCellValue(cells.get(i));
}

public static Object from(InputStream inputStream) {
try {
return from(SpreadsheetMLPackage.load(inputStream));
} catch (Docx4JException e) {
throw new OfficeStamperException(e);
}
}

private static Object from(SpreadsheetMLPackage spreadsheetPackage) {
return new ExcelContext(spreadsheetPackage);
}

@Override
public Set<Entry<String, List<Map<String, String>>>> entrySet() {
return source.entrySet();
}
}
148 changes: 138 additions & 10 deletions examples/src/main/java/pro/verron/officestamper/Main.java
Original file line number Diff line number Diff line change
@@ -1,28 +1,156 @@
package pro.verron.officestamper;


import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import pro.verron.officestamper.api.OfficeStamperException;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;

import static pro.verron.officestamper.Examples.stampDiagnostic;
import static java.nio.file.Files.newOutputStream;

public class Main {
@Command(name = "officestamper", mixinStandardHelpOptions = true, description = "Office Stamper CLI tool")
public class Main
implements Runnable {

private static final Logger logger = Utils.getLogger();

public static void main(String[] args)
throws Exception {
stampDiagnostic(createOutStream("Diagnostic-"));
@Option(names = {"-i", "--input-file"},
required = true,
description = "Input file path (csv, properties, html, xml, json, excel) or a keyword for documented data"
+ " sources") private String inputFile;

@Option(names = {"-t", "--template-file"},
required = true,
description = "Template file path or a keyword for documented template packages") private String templateFile;

@Option(names = {"-o", "--output-path"},
defaultValue = "default-output.docx",
description = "Output file path") private String outputPath;

@Option(names = {"-s", "--stamper"},
defaultValue = "diagnostic",
description = "Stamper type (diagnostic, powerpoint)") private String stamperType;

public static void main(String[] args) {
var main = new Main();
int exitCode = new CommandLine(main).execute(args);
System.exit(exitCode);
}

@Override
public void run() {
if (inputFile == null || templateFile == null) {
logger.log(Level.SEVERE, "Input file and template file must be provided");
return;
}

stamperType = stamperType.toLowerCase();

logger.log(Level.INFO, "Input File: {}", inputFile);
logger.log(Level.INFO, "Template File: {}", templateFile);
logger.log(Level.INFO, "Output Path: {}", outputPath);
logger.log(Level.INFO, "Stamper Type: {}", stamperType);

final var context = switch (inputFile) {
case "diagnostic" -> createDiagnosticContext();
default -> contextualise(Path.of(inputFile));
};

final var templateStream = switch (templateFile) {
case "diagnostic" -> loadDiagnosticTemplate();
default -> streamFile(Path.of(templateFile));
};

final var outputStream = createOutputStream(Path.of(outputPath));

final var stamper = switch (stamperType) {
case "word" -> new WordStamper();
case "powerpoint" -> new PowerPointStamper();
default -> throw new OfficeStamperException("Invalid stamper type: " + stamperType);
};

stamper.stamp(context, templateStream, outputStream);
}

private static Object createDiagnosticContext() {
logger.info("""
Create a context with: \
system environment variables, \
jvm properties, \
and user preferences""");
var diagnosticMaker = new Diagnostic();
var map = new TreeMap<String, Object>();
map.put("reportDate", diagnosticMaker.date());
map.put("reportUser", diagnosticMaker.user());
map.put("environment", diagnosticMaker.environmentVariables());
map.put("properties", diagnosticMaker.jvmProperties());
map.put("preferences", diagnosticMaker.userPreferences());
return map;
}

private static OutputStream createOutStream(String prefix)
throws IOException {
var outputPath = Files.createTempFile(prefix, ".docx");
logger.log(Level.INFO, "Stamping to file: ", outputPath);
return Files.newOutputStream(outputPath);
private Object contextualise(Path path) {
String fileName = path.getFileName()
.toString()
.toLowerCase();
if (fileName.endsWith(".csv")) return processCsv(path);
if (fileName.endsWith(".properties")) return processProperties(path);
if (fileName.endsWith(".html") || fileName.endsWith(".xml")) return processXmlOrHtml(path);
if (fileName.endsWith(".json")) return processJson(path);
if (fileName.endsWith(".xlsx")) return processExcel(path);
throw new OfficeStamperException("Unsupported file type: " + fileName);
}

private static InputStream loadDiagnosticTemplate() {
logger.info("Load the internally packaged 'Diagnostic.docx' template resource");
return Utils.streamResource("Diagnostic.docx");
}

private static InputStream streamFile(Path path) {
try {
return Files.newInputStream(path);
} catch (IOException e) {
throw new OfficeStamperException(e);
}
}

private OutputStream createOutputStream(Path path) {
try {
return newOutputStream(path);
} catch (IOException e) {
throw new OfficeStamperException(e);
}
}

private Object processCsv(Path path) {
throw new OfficeStamperException("Not yet implemented.");
}

private Object processProperties(Path path) {
throw new OfficeStamperException("Not yet implemented.");
}

private Object processXmlOrHtml(Path path) {
throw new OfficeStamperException("Not yet implemented.");
}

private Object processJson(Path path) {
throw new OfficeStamperException("Not yet implemented.");
}

private Object processExcel(Path path) {
try {
return ExcelContext.from(Files.newInputStream(path));
} catch (IOException e) {
throw new OfficeStamperException(e);
}
}
}
Loading

0 comments on commit 8e046c8

Please sign in to comment.