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
42 changes: 29 additions & 13 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ on:
push:
branches:
- main
- develop
pull_request:
branches:
- main
# Run CI for any incoming PR regardless of the base branch so
# feature branches targeting `develop` (e.g. v1.7 prep) also
# exercise the full pipeline.
workflow_dispatch:
schedule:
- cron: '0 5 * * 1'
Expand All @@ -26,11 +28,11 @@ jobs:
- name: Check out repository
uses: actions/checkout@v4

- name: Set up Temurin JDK 21
- name: Set up Temurin JDK 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
java-version: '17'
cache: maven

- name: Run guards
Expand All @@ -40,28 +42,42 @@ jobs:
test

build-and-test:
name: Build and run tests
name: Build and run tests (JDK ${{ matrix.java }})
if: github.event_name != 'schedule'
needs: architecture-and-documentation-guards
runs-on: ubuntu-latest
strategy:
# Don't cancel sibling matrix jobs on the first failure — a
# regression on one JDK is informative even if other JDKs are
# still running.
fail-fast: false
matrix:
# 17 = baseline, 21 = previous baseline still supported,
# 25 = current LTS, validates the byte-buddy / Mockito 5.23
# bumps that #10 motivated.
java: ['17', '21', '25']
env:
JAVA_TOOL_OPTIONS: -Djava.awt.headless=true

steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Set up Temurin JDK 21
- name: Set up Temurin JDK ${{ matrix.java }}
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
java-version: ${{ matrix.java }}
cache: maven

- name: Build and run tests
run: ./mvnw -B -ntp clean verify

- name: Generate Javadoc
# Run only on the baseline JDK — Javadoc output is identical
# across JVMs and one pass is enough to catch broken @link
# references.
if: matrix.java == '17'
run: ./mvnw -B -ntp javadoc:javadoc

examples-generation:
Expand All @@ -76,11 +92,11 @@ jobs:
- name: Check out repository
uses: actions/checkout@v4

- name: Set up Temurin JDK 21
- name: Set up Temurin JDK 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
java-version: '17'
cache: maven

- name: Install root artifact
Expand Down Expand Up @@ -114,11 +130,11 @@ jobs:
- name: Check out repository
uses: actions/checkout@v4

- name: Set up Temurin JDK 21
- name: Set up Temurin JDK 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
java-version: '17'
cache: maven

- name: Install root artifact
Expand Down Expand Up @@ -153,11 +169,11 @@ jobs:
- name: Check out repository
uses: actions/checkout@v4

- name: Set up Temurin JDK 21
- name: Set up Temurin JDK 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
java-version: '17'
cache: maven

- name: Restore benchmark history cache
Expand Down
39 changes: 39 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,45 @@
All notable changes to GraphCompose are documented here. Versions
follow semantic versioning; release dates are ISO 8601.

## v1.6.1 — Planned

Maintenance + compatibility patch. Drops the Java 21 source/target
baseline to **Java 17+** so the library can ship into older
enterprise stacks without a fork, and refreshes test/build
dependencies. **No public API change** — engine, DSL, themes,
templates, and backend records all stay source-compatible with
v1.6.0; existing v1.6.0 callers compile and behave unchanged.

