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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ build
.pdf
.db
logs/
/application.yaml
30 changes: 28 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,34 @@ Keep the JAR and launch script together when moving the application.
commands above use `build/libs/`. Each location has its own database.
- Reports are written to `report.pdf` in the working directory, replacing
the previous report, and opened with the system's default PDF viewer.
- The report organization name is configured in
`src/main/java/openconsignment/common/Constants.java`.

## Application configuration

At startup, the application reads UTF-8 `application.yaml` beside the JAR, regardless
of the current working directory. If this file does not exist, the application copies
`src/main/resources/application.yaml` from inside the JAR to that location and loads
the new file. Existing configuration files are never overwritten.

Edit the generated file to set your organization name:

```yaml
organizationName: "YOUR ORGANIZATION"
```

This name appears on the welcome screen and in buyer, supplier, and transport
ledger reports. The bundled YAML is used only as the template for first-run creation;
properties are not merged or filled in from it when an external file already exists.
YAML is bound directly to configuration POJOs using SnakeYAML. Quote the name to
preserve its exact text; scalar values such as numbers are converted to strings.
Surrounding whitespace is trimmed. A blank or null name, malformed YAML, or an
unreadable configuration file is reported as a startup error. An empty file or missing
required property also fails validation. The application needs write access to the
JAR directory when creating the file for the first time.

For Gradle/IDE runs, the file is created and read in the working directory (the
repository root for `./gradlew run`). The example file is included in `build/libs/`
when building. Restart the application after editing the file. Updating the JAR
does not replace an existing configuration file.

## Logging

Expand Down
239 changes: 120 additions & 119 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,136 +1,137 @@
import org.gradle.api.tasks.Copy
plugins {
application
java
jacoco
alias(libs.plugins.javafx)
alias(libs.plugins.spotless)
alias(libs.plugins.shadow)
}
repositories {
mavenCentral()
}
dependencies {
implementation(libs.controlsfx)
implementation(libs.jasperreports)
implementation(libs.sqlite)
import org.gradle.api.tasks.Copy

plugins {
application
java
jacoco
alias(libs.plugins.javafx)
alias(libs.plugins.spotless)
alias(libs.plugins.shadow)
}

repositories {
mavenCentral()
}

dependencies {
implementation(libs.controlsfx)
implementation(libs.jasperreports)
implementation(libs.sqlite)
implementation(libs.olap4j)
implementation(libs.slf4j.api)
implementation(libs.snakeyaml)
runtimeOnly(libs.log4j.slf4j2.impl)
runtimeOnly(libs.log4j.core)
// implementation(libs.itext)
implementation(libs.dagger)
annotationProcessor(libs.dagger.compiler)
implementation(libs.javafx.controls)
implementation(libs.javafx.base)
implementation(libs.javafx.graphics)
implementation(libs.javafx.fxml)
compileOnly(libs.lombok)
annotationProcessor(libs.lombok)
testImplementation(libs.testfx.junit5)
testImplementation(libs.testfx.monocle)
testImplementation(libs.hamcrest)
testImplementation(platform(libs.junit.bom))
testImplementation(libs.junit.jupiter)
testRuntimeOnly(libs.junit.platform.launcher)
testImplementation(libs.testfx.core)
testImplementation(libs.mockito.core)
testImplementation(libs.mockito.junit.jupiter)
}
javafx {
version = libs.versions.javafx.get()
modules = listOf("javafx.controls", "javafx.fxml", "javafx.graphics", "javafx.base")
}
// implementation(libs.itext)

implementation(libs.dagger)
annotationProcessor(libs.dagger.compiler)

implementation(libs.javafx.controls)
implementation(libs.javafx.base)
implementation(libs.javafx.graphics)
implementation(libs.javafx.fxml)

compileOnly(libs.lombok)
annotationProcessor(libs.lombok)

testImplementation(libs.testfx.junit5)
testImplementation(libs.testfx.monocle)
testImplementation(libs.hamcrest)

testImplementation(platform(libs.junit.bom))
testImplementation(libs.junit.jupiter)
testRuntimeOnly(libs.junit.platform.launcher)

testImplementation(libs.testfx.core)

testImplementation(libs.mockito.core)
testImplementation(libs.mockito.junit.jupiter)
}

javafx {
version = libs.versions.javafx.get()
modules = listOf("javafx.controls", "javafx.fxml", "javafx.graphics", "javafx.base")
}

tasks.test {
useJUnitPlatform()
finalizedBy(tasks.jacocoTestReport)
systemProperty("logging.test.classpath", sourceSets.test.get().runtimeClasspath.asPath)
providers.gradleProperty("loggingTestJar").orNull?.let { systemProperty("logging.test.jar", file(it).absolutePath) }

// Keep UI tests independent of desktop focus and native mouse permissions.
// Use -PheadlessTests=false when debugging tests with visible windows.
val headlessTests = providers.gradleProperty("headlessTests").map { it.toBoolean() }.getOrElse(true)
if (headlessTests) {
jvmArgs(
"-Dtestfx.robot=glass",
"-Dtestfx.headless=true",
"-Dprism.order=sw",
"-Dprism.text=t2k",
"-Djava.awt.headless=true"
)
}
}
jacoco {
toolVersion = "0.8.12"
}
tasks.jacocoTestReport {
dependsOn(tasks.test)
reports {
xml.required.set(true)
html.required.set(true)
}
}
spotless {
java {
lineEndings = com.diffplug.spotless.LineEnding.UNIX
target("src/**/*.java")
palantirJavaFormat()
removeUnusedImports()
endWithNewline()
trimTrailingWhitespace()
}
}
application {
mainClass.set("openconsignment.Launcher")
}
tasks.named<JavaExec>("run") {
jvmArgs(
"--add-exports=javafx.base/com.sun.javafx.event=ALL-UNNAMED",
"--add-exports=javafx.controls/com.sun.javafx.scene.control=ALL-UNNAMED",
"--add-exports=javafx.controls/com.sun.javafx.scene.control.behavior=ALL-UNNAMED",
"--add-exports=javafx.base/com.sun.javafx.binding=ALL-UNNAMED",
"--add-exports=javafx.graphics/com.sun.javafx.stage=ALL-UNNAMED"
)
}
jvmArgs(
"-Dtestfx.robot=glass",
"-Dtestfx.headless=true",
"-Dprism.order=sw",
"-Dprism.text=t2k",
"-Djava.awt.headless=true"
)
}
}

jacoco {
toolVersion = "0.8.12"
}

tasks.jacocoTestReport {
dependsOn(tasks.test)
reports {
xml.required.set(true)
html.required.set(true)
}
}

spotless {
java {
lineEndings = com.diffplug.spotless.LineEnding.UNIX
target("src/**/*.java")

palantirJavaFormat()
removeUnusedImports()
endWithNewline()
trimTrailingWhitespace()
}
}

application {
mainClass.set("openconsignment.Launcher")
}

tasks.named<JavaExec>("run") {
jvmArgs(
"--add-exports=javafx.base/com.sun.javafx.event=ALL-UNNAMED",
"--add-exports=javafx.controls/com.sun.javafx.scene.control=ALL-UNNAMED",
"--add-exports=javafx.controls/com.sun.javafx.scene.control.behavior=ALL-UNNAMED",
"--add-exports=javafx.base/com.sun.javafx.binding=ALL-UNNAMED",
"--add-exports=javafx.graphics/com.sun.javafx.stage=ALL-UNNAMED"
)
}

tasks.named<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar>("shadowJar") {
archiveClassifier.set("")
mergeServiceFiles()
}
tasks.named("distZip") { enabled = false }
tasks.named("distTar") { enabled = false }
tasks.named("startScripts") { enabled = false }
tasks.named<Jar>("jar") { enabled = false }
val copyScripts by tasks.registering(Copy::class) {
from("scripts") {
include("start.sh", "start.bat", "configure-backup.py", "templates/**")
}
into(layout.buildDirectory.dir("libs"))
doLast {
println("Copied scripts to build directory")
}
}
tasks.named("shadowJar") {
dependsOn(copyScripts)
}

tasks.named("distZip") { enabled = false }
tasks.named("distTar") { enabled = false }
tasks.named("startScripts") { enabled = false }
tasks.named<Jar>("jar") { enabled = false }

val copyScripts by tasks.registering(Copy::class) {
from("scripts") {
include("start.sh", "start.bat", "configure-backup.py", "templates/**")
}
into(layout.buildDirectory.dir("libs"))
doLast {
println("Copied scripts to build directory")
}
}

tasks.named("shadowJar") {
dependsOn(copyScripts)
}
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ junitbom = "5.10.2"
mockito = "5.14.2"
slf4j = "2.0.17"
log4j = "2.24.3"
snakeyaml = "2.3"

spotless = "7.0.2"
shadow = "7.1.2"
Expand All @@ -27,6 +28,7 @@ jasperreports = { module = "net.sf.jasperreports:jasperreports", version.ref = "
sqlite = { module = "org.xerial:sqlite-jdbc", version.ref = "sqlite" }
olap4j = { module = "org.olap4j:olap4j", version.ref = "olap4j" }
slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" }
snakeyaml = { module = "org.yaml:snakeyaml", version.ref = "snakeyaml" }
log4j-slf4j2-impl = { module = "org.apache.logging.log4j:log4j-slf4j2-impl", version.ref = "log4j" }
log4j-core = { module = "org.apache.logging.log4j:log4j-core", version.ref = "log4j" }
#itext = { module = "com.lowagie:itext", version.ref = "itext" }
Expand Down
Empty file modified scripts/configure-backup.py
100644 → 100755
Empty file.
Empty file modified scripts/start.sh
100644 → 100755
Empty file.
1 change: 1 addition & 0 deletions src/main/java/openconsignment/Launcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public void start(Stage stage) {
throw new RuntimeException("Resource not found");
}
AppComponent component = DaggerAppComponent.create();
component.configuration();
FXMLLoader loader = new FXMLLoader(resource);
loader.setControllerFactory(component.controllerFactory());
Parent root = loader.load();
Expand Down
31 changes: 31 additions & 0 deletions src/main/java/openconsignment/common/AppConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package openconsignment.common;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

/** Application settings bound from YAML once at startup. */
@Getter
@Setter
@NoArgsConstructor
public class AppConfig {
private String organizationName;

public static AppConfig forOrganization(String organizationName) {
AppConfig configuration = new AppConfig();
configuration.setOrganizationName(organizationName);
configuration.validate();
return configuration;
}

public String organizationName() {
return organizationName;
}

public void validate() {
if (organizationName == null || organizationName.isBlank()) {
throw new IllegalArgumentException("organizationName must not be blank");
}
organizationName = organizationName.strip();
}
}
1 change: 0 additions & 1 deletion src/main/java/openconsignment/common/Constants.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

public class Constants {
public static final String ORGANIZATION_NAME_KEY = "organizationName";
public static final String ORGANIZATION_NAME = "SATYAM AGENCIES";
public static final String REPORT_FILE_NAME = "report.pdf";
public static final String DATE_TIME_FORMAT = "dd-MM-yyyy";
public static final String DATABASE_FILE_NAME = "database.db";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,22 @@
import javafx.scene.control.Hyperlink;
import javafx.scene.text.Text;
import javax.inject.Inject;
import openconsignment.common.Constants;
import openconsignment.common.AppConfig;
import openconsignment.common.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class WelcomeScreenController implements Initializable {
private static final Logger logger = LoggerFactory.getLogger(WelcomeScreenController.class);
private final AppConfig configuration;

@FXML
private Text organization_name;

@Inject
public WelcomeScreenController() {}
public WelcomeScreenController(AppConfig configuration) {
this.configuration = configuration;
}

@FXML
private void handleOpenGithub(ActionEvent event) {
Expand All @@ -38,6 +41,6 @@ private void handleOpenGithub(ActionEvent event) {

@Override
public void initialize(URL url, ResourceBundle rb) {
organization_name.setText(Constants.ORGANIZATION_NAME);
organization_name.setText(configuration.organizationName());
}
}
Loading
Loading