diff --git a/.cursor/rules/ascii-source.mdc b/.cursor/rules/ascii-source.mdc new file mode 100644 index 0000000..ad837c1 --- /dev/null +++ b/.cursor/rules/ascii-source.mdc @@ -0,0 +1,22 @@ +--- +description: Tracked source and docs must be ASCII only +alwaysApply: true +--- + +# ASCII source + +Every file you **add or edit** must be **ASCII only** (bytes 0x00-0x7F). Scan touched paths before finishing. + +Replace: + +| Non-ASCII | ASCII | +| ------------------ | ----------- | +| em/en dash, minus | `-` | +| ellipsis | `...` | +| curly quotes | `'` / `"` | +| arrows | `->` / `<-` | +| non-breaking space | space | + +Do not introduce Unicode symbols, emoji, or locale punctuation in code, comments, XML, or docs. + +WSDL/XSD copied from the licensed toolkit may contain non-ASCII. Do not add more. Prefer ASCII in files you author. diff --git a/.cursor/rules/documentation-standards.mdc b/.cursor/rules/documentation-standards.mdc new file mode 100644 index 0000000..778c751 --- /dev/null +++ b/.cursor/rules/documentation-standards.mdc @@ -0,0 +1,50 @@ +--- +description: Documentation and build scripts - implementor-first, Maven Central default, no secrets +alwaysApply: true +--- + +# Documentation and build scripts + +Applies when writing or editing **README**, **CONTRIBUTING**, integrator guides, **SECURITY**, release notes, **build.*** / **build.xml** header comments, **settings.xml.example**, **local.properties.example**, and similar tracked material. + +## Priority: implementors + +- Primary audience: developers **consuming** published Maven artifacts. +- Contributor/maintainer workflow lives in **CONTRIBUTING.md**, **MAINTAINERS.md**, or labelled subsections - not mixed into the integrator main flow. +- Do not duplicate agent workflow in README or CONTRIBUTING (**AGENTS.md** -> `.cursor/rules/`). + +## Default narrative: Maven Central + +- Write as if **released** coordinates are on Maven Central (no SNAPSHOT, sibling-repo checkout, or `-P...` profiles in the main integrator path). +- Use **version numbers** in integrator docs - not Git branch names. +- SNAPSHOT installs and sibling `mvn install` belong under **Local development** / **Local builds** only. + +## Structure + +| Where | What | +| ----- | ---- | +| README / integrator docs | Dependency, runtime config, capabilities | +| Local development | Unpublished builds, SNAPSHOT, source-tree prerequisites | +| By OS (when needed) | Separate **Windows**, **macOS**, **Linux** blocks (wrappers, cleanup, path notes) | + +## Paths + +- **Relative paths only** (e.g. `src/main/resources/`, `pom.xml` beside repo root). +- **Never** hardcode machine paths (`C:\...`, `/Users/...`, `~/`, worktree folders) in tracked docs. +- Override only when strictly required to remove ambiguity - say why. + +## Current state only + +- No migration, legacy, or branch-history commentary in user-facing docs. +- Describe **what is true now**; omit "removed", "used to", and repo archaeology in integrator material. +- **CHANGELOG** entries: current release capabilities; avoid branch names and migration essays in new notes. + +## Secrets - MUST NOT + +- **Never** put real or plausible **passwords**, **tokens**, **API keys**, **private keys**, or **keystore passphrases** in tracked files (code, comments, examples, docs). +- Use placeholders or empty values in `.example` files. +- See **SECURITY.md**. + +## Override + +These rules may be overridden **only when necessary** to clear ambiguity (e.g. maintainer branch mapping in **MAINTAINERS.md** or `.cursor/rules/`, clearly labelled maintainer-only). diff --git a/.cursor/rules/multi-branch-worktrees.mdc b/.cursor/rules/multi-branch-worktrees.mdc new file mode 100644 index 0000000..aa0018b --- /dev/null +++ b/.cursor/rules/multi-branch-worktrees.mdc @@ -0,0 +1,45 @@ +--- +description: Use git worktree when editing multiple branches of the same repository +alwaysApply: true +--- + +# Multi-branch work - use git worktree + +When a task requires **changes on two or more branches of the same Git repository**, use **`git worktree`** - do **not** hop branches with `git checkout` while carrying uncommitted edits or stashes. + +## When this applies + +- Same-repo edits across release lines (e.g. `java-8`, `java-11`) +- Applying the same fix to several branches before commit +- Comparing or building two branches side by side + +## When checkout is fine + +- Single branch for the whole task +- Read-only inspection: `git show :path` +- Clean working tree and only one target branch + +## Workflow + +1. **Primary clone** stays on the main working branch; do not stash-and-hop to edit other branches. +2. **Add a worktree per extra branch** (paths are local - never commit them to tracked docs): + + ```bash + git worktree add ../- + ``` + +3. **Edit and commit in each worktree** independently; verify build/tests per branch. +4. **Remove when done**: `git worktree remove ../-` (or `git worktree prune` after manual delete). + +## Do not + +- `git checkout` other branches with uncommitted changes (Git will block or force stash churn). +- Stash the same files across multiple branch switches - causes merge conflicts on `stash pop`. +- Put worktree directory paths in README, CONTRIBUTING, or other tracked docs (see repo **`worktrees.mdc`** / **`documentation-standards.mdc`**). + +## Example (maintainer-local paths only) + +```bash +git worktree add java-8 +git worktree add java-11 +``` diff --git a/.cursor/rules/smi-common-utils-project.mdc b/.cursor/rules/smi-common-utils-project.mdc new file mode 100644 index 0000000..6e8cb10 --- /dev/null +++ b/.cursor/rules/smi-common-utils-project.mdc @@ -0,0 +1,38 @@ +# smi-common-utils-java + +Maven artifact **`au.gov.nehta:smi-common-utils`** - Java utility classes to assist with CDA library development. **Not** facade clients or HI/MHR services. + +## Documentation + +Follow **`documentation-standards.mdc`**. Integrator docs use **version numbers** - not Git branch names. + +The **first number** of the Maven version is the **Java SE** target. + +| Version | Java | Logging | +| ---------- | ---- | ---------------------------------- | +| **8.0.0** | 8 | **SLF4J 1.7.x** (javax-compatible) | +| **11.0.0** | 11 | **SLF4J 2.0.18** | + +**Git branch mapping (maintainers only - never in integrator docs):** + +| Version | Official Git branch | +| ---------- | ------------------- | +| **8.0.0** | `java-8` | +| **11.0.0** | `java-11` | + +## This 11.0.0 line + +**`11.0.0-SNAPSHOT`**: Java **11**, SLF4J **2.0.18**. No JAX-WS dependencies. + +## Build + +- JDK **11+**, from repo root: `mvn -B "-Dgpg.skip=true" clean verify` (quote `-D` on PowerShell). +- Default Surefire: all tests. +- **`-Pcoverage`**: JaCoCo report. + +## Doc split + +- **`documentation-standards.mdc`**: implementor-first, Maven Central default, local/OS sections, relative paths, no secrets +- **`README.md`**: integrators (Maven Central default) +- **`CONTRIBUTING.md`**: build from source, local SNAPSHOT +- **`MAINTAINERS.md`**: POM internals diff --git a/.cursor/rules/worktrees.mdc b/.cursor/rules/worktrees.mdc new file mode 100644 index 0000000..ee4b788 --- /dev/null +++ b/.cursor/rules/worktrees.mdc @@ -0,0 +1,37 @@ +--- +description: Worktree paths are local only - versions belong in integrator docs +alwaysApply: true +--- + +# Worktrees (local convenience only) + +**`git worktree`** lets a maintainer check out multiple branches of the **same repository** at once. Worktree **directory paths are machine-specific** and must **not** appear in tracked documentation. + +## What tracked docs describe + +Follow **`documentation-standards.mdc`** (implementor-first, Maven Central default). + +| Doc | Audience | Use | +| --- | --------- | --- | +| **README**, integrator guides | Implementors | **Maven version** (`11.0.0`), Central coordinates, JAR contents | +| **CONTRIBUTING** | Contributors | Build from source, **Local builds**, OS-specific steps | +| **MAINTAINERS**, **`.cursor/rules/`** | Maintainers | POM internals; **branch mapping** only when labelled maintainer-only | + +Integrator docs: **version numbers**, not Git branch names. Local/SNAPSHOT flows: **CONTRIBUTING** only. + +## Do not put in tracked docs + +- Hardcoded worktree or machine paths (`wt/`, `C:\...`, `/Users/...`, `~/`, `this worktree`, `primary clone`) +- References to **`wt/README.md`** or other local-only layout files +- "Open folder X to switch branches" - IDE/local workflow, not repo documentation + +## Allowed (maintainer-only, when labelled) + +- **Git branch mapping** table (version -> branch) in **MAINTAINERS.md** or agent rules +- Generic worktree example only: `git worktree add ` (placeholder **``**, never a committed example) +- Optional **untracked** local notes outside the repo + +## When editing docs + +1. Identify the **Maven version / release line** the text applies to, not the worktree folder Cursor has open. +2. Remove or rewrite any committed worktree path tables or branch names in integrator-facing files. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..4e09d2c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,23 @@ +name: CI + +on: + push: + branches: [ java-11 ] + pull_request: + branches: [ java-11 ] + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: "11" + cache: maven + - name: Maven verify + run: mvn -B "-Dgpg.skip=true" clean verify diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..f1b6928 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,13 @@ +# Cursor agents + +Rules: `.cursor/rules/*.mdc`. Do not duplicate in README or CONTRIBUTING. + +| Concern | Rule | +|---------|------| +| ASCII only in authored files | `ascii-source.mdc` | +| Documentation and build scripts (implementor-first, Central, local, OS, paths, no secrets) | `documentation-standards.mdc` | +| smi-common-utils scope, build, version lines | `smi-common-utils-project.mdc` | +| Worktrees local only; versions in integrator docs | `worktrees.mdc` | +| Multi-branch edits in one repo - use worktree, not checkout/stash | `multi-branch-worktrees.mdc` | + +Contributors (humans): README, CONTRIBUTING, MAINTAINERS, SECURITY, CHANGELOG. diff --git a/CHANGELOG.md b/CHANGELOG.md index 35eff75..1f8be5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,33 @@ -# Change Log/Revision History +# Changelog -= 1.2.1 = -========= -- Converted to Maven to allow deployment to Maven repo +## 11.0.0 -No record of previous releases available. +- Java 11 release line (`maven.compiler.release=11`) +- Jakarta XML Bind API 4.0.5 +- SLF4J 2.0.18 +- Java 11 idioms: NIO.2 UTF-8 file I/O, `List.of`, `Objects.equals`, `DateTimeFormatter`, `ConcurrentHashMap`, try-with-resources, `Class` + +## 8.0.0 + +- Version line aligned to Java 8 (`maven.compiler.release=8`) +- Distribution migrated to Sonatype Central Portal (`central-publishing-maven-plugin`) +- Updated `slf4j` to 1.7.36 (last 1.7.x on Maven Central) +- Plugin versions updated: compiler 3.15.0, surefire 3.5.5, javadoc 3.12.0, source 3.4.0, gpg 3.2.8 +- GPG signing skipped by default; enabled via `-Prelease` + +## 1.2.1 + +- Converted to Maven +- Replaced external dependencies with Maven ones + +## 1.2.0 + +- Added support for JVM 1.7_21+ + +## 1.0 -= 1.2.0 = -========= -17 Feb 2010 - -= 1.0 = -======= - Initial release + +## Copyright + +Copyright 2009 NEHTA. Copyright 2021-2026 ADHA. Apache License 2.0 - see **LICENSE.txt**. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..790f4ef --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,46 @@ +# Contributing + +## Prerequisites + +- JDK 11 +- Maven 3.6+ + +## Local builds + +``` +# Windows +mvn -B "-Dgpg.skip=true" clean verify + +# macOS / Linux +mvn -B -Dgpg.skip=true clean verify +``` + +## Local SNAPSHOT install + +To install to your local Maven repository without signing: + +``` +mvn -B "-Dgpg.skip=true" clean install +``` + +## Running tests + +Tests run automatically during `verify`. To skip: + +``` +mvn -B "-Dgpg.skip=true" -DskipTests clean package +``` + +## Code style + +- Java 11 source and target (`maven.compiler.release=11`) +- All source files must be ASCII only +- SLF4J 2.0.18 API + +## Pull requests + +Target the `java-11` branch. Ensure `mvn verify` passes before opening a PR. + +## Copyright + +Copyright 2009 NEHTA. Copyright 2021-2026 ADHA. Apache License 2.0 - see **LICENSE.txt**. diff --git a/MAINTAINERS.md b/MAINTAINERS.md new file mode 100644 index 0000000..aa420fe --- /dev/null +++ b/MAINTAINERS.md @@ -0,0 +1,34 @@ +# Maintainers + +## Artifact + +`au.gov.nehta:smi-common-utils` - published to Maven Central via Sonatype Central Portal. + +## Version lines + +| Version | Java | Branch | +| ---------- | ------------ | --------- | +| **8.0.0** | 8 / javax | `java-8` | +| **11.0.0** | 11 / Jakarta | `java-11` | + +## Key dependency + +| Property | Value | +| --------------- | -------- | +| `slf4j.version` | `2.0.18` | + +To update: change `slf4j.version` in `pom.xml` properties, run `mvn verify`. + +## Release process + +1. Ensure `java-11` branch is green. +2. `mvn -B release:prepare release:perform -Prelease` (GPG key required). +3. Sonatype Central Portal auto-publishes after closing. + +## Publishing credentials + +`settings.xml` with `` - see `settings.xml.example`. + +## Copyright + +Copyright 2009 NEHTA. Copyright 2021-2026 ADHA. Apache License 2.0 - see **LICENSE.txt**. diff --git a/README.md b/README.md index 0ceb09d..6e9f70b 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,41 @@ -# SMI Common Utils Library +# smi-common-utils - SMI Common Utilities -This is a collection of utility classes helpful for CDA libraries. +Java utility classes to assist with CDA library development for the Australian Digital Health Agency. -Building and running the code -============================= +## Dependency (Maven Central) -The project is supplied with a Maven pom.xml file. Use Maven to build the code. +```xml + + au.gov.nehta + smi-common-utils + 11.0.0 + +``` -Source code -=========== +## Runtime requirements -The source code is the src/main/java directory structure. +- Java 11 or later +- SLF4J 2.0.18 API is a compile dependency; include an SLF4J binding on your classpath at runtime -Licensing -========= +## Versioning -Copyright 2009 NEHTA +| Version | Java | Logging | +| ---------- | ---- | ------------ | +| **8.0.0** | 8 | SLF4J 1.7.x | +| **11.0.0** | 11 | SLF4J 2.0.18 | -Copyright 2021 ADHA +## Building from source -Licensed under the NEHTA/ADHA Open Source (Apache) License; you may not use this -file except in compliance with the License. A copy of the License is in the -'LICENSE.txt' file, which should be provided with this work. +See **CONTRIBUTING.md**. -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -License for the specific language governing permissions and limitations -under the License. +## Local development + +See **CONTRIBUTING.md** - Local builds section. + +## License + +Apache License 2.0 - see **LICENSE.txt**. + +## Copyright + +Copyright 2009 NEHTA. Copyright 2021-2026 ADHA. Apache License 2.0 - see **LICENSE.txt**. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..d38cea9 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,18 @@ +# Security + +## Reporting a vulnerability + +Report security vulnerabilities to: **admin.opensource@digitalhealth.gov.au** + +Do not open a public GitHub issue for security vulnerabilities. + +## Supported versions + +| Version | Supported | +| ------- | --------- | +| 11.0.0 | Yes | +| 8.0.0 | Yes | + +## Copyright + +Copyright 2009 NEHTA. Copyright 2021-2026 ADHA. Apache License 2.0 - see **LICENSE.txt**. diff --git a/pom.xml b/pom.xml index 926907a..9f1a418 100644 --- a/pom.xml +++ b/pom.xml @@ -1,10 +1,15 @@ + 4.0.0 au.gov.nehta smi-common-utils - 1.2.2-SNAPSHOT + 11.0.0-SNAPSHOT SMI Common Utils Java classes to assist with CDA library development https://github.com/AuDigitalHealth/smi-common-utils-java @@ -12,7 +17,7 @@ Apache License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0.txt + https://www.apache.org/licenses/LICENSE-2.0.txt repo @@ -22,8 +27,8 @@ Admin Open Source admin.opensource@digitalhealth.gov.au Australian Digital Health Agency - www.digitalhealth.gov.au - www.digitalhealth.gov.au + https://www.digitalhealth.gov.au/ + https://www.digitalhealth.gov.au/ Admin @@ -32,55 +37,107 @@ - scm:git:https://github.com/AuDigitalHealth/smi-common-utils-java.git - - - scm:git:https://github.com/AuDigitalHealth/smi-common-utils-java.git - - https://github.com/AuDigitalHealth/smi-common-utils-java.git - smi-common-utils-1.2.1 + scm:git:${scm.repo.url} + scm:git:${scm.repo.url} + ${scm.repo.url} + ${scm.tag} + + GitHub + https://github.com/AuDigitalHealth/smi-common-utils-java/issues + + + + GitHub Actions + https://github.com/AuDigitalHealth/smi-common-utils-java/actions + + - nexus-snapshots - https://oss.sonatype.org/content/repositories/snapshots + central + https://central.sonatype.com/repository/maven-snapshots/ - nexus-releases - https://oss.sonatype.org/service/local/staging/deploy/maven2 + central + https://central.sonatype.com + + 11 + UTF-8 + ${encoding} + ${encoding} + ${encoding} + ${encoding} + false + true + https://github.com/AuDigitalHealth/smi-common-utils-java.git + HEAD + true + false + deploy + release + @{project.artifactId}-@{project.version} + true + true + [maven-release-plugin] + + 2.0.18 + 4.0.5 + 4.13.2 + 3.5.0 + 3.15.0 + 3.5.6 + 3.12.0 + 3.4.0 + 3.1.4 + 3.3.1 + 0.11.0 + 3.2.8 + 0.8.15 + -Xmx1024m + + org.slf4j slf4j-api - 1.7.30 + ${slf4j.version} - org.slf4j - slf4j-simple - 1.7.30 + jakarta.xml.bind + jakarta.xml.bind-api + ${jakarta.xml.bind.api.version} + + + junit + junit + ${junit.version} + test - - 1.8 - 1.8 - UTF-8 - ${encoding} - ${encoding} - ${encoding} - ${encoding} - - + + org.apache.maven.plugins + maven-clean-plugin + ${plugin.clean.version} + + true + false + true + + org.apache.maven.plugins maven-gpg-plugin - 1.6 + ${plugin.gpg.version} + + ${gpg.skip} + sign-artifacts @@ -94,12 +151,13 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.2.0 + ${plugin.javadoc.version} - true - + false none + false ${encoding} + 11 @@ -113,12 +171,12 @@ org.apache.maven.plugins maven-source-plugin - 3.2.1 + ${plugin.source.version} attach-sources - jar + jar-no-fork @@ -126,75 +184,106 @@ org.apache.maven.plugins maven-deploy-plugin - 3.0.0-M1 + ${plugin.deploy.version} true - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.8 + org.sonatype.central + central-publishing-maven-plugin + ${plugin.central.publishing.version} true - nexus-releases - https://oss.sonatype.org/ - true - - - - org.apache.maven.plugins - maven-surefire-report-plugin - 3.0.0-M4 - - false - CDA Schema Java Tests Output - ${basedir}/target/surefire-reports + central + true maven-surefire-plugin - 2.22.2 + ${plugin.surefire.version} + ${skipTests} UTF-8 true - 3 - true - -Xmx1024m + ${argLine} org.apache.maven.plugins maven-release-plugin - 3.0.0-M1 + ${plugin.release.version} - mavenization false - release - true - false - deploy + ${release.profiles} + ${release.localCheckout} + ${release.dryRun} + ${release.goals} + ${release.tagNameFormat} + ${release.pushChanges} + ${release.remoteTagging} + ${release.scmCommentPrefix} maven-compiler-plugin - 3.8.1 + ${plugin.compiler.version} + ${maven.compiler.release} + ${project.build.sourceEncoding} + none -Xlint:unchecked - -verbose - ${maven.compiler.source} - ${maven.compiler.target} - ${project.build.sourceEncoding} - true + true lines,vars,source true true - true - \ No newline at end of file + + + release + + false + + + + dev-javadoc-off + + true + + + + coverage + + + + org.jacoco + jacoco-maven-plugin + ${jacoco.version} + + + jacoco-prepare-agent + + prepare-agent + + + + jacoco-report + verify + + report + + + + + + + + + + diff --git a/settings.xml.example b/settings.xml.example new file mode 100644 index 0000000..fee3e9f --- /dev/null +++ b/settings.xml.example @@ -0,0 +1,28 @@ + + + + + diff --git a/src/main/java/au/gov/nehta/common/utils/AbstractProcessScheduler.java b/src/main/java/au/gov/nehta/common/utils/AbstractProcessScheduler.java index 32648bd..82779c8 100644 --- a/src/main/java/au/gov/nehta/common/utils/AbstractProcessScheduler.java +++ b/src/main/java/au/gov/nehta/common/utils/AbstractProcessScheduler.java @@ -1,13 +1,12 @@ package au.gov.nehta.common.utils; +import java.util.concurrent.atomic.AtomicBoolean; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * This class facilitates the java process to run periodically for the specified - * time interval. This scheduled thread can be stopped, restarted and cancelled. - * - * @author VinSekar + * Runs a subclass task periodically for a configured sleep interval. The + * scheduled thread can be stopped and restarted. */ public abstract class AbstractProcessScheduler extends Thread { @@ -15,24 +14,14 @@ public abstract class AbstractProcessScheduler extends Thread { .getLogger(AbstractProcessScheduler.class); /** - * The time interval in seconds nanoseconds + * Sleep interval in milliseconds between task executions. */ private final int interval; - /** - * Flag value to stop the thread. - */ - private boolean stopThread = false; + private final AtomicBoolean stopThread = new AtomicBoolean(false); /** - * The timer instance to run the scheduled task for the specified time - * interval. - */ - - /** - * Default constructor with accepts the time interval as an argument. - * - * @param interval + * @param interval sleep interval in milliseconds */ public AbstractProcessScheduler(int interval) { this.interval = interval; @@ -44,19 +33,18 @@ public void run() { } /** - * This method runs the current thread periodically for the provided interval. + * Runs {@link #performThreadTask()} until {@link #stopTimerThread()} is called + * or the thread is interrupted. */ public void runPeriodically() { try { - this.stopThread = false; - // This thread check for every x time interval to stop/continue the - // thread. - while (!this.stopThread) { + this.stopThread.set(false); + while (!this.stopThread.get()) { Thread.sleep(this.interval); performThreadTask(); } - } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); String errMsg = "The thread running periodic task,'" + this.getClass().getSimpleName() + "', was interrupted while sleeping."; @@ -65,34 +53,29 @@ public void runPeriodically() { } /** - * Implement the piece of code to be executed for the scheduled time interval. + * Implement the code to execute on each interval. */ public abstract void performThreadTask(); /** - * This method stops the scheduled thread. This method is thread safe. + * Stops the scheduled loop. Thread-safe. */ - public synchronized void stopTimerThread() { - this.stopThread = true; + public void stopTimerThread() { + this.stopThread.set(true); } /** - * This method starts the scheduler thread. This method is thread safe. + * Clears the stop flag and runs the periodic loop on the calling thread. */ - public synchronized void startThread() { - this.stopThread = false; + public void startThread() { + this.stopThread.set(false); runPeriodically(); } /** - * This method returns true if the timer thread is running. - * - * @return true if timer thread is running + * @return true if the stop flag is not set */ public boolean isThreadRunning() { - if (!this.stopThread) { - return true; - } - return false; + return !this.stopThread.get(); } } diff --git a/src/main/java/au/gov/nehta/common/utils/ArgumentUtils.java b/src/main/java/au/gov/nehta/common/utils/ArgumentUtils.java index 5f07677..f916a70 100644 --- a/src/main/java/au/gov/nehta/common/utils/ArgumentUtils.java +++ b/src/main/java/au/gov/nehta/common/utils/ArgumentUtils.java @@ -1,6 +1,7 @@ package au.gov.nehta.common.utils; import java.util.Collection; +import java.util.Objects; /** @@ -48,11 +49,11 @@ public static void checkNotNull(Object value, String name) { * @throws NullPointerException If the collection argument is null. * @throws IllegalArgumentException If the collection argument does not contain at least one item. */ - public static void checkNotNullNorEmpty(Collection collection, String name) { + public static void checkNotNullNorEmpty(Collection collection, String name) { assert (name != null) : "'name' is null."; checkNotNull(collection, name); - if (collection.size() == 0) { + if (collection.isEmpty()) { throw new IllegalArgumentException(name + " must contain at least one item."); } @@ -72,22 +73,22 @@ public static void checkNotNullNorBlank(String value, String name) { assert (name != null) : "'name' is null."; checkNotNull(value, name); - if ((value.length() == 0) || (value.trim().length() == 0)) { + if (value.isEmpty() || value.trim().isEmpty()) { throw new IllegalArgumentException(name + " cannot be a blank string."); } } /** - * Check that an argument is blank (null or empty string). + * Check that an argument is blank (null, empty, or whitespace-only after trim). * * @param value Value of the string argument to check. * @param name Name of the string argument. - * @throws IllegalArgumentException If the value argument is not null or an empty string. + * @throws IllegalArgumentException If the value argument is not blank. */ public static void checkNullOrBlank(String value, String name) { assert (name != null) : "'name' is null."; - if ((value != null) && (value.trim().length() > 0)) { + if ((value != null) && !value.trim().isEmpty()) { throw new IllegalArgumentException("A value is not permitted for " + name + "."); } @@ -115,20 +116,20 @@ public static void checkMaxLength(String value, int maxLen, String name) { * Checks if a collection argument is null or has no items. * * @param collection collection to check. - * @return true if collection is collection is null or has no items. + * @return true if collection is null or has no items. */ - public static boolean isNullOrEmpty(Collection collection) { - return ((collection == null) || (collection.size() == 0)); + public static boolean isNullOrEmpty(Collection collection) { + return ((collection == null) || collection.isEmpty()); } /** * Checks if a string argument is null or has no non-whitespace characters. * * @param value string value to check. - * @return true if collection is collection is null or has no items. + * @return true if value is null, empty, or whitespace-only after trim. */ public static boolean isNullOrBlank(String value) { - return ((value == null) || (value.length() == 0) || (value.trim().length() == 0)); + return ((value == null) || value.isEmpty() || value.trim().isEmpty()); } /** @@ -140,11 +141,7 @@ public static boolean isNullOrBlank(String value) { * returns true. */ public static boolean isEqual(Object value1, Object value2) { - if (value1 == null) { - return (value2 == null); - } else { - return value2 != null && value1.equals(value2); - } + return Objects.equals(value1, value2); } /* diff --git a/src/main/java/au/gov/nehta/common/utils/DomUtils.java b/src/main/java/au/gov/nehta/common/utils/DomUtils.java index fb5f3b3..4b44998 100644 --- a/src/main/java/au/gov/nehta/common/utils/DomUtils.java +++ b/src/main/java/au/gov/nehta/common/utils/DomUtils.java @@ -11,11 +11,12 @@ package au.gov.nehta.common.utils; import java.io.File; -import java.io.FileWriter; import java.io.IOException; import java.io.Reader; import java.io.StringWriter; import java.io.Writer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.util.ArrayList; import java.util.List; @@ -44,17 +45,16 @@ */ public final class DomUtils { - /* - * Factory instances. - */ - private static DocumentBuilderFactory DOCBUILDER_FACTORY; + private static final DocumentBuilderFactory DOCBUILDER_FACTORY = createDocumentBuilderFactory(); + + private static DocumentBuilderFactory createDocumentBuilderFactory() { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + return factory; + } private static DocumentBuilder getDocumentBuilder() throws ParserConfigurationException { - if (DOCBUILDER_FACTORY == null) { - DOCBUILDER_FACTORY = DocumentBuilderFactory.newInstance(); - DOCBUILDER_FACTORY.setNamespaceAware(true); - } return DOCBUILDER_FACTORY.newDocumentBuilder(); } @@ -190,7 +190,9 @@ public static void serialise(Document xmlDoc, File file) assert (xmlDoc != null) : "'xmlDoc' is null."; assert (file != null) : "'file' is null."; - serialiseNode(xmlDoc, new FileWriter(file)); + try (Writer writer = Files.newBufferedWriter(file.toPath(), StandardCharsets.UTF_8)) { + serialiseNode(xmlDoc, writer); + } } /** @@ -222,13 +224,11 @@ private static void serialiseNode(Node xmlNode, Writer writer) Transformer transformer = factory.newTransformer(); transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); - try { + try (Writer owned = writer) { DOMSource source = new DOMSource(xmlNode); - StreamResult result = new StreamResult(writer); + StreamResult result = new StreamResult(owned); transformer.transform(source, result); - writer.flush(); - } finally { - writer.close(); + owned.flush(); } } @@ -293,12 +293,12 @@ public static Element getChildElement(Element element, String namespace, String tagName) { assert (element != null) : "'element' is null."; assert (namespace != null) : "'namespace' is null."; - assert (namespace.trim().length() > 0) : "'namespace' is a blank string."; + assert (!namespace.trim().isEmpty()) : "'namespace' is a blank string."; assert (tagName != null) : "'tagName' is null."; - assert (tagName.trim().length() > 0) : "'tagName' is a blank string."; + assert (!tagName.trim().isEmpty()) : "'tagName' is a blank string."; List childElems = getChildElements(element, namespace, tagName); - if (childElems.size() == 0) { + if (childElems.isEmpty()) { throw new IllegalArgumentException("No '{" + namespace + "}" + tagName + "' element found."); } else if (childElems.size() > 1) { @@ -326,9 +326,9 @@ public static List getChildElements(Element element, String namespace, String tagName) { assert (element != null) : "'element' is null."; assert (namespace != null) : "'namespace' is null."; - assert (namespace.trim().length() > 0) : "'namespace' is a blank string."; + assert (!namespace.trim().isEmpty()) : "'namespace' is a blank string."; assert (tagName != null) : "'tagName' is null."; - assert (tagName.trim().length() > 0) : "'tagName' is a blank string."; + assert (!tagName.trim().isEmpty()) : "'tagName' is a blank string."; List childElems = new ArrayList<>(); NodeList childNodes = element.getChildNodes(); @@ -359,9 +359,9 @@ public static void addAttribute(Element elem, String attrName, String attrValue) { assert (elem != null) : "'elem' is null."; assert (attrName != null) : "'attrName' is null."; - assert (attrName.trim().length() > 0) : "'attrName' is a blank string."; + assert (!attrName.trim().isEmpty()) : "'attrName' is a blank string."; assert (attrValue != null) : "'attrValue' is null."; - assert (attrValue.trim().length() > 0) : "'attrValue' is a blank string."; + assert (!attrValue.trim().isEmpty()) : "'attrValue' is a blank string."; Document ownerDoc = elem.getOwnerDocument(); @@ -409,9 +409,9 @@ public static void checkNotNullOrEmpty(Document xmlDoc, String name) { public static boolean checkElement(Element elem, String name, String namespace) { assert (elem != null) : "'elem' is null."; assert (name != null) : "'name' is null."; - assert (name.trim().length() > 0) : "'name' is a blank string."; + assert (!name.trim().isEmpty()) : "'name' is a blank string."; assert (namespace != null) : "'namespace' is null."; - assert (namespace.trim().length() > 0) : "'namespace' is a blank string."; + assert (!namespace.trim().isEmpty()) : "'namespace' is a blank string."; return (elem.getLocalName().equals(name) && elem.getNamespaceURI().equals( namespace)); @@ -447,7 +447,7 @@ public static void removeFormatting(Element xmlElem) { // Loop through child nodes boolean hasChildElements = false; - List nodesToRemove = new ArrayList(); + List nodesToRemove = new ArrayList<>(); for (int idx = 0; idx < children.getLength(); idx++) { Node childNode = children.item(idx); if (childNode instanceof Element) { @@ -464,7 +464,7 @@ public static void removeFormatting(Element xmlElem) { // If the child text node is made up of whitespace only, mark it for // removal String childTextData = textChildNode.getData(); - if (childTextData.trim().length() == 0) { + if (childTextData.trim().isEmpty()) { nodesToRemove.add(textChildNode); } } diff --git a/src/main/java/au/gov/nehta/common/utils/IOUtils.java b/src/main/java/au/gov/nehta/common/utils/IOUtils.java index 3836d85..eb84937 100644 --- a/src/main/java/au/gov/nehta/common/utils/IOUtils.java +++ b/src/main/java/au/gov/nehta/common/utils/IOUtils.java @@ -1,9 +1,6 @@ package au.gov.nehta.common.utils; -import java.io.BufferedWriter; import java.io.File; -import java.io.FileReader; -import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -12,43 +9,46 @@ import java.io.Reader; import java.io.StringWriter; import java.io.Writer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; /** - * Utility class that common I/O functions. + * Utility class for common I/O functions. + * + *

+ * File and stream text I/O uses UTF-8. */ public final class IOUtils { - /* - * Size of buffer array for read() methods. - */ - private static final int CHAR_BUFFER_SIZE = 1024; - /** - * Reads the content of a file into a string. + * Reads the content of a file into a string (UTF-8). * * @param inputFile File to read from. Cannot be null. * @return Text content from the file. - * @throws IOException Thrown when the file cannot be found, or there are I/O errors + * @throws IOException Thrown when the file cannot be found, or there are I/O + * errors * reading from the file. */ public static String read(File inputFile) throws IOException { assert (inputFile != null) : "'inputFile' is null."; - return read(new FileReader(inputFile)); + return Files.readString(inputFile.toPath(), StandardCharsets.UTF_8); } /** - * Reads the content of a byte stream into a string. The byte stream will be + * Reads the content of a byte stream into a string (UTF-8). The byte stream + * will be * closed after the contents are read. * * @param inputStream Byte stream to read from. Cannot be null. * @return Text content from the byte stream. - * @throws IOException Thrown when there are I/O errors reading from the byte stream. + * @throws IOException Thrown when there are I/O errors reading from the byte + * stream. */ public static String read(InputStream inputStream) throws IOException { assert (inputStream != null) : "'inputStream' is null."; - return read(new InputStreamReader(inputStream)); + return read(new InputStreamReader(inputStream, StandardCharsets.UTF_8)); } /** @@ -57,79 +57,74 @@ public static String read(InputStream inputStream) throws IOException { * * @param reader Character stream to read from. Cannot be null. * @return Text content from the character stream. - * @throws IOException Thrown when there are I/O errors reading from the character + * @throws IOException Thrown when there are I/O errors reading from the + * character * stream. */ public static String read(Reader reader) throws IOException { assert (reader != null) : "'reader' is null."; - try { - StringWriter sw = new StringWriter(); - char[] buffer = new char[CHAR_BUFFER_SIZE]; - int readIdx = reader.read(buffer, 0, buffer.length); - while (readIdx >= 0) { - sw.write(buffer, 0, readIdx); - readIdx = reader.read(buffer, 0, buffer.length); - } - sw.flush(); + try (Reader owned = reader; StringWriter sw = new StringWriter()) { + owned.transferTo(sw); return sw.toString(); - } finally { - reader.close(); } } /** - * Writes text content to a file. + * Writes text content to a file (UTF-8). * * @param outputFile File to write to. Cannot be null. - * @param contents Text content to write to the file. Cannot be null nor a blank + * @param contents Text content to write to the file. Cannot be null nor a + * blank * string. * @throws IOException Thrown when there are I/O errors writing to the file. */ public static void write(File outputFile, String contents) throws IOException { assert (outputFile != null) : "'outputFile' is null."; assert (contents != null) : "'contents' is null."; - assert (contents.trim().length() > 0) : "'contents' is a blank string."; + assert (!contents.trim().isEmpty()) : "'contents' is a blank string."; - write(new FileWriter(outputFile), contents); + Files.writeString(outputFile.toPath(), contents, StandardCharsets.UTF_8); } /** - * Writes text content to a character stream. + * Writes text content to a byte stream (UTF-8). * - * @param outputStream Character stream to write to. Cannot be null. - * @param contents Text content to write to the character stream. Cannot be null nor + * @param outputStream Byte stream to write to. Cannot be null. + * @param contents Text content to write to the byte stream. Cannot be null + * nor * a blank string. - * @throws IOException Thrown when there are I/O errors writing to the character stream. + * @throws IOException Thrown when there are I/O errors writing to the byte + * stream. */ public static void write(OutputStream outputStream, String contents) throws IOException { assert (outputStream != null) : "'outputStream' is null."; assert (contents != null) : "'contents' is null."; - assert (contents.trim().length() > 0) : "'contents' is a blank string."; + assert (!contents.trim().isEmpty()) : "'contents' is a blank string."; - write(new OutputStreamWriter(outputStream), contents); + write(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8), contents); } /** - * Writes text content to a byte stream. + * Writes text content to a character stream. The stream is closed after + * writing. * - * @param writer Byte stream to write to. Cannot be null. - * @param contents Text content to write to the byte stream. Cannot be null nor + * @param writer Character stream to write to. Cannot be null. + * @param contents Text content to write to the character stream. Cannot be null + * nor * a blank string. - * @throws IOException Thrown when there are I/O errors writing to the byte stream. + * @throws IOException Thrown when there are I/O errors writing to the character + * stream. */ public static void write(Writer writer, String contents) throws IOException { assert (writer != null) : "'writer' is null."; assert (contents != null) : "'contents' is null."; - assert (contents.trim().length() > 0) : "'contents' is a blank string."; - - try { - BufferedWriter bw = new BufferedWriter(writer); - bw.append(contents); - bw.flush(); - } finally { - writer.close(); + assert (!contents.trim().isEmpty()) : "'contents' is a blank string."; + + try (Writer owned = writer) { + owned.write(contents); + owned.flush(); } } @@ -139,4 +134,4 @@ public static void write(Writer writer, String contents) throws IOException { private IOUtils() { } -} \ No newline at end of file +} diff --git a/src/main/java/au/gov/nehta/common/utils/JaxbUtils.java b/src/main/java/au/gov/nehta/common/utils/JaxbUtils.java index 8ef9366..529ffaf 100644 --- a/src/main/java/au/gov/nehta/common/utils/JaxbUtils.java +++ b/src/main/java/au/gov/nehta/common/utils/JaxbUtils.java @@ -17,14 +17,14 @@ import java.io.Reader; import java.io.StringWriter; import java.io.Writer; -import java.util.HashMap; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; -import javax.xml.bind.JAXBContext; -import javax.xml.bind.JAXBElement; -import javax.xml.bind.JAXBException; -import javax.xml.bind.Marshaller; -import javax.xml.bind.Unmarshaller; +import jakarta.xml.bind.JAXBContext; +import jakarta.xml.bind.JAXBElement; +import jakarta.xml.bind.JAXBException; +import jakarta.xml.bind.Marshaller; +import jakarta.xml.bind.Unmarshaller; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.Source; import javax.xml.transform.dom.DOMSource; @@ -38,15 +38,12 @@ */ public final class JaxbUtils { - /* - * Cache of package name to JAXB context objects. - */ - private static Map contextCache = new HashMap<>(); + private static final Map CONTEXT_CACHE = new ConcurrentHashMap<>(); /** * Unmarshals the XML data in a DOM node into a Java object using JAXB. This * method simplifies the call to the method: {@code - * javax.xml.bind.Unmarshaller#unmarshal(Source)}. + * jakarta.xml.bind.Unmarshaller#unmarshal(Source)}. * * @param * @param xmlNode DOM node to read the XML data from. Cannot be null. @@ -67,7 +64,7 @@ public static T unmarshal(Node xmlNode, Class jaxbClass) /** * Unmarshals the XML data from a character stream into a Java object using * JAXB. This method simplifies the call to the method: {@code - * javax.xml.bind.Unmarshaller#unmarshal(Source)}. + * jakarta.xml.bind.Unmarshaller#unmarshal(Source)}. * * @param * @param reader Character stream to read the XML data from. Cannot be null. @@ -102,7 +99,7 @@ private static T unmarshal(Source xmlContents, Class jaxbClass) /** * Marshals a JAXBElement object into XML data to write to a character stream. * This method simplifies the call to the method: {@code - * javax.xml.bind.Marshaller#marshal(Object, Writer)}. A JAXBElement can be + * jakarta.xml.bind.Marshaller#marshal(Object, Writer)}. A JAXBElement can be * created from the ObjectFactory generated by JAXB for each package. * * @param value JAXBElement to marshal into XML data. Cannot be null. @@ -110,7 +107,7 @@ private static T unmarshal(Source xmlContents, Class jaxbClass) * @throws JAXBException If there is a problem with marshalling. * @throws IOException If there is a problem with writing. */ - public static void marshal(JAXBElement value, Writer output) + public static void marshal(JAXBElement value, Writer output) throws JAXBException, IOException { assert (value != null) : "'value' is null."; assert (output != null) : "'output' is null."; @@ -124,14 +121,14 @@ public static void marshal(JAXBElement value, Writer output) /** * Marshals a JAXBElement object into a XML data string. This method * simplifies the call to the method: {@code - * javax.xml.bind.Marshaller#marshal(Object, Writer)}. A JAXBElement can be + * jakarta.xml.bind.Marshaller#marshal(Object, Writer)}. A JAXBElement can be * created from the ObjectFactory generated by JAXB for each package. * * @param value JAXBElement to marshal into XML data. Cannot be null. * @return XML representation of the JAXBElement. * @throws JAXBException If there is a problem with marshalling. */ - public static String marshalToString(JAXBElement value) throws JAXBException { + public static String marshalToString(JAXBElement value) throws JAXBException { assert (value != null) : "'value' is null."; JAXBContext context = getContext(value.getDeclaredType()); @@ -145,7 +142,7 @@ public static String marshalToString(JAXBElement value) throws JAXBException { /** * Marshals a JAXBElement object into a DOM document. This method * simplifies the call to the method: {@code - * javax.xml.bind.Marshaller#marshal(Object, Node)}. A JAXBElement can be + * jakarta.xml.bind.Marshaller#marshal(Object, Node)}. A JAXBElement can be * created from the ObjectFactory generated by JAXB for each package. * * @param value JAXBElement to marshal into XML data. Cannot be null. @@ -153,7 +150,7 @@ public static String marshalToString(JAXBElement value) throws JAXBException { * @throws JAXBException If there is a problem with marshalling. * @throws ParserConfigurationException If there is a config problem. */ - public static Document marshalToDom(JAXBElement value) throws JAXBException, + public static Document marshalToDom(JAXBElement value) throws JAXBException, ParserConfigurationException { assert (value != null) : "'value' is null."; @@ -164,7 +161,7 @@ public static Document marshalToDom(JAXBElement value) throws JAXBException, return doc; } - private static JAXBContext getContext(Class jaxbClass) throws JAXBException { + private static JAXBContext getContext(Class jaxbClass) throws JAXBException { assert (jaxbClass != null); assert (jaxbClass.getPackage() != null); @@ -175,11 +172,12 @@ private static JAXBContext getContext(String packageName) throws JAXBException { assert (packageName != null); - JAXBContext context = JaxbUtils.contextCache.get(packageName); - if (context == null) { - context = JAXBContext.newInstance(packageName); - JaxbUtils.contextCache.put(packageName, context); + JAXBContext cached = CONTEXT_CACHE.get(packageName); + if (cached != null) { + return cached; } - return context; + JAXBContext created = JAXBContext.newInstance(packageName); + JAXBContext raced = CONTEXT_CACHE.putIfAbsent(packageName, created); + return raced != null ? raced : created; } } diff --git a/src/main/java/au/gov/nehta/common/utils/MappedNamespaceContext.java b/src/main/java/au/gov/nehta/common/utils/MappedNamespaceContext.java index 38ce9a9..d84e1a1 100644 --- a/src/main/java/au/gov/nehta/common/utils/MappedNamespaceContext.java +++ b/src/main/java/au/gov/nehta/common/utils/MappedNamespaceContext.java @@ -17,6 +17,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Objects; import javax.xml.namespace.NamespaceContext; @@ -31,33 +32,21 @@ class MappedNamespaceContext implements NamespaceContext { this.nsMap = nsMap; } - /** - * @see NamespaceContext#getNamespaceURI(String) - */ @Override public String getNamespaceURI(String prefix) { return this.nsMap.get(prefix); } - /** - * @see NamespaceContext#getPrefix(String) - */ @Override public String getPrefix(String namespaceURI) { - String prefix = null; - for (String currPrefix : this.nsMap.keySet()) { - String currNsUri = this.nsMap.get(currPrefix); - if (currNsUri.equals(namespaceURI)) { - prefix = currPrefix; - break; + for (Map.Entry entry : this.nsMap.entrySet()) { + if (Objects.equals(entry.getValue(), namespaceURI)) { + return entry.getKey(); } } - return prefix; + return null; } - /** - * @see NamespaceContext#getPrefixes(String) - */ @Override public Iterator getPrefixes(String namespaceURI) { List prefixList = new ArrayList<>(); diff --git a/src/main/java/au/gov/nehta/common/utils/PropertyUtils.java b/src/main/java/au/gov/nehta/common/utils/PropertyUtils.java index 29cb632..772f6d4 100644 --- a/src/main/java/au/gov/nehta/common/utils/PropertyUtils.java +++ b/src/main/java/au/gov/nehta/common/utils/PropertyUtils.java @@ -12,12 +12,13 @@ import java.io.File; import java.io.FileNotFoundException; -import java.io.FileReader; import java.io.IOException; import java.io.InputStream; import java.io.Reader; -import java.util.Arrays; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.util.List; +import java.util.Locale; import java.util.Properties; /** @@ -28,8 +29,7 @@ public final class PropertyUtils { /* * Text that will be accepted as a 'true' boolean value. */ - private static final List TRUE_VALUES = Arrays.asList("true", "yes", - "on", "1"); + private static final List TRUE_VALUES = List.of("true", "yes", "on", "1"); /** * Gets the value of the system property with the given name, throwing an @@ -41,7 +41,7 @@ public final class PropertyUtils { */ public static String getSystemProperty(String name) { assert (name != null) : "'name' is null."; - assert (name.trim().length() > 0) : "'name' is a blank string."; + assert (!name.trim().isEmpty()) : "'name' is a blank string."; String value = System.getProperty(name); if (value == null) { @@ -61,7 +61,7 @@ public static String getSystemProperty(String name) { */ public static String getSystemProperty(String name, String defaultValue) { assert (name != null) : "'name' is null."; - assert (name.trim().length() > 0) : "'name' is a blank string."; + assert (!name.trim().isEmpty()) : "'name' is a blank string."; return System.getProperty(name, defaultValue); } @@ -74,14 +74,14 @@ public static String getSystemProperty(String name, String defaultValue) { */ public static void setSystemProperty(String name, String value) { assert (name != null) : "'name' is null."; - assert (name.trim().length() > 0) : "'name' is a blank string."; + assert (!name.trim().isEmpty()) : "'name' is a blank string."; assert (value != null) : "'value' is null."; System.setProperty(name, value); } /** - * Load the properties from the given file. + * Load the properties from the given file (UTF-8). * * @param file Path to properties file. * @return The properties loaded from the properties file. @@ -92,7 +92,9 @@ public static Properties loadProperties(File file) throws FileNotFoundException, IOException { assert (file != null) : "'file' is null."; - return loadProperties(new FileReader(file)); + try (Reader reader = Files.newBufferedReader(file.toPath(), StandardCharsets.UTF_8)) { + return loadProperties(reader); + } } /** @@ -140,7 +142,7 @@ public static boolean getBooleanProperty(Properties properties, String name, boolean defaultValue) { assert (properties != null) : "'properties' is null."; assert (name != null) : "'name' is null."; - assert (name.trim().length() > 0) : "'name' is a blank string."; + assert (!name.trim().isEmpty()) : "'name' is a blank string."; // Retrieve property value from properties object String propertyValue = properties.getProperty(name); @@ -151,7 +153,7 @@ public static boolean getBooleanProperty(Properties properties, String name, } // Remove leading and trailing whitespaces and convert to lower case - propertyValue = propertyValue.trim().toLowerCase(); + propertyValue = propertyValue.trim().toLowerCase(Locale.ROOT); // Check if the property value matches any of the acceptable 'true' values return TRUE_VALUES.contains(propertyValue); diff --git a/src/main/java/au/gov/nehta/common/utils/ResourceLocator.java b/src/main/java/au/gov/nehta/common/utils/ResourceLocator.java index 8be945a..829c89a 100644 --- a/src/main/java/au/gov/nehta/common/utils/ResourceLocator.java +++ b/src/main/java/au/gov/nehta/common/utils/ResourceLocator.java @@ -6,7 +6,6 @@ import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; -import java.util.Arrays; import java.util.List; /** @@ -24,12 +23,9 @@ public enum SearchOrderItem { } - private static final List DEFAULT_SEARCH_ORDER = Arrays - .asList(SearchOrderItem.FILE_SYSTEM, SearchOrderItem.CLASSPATH); + private static final List DEFAULT_SEARCH_ORDER = List.of( + SearchOrderItem.FILE_SYSTEM, SearchOrderItem.CLASSPATH); - /* - * Singleton instance. - */ private static final ResourceLocator instance = new ResourceLocator(); /** @@ -41,9 +37,6 @@ public static ResourceLocator getInstance() { return instance; } - /* - * Private constructor to prevent instantiation. - */ private ResourceLocator() { } @@ -67,7 +60,7 @@ public InputStream find(String resourcePath) { * provided. * @return byte stream to the resource or null if it cannot be found. */ - public InputStream find(String resourcePath, Class referenceClass) { + public InputStream find(String resourcePath, Class referenceClass) { return find(resourcePath, referenceClass, DEFAULT_SEARCH_ORDER); } @@ -100,10 +93,10 @@ public InputStream find(String resourcePath, List searchOrder) * the classpath. * @return byte stream to the resource or null if it cannot be found. */ - public InputStream find(String resourcePath, Class referenceClass, + public InputStream find(String resourcePath, Class referenceClass, List searchOrder) { assert (resourcePath != null) : "'resourcePath' is null."; - assert (resourcePath.trim().length() > 0) : "'resourcePath' is a blank string."; + assert (!resourcePath.trim().isEmpty()) : "'resourcePath' is a blank string."; List actualSearchOrder = searchOrder; if (ArgumentUtils.isNullOrEmpty(searchOrder)) { @@ -113,16 +106,13 @@ public InputStream find(String resourcePath, Class referenceClass, for (SearchOrderItem currSearchOrderItem : actualSearchOrder) { if (currSearchOrderItem == SearchOrderItem.FILE_SYSTEM) { try { - // Found in file system return new FileInputStream(resourcePath); - } catch (FileNotFoundException e) { // Resource can't be found in file system; keep looking } } else if (currSearchOrderItem == SearchOrderItem.CLASSPATH) { InputStream in = getStreamFromClasspath(resourcePath, referenceClass); if (in != null) { - // Found in class path return in; } } else if (currSearchOrderItem == SearchOrderItem.REMOTE_URL) { @@ -161,12 +151,10 @@ public InputStream getStreamFromClasspath(String resourcePath) { * @return byte stream to the resource or null if it cannot be found. */ public InputStream getStreamFromClasspath(String resourcePath, - Class referenceClass) { + Class referenceClass) { assert (resourcePath != null) : "'resourcePath' is null."; - assert (resourcePath.trim().length() > 0) : "'resourcePath' is a blank string."; + assert (!resourcePath.trim().isEmpty()) : "'resourcePath' is a blank string."; - // Note: referenceClass.getResourceAsStream(resourcePath) returns null as of - // JDK 1.6. This is the reason why resources are read from the class loader. ClassLoader classLoader = getClassLoader(referenceClass); return classLoader.getResourceAsStream(resourcePath); } @@ -192,27 +180,21 @@ public URL getUrlFromClasspath(String resourcePath) { * provided. * @return URL to the resource or null if it cannot be found. */ - public URL getUrlFromClasspath(String resourcePath, Class referenceClass) { + public URL getUrlFromClasspath(String resourcePath, Class referenceClass) { assert (resourcePath != null) : "'resourcePath' is null."; - assert (resourcePath.trim().length() > 0) : "'resourcePath' is a blank string."; + assert (!resourcePath.trim().isEmpty()) : "'resourcePath' is a blank string."; - // Note: referenceClass.getResourceAsStream(resourcePath) returns null as of - // JDK 1.6. This is the reason why resources are read from the class loader. ClassLoader classLoader = getClassLoader(referenceClass); return classLoader.getResource(resourcePath); } - private ClassLoader getClassLoader(Class referenceClass) { + private ClassLoader getClassLoader(Class referenceClass) { ClassLoader classLoader = null; - // Try to get class loader from reference class if (referenceClass != null) { classLoader = referenceClass.getClassLoader(); } - // Use system class loader if reference class is null or the reference - // class's class loader is null (Some implementations use null to represent - // the bootstrap class loader in the getClassLoader() method). if (classLoader == null) { classLoader = ClassLoader.getSystemClassLoader(); } diff --git a/src/main/java/au/gov/nehta/common/utils/SimpleXsdResourceResolver.java b/src/main/java/au/gov/nehta/common/utils/SimpleXsdResourceResolver.java index e0c7a47..b481371 100644 --- a/src/main/java/au/gov/nehta/common/utils/SimpleXsdResourceResolver.java +++ b/src/main/java/au/gov/nehta/common/utils/SimpleXsdResourceResolver.java @@ -21,6 +21,7 @@ public abstract class SimpleXsdResourceResolver implements LSResourceResolver { + @Override public LSInput resolveResource(String type, String namespaceURI, String publicId, String systemId, String baseURI) { if (!ArgumentUtils.isNullOrBlank(systemId)) { diff --git a/src/main/java/au/gov/nehta/common/utils/StandardFormatter.java b/src/main/java/au/gov/nehta/common/utils/StandardFormatter.java index 04adb75..1ecb845 100644 --- a/src/main/java/au/gov/nehta/common/utils/StandardFormatter.java +++ b/src/main/java/au/gov/nehta/common/utils/StandardFormatter.java @@ -1,20 +1,20 @@ package au.gov.nehta.common.utils; -import java.text.DateFormat; -import java.text.SimpleDateFormat; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; import java.util.Date; +import java.util.Locale; /** * Singleton class that formats information in a standard and consistent way. */ public final class StandardFormatter { - /* - * Singleton instance. - */ private static final StandardFormatter INSTANCE = new StandardFormatter(); - private static final String DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss.SSSZ"; + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter + .ofPattern("yyyy-MM-dd HH:mm:ss.SSSZ", Locale.ROOT); private static final String UNKNOWN_DATE = ""; @@ -31,9 +31,6 @@ public static StandardFormatter getInstance() { return INSTANCE; } - /* - * Private constructor to prevent instantiation. - */ private StandardFormatter() { } @@ -51,16 +48,15 @@ public String formatDateTime(long timeInMillis) { * Formats a date-time, e.g. 2008-12-30 12:45:00.000+1000. * * @param date date object - * @return formatted string representing date-time or an empty string if date - * is null + * @return formatted string representing date-time or {@code } if + * date is null */ public String formatDateTime(Date date) { - String formattedStr = UNKNOWN_DATE; - if (date != null) { - DateFormat formatter = new SimpleDateFormat(DATE_TIME_FORMAT); - formattedStr = formatter.format(date); + if (date == null) { + return UNKNOWN_DATE; } - return formattedStr; + return DATE_TIME_FORMATTER.format( + Instant.ofEpochMilli(date.getTime()).atZone(ZoneId.systemDefault())); } /** @@ -71,18 +67,12 @@ public String formatDateTime(Date date) { * @return formatted string representing the code location */ public String formatLocation(String className, String methodName) { - // Class name - String classNameStr = className; - if (ArgumentUtils.isNullOrBlank(className)) { - classNameStr = UNKNOWN_CLASS; - } - - // Method name - String methodNameStr = methodName; - if (ArgumentUtils.isNullOrBlank(methodName)) { - methodNameStr = UNKNOWN_METHOD; - } - + String classNameStr = ArgumentUtils.isNullOrBlank(className) + ? UNKNOWN_CLASS + : className; + String methodNameStr = ArgumentUtils.isNullOrBlank(methodName) + ? UNKNOWN_METHOD + : methodName; return String.format("%s.%s()", classNameStr, methodNameStr); } } diff --git a/src/main/java/au/gov/nehta/common/utils/UriUtils.java b/src/main/java/au/gov/nehta/common/utils/UriUtils.java index 1f82f76..abaf9a6 100644 --- a/src/main/java/au/gov/nehta/common/utils/UriUtils.java +++ b/src/main/java/au/gov/nehta/common/utils/UriUtils.java @@ -26,6 +26,6 @@ public final class UriUtils { * @return A randomly generated URI. */ public static String randomUri() { - return "urn:uuid:" + UUID.randomUUID().toString(); + return "urn:uuid:" + UUID.randomUUID(); } } diff --git a/src/main/java/au/gov/nehta/common/utils/XPathUtils.java b/src/main/java/au/gov/nehta/common/utils/XPathUtils.java index ff3fd9e..d544e8b 100644 --- a/src/main/java/au/gov/nehta/common/utils/XPathUtils.java +++ b/src/main/java/au/gov/nehta/common/utils/XPathUtils.java @@ -56,7 +56,7 @@ public static T evaluate(String xpath, Node contextNode, Map namespaceMap, Class expectedType) throws XPathExpressionException { assert (xpath != null) : "'xpath' is null."; - assert (xpath.trim().length() > 0) : "'xpath' is a blank string."; + assert (!xpath.trim().isEmpty()) : "'xpath' is a blank string."; assert (contextNode != null) : "'contextNode' is null."; assert (expectedType != null) : "'expectedType' is null."; @@ -101,7 +101,7 @@ public static Object evaluate(String xpath, Node contextNode, Map namespaceMap, QName returnType) throws XPathExpressionException { assert (xpath != null) : "'xpath' is null."; - assert (xpath.trim().length() > 0) : "'xpath' is a blank string."; + assert (!xpath.trim().isEmpty()) : "'xpath' is a blank string."; assert (contextNode != null) : "'contextNode' is null."; assert (returnType != null) : "'returnType' is null."; @@ -132,7 +132,7 @@ public static Object evaluate(String xpath, Node contextNode, public static int getCount(String xpath, Node contextNode, Map namespaceMap) throws XPathExpressionException { assert (xpath != null) : "'xpath' is null."; - assert (xpath.trim().length() > 0) : "'xpath' is a blank string."; + assert (!xpath.trim().isEmpty()) : "'xpath' is a blank string."; assert (contextNode != null) : "'contextNode' is null."; Double doubleCount = evaluate("count(" + xpath + ")", contextNode, @@ -154,7 +154,7 @@ public static int getCount(String xpath, Node contextNode, public static List getElementList(String xpath, Node contextNode, Map namespaceMap) throws XPathExpressionException { assert (xpath != null) : "'xpath' is null."; - assert (xpath.trim().length() > 0) : "'xpath' is a blank string."; + assert (!xpath.trim().isEmpty()) : "'xpath' is a blank string."; assert (contextNode != null) : "'contextNode' is null."; NodeList nodeList = evaluate(xpath, contextNode, namespaceMap, diff --git a/src/main/java/au/gov/nehta/common/utils/XsdUtils.java b/src/main/java/au/gov/nehta/common/utils/XsdUtils.java index f2b6cac..8e3a295 100644 --- a/src/main/java/au/gov/nehta/common/utils/XsdUtils.java +++ b/src/main/java/au/gov/nehta/common/utils/XsdUtils.java @@ -56,7 +56,7 @@ public static void validate(File dataFile, File schemaFile) Node dataNode = DomUtils.parse(dataFile); Node schemaNode = DomUtils.parse(schemaFile); - validate(dataNode, schemaNode, new XsdUtils().new FileXsdResourceResolver( + validate(dataNode, schemaNode, new FileXsdResourceResolver( schemaFile.getParentFile())); } @@ -120,11 +120,11 @@ public static void validate(Node dataNode, Node schemaNode, private XsdUtils() { } - private class FileXsdResourceResolver extends SimpleXsdResourceResolver { + private static final class FileXsdResourceResolver extends SimpleXsdResourceResolver { - private File baseDir; + private final File baseDir; - public FileXsdResourceResolver(File baseDir) { + private FileXsdResourceResolver(File baseDir) { this.baseDir = baseDir; } diff --git a/src/main/java/au/gov/nehta/common/utils/XsltUtils.java b/src/main/java/au/gov/nehta/common/utils/XsltUtils.java index 043c799..ae5d46e 100644 --- a/src/main/java/au/gov/nehta/common/utils/XsltUtils.java +++ b/src/main/java/au/gov/nehta/common/utils/XsltUtils.java @@ -274,9 +274,8 @@ public static void transform(Source xsltSource, Source inputSource, // Set parameters if (parameters != null) { - for (String paramName : parameters.keySet()) { - String paramValue = parameters.get(paramName); - transformer.setParameter(paramName, paramValue); + for (Map.Entry entry : parameters.entrySet()) { + transformer.setParameter(entry.getKey(), entry.getValue()); } } diff --git a/src/test/java/au/gov/nehta/common/utils/ArgumentUtilsTest.java b/src/test/java/au/gov/nehta/common/utils/ArgumentUtilsTest.java new file mode 100644 index 0000000..5adc3d4 --- /dev/null +++ b/src/test/java/au/gov/nehta/common/utils/ArgumentUtilsTest.java @@ -0,0 +1,64 @@ +package au.gov.nehta.common.utils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.Collections; +import java.util.List; +import org.junit.Test; + +public class ArgumentUtilsTest { + + @Test + public void isNullOrBlank_treatsNullEmptyAndWhitespaceAsBlank() { + assertTrue(ArgumentUtils.isNullOrBlank(null)); + assertTrue(ArgumentUtils.isNullOrBlank("")); + assertTrue(ArgumentUtils.isNullOrBlank(" ")); + assertFalse(ArgumentUtils.isNullOrBlank("a")); + } + + @Test + public void isNullOrEmpty_treatsNullAndEmptyCollection() { + assertTrue(ArgumentUtils.isNullOrEmpty(null)); + assertTrue(ArgumentUtils.isNullOrEmpty(Collections.emptyList())); + assertFalse(ArgumentUtils.isNullOrEmpty(List.of("x"))); + } + + @Test + public void isEqual_matchesObjectsEqualsSemantics() { + assertTrue(ArgumentUtils.isEqual(null, null)); + assertFalse(ArgumentUtils.isEqual(null, "a")); + assertTrue(ArgumentUtils.isEqual("a", "a")); + assertFalse(ArgumentUtils.isEqual("a", "b")); + } + + @Test(expected = IllegalArgumentException.class) + public void checkNotNullNorBlank_rejectsWhitespaceOnly() { + ArgumentUtils.checkNotNullNorBlank(" ", "name"); + } + + @Test + public void checkMaxLength_allowsNullAndWithinLimit() { + ArgumentUtils.checkMaxLength(null, 1, "name"); + ArgumentUtils.checkMaxLength("ab", 2, "name"); + } + + @Test(expected = IllegalArgumentException.class) + public void checkMaxLength_rejectsTooLong() { + ArgumentUtils.checkMaxLength("abc", 2, "name"); + } + + @Test + public void checkNullOrBlank_allowsNullAndBlank() { + ArgumentUtils.checkNullOrBlank(null, "name"); + ArgumentUtils.checkNullOrBlank("", "name"); + ArgumentUtils.checkNullOrBlank(" ", "name"); + } + + @Test + public void checkNotNullNorEmpty_acceptsNonEmpty() { + ArgumentUtils.checkNotNullNorEmpty(List.of("x"), "items"); + assertEquals(1, List.of("x").size()); + } +} diff --git a/src/test/java/au/gov/nehta/common/utils/DomUtilsTest.java b/src/test/java/au/gov/nehta/common/utils/DomUtilsTest.java new file mode 100644 index 0000000..3d4f9dd --- /dev/null +++ b/src/test/java/au/gov/nehta/common/utils/DomUtilsTest.java @@ -0,0 +1,33 @@ +package au.gov.nehta.common.utils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.StringReader; +import org.junit.Test; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +public class DomUtilsTest { + + @Test + public void parseAndSerialise_roundTripsSimpleElement() throws Exception { + Document doc = DomUtils.parse(new StringReader("x")); + assertFalse(DomUtils.isDocumentEmpty(doc)); + Element child = DomUtils.getFirstChildElement(doc.getDocumentElement()); + assertEquals("child", child.getLocalName()); + assertEquals("x", child.getTextContent()); + String xml = DomUtils.serialiseToString(doc.getDocumentElement()); + assertTrue(xml.contains("child")); + assertTrue(xml.contains("x")); + } + + @Test + public void removeFormatting_stripsWhitespaceOnlyTextNodes() throws Exception { + Document doc = DomUtils.parse(new StringReader("\n text \n")); + DomUtils.removeFormatting(doc); + Element child = DomUtils.getFirstChildElement(doc.getDocumentElement()); + assertEquals("text", child.getTextContent()); + } +} diff --git a/src/test/java/au/gov/nehta/common/utils/IOUtilsTest.java b/src/test/java/au/gov/nehta/common/utils/IOUtilsTest.java new file mode 100644 index 0000000..6430dd5 --- /dev/null +++ b/src/test/java/au/gov/nehta/common/utils/IOUtilsTest.java @@ -0,0 +1,42 @@ +package au.gov.nehta.common.utils; + +import static org.junit.Assert.assertEquals; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public class IOUtilsTest { + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + @Test + public void readWrite_fileRoundTripUsesUtf8() throws Exception { + File file = temp.newFile("sample.txt"); + String contents = "cafe \u00e9"; + IOUtils.write(file, contents); + assertEquals(contents, IOUtils.read(file)); + assertEquals(contents, Files.readString(file.toPath(), StandardCharsets.UTF_8)); + } + + @Test + public void read_inputStreamUsesUtf8AndCloses() throws Exception { + byte[] bytes = "hello \u20ac".getBytes(StandardCharsets.UTF_8); + ByteArrayInputStream in = new ByteArrayInputStream(bytes); + assertEquals("hello \u20ac", IOUtils.read(in)); + assertEquals(-1, in.read()); + } + + @Test + public void write_outputStreamUsesUtf8() throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + IOUtils.write(out, "ping"); + assertEquals("ping", out.toString(StandardCharsets.UTF_8)); + } +} diff --git a/src/test/java/au/gov/nehta/common/utils/PropertyUtilsTest.java b/src/test/java/au/gov/nehta/common/utils/PropertyUtilsTest.java new file mode 100644 index 0000000..80cc167 --- /dev/null +++ b/src/test/java/au/gov/nehta/common/utils/PropertyUtilsTest.java @@ -0,0 +1,26 @@ +package au.gov.nehta.common.utils; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.Properties; +import org.junit.Test; + +public class PropertyUtilsTest { + + @Test + public void getBooleanProperty_acceptsConfiguredTrueTokensCaseInsensitively() { + Properties properties = new Properties(); + properties.setProperty("a", "TRUE"); + properties.setProperty("b", "Yes"); + properties.setProperty("c", "on"); + properties.setProperty("d", "1"); + properties.setProperty("e", "no"); + assertTrue(PropertyUtils.getBooleanProperty(properties, "a", false)); + assertTrue(PropertyUtils.getBooleanProperty(properties, "b", false)); + assertTrue(PropertyUtils.getBooleanProperty(properties, "c", false)); + assertTrue(PropertyUtils.getBooleanProperty(properties, "d", false)); + assertFalse(PropertyUtils.getBooleanProperty(properties, "e", true)); + assertTrue(PropertyUtils.getBooleanProperty(properties, "missing", true)); + } +} diff --git a/src/test/java/au/gov/nehta/common/utils/StandardFormatterTest.java b/src/test/java/au/gov/nehta/common/utils/StandardFormatterTest.java new file mode 100644 index 0000000..7d1f3b5 --- /dev/null +++ b/src/test/java/au/gov/nehta/common/utils/StandardFormatterTest.java @@ -0,0 +1,29 @@ +package au.gov.nehta.common.utils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.Date; +import org.junit.Test; + +public class StandardFormatterTest { + + @Test + public void formatDateTime_nullReturnsUnknown() { + assertEquals("", StandardFormatter.getInstance().formatDateTime((Date) null)); + } + + @Test + public void formatDateTime_formatsKnownPattern() { + String formatted = StandardFormatter.getInstance().formatDateTime(0L); + assertTrue(formatted.matches("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\.\\d{3}[+-]\\d{4}")); + } + + @Test + public void formatLocation_substitutesUnknowns() { + assertEquals(".()", + StandardFormatter.getInstance().formatLocation(null, " ")); + assertEquals("au.Example.run()", + StandardFormatter.getInstance().formatLocation("au.Example", "run")); + } +} diff --git a/src/test/java/au/gov/nehta/common/utils/UriUtilsTest.java b/src/test/java/au/gov/nehta/common/utils/UriUtilsTest.java new file mode 100644 index 0000000..997267f --- /dev/null +++ b/src/test/java/au/gov/nehta/common/utils/UriUtilsTest.java @@ -0,0 +1,15 @@ +package au.gov.nehta.common.utils; + +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +public class UriUtilsTest { + + @Test + public void randomUri_usesUuidUrn() { + String uri = UriUtils.randomUri(); + assertTrue(uri.startsWith("urn:uuid:")); + assertTrue(uri.length() > "urn:uuid:".length()); + } +}