Co-developed with external contributor
[@jottinger](https://github.com/jottinger)
([#8](https://github.com/DemchaAV/GraphCompose/issues/8),
[#10](https://github.com/DemchaAV/GraphCompose/issues/10)).

### Toolchain

- **Java 17 baseline.** `<maven.compiler.release>` flips from `21`
to `17` across `pom.xml`, `examples/pom.xml`, and `benchmarks/pom.xml`.
Engine source loses the Java 21–only constructs
(switch-with-type-patterns, switch-with-deconstruction,
`List.getFirst()`, `Thread.threadId()`) in favour of Java 17
–compatible forms. CI runs against Temurin JDK 17.
- **Dependency refresh + CVE pass.** Bumps Jackson `2.20.1 → 2.21.3`,
Logback `1.5.18 → 1.5.32`, Lombok `1.18.38 → 1.18.46`, POI
`5.4.0 → 5.5.1`, SnakeYAML `2.4 → 2.6`, AssertJ `3.27.3 → 3.27.6`,
JUnit `5.12.2 → 5.14.4`, Mockito `5.20.0 → 5.23.0`. Adds explicit
ByteBuddy `1.18.7` so Mockito works on the Java 25+ access rules.
Maven plugin bumps: `maven-compiler-plugin 3.13 → 3.15`,
`maven-surefire-plugin 3.2.5 → 3.5.5`, `exec-maven-plugin 3.5 → 3.6.2`.

### Looking ahead

Maven Central distribution
([#7](https://github.com/DemchaAV/GraphCompose/issues/7)) remains
on the **v1.7.0** roadmap alongside the JMH benchmark migration;
v1.6.1 stays on JitPack as a maintenance release.

---

## v1.6.0 — 2026-05-07

The "expressive" release. Closes the remaining canonical-vs-legacy
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<a href="https://github.com/DemchaAV/GraphCompose/actions/workflows/ci.yml?query=branch%3Amain"><img src="https://img.shields.io/github/actions/workflow/status/DemchaAV/GraphCompose/ci.yml?branch=main&style=for-the-badge&label=CI" alt="CI"/></a>
<a href="https://github.com/DemchaAV/GraphCompose/releases/latest"><img src="https://img.shields.io/github/v/release/DemchaAV/GraphCompose?style=for-the-badge&label=Release" alt="Latest release"/></a>
<a href="https://jitpack.io/#DemchaAV/GraphCompose"><img src="https://img.shields.io/jitpack/v/github/DemchaAV/GraphCompose?style=for-the-badge&label=JitPack" alt="JitPack"/></a>
<img src="https://img.shields.io/badge/Java-21-orange?style=for-the-badge&logo=openjdk" alt="Java 21"/>
<img src="https://img.shields.io/badge/Java-17%2B-orange?style=for-the-badge&logo=openjdk" alt="Java 17+"/>
<img src="https://img.shields.io/badge/PDFBox-3.0-red?style=for-the-badge" alt="PDFBox 3.0"/>
<img src="https://img.shields.io/badge/License-MIT-blue?style=for-the-badge" alt="MIT License"/>
</p>
Expand Down
14 changes: 7 additions & 7 deletions benchmarks/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@

<properties>
<graphcompose.version>1.6.0</graphcompose.version>
<maven.compiler.release>21</maven.compiler.release>
<maven.compiler.release>17</maven.compiler.release>

<junit.bom.version>5.12.2</junit.bom.version>
<assertj.version>3.27.3</assertj.version>
<logback.version>1.5.18</logback.version>
<junit.bom.version>5.14.4</junit.bom.version>
<assertj.version>3.27.6</assertj.version>
<logback.version>1.5.32</logback.version>

<openhtmltopdf.version>1.0.10</openhtmltopdf.version>
<itextpdf.version>5.5.13.3</itextpdf.version>
Expand Down Expand Up @@ -116,20 +116,20 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<version>3.15.0</version>
<configuration>
<release>${maven.compiler.release}</release>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
<version>3.5.5</version>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.5.0</version>
<version>3.6.2</version>
</plugin>
</plugins>
</build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ private void run(Input input) throws Exception {
reportFiles.add(new ReportFile(reportPath, JSON.readTree(Files.readAllBytes(reportPath))));
}

SuiteType suiteType = detectSuiteType(reportFiles.getFirst().report());
SuiteType suiteType = detectSuiteType(reportFiles.get(0).report());
for (ReportFile reportFile : reportFiles) {
SuiteType currentType = detectSuiteType(reportFile.report());
if (currentType != suiteType) {
Expand All @@ -58,7 +58,7 @@ private void run(Input input) throws Exception {
}

private void aggregateCurrentSpeed(List<ReportFile> reportFiles) throws Exception {
JsonNode firstReport = reportFiles.getFirst().report();
JsonNode firstReport = reportFiles.get(0).report();
String profile = firstReport.path("profile").asText("full");
for (ReportFile reportFile : reportFiles) {
String currentProfile = reportFile.report().path("profile").asText("full");
Expand Down Expand Up @@ -135,7 +135,7 @@ private void aggregateCurrentSpeed(List<ReportFile> reportFiles) throws Exceptio
}

private List<CurrentSpeedLatencyMedianRow> aggregateCurrentSpeedLatency(List<ReportFile> reportFiles) {
List<JsonNode> firstRows = iterable(reportFiles.getFirst().report().path("latency"));
List<JsonNode> firstRows = iterable(reportFiles.get(0).report().path("latency"));
Map<String, JsonNode> firstByScenario = indexBy(firstRows, "scenario");

for (ReportFile reportFile : reportFiles) {
Expand All @@ -150,7 +150,7 @@ private List<CurrentSpeedLatencyMedianRow> aggregateCurrentSpeedLatency(List<Rep
List<JsonNode> rows = reportFiles.stream()
.map(reportFile -> indexBy(iterable(reportFile.report().path("latency")), "scenario").get(scenario))
.toList();
JsonNode exemplar = rows.getFirst();
JsonNode exemplar = rows.get(0);
return new CurrentSpeedLatencyMedianRow(
scenario,
exemplar.path("description").asText(""),
Expand All @@ -166,7 +166,7 @@ private List<CurrentSpeedLatencyMedianRow> aggregateCurrentSpeedLatency(List<Rep
}

private List<CurrentSpeedThroughputMedianRow> aggregateCurrentSpeedThroughput(List<ReportFile> reportFiles) {
List<JsonNode> firstRows = iterable(reportFiles.getFirst().report().path("throughput"));
List<JsonNode> firstRows = iterable(reportFiles.get(0).report().path("throughput"));
Map<String, JsonNode> firstByScenario = indexThroughput(firstRows);

for (ReportFile reportFile : reportFiles) {
Expand All @@ -181,7 +181,7 @@ private List<CurrentSpeedThroughputMedianRow> aggregateCurrentSpeedThroughput(Li
List<JsonNode> rows = reportFiles.stream()
.map(reportFile -> indexThroughput(iterable(reportFile.report().path("throughput"))).get(key))
.toList();
JsonNode exemplar = rows.getFirst();
JsonNode exemplar = rows.get(0);
return new CurrentSpeedThroughputMedianRow(
exemplar.path("scenario").asText(),
exemplar.path("threads").asInt(),
Expand All @@ -196,7 +196,7 @@ private void aggregateComparative(List<ReportFile> reportFiles) throws Exception
int warmupIterations = requireIntConsistency(reportFiles, "warmupIterations");
int measurementIterations = requireIntConsistency(reportFiles, "measurementIterations");

List<JsonNode> firstRows = iterable(reportFiles.getFirst().report().path("libraries"));
List<JsonNode> firstRows = iterable(reportFiles.get(0).report().path("libraries"));
Map<String, JsonNode> firstByLibrary = indexBy(firstRows, "library");
for (ReportFile reportFile : reportFiles) {
Map<String, JsonNode> currentByLibrary = indexBy(iterable(reportFile.report().path("libraries")), "library");
Expand Down Expand Up @@ -246,7 +246,7 @@ private void aggregateComparative(List<ReportFile> reportFiles) throws Exception
}

private static int requireIntConsistency(List<ReportFile> reportFiles, String fieldName) {
int expected = reportFiles.getFirst().report().path(fieldName).asInt();
int expected = reportFiles.get(0).report().path(fieldName).asInt();
for (ReportFile reportFile : reportFiles) {
int current = reportFile.report().path(fieldName).asInt();
if (current != expected) {
Expand All @@ -259,7 +259,7 @@ private static int requireIntConsistency(List<ReportFile> reportFiles, String fi
}

private static List<Integer> requireIntegerArrayConsistency(List<ReportFile> reportFiles, String fieldName) {
List<Integer> expected = iterable(reportFiles.getFirst().report().path(fieldName)).stream()
List<Integer> expected = iterable(reportFiles.get(0).report().path(fieldName)).stream()
.map(JsonNode::asInt)
.toList();
for (ReportFile reportFile : reportFiles) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,14 +95,14 @@ private static ComparativeRow runBenchmark(String name, BenchmarkTask task) thro
for (int i = 0; i < MEASUREMENT_ITERATIONS; i++) {
System.gc(); // Форсируем сборку мусора перед каждым замером для чистоты аллокации

long startBytes = bean.getThreadAllocatedBytes(Thread.currentThread().threadId());
long startBytes = bean.getThreadAllocatedBytes(Thread.currentThread().getId());
long startTime = System.nanoTime();

// Выполняем задачу и получаем байты PDF
byte[] pdfBytes = task.runAndGetBytes();

long endTime = System.nanoTime();
long endBytes = bean.getThreadAllocatedBytes(Thread.currentThread().threadId());
long endBytes = bean.getThreadAllocatedBytes(Thread.currentThread().getId());

totalTimeNs += (endTime - startTime);
totalAllocatedBytes += (endBytes - startBytes);
Expand Down
2 changes: 1 addition & 1 deletion examples/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<properties>
<graphcompose.version>${project.version}</graphcompose.version>
<logback.version>1.5.18</logback.version>
<maven.compiler.release>21</maven.compiler.release>
<maven.compiler.release>17</maven.compiler.release>

<junit.bom.version>5.12.2</junit.bom.version>
<assertj.version>3.27.3</assertj.version>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -604,27 +604,27 @@ private static int totalColumnsCovered(List<DocumentTableCell> cells) {
* cells is always 4.
*/
private static List<DocumentTableCell> dayCells(DayShift dayShift, Theme theme) {
if (dayShift == null) {
if (dayShift == null || dayShift instanceof DayShift.EmptyDay) {
return List.of(emptyDayCell(theme));
}
return switch (dayShift) {
case DayShift.EmptyDay e ->
List.of(emptyDayCell(theme));
case DayShift.FullStatus(ShiftStatus s) ->
List.of(fullStatusCell(s, theme));
case DayShift.CrossMeal(Shift shift) ->
shiftAcrossDay(shift, theme);
case DayShift.Halves(Half lunch, Half dinner) ->
mergedHalfCells(lunch, dinner, theme);
};
if (dayShift instanceof DayShift.FullStatus fs) {
return List.of(fullStatusCell(fs.status(), theme));
}
if (dayShift instanceof DayShift.CrossMeal cm) {
return shiftAcrossDay(cm.shift(), theme);
}
if (dayShift instanceof DayShift.Halves h) {
return mergedHalfCells(h.lunch(), h.dinner(), theme);
}
throw new IllegalStateException("Unhandled DayShift variant: " + dayShift);
}

private static List<DocumentTableCell> mergedHalfCells(Half lunch, Half dinner, Theme theme) {
// Both halves on the same status fill → merge to colSpan(4).
if (lunch instanceof Half.StatusFill(ShiftStatus ls)
&& dinner instanceof Half.StatusFill(ShiftStatus ds)
&& ls == ds) {
return List.of(fullStatusCell(ls, theme));
if (lunch instanceof Half.StatusFill ls
&& dinner instanceof Half.StatusFill ds
&& ls.status() == ds.status()) {
return List.of(fullStatusCell(ls.status(), theme));
}
List<DocumentTableCell> cells = new ArrayList<>();
cells.addAll(halfCells(lunch, theme));
Expand All @@ -633,11 +633,16 @@ private static List<DocumentTableCell> mergedHalfCells(Half lunch, Half dinner,
}

private static List<DocumentTableCell> halfCells(Half half, Theme theme) {
return switch (half) {
case Half.Empty e -> List.of(emptyHalfCell(theme));
case Half.StatusFill(ShiftStatus s) -> List.of(statusHalfCell(s, theme));
case Half.Working(Shift shift) -> shiftHalfCells(shift, theme);
};
if (half instanceof Half.Empty) {
return List.of(emptyHalfCell(theme));
}
if (half instanceof Half.StatusFill sf) {
return List.of(statusHalfCell(sf.status(), theme));
}
if (half instanceof Half.Working w) {
return shiftHalfCells(w.shift(), theme);
}
throw new IllegalStateException("Unhandled Half variant: " + half);
}

private static DocumentTableCell emptyDayCell(Theme theme) {
Expand Down
Loading
Loading