This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
OpenFixity is a Dropwizard-based web application for monitoring digital file integrity (fixity). It scans file system paths, computes cryptographic hashes, and tracks changes over time. It provides both a REST API and a Mustache-templated web UI.
# Build, test and run static analysis. Use this, not `package`.
# SpotBugs is bound to the verify phase, and package runs BEFORE verify, so
# `mvn clean package` silently skips the analysis.
mvn clean verify
# Build executable über-JAR only
mvn clean package
# Run the server (dev config)
java -jar target/open-fixity-0.1.1-ALPHA.jar server dev-server.yml
# Run all tests
mvn test
# Run a specific test class
mvn test -Dtest=ScanUpdaterDatabaseTest
# Run a specific test method
mvn test -Dtest=ScanUpdaterDatabaseTest#testFirstScanFilesAreAdded
# Skip tests during build
mvn clean package -DskipTestsThe app follows a layered Dropwizard architecture:
Entry point: OpenFixityServer (extends Application<OpenFixityConfiguration>) in apps/server/ — registers Hibernate entities, REST resources, exception mappers, and the Quartz scheduler.
Layers:
apps/server/resources/api/— JSON REST endpoints (Collections, Paths, Digests, Folders, Scheduler, ScanResults)apps/server/resources/views/— HTML view endpoints backed by Mustache templatesapps/dao/— Hibernate entities + DAOs:Collection,CollectionPath,PathRegistration,PathScan,FileScanRecord,FolderScanRecord,DigestRecord,PathSummaryRecordapps/schedule/— Quartz-based scan job scheduling (ScheduleManager,ScanJob,ScanUpdater,BatchScanner)core/paths/— Scanning logic:PathScannerImplandAbstractPathScannerimplementPathScanner; producesPathScanResult/FileScanResultcore/digests/— Hash computation:HasherImplimplementsHasher;Algorithmsenum lists supported algorithms
Data flow for a scan:
CollectionPathrepresents a registered filesystem root pathScanJob(Quartz) creates aBatchScanner(extendsAbstractPathScanner) and callsscan()AbstractPathScannerwalks the directory tree, callingprocessResults()per batch;BatchScanneraccumulatesFileScanRecordobjects in-memory on thePathScanPathScan.updateFrom()finalises status, populatesFolderScanRecordaggregates, and computesdamagedCount/deniedCountintoPathSummaryRecordScanUpdater.updateDatabase()loads the persistedCollectionPath, compares with the previous scan, then cascades the fullPathScan→FileScanRecord→DigestRecord/FolderScanRecordgraph to H2 in a single transaction
Entity relationships:
CollectionPath
└── PathScan (many)
├── PathSummaryRecord (one-to-one, eager) — stores totalFiles, totalBytes, damagedCount, deniedCount
├── FileScanRecord (one-to-many, lazy, @BatchSize(100))
│ └── DigestRecord (many-to-many via digest_calculation, lazy, @BatchSize(50))
└── FolderScanRecord (one-to-many, lazy)
└── FileScanRecord (one-to-many, lazy, back-reference)
Database: H2 (file-based at ~/.openfixity/open-fixity), managed by Hibernate with hbm2ddl.auto: update.
Config file: dev-server.yml sets the application port (8080), the H2 connection URL, log levels, and JDBC batch size. It is committed, and is the config the README tells people to run with. It holds no secrets. Note it does not pin the admin connector, which therefore defaults to port 8081.
- Java 17, Maven 3.9+
- Dropwizard 5.0.1 (Jetty, Jersey, Jackson)
- Hibernate 6.x / Jakarta Persistence
- Quartz Scheduler 2.5.2
- H2 2.4.240
- JUnit 5 for tests, EqualsVerifier for entity equality tests
Quartz jobs run on threads that have no Hibernate session. To use DAOs from a ScanJob, wrap the target class with UnitOfWorkAwareProxyFactory:
ScanUpdater updater = new UnitOfWorkAwareProxyFactory(OpenFixityServer.getHibernate())
.create(ScanUpdater.class, CollectionPathDAO.class, cpDAO);@UnitOfWork on the proxy's public methods then opens and commits a Hibernate session automatically. @UnitOfWork on private methods is silently ignored — CGLIB proxies cannot intercept private calls. Any private helper that appears to have its own unit of work is actually running in the caller's session.
Every enum field on an entity must be annotated @Enumerated(EnumType.STRING). JPA
defaults to ORDINAL, which stores the constant's position, so reordering or inserting a
constant silently rewrites the meaning of every row already written.
This is not hypothetical. Algorithms was reordered during development: SHA_1 moved from
ordinal 2 to 0, SHA_256 from 4 to 1. DigestRecord.algorithm was stored as an ordinal, so
any database written before the reorder would have had every digest re-attributed to a
different algorithm. A SHA-1 hash would read back as SHA-512. Since checkDigests() matches
on algorithm, the fixity comparison would either find no match and report UNVERIFIED,
quietly abandoning the check, or compare hashes from two different algorithms and report
CHANGED on a file that never changed.
EnumMappingTest reflects over every registered entity and fails if any enum lands on an
integer column. Do not weaken it into a hand-written list of columns; that is exactly how the
DigestRecord one was missed.
Changing an enum mapping changes the column type, and hbm2ddl.auto: update cannot convert
TINYINT to ENUM. Existing dev databases must be deleted (see Dev Database below).
All front-end assets (Bootstrap, Bootstrap Icons, jQuery, bootstrap-table) are vendored
under assets/css/vendor/ and assets/js/vendor/ and served from the application. Do not
reintroduce a CDN <link> or <script>. OpenFixity runs on air-gapped and network-restricted
preservation workstations, where a CDN-dependent page renders unstyled, without icons, and
with non-functional modals.
Icons are Bootstrap Icons (<i class="bi bi-...">), not FontAwesome. The icon fonts live
in assets/css/vendor/fonts/ because the icons stylesheet resolves them by relative path.
Bootstrap is loaded as the bundle, which embeds Popper, so dropdowns and tooltips work if
anyone adds one.
Third-party components are attributed in NOTICE. Add to it if you bundle anything new.
- Use
@org.hibernate.annotations.BatchSizeon lazy@OneToMany/@ManyToManycollections to avoid N+1 SELECT queries. Current batch sizes:PathScan.results= 100,FileScanRecord.digestResults= 50. - Use
JOIN FETCHin named queries for associations needed on list pages (e.g.PathScan.findAllfetchescollectionPathto avoid N+1 per row). - Store derived counts (damaged, denied) in
PathSummaryRecordat scan-completion time rather than computing them from lazy collections at read time. - Enable JDBC batching in
dev-server.yml(hibernate.jdbc.batch_size: 50,hibernate.order_inserts: true,hibernate.order_updates: true) to group large cascade inserts.
Tests live in src/test/java/org/openpreservation/fixity/. JUnit 5 style (@Test annotations). Entity equality tests use EqualsVerifier.
Test session infrastructure (TestSessionFactory):
- Builds a shared in-memory H2 session factory (
jdbc:h2:mem:testdb) for all DAO/integration tests beginTransaction()— opens a session and binds it to the current thread viaManagedSessionContextrollback()— rolls back and closes; used in@AfterEachto keep tests isolated@UnitOfWorkannotations onScanUpdaterare not active in tests (no proxy); methods run in whatever session is currently bound to the thread
Tests that need committed data or concurrency (e.g. ScanUpdaterConcurrentTest) build their own isolated SessionFactory with a unique in-memory DB URL and manage their own inSession() commit/rollback lifecycle. This avoids polluting the shared TestSessionFactory DB.
The H2 file database lives at ~/.openfixity/open-fixity.*. With hbm2ddl.auto: update, Hibernate runs ALTER TABLE on startup to apply schema changes. This works for adding nullable columns or columns with a DEFAULT, but adding a NOT NULL column without a DEFAULT to a table with existing rows will fail. When that happens, delete the DB files and restart:
rm ~/.openfixity/open-fixity.*
java -jar target/open-fixity-0.1.1-ALPHA.jar server dev-server.ymlFEAT: Add new feature
FIX: Bug fix
PERF: Performance improvement
TEST: Add or update tests
DOCS: Documentation only
REFACTOR: Code change with no behaviour change