Hello Jacques,

Please give me some time. I will look at your request/email and revert back
soon.

Thank you,
Ashish

On Mon, 27 Jul 2026 at 15:41, Jacques Le Roux via dev <[email protected]>
wrote:

> Hi,
>
> Nobody care about licences?
>
> TIA
>
> Jacques
>
> Le 20/07/2026 à 11:51, Jacques Le Roux via dev a écrit :
> > Hi Ashish,
> >
> > I Just had a look at
> https://nightlies.apache.org/ofbiz/trunk/rat-output.html
> >
> > Actually IMO there is more than gradle.lockfile.
> > I don't think a LICENSE file there is necessary .It's the only plugin
> with it, since there is already one LICENSE file at plugins root. Same for
> > framework an applis.
> > For README.md there is no Apache license in top. It's not mandatory
> since it's in rat-excludes.txt (tools repo). Some README.md have an Apache
> > license inside.
> > Though I added this linethat in rat-excludes.txt in 2017, I wonder now
> if we should not remove this line and add the Apache license inside all
> > README.md.
> >
> > What do you (and people at large) think?
> >
> > TIA
> >
> > Jacques
> >
> > Le 10/07/2026 à 15:46, [email protected] a écrit :
> >> This is an automated email from the ASF dual-hosted git repository.
> >>
> >> ashishvijaywargiya pushed a commit to branch trunk
> >> in repositoryhttps://gitbox.apache.org/repos/asf/ofbiz-plugins.git
> >>
> >>
> >> The following commit(s) were added to refs/heads/trunk by this push:
> >>       new fe1a266d6 Add devreload plugin inside plugins directory (#316)
> >> fe1a266d6 is described below
> >>
> >> commit fe1a266d60425b3b73469f185cb68bc27012495d
> >> Author: Ashish Vijaywargiya<[email protected]>
> >> AuthorDate: Fri Jul 10 19:16:19 2026 +0530
> >>
> >>      Add devreload plugin inside plugins directory (#316)
> >>           devreload plugin will give us option to work in Java
> files(events and
> >>      services) and services.xml files without restarting ofbiz.
> >> ---
> >>   devreload/LICENSE                                  | 201 +++++
> >>   devreload/README.md                                |  56 ++
> >>   devreload/build.gradle                             | 187 +++++
> >>   devreload/gradle.lockfile                          |   4 +
> >>   devreload/ofbiz-component.xml                      |  30 +
> >>   .../apache/ofbiz/devreload/DevReloadContainer.java | 860
> +++++++++++++++++++++
> >>   .../org/apache/ofbiz/devreload/HotSwapAgent.java   | 109 +++
> >>   7 files changed, 1447 insertions(+)
> >>
> >> diff --git a/devreload/LICENSE b/devreload/LICENSE
> >> new file mode 100644
> >> index 000000000..261eeb9e9
> >> --- /dev/null
> >> +++ b/devreload/LICENSE
> >> @@ -0,0 +1,201 @@
> >> +                                 Apache License
> >> +                           Version 2.0, January 2004
> >> +http://www.apache.org/licenses/
> >> +
> >> +   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
> >> +
> >> +   1. Definitions.
> >> +
> >> +      "License" shall mean the terms and conditions for use,
> reproduction,
> >> +      and distribution as defined by Sections 1 through 9 of this
> document.
> >> +
> >> +      "Licensor" shall mean the copyright owner or entity authorized by
> >> +      the copyright owner that is granting the License.
> >> +
> >> +      "Legal Entity" shall mean the union of the acting entity and all
> >> +      other entities that control, are controlled by, or are under
> common
> >> +      control with that entity. For the purposes of this definition,
> >> +      "control" means (i) the power, direct or indirect, to cause the
> >> +      direction or management of such entity, whether by contract or
> >> +      otherwise, or (ii) ownership of fifty percent (50%) or more of
> the
> >> +      outstanding shares, or (iii) beneficial ownership of such entity.
> >> +
> >> +      "You" (or "Your") shall mean an individual or Legal Entity
> >> +      exercising permissions granted by this License.
> >> +
> >> +      "Source" form shall mean the preferred form for making
> modifications,
> >> +      including but not limited to software source code, documentation
> >> +      source, and configuration files.
> >> +
> >> +      "Object" form shall mean any form resulting from mechanical
> >> +      transformation or translation of a Source form, including but
> >> +      not limited to compiled object code, generated documentation,
> >> +      and conversions to other media types.
> >> +
> >> +      "Work" shall mean the work of authorship, whether in Source or
> >> +      Object form, made available under the License, as indicated by a
> >> +      copyright notice that is included in or attached to the work
> >> +      (an example is provided in the Appendix below).
> >> +
> >> +      "Derivative Works" shall mean any work, whether in Source or
> Object
> >> +      form, that is based on (or derived from) the Work and for which
> the
> >> +      editorial revisions, annotations, elaborations, or other
> modifications
> >> +      represent, as a whole, an original work of authorship. For the
> purposes
> >> +      of this License, Derivative Works shall not include works that
> remain
> >> +      separable from, or merely link (or bind by name) to the
> interfaces of,
> >> +      the Work and Derivative Works thereof.
> >> +
> >> +      "Contribution" shall mean any work of authorship, including
> >> +      the original version of the Work and any modifications or
> additions
> >> +      to that Work or Derivative Works thereof, that is intentionally
> >> +      submitted to Licensor for inclusion in the Work by the copyright
> owner
> >> +      or by an individual or Legal Entity authorized to submit on
> behalf of
> >> +      the copyright owner. For the purposes of this definition,
> "submitted"
> >> +      means any form of electronic, verbal, or written communication
> sent
> >> +      to the Licensor or its representatives, including but not
> limited to
> >> +      communication on electronic mailing lists, source code control
> systems,
> >> +      and issue tracking systems that are managed by, or on behalf of,
> the
> >> +      Licensor for the purpose of discussing and improving the Work,
> but
> >> +      excluding communication that is conspicuously marked or otherwise
> >> +      designated in writing by the copyright owner as "Not a
> Contribution."
> >> +
> >> +      "Contributor" shall mean Licensor and any individual or Legal
> Entity
> >> +      on behalf of whom a Contribution has been received by Licensor
> and
> >> +      subsequently incorporated within the Work.
> >> +
> >> +   2. Grant of Copyright License. Subject to the terms and conditions
> of
> >> +      this License, each Contributor hereby grants to You a perpetual,
> >> +      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
> >> +      copyright license to reproduce, prepare Derivative Works of,
> >> +      publicly display, publicly perform, sublicense, and distribute
> the
> >> +      Work and such Derivative Works in Source or Object form.
> >> +
> >> +   3. Grant of Patent License. Subject to the terms and conditions of
> >> +      this License, each Contributor hereby grants to You a perpetual,
> >> +      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
> >> +      (except as stated in this section) patent license to make, have
> made,
> >> +      use, offer to sell, sell, import, and otherwise transfer the
> Work,
> >> +      where such license applies only to those patent claims licensable
> >> +      by such Contributor that are necessarily infringed by their
> >> +      Contribution(s) alone or by combination of their Contribution(s)
> >> +      with the Work to which such Contribution(s) was submitted. If You
> >> +      institute patent litigation against any entity (including a
> >> +      cross-claim or counterclaim in a lawsuit) alleging that the Work
> >> +      or a Contribution incorporated within the Work constitutes direct
> >> +      or contributory patent infringement, then any patent licenses
> >> +      granted to You under this License for that Work shall terminate
> >> +      as of the date such litigation is filed.
> >> +
> >> +   4. Redistribution. You may reproduce and distribute copies of the
> >> +      Work or Derivative Works thereof in any medium, with or without
> >> +      modifications, and in Source or Object form, provided that You
> >> +      meet the following conditions:
> >> +
> >> +      (a) You must give any other recipients of the Work or
> >> +          Derivative Works a copy of this License; and
> >> +
> >> +      (b) You must cause any modified files to carry prominent notices
> >> +          stating that You changed the files; and
> >> +
> >> +      (c) You must retain, in the Source form of any Derivative Works
> >> +          that You distribute, all copyright, patent, trademark, and
> >> +          attribution notices from the Source form of the Work,
> >> +          excluding those notices that do not pertain to any part of
> >> +          the Derivative Works; and
> >> +
> >> +      (d) If the Work includes a "NOTICE" text file as part of its
> >> +          distribution, then any Derivative Works that You distribute
> must
> >> +          include a readable copy of the attribution notices contained
> >> +          within such NOTICE file, excluding those notices that do not
> >> +          pertain to any part of the Derivative Works, in at least one
> >> +          of the following places: within a NOTICE text file
> distributed
> >> +          as part of the Derivative Works; within the Source form or
> >> +          documentation, if provided along with the Derivative Works;
> or,
> >> +          within a display generated by the Derivative Works, if and
> >> +          wherever such third-party notices normally appear. The
> contents
> >> +          of the NOTICE file are for informational purposes only and
> >> +          do not modify the License. You may add Your own attribution
> >> +          notices within Derivative Works that You distribute,
> alongside
> >> +          or as an addendum to the NOTICE text from the Work, provided
> >> +          that such additional attribution notices cannot be construed
> >> +          as modifying the License.
> >> +
> >> +      You may add Your own copyright statement to Your modifications
> and
> >> +      may provide additional or different license terms and conditions
> >> +      for use, reproduction, or distribution of Your modifications, or
> >> +      for any such Derivative Works as a whole, provided Your use,
> >> +      reproduction, and distribution of the Work otherwise complies
> with
> >> +      the conditions stated in this License.
> >> +
> >> +   5. Submission of Contributions. Unless You explicitly state
> otherwise,
> >> +      any Contribution intentionally submitted for inclusion in the
> Work
> >> +      by You to the Licensor shall be under the terms and conditions of
> >> +      this License, without any additional terms or conditions.
> >> +      Notwithstanding the above, nothing herein shall supersede or
> modify
> >> +      the terms of any separate license agreement you may have executed
> >> +      with Licensor regarding such Contributions.
> >> +
> >> +   6. Trademarks. This License does not grant permission to use the
> trade
> >> +      names, trademarks, service marks, or product names of the
> Licensor,
> >> +      except as required for reasonable and customary use in
> describing the
> >> +      origin of the Work and reproducing the content of the NOTICE
> file.
> >> +
> >> +   7. Disclaimer of Warranty. Unless required by applicable law or
> >> +      agreed to in writing, Licensor provides the Work (and each
> >> +      Contributor provides its Contributions) on an "AS IS" BASIS,
> >> +      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
> >> +      implied, including, without limitation, any warranties or
> conditions
> >> +      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
> >> +      PARTICULAR PURPOSE. You are solely responsible for determining
> the
> >> +      appropriateness of using or redistributing the Work and assume
> any
> >> +      risks associated with Your exercise of permissions under this
> License.
> >> +
> >> +   8. Limitation of Liability. In no event and under no legal theory,
> >> +      whether in tort (including negligence), contract, or otherwise,
> >> +      unless required by applicable law (such as deliberate and grossly
> >> +      negligent acts) or agreed to in writing, shall any Contributor be
> >> +      liable to You for damages, including any direct, indirect,
> special,
> >> +      incidental, or consequential damages of any character arising as
> a
> >> +      result of this License or out of the use or inability to use the
> >> +      Work (including but not limited to damages for loss of goodwill,
> >> +      work stoppage, computer failure or malfunction, or any and all
> >> +      other commercial damages or losses), even if such Contributor
> >> +      has been advised of the possibility of such damages.
> >> +
> >> +   9. Accepting Warranty or Additional Liability. While redistributing
> >> +      the Work or Derivative Works thereof, You may choose to offer,
> >> +      and charge a fee for, acceptance of support, warranty, indemnity,
> >> +      or other liability obligations and/or rights consistent with this
> >> +      License. However, in accepting such obligations, You may act only
> >> +      on Your own behalf and on Your sole responsibility, not on behalf
> >> +      of any other Contributor, and only if You agree to indemnify,
> >> +      defend, and hold each Contributor harmless for any liability
> >> +      incurred by, or claims asserted against, such Contributor by
> reason
> >> +      of your accepting any such warranty or additional liability.
> >> +
> >> +   END OF TERMS AND CONDITIONS
> >> +
> >> +   APPENDIX: How to apply the Apache License to your work.
> >> +
> >> +      To apply the Apache License to your work, attach the following
> >> +      boilerplate notice, with the fields enclosed by brackets "[]"
> >> +      replaced with your own identifying information. (Don't include
> >> +      the brackets!)  The text should be enclosed in the appropriate
> >> +      comment syntax for the file format. We also recommend that a
> >> +      file or class name and description of purpose be included on the
> >> +      same "printed page" as the copyright notice for easier
> >> +      identification within third-party archives.
> >> +
> >> +   Copyright [yyyy] [name of copyright owner]
> >> +
> >> +   Licensed under the Apache License, Version 2.0 (the "License");
> >> +   you may not use this file except in compliance with the License.
> >> +   You may obtain a copy of the License at
> >> +
> >> +http://www.apache.org/licenses/LICENSE-2.0
> >> +
> >> +   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.
> >> diff --git a/devreload/README.md b/devreload/README.md
> >> new file mode 100644
> >> index 000000000..246d9495c
> >> --- /dev/null
> >> +++ b/devreload/README.md
> >> @@ -0,0 +1,56 @@
> >> +# devreload
> >> +
> >> +Development-only hot-reload for OFBiz. Edit a Java service/event
> method, a
> >> +`services.xml` file, or add a brand-new method, and the change is live
> in under a
> >> +second — no restart, ever.
> >> +
> >> +The plugin is entirely self-contained: dropping this directory into a
> checkout (or
> >> +removing it) has zero effect on the rest of OFBiz either way.
> >> +
> >> +## Requirements
> >> +
> >> +This plugin needs a **DCEVM (Dynamic Code Evolution Virtual Machine)**
> JVM to run.
> >> +Set it up once, before running `./gradlew ofbizDev --no-watch-fs`:
> >> +
> >> +1. Get a DCEVM-patched JVM. Easiest source: JetBrains Runtime (JBR),
> bundled with
> >> +   IntelliJ IDEA under `<IDE install>/jbr` (`.../Contents/jbr` on
> macOS). Standalone
> >> +   DCEVM builds work too.
> >> +2. Point at it — set the `DCEVM_HOME` env var (e.g. in your shell
> profile) so every
> >> +   future run picks it up automatically, or pass
> `-PdcevmHome=/path/to/jvm` each time
> >> +   instead.
> >> +
> >> +Add this to your shell profile (`~/.zshrc`, `~/.bashrc`, etc.) so it's
> always set:
> >> +
> >> +```
> >> +export DCEVM_HOME="/Applications/IntelliJ IDEA.app/Contents/jbr"
> >> +```
> >> +
> >> +(adjust the path to wherever your DCEVM-patched JVM lives — on macOS
> this is IntelliJ's
> >> +bundled JetBrains Runtime by default). Reload your shell (or run
> `source ~/.zshrc`) and
> >> +every future `./gradlew ofbizDev --no-watch-fs` picks it up
> automatically, with nothing
> >> +else to set.
> >> +
> >> +## Usage
> >> +
> >> +```
> >> +./gradlew ofbizDev --no-watch-fs
> >> +```
> >> +
> >> +This is the only supported command — always run it exactly like this.
> It boots OFBiz
> >> +and everything hot-swaps live, no restart: method-body edits,
> `services.xml` changes,
> >> +and structural changes (new/removed methods or fields, changed
> signatures) alike.
> >> +
> >> +`--no-watch-fs` disables Gradle's own file-system watching, which
> otherwise competes
> >> +with this plugin's `WatchService` for the same macOS per-process
> directory-watch
> >> +ceiling on a full checkout — without it, some directories can silently
> go unwatched.
> >> +It's harmless to include even on a smaller/scoped checkout, so it's
> part of the one
> >> +command to remember.
> >> +
> >> +If DCEVM isn't set up, it fails immediately — before compiling or
> booting anything —
> >> +with the same setup steps as above.
> >> +
> >> +Scope to specific components for a faster startup:
> >> +
> >> +```
> >> +./gradlew ofbizDev --no-watch-fs -Photreload.components=devreload,party
> >> +```
> >> diff --git a/devreload/build.gradle b/devreload/build.gradle
> >> new file mode 100644
> >> index 000000000..148fcb902
> >> --- /dev/null
> >> +++ b/devreload/build.gradle
> >> @@ -0,0 +1,187 @@
> >> +/*
> >> + * Licensed to the Apache Software Foundation (ASF) under one
> >> + * or more contributor license agreements.  See the NOTICE file
> >> + * distributed with this work for additional information
> >> + * regarding copyright ownership.  The ASF licenses this file
> >> + * to you under the Apache License, Version 2.0 (the
> >> + * "License"); you may not use this file except in compliance
> >> + * with the License.  You may obtain a copy of the License at
> >> + *
> >> + *http://www.apache.org/licenses/LICENSE-2.0
> >> + *
> >> + * 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.
> >> + */
> >> +
> >> +// Optional -Photreload.components=compA,compB, forwarded as
> >> +// -Dofbiz.hotreload.components to DevReloadContainer, which then only
> watches those
> >> +// components' servicedef/source directories instead of every
> component in the checkout.
> >> +// See DevReloadContainer's `allowedComponents` javadoc for why:
> macOS's WatchService has
> >> +// a practical per-process ceiling on watched directories well under a
> typical ulimit,
> >> +// and a checkout the size of OFBiz trunk (~50+ components) can exceed
> it.
> >> +static List<String> hotreloadComponentArgs(Project project) {
> >> +    String components = project.findProperty('hotreload.components')
> >> +    return components ?
> ["-Dofbiz.hotreload.components=${components}".toString()] : []
> >> +}
> >> +
> >> +// Optional -Photreload.watchBuildOutput=true, forwarded as
> >> +// -Dofbiz.hotreload.watchBuildOutput to DevReloadContainer. Off by
> default: watching
> >> +// build/classes/java/main only matters for picking up
> externally-produced .class files
> >> +// (e.g. `./gradlew -t classes` running in a second terminal) --
> in-process compiles hot-swap
> >> +// directly and never need it -- and unlike source/servicedef
> watching, that tree isn't
> >> +// narrowed by -Photreload.components, so on a full checkout it
> roughly doubles the total
> >> +// directories watched for no benefit to the common (in-process-only)
> workflow.
> >> +static List<String> hotreloadWatchBuildOutputArgs(Project project) {
> >> +    Object watchBuildOutput =
> project.findProperty('hotreload.watchBuildOutput')
> >> +    return watchBuildOutput ?
> ["-Dofbiz.hotreload.watchBuildOutput=${watchBuildOutput}".toString()] : []
> >> +}
> >> +
> >> +// DevReloadContainer's own in-process-compile output directory.
> Deliberately separate
> >> +// from Gradle's own build/classes/java/main (see DevReloadContainer's
> hotReloadOutputDir
> >> +// field javadoc for why) and placed ahead of the normal runtime
> classpath below, so a
> >> +// class compiled here but never yet loaded in the running JVM picks
> up the live version
> >> +// on its first load too, not just already-loaded ones. Passed down to
> the JVM as a system
> >> +// property (see commonHotReloadJvmArgs) rather than hardcoded
> independently on the Java
> >> +// side too, so this path exists as a single source of truth.
> >> +static File hotReloadOutputDir(Project project) {
> >> +    return new File(project.rootDir, 'build/devreload/classes')
> >> +}
> >> +
> >> +// Base JVM args for ofbizDev: the hot-reload activation flags, the
> output-directory
> >> +// location, and the optional component/watch scoping args.
> >> +// -XX:+AllowEnhancedClassRedefinition is added separately by the
> caller, once a
> >> +// DCEVM-patched JVM has actually been resolved (required -- see
> below).
> >> +static List<String> commonHotReloadJvmArgs(Project project) {
> >> +    List<String> args = ['-Dofbiz.hotreload=true',
> '-Djdk.attach.allowAttachSelf=true',
> >> +
> "-Dofbiz.hotreload.outputDir=${hotReloadOutputDir(project).absolutePath}".toString()]
> >> +    args += hotreloadComponentArgs(project)
> >> +    args += hotreloadWatchBuildOutputArgs(project)
> >> +    return args
> >> +}
> >> +
> >> +// Locates a DCEVM-patched `java` executable from an explicit
> -PdcevmHome=... or
> >> +// DCEVM_HOME env var -- deliberately not auto-detected by guessing
> per-OS IDE install
> >> +// paths (IntelliJ's JBR bundles DCEVM and is a convenient source of
> one, but its
> >> +// install location varies too much across OS/Toolbox/version to guess
> reliably). One
> >> +// explicit way in is easier to reason about, document, and keep
> working than several
> >> +// silent path guesses that can each go stale independently.
> `ofbizDev` below requires
> >> +// this to resolve to something -- it fails fast with setup
> instructions otherwise.
> >> +//
> >> +// Two ways to set it once and forget it, so every future plain
> `./gradlew ofbizDev`
> >> +// just works with no flag: export DCEVM_HOME in a shell profile
> (picked up via
> >> +// System.getenv() below), or add a `dcevmHome=/path/to/jvm` line to
> >> +// `~/.gradle/gradle.properties` (GRADLE_USER_HOME -- picked up via
> project.findProperty()
> >> +// below, same as -PdcevmHome on the command line, just persisted
> instead of retyped).
> >> +// The latter needs the Gradle property name `dcevmHome`, not
> `DCEVM_HOME` -- gradle.properties
> >> +// holds Gradle properties, not env vars, so the two names aren't
> interchangeable there.
> >> +def resolveDcevmJavaExecutable = {
> >> +    String home = project.findProperty('dcevmHome') ?:
> System.getenv('DCEVM_HOME')
> >> +    if (!home) {
> >> +        return null
> >> +    }
> >> +    String exeName =
> org.gradle.internal.os.OperatingSystem.current().isWindows() ? 'java.exe' :
> 'java'
> >> +    File exe = new File(home, "bin/${exeName}")
> >> +    if (!exe.exists()) {
> >> +        // macOS JDK/JBR app bundles nest an extra Contents/Home --
> e.g. IntelliJ's bundled
> >> +        // JetBrains Runtime lives at "<IDE
> install>/Contents/jbr/Contents/Home/bin/java",
> >> +        // one level deeper than the jbr folder itself (which is what
> README/error-message
> >> +        // guidance points DCEVM_HOME/-PdcevmHome at). Fall back to
> that layout before
> >> +        // giving up, so the documented "point at <IDE
> install>/Contents/jbr" guidance
> >> +        // actually resolves on macOS instead of silently finding
> nothing.
> >> +        exe = new File(home, "Contents/Home/bin/${exeName}")
> >> +    }
> >> +    return exe.exists() ? exe.absolutePath : null
> >> +}
> >> +
> >> +// Convenience task: `./gradlew ofbizDev` starts OFBiz with hot-reload
> enabled
> >> +// (-Dofbiz.hotreload=true), plus the JVM flag DevReloadContainer's
> self-attaching
> >> +// HotSwapAgent needs (-Djdk.attach.allowAttachSelf=true). Registered
> on the root
> >> +// project, mirroring the classpath/mainClass wiring of the built-in
> `ofbiz` command
> >> +// task, so this plugin needs no change to the root build.gradle to be
> usable.
> >> +//
> >> +// A DCEVM-patched JVM is required, not optional: stock-JDK hot-reload
> used to be a
> >> +// supported fallback, but a stock JVM's redefineClasses can't
> hot-swap a new/removed
> >> +// method or field anyway (see DevReloadContainer's class-level
> javadoc), so silently
> >> +// running in that degraded mode just defers the surprise to whenever
> someone adds a
> >> +// method. Simple by design: if DCEVM_HOME/-PdcevmHome resolves to a
> real JVM, this task
> >> +// just runs -- no other flag, no mode to pick. If it doesn't resolve,
> running `ofbizDev`
> >> +// fails immediately with setup instructions, before OFBiz attempts to
> boot -- but the
> >> +// failure has to be raised from a doFirst task action, not directly
> in this
> >> +// configuration block: this block runs whenever the project is
> evaluated, which in this
> >> +// build happens on every `./gradlew <anything>` invocation, not just
> when `ofbizDev`
> >> +// itself is requested (confirmed empirically -- even `./gradlew help`
> realizes this
> >> +// task, most likely because the Develocity plugin applied in
> settings.gradle touches
> >> +// every registered task for build-scan data). Throwing here
> unconditionally would break
> >> +// every Gradle command whenever DCEVM isn't set up, which is exactly
> the "zero effect on
> >> +// anyone not using it" guarantee this plugin promises everywhere else
> (see README).
> >> +//
> >> +// JavaExec's `executable` property is pointed at the resolved DCEVM
> JVM binary during
> >> +// task configuration -- no toolchain is configured anywhere in this
> project for that to
> >> +// conflict with. One non-obvious wrinkle: `executable` has to be set
> directly in the
> >> +// task's configuration block, not from a task action (e.g. inside
> doFirst). Doing it
> >> +// from a task action fails with "Toolchain from executable property
> does not match
> >> +// toolchain from javaLauncher property" -- by the time a task action
> runs, javaLauncher's
> >> +// convention has already been finalized for execution, and overriding
> executable at that
> >> +// point conflicts with it. Setting it during configuration, before
> that convention
> >> +// finalizes, works cleanly. That's also *why* the DCEVM lookup itself
> still has to happen
> >> +// eagerly in this block (only the throw is deferred to doFirst):
> `executable` can't be
> >> +// set any later.
> >> +rootProject.tasks.register('ofbizDev', JavaExec) {
> >> +    group = 'OFBiz Server Commands'
> >> +    description = 'Start OFBiz with hot-reload (requires a
> DCEVM-patched JVM -- see README). Java source, ' +
> >> +            'services.xml, and structural changes (new/removed methods
> or fields, changed signatures) all ' +
> >> +            'reload live with no restart. Point
> -PdcevmHome=/path/to/jvm or set DCEVM_HOME to enable; fails ' +
> >> +            'fast with setup instructions if neither resolves.
> Optionally scope to specific components with ' +
> >> +            '-Photreload.components=compA,compB, or watch
> build/classes/java/main too (for externally-produced ' +
> >> +            '.class files) with -Photreload.watchBuildOutput=true.'
> >> +    classpath = rootProject.files(hotReloadOutputDir(project)) +
> rootProject.sourceSets.main.runtimeClasspath
> >> +    mainClass = rootProject.application.mainClass
> >> +    List<String> jvmArgsList =
> rootProject.application.applicationDefaultJvmArgs +
> commonHotReloadJvmArgs(project)
> >> +    String dcevmJava = resolveDcevmJavaExecutable()
> >> +    if (dcevmJava) {
> >> +        // Only wired up when actually needed: compiling everything
> first just to fail in
> >> +        // doFirst afterwards would make the "no DCEVM" error slow to
> reach on a cold build.
> >> +        dependsOn rootProject.tasks.named('classes')
> >> +        jvmArgsList = jvmArgsList +
> ['-XX:+AllowEnhancedClassRedefinition']
> >> +        logger.lifecycle("ofbizDev: DCEVM-patched JVM resolved at
> ${dcevmJava}")
> >> +        executable = dcevmJava
> >> +    } else {
> >> +        // Deferred to a task action (doFirst) instead of thrown
> directly here: this
> >> +        // configuration block runs whenever this project is
> evaluated, which in this build
> >> +        // happens for every `./gradlew <anything>` invocation, not
> just when `ofbizDev`
> >> +        // itself is requested (confirmed empirically -- even
> `./gradlew help` realizes this
> >> +        // task's configuration block, most likely due to the
> Develocity plugin applied in
> >> +        // settings.gradle touching every registered task for
> build-scan data). Throwing here
> >> +        // directly would break every Gradle command whenever DCEVM
> isn't set up, violating
> >> +        // this plugin's "zero effect on anyone not using it"
> guarantee (see README). A
> >> +        // doFirst block, by contrast, only actually runs when
> `ofbizDev` is the task Gradle
> >> +        // is executing.
> >> +        String attemptedHome = project.findProperty('dcevmHome') ?:
> System.getenv('DCEVM_HOME')
> >> +        String detail = attemptedHome
> >> +                ? "DCEVM_HOME/-PdcevmHome is set to
> '${attemptedHome}', but no 'bin/java' (or macOS " +
> >> +                        "'Contents/Home/bin/java') was found there --
> double-check the path."
> >> +                : "Neither -PdcevmHome nor the DCEVM_HOME env var is
> set."
> >> +        doFirst {
> >> +            throw new GradleException(
> >> +                    "ofbizDev requires a DCEVM-patched JVM -- plain
> stock-JDK hot-reload is not supported.\n\n" +
> >> +                    "${detail}\n\n" +
> >> +                    "To fix this:\n" +
> >> +                    "  1. Get a DCEVM-patched JVM. The easiest source
> is JetBrains Runtime (JBR), which " +
> >> +                    "bundles DCEVM and ships with IntelliJ IDEA under
> '<IDE install>/jbr' " +
> >> +                    "('.../Contents/jbr' on macOS). Standalone DCEVM
> builds work too.\n" +
> >> +                    "  2. Point at it -- set the DCEVM_HOME env var
> once (e.g. in your shell profile, so " +
> >> +                    "every future run picks it up automatically) or
> pass -PdcevmHome=/path/to/jvm each " +
> >> +                    "time.\n\n" +
> >> +                    "See plugins/devreload/README.md for the full
> setup guide.")
> >> +        }
> >> +    }
> >> +    jvmArgs(jvmArgsList)
> >> +}
> >> +
> >> +dependencyLocking {
> >> +    lockAllConfigurations()
> >> +}
> >> diff --git a/devreload/gradle.lockfile b/devreload/gradle.lockfile
> >> new file mode 100644
> >> index 000000000..b9b85588a
> >> --- /dev/null
> >> +++ b/devreload/gradle.lockfile
> >> @@ -0,0 +1,4 @@
> >> +# This is a Gradle generated file for dependency locking.
> >> +# Manual edits can break the build and are not advised.
> >> +# This file is expected to be part of source control.
> >> +empty=pluginLibsCompile,pluginLibsCompileOnly,pluginLibsRuntime
> >> diff --git a/devreload/ofbiz-component.xml
> b/devreload/ofbiz-component.xml
> >> new file mode 100644
> >> index 000000000..c9ecab1fb
> >> --- /dev/null
> >> +++ b/devreload/ofbiz-component.xml
> >> @@ -0,0 +1,30 @@
> >> +<?xml version="1.0" encoding="UTF-8"?>
> >> +<!--
> >> +Licensed to the Apache Software Foundation (ASF) under one
> >> +or more contributor license agreements.  See the NOTICE file
> >> +distributed with this work for additional information
> >> +regarding copyright ownership.  The ASF licenses this file
> >> +to you under the Apache License, Version 2.0 (the
> >> +"License"); you may not use this file except in compliance
> >> +with the License.  You may obtain a copy of the License at
> >> +
> >> +http://www.apache.org/licenses/LICENSE-2.0
> >> +
> >> +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.
> >> +-->
> >> +
> >> +<ofbiz-component name="devreload" enabled="true"
> >> +        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
> >> + xsi:noNamespaceSchemaLocation="
> https://ofbiz.apache.org/dtds/ofbiz-component.xsd";>
> >> +    <resource-loader name="main" type="component"/>
> >> +
> >> +    <!-- Development hot-reload container: a no-op unless started with
> >> +         -Dofbiz.hotreload=true, so it is safe to leave enabled in
> every
> >> +         environment. See README.md for usage. -->
> >> +    <container name="dev-reload-container" loaders="main"
> class="org.apache.ofbiz.devreload.DevReloadContainer"/>
> >> +</ofbiz-component>
> >> diff --git
> a/devreload/src/main/java/org/apache/ofbiz/devreload/DevReloadContainer.java
>
> >>
> b/devreload/src/main/java/org/apache/ofbiz/devreload/DevReloadContainer.java
> >> new file mode 100644
> >> index 000000000..1f9539cb2
> >> --- /dev/null
> >> +++
> b/devreload/src/main/java/org/apache/ofbiz/devreload/DevReloadContainer.java
> >> @@ -0,0 +1,860 @@
> >>
> +/*******************************************************************************
> >> + * Licensed to the Apache Software Foundation (ASF) under one
> >> + * or more contributor license agreements.  See the NOTICE file
> >> + * distributed with this work for additional information
> >> + * regarding copyright ownership.  The ASF licenses this file
> >> + * to you under the Apache License, Version 2.0 (the
> >> + * "License"); you may not use this file except in compliance
> >> + * with the License.  You may obtain a copy of the License at
> >> + *
> >> + *http://www.apache.org/licenses/LICENSE-2.0
> >> + *
> >> + * 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.
> >> +
> *******************************************************************************/
> >> +package org.apache.ofbiz.devreload;
> >> +
> >> +import java.io.IOException;
> >> +import java.lang.instrument.ClassDefinition;
> >> +import java.lang.instrument.Instrumentation;
> >> +import java.lang.management.ManagementFactory;
> >> +import java.net.URI;
> >> +import java.net.URISyntaxException;
> >> +import java.net.URL;
> >> +import java.nio.file.ClosedWatchServiceException;
> >> +import java.nio.file.FileVisitResult;
> >> +import java.nio.file.Files;
> >> +import java.nio.file.Path;
> >> +import java.nio.file.Paths;
> >> +import java.nio.file.SimpleFileVisitor;
> >> +import java.nio.file.StandardWatchEventKinds;
> >> +import java.nio.file.WatchEvent;
> >> +import java.nio.file.WatchKey;
> >> +import java.nio.file.WatchService;
> >> +import java.nio.file.attribute.BasicFileAttributes;
> >> +import java.util.ArrayList;
> >> +import java.util.Arrays;
> >> +import java.util.HashSet;
> >> +import java.util.List;
> >> +import java.util.Set;
> >> +import java.util.concurrent.Executors;
> >> +import java.util.concurrent.RejectedExecutionException;
> >> +import java.util.concurrent.ScheduledExecutorService;
> >> +import java.util.concurrent.ScheduledFuture;
> >> +import java.util.concurrent.TimeUnit;
> >> +import java.util.function.Consumer;
> >> +import java.util.stream.Collectors;
> >> +import javax.tools.JavaCompiler;
> >> +import javax.tools.StandardJavaFileManager;
> >> +import javax.tools.StandardLocation;
> >> +import javax.tools.ToolProvider;
> >> +
> >> +import org.apache.commons.io.FileUtils;
> >> +import org.apache.ofbiz.base.component.ComponentConfig;
> >> +import org.apache.ofbiz.base.config.GenericConfigException;
> >> +import org.apache.ofbiz.base.container.Container;
> >> +import org.apache.ofbiz.base.container.ContainerException;
> >> +import org.apache.ofbiz.base.start.StartupCommand;
> >> +import org.apache.ofbiz.base.util.Debug;
> >> +import org.apache.ofbiz.base.util.cache.UtilCache;
> >> +
> >> +/**
> >> + * Development-only container that watches Java sources/classes and
> {@code services.xml}
> >> + * files and applies changes to a running OFBiz instance without a
> restart.
> >> + *
> >> + * <h2>Activation</h2>
> >> + * Add {@code -Dofbiz.hotreload=true
> -Djdk.attach.allowAttachSelf=true} to your JVM
> >> + * arguments, then start OFBiz normally (or run {@code ./gradlew
> ofbizDev}, provided by
> >> + * this plugin, which sets both automatically — the only supported
> command). That Gradle
> >> + * task requires a DCEVM-patched JVM and fails fast with setup
> instructions if none is
> >> + * resolvable; see "Structural changes" below for why.
> >> + *
> >> + * <h2>How Java hot-reload works</h2>
> >> + * <ol>
> >> + *   <li>At startup this container self-attaches {@link HotSwapAgent}
> to the current JVM
> >> + *       via the Attach API, obtaining a live {@link Instrumentation}
> instance — no
> >> + *       {@code -javaagent} flag needed.</li>
> >> + *   <li>A {@link WatchService} thread monitors every component's
> {@code src/main/java}
> >> + *       directory. On save, changed {@code .java} files are compiled
> in-process (a JDK,
> >> + *       not just a JRE, is required) into {@code
> build/devreload/classes/} — a directory
> >> + *       private to this plugin, deliberately <em>not</em> Gradle's own
> >> + *       {@code build/classes/java/main/}. Writing into Gradle's
> managed output would
> >> + *       leave its incremental-build cache unaware of the change: if a
> source file is
> >> + *       later reverted to content Gradle already has a snapshot for
> (e.g. via
> >> + *       {@code git checkout}) while no hot-reload session is
> watching, Gradle's next
> >> + *       {@code compileJava} would see matching input and skip
> recompiling, silently
> >> + *       leaving the stale hot-swapped {@code .class} file in place.
> This plugin's output
> >> + *       directory is cleared and recreated on every {@link #init}, so
> a fresh run never
> >> + *       inherits a previous session's bytecode, and is placed ahead
> of Gradle's own
> >> + *       output on the classpath (see {@code build.gradle}) so it
> always wins when a class
> >> + *       exists in both places. Running {@code ./gradlew -t classes}
> externally in a
> >> + *       second terminal still works — the same WatchService also
> monitors Gradle's own
> >> + *       output directory directly for externally-produced {@code
> .class} files, when
> >> + *       {@code -Dofbiz.hotreload.watchBuildOutput=true}.</li>
> >> + *   <li>Changes are debounced for 300 ms so a single compile run is
> handled as one batch.
> >> + *       Each changed class already loaded in the JVM is updated in
> place via
> >> + *       {@link Instrumentation#redefineClasses}, the same mechanism
> an IDE debugger uses
> >> + *       for HotSwap. Because the {@link Class} object's identity
> never changes, every
> >> + *       existing reference to it — including caches inside {@code
> JavaEventHandler} and
> >> + *       {@code StandardJavaEngine} — automatically executes the new
> method bodies on the
> >> + *       next call. No framework code needs to know this plugin
> exists.</li>
> >> + *   <li>Brand-new classes need no special handling at all: they
> simply get loaded
> >> + *       normally, from this plugin's build output directory, the
> first time something
> >> + *       references them.</li>
> >> + * </ol>
> >> + *
> >> + * <h2>Structural changes</h2>
> >> + * On a stock JVM, {@code redefineClasses} can only replace method
> bodies and static
> >> + * initializers of a class that is already loaded — adding/removing
> methods or fields,
> >> + * changing a method signature, or changing the class hierarchy still
> requires a restart.
> >> + * This is a hard JVM limitation, not something this plugin can work
> around on a stock
> >> + * JVM. Running on a DCEVM-patched JVM with {@code
> -XX:+AllowEnhancedClassRedefinition}
> >> + * lifts that restriction transparently: this class calls the exact
> same
> >> + * {@code redefineClasses} API either way, so structural changes just
> work when that flag
> >> + * is present. Because a stock JVM would silently defer that surprise
> to whenever someone
> >> + * adds a method, {@code ./gradlew ofbizDev}'s build.gradle task
> treats a DCEVM-patched
> >> + * JVM as required, not optional: it resolves one via {@code
> -PdcevmHome}/
> >> + * {@code DCEVM_HOME}, launches with that flag automatically, and
> fails fast with setup
> >> + * instructions instead of starting OFBiz at all if none is
> resolvable. Plain stock-JDK
> >> + * operation is not a supported outcome of that command.
> >> + *
> >> + * <h2>How services.xml changes are handled</h2>
> >> + * Every component's {@code servicedef/} directory is also watched; on
> change, the
> >> + * {@code service.ModelServiceMapByModel} {@link UtilCache} entry is
> cleared directly, so
> >> + * the new/edited definition is re-read on the next service call.
> >> + *
> >> + * <h2>Directory watch limits</h2>
> >> + * The OS may refuse to watch a directory once a process-wide ceiling
> is reached (most
> >> + * commonly hit on macOS on a full checkout). This container does not
> try to work around
> >> + * that itself: a directory whose registration fails is simply left
> unwatched, with a
> >> + * warning naming it, rather than silently falling back to some slower
> alternate
> >> + * mechanism. Narrow the set of directories with {@code
> -Dofbiz.hotreload.components}
> >> + * (or {@code -Photreload.components=compA,compB} with the {@code
> ofbizDev} Gradle task)
> >> + * to fit under the ceiling — see this component's README for the full
> guidance.
> >> + *
> >> + * <h2>Scope</h2>
> >> + * This container is intentionally dev-only. It has no effect when the
> system property is
> >> + * absent, so it is safe to leave the registration in this component's
> >> + * {@code ofbiz-component.xml} for all environments.
> >> + */
> >> +public class DevReloadContainer implements Container {
> >> +
> >> +    private static final String MODULE =
> DevReloadContainer.class.getName();
> >> +    private static final String SERVICE_MODEL_CACHE_NAME =
> "service.ModelServiceMapByModel";
> >> +
> >> +    private String name;
> >> +    private WatchService watchService;
> >> +    private Thread watchThread;
> >> +    private ScheduledExecutorService debounceExecutor;
> >> +    private Instrumentation instrumentation;
> >> +
> >> +    private final Debouncer<String> classReloadDebouncer = new
> Debouncer<>(this::applyReload);
> >> +    private final Debouncer<Path> xmlReloadDebouncer = new
> Debouncer<>(this::applyServiceXmlReload);
> >> +    private final Debouncer<Path> compileDebouncer = new
> Debouncer<>(this::applyCompile);
> >> +
> >> +    // Counts across
> registerServicedefDirs()/registerSourceDirs()/registerAll(), so
> >> +    // start() can emit one aggregated warning instead of leaving
> individual failures
> >> +    // scattered in the log where they're easy to miss.
> >> +    private int watchDirsAttempted = 0;
> >> +    private int watchDirsFailed = 0;
> >> +
> >> +    /** Gradle's own compiled-output directory, {@code
> build/classes/java/main}. Read-only
> >> +     *  from this class's perspective: only ever watched (when {@code
> watchBuildOutput} is
> >> +     *  on) for externally-produced {@code .class} files, never
> written to directly. */
> >> +    private Path classesDir;
> >> +
> >> +    /**
> >> +     * This plugin's own compiled-output directory, {@code
> build/devreload/classes},
> >> +     * deliberately separate from Gradle's {@link #classesDir}.
> In-process compiles
> >> +     * (see {@link #applyCompile}) write here instead of into Gradle's
> managed output, so
> >> +     * Gradle's incremental {@code compileJava} up-to-date check is
> never confused by
> >> +     * writes it didn't make itself. Cleared and recreated fresh on
> every {@link #init},
> >> +     * and placed ahead of Gradle's output on the runtime classpath
> (see
> >> +     * {@code plugins/devreload/build.gradle}) — so when a class
> exists in both
> >> +     * directories, this one always wins (see {@link
> #resolveClassFile}).
> >> +     */
> >> +    private Path hotReloadOutputDir;
> >> +
> >> +    // Populated in start() before the watch thread launches;
> read-only after that.
> >> +    private final Set<Path> servicedefDirs = new HashSet<>();
> >> +    private final Set<Path> sourceRootDirs = new HashSet<>();
> >> +
> >> +    /**
> >> +     * Component names to watch, from {@code
> -Dofbiz.hotreload.components}; {@code null}
> >> +     * means watch every component. Set this property to a
> comma-separated list of
> >> +     * component names to keep the total number of watched directories
> under the OS's
> >> +     * per-process ceiling on a large checkout.
> >> +     */
> >> +    private Set<String> allowedComponents;
> >> +
> >> +    /**
> >> +     * Whether {@link #classesDir} ({@code build/classes/java/main})
> itself is watched, from
> >> +     * {@code -Dofbiz.hotreload.watchBuildOutput}; defaults to {@code
> false}. This tree mirrors
> >> +     * every component's source tree and, unlike {@link
> #sourceRootDirs}, is <em>not</em>
> >> +     * narrowed by {@link #allowedComponents} (compiled output isn't
> organized per component),
> >> +     * so on a full checkout it roughly doubles the total directories
> watched. It only exists to
> >> +     * pick up externally-produced {@code .class} files (e.g. running
> {@code ./gradlew -t
> >> +     * classes} in a second terminal); in-process compiles hot-swap
> directly and never need it.
> >> +     * Off by default so unscoped runs need meaningfully fewer real
> watch registrations.
> >> +     */
> >> +    private boolean watchBuildOutput;
> >> +
> >> +    @Override
> >> +    public void init(List<StartupCommand> ofbizCommands, String name,
> String configFile) throws ContainerException {
> >> +        this.name = name;
> >> +
> >> +        if
> (!"true".equalsIgnoreCase(System.getProperty("ofbiz.hotreload"))) {
> >> +            Debug.logInfo("DevReloadContainer is disabled. Use
> -Dofbiz.hotreload=true to enable.", MODULE);
> >> +            return;
> >> +        }
> >> +
> >> +        parseHotReloadProperties();
> >> +        attachHotSwapAgent();
> >> +
> >> +        if (!prepareClassesDir()) {
> >> +            return;
> >> +        }
> >> +        if (!prepareHotReloadOutputDir()) {
> >> +            return;
> >> +        }
> >> +        createWatchService();
> >> +        registerBuildOutputWatchIfEnabled();
> >> +        startDebounceExecutor();
> >> +
> >> +        Debug.logInfo("DevReloadContainer ready — compiled output at "
> + hotReloadOutputDir.toAbsolutePath(), MODULE);
> >> +    }
> >> +
> >> +    /**
> >> +     * Reads {@code -Dofbiz.hotreload.components} and {@code
> -Dofbiz.hotreload.watchBuildOutput},
> >> +     * populating {@link #allowedComponents} and {@link
> #watchBuildOutput}.
> >> +     */
> >> +    private void parseHotReloadProperties() {
> >> +        String componentsProperty =
> System.getProperty("ofbiz.hotreload.components");
> >> +        if (componentsProperty != null &&
> !componentsProperty.isBlank()) {
> >> +            allowedComponents =
> Arrays.stream(componentsProperty.split(","))
> >> +                    .map(String::trim)
> >> +                    .filter(s -> !s.isEmpty())
> >> +                    .collect(Collectors.toSet());
> >> +            Debug.logInfo("Hot-reload: scoped to components " +
> allowedComponents
> >> +                    + " (set via -Dofbiz.hotreload.components)",
> MODULE);
> >> +        }
> >> +
> >> +        watchBuildOutput =
> "true".equalsIgnoreCase(System.getProperty("ofbiz.hotreload.watchBuildOutput"));
> >> +    }
> >> +
> >> +    /**
> >> +     * Self-attaches {@link HotSwapAgent} so Java class redefinition
> is available. Failure
> >> +     * here is non-fatal and never aborts {@link #init}: Java hot-swap
> is simply disabled
> >> +     * while {@code services.xml} auto-reload still works.
> >> +     */
> >> +    private void attachHotSwapAgent() {
> >> +        try {
> >> +            instrumentation = HotSwapAgent.install();
> >> +            Debug.logInfo("Hot-reload: self-attached HotSwapAgent —
> Java class redefinition is available.", MODULE);
> >> +            if (enhancedRedefinitionRequested()) {
> >> +                Debug.logInfo("Hot-reload:
> -XX:+AllowEnhancedClassRedefinition detected — running on a JVM "
> >> +                        + "(e.g. JetBrains Runtime) that can also
> hot-swap structural changes (added/removed "
> >> +                        + "methods or fields, changed signatures), not
> just method bodies.", MODULE);
> >> +            } else {
> >> +                Debug.logWarning("Hot-reload: structural changes
> (added/removed methods or fields, changed "
> >> +                        + "signatures) will require a restart on this
> JVM. './gradlew ofbizDev' requires a "
> >> +                        + "DCEVM-patched JVM and refuses to start
> without one, so you're most likely seeing "
> >> +                        + "this because hot-reload was activated some
> other way on a stock JDK. See "
> >> +                        + "plugins/devreload/README.md to set up
> DCEVM_HOME.", MODULE);
> >> +            }
> >> +        } catch (Exception e) {
> >> +            Debug.logWarning("Hot-reload: could not self-attach
> HotSwapAgent (" + e.getMessage()
> >> +                    + "). Add -Djdk.attach.allowAttachSelf=true to JVM
> args. "
> >> +                    + "Java class changes will require a restart;
> services.xml auto-reload still works.", MODULE);
> >> +        }
> >> +    }
> >> +
> >> +    /** Sets {@link #classesDir}. Returns {@code false} (after
> logging) if it doesn't exist yet. */
> >> +    private boolean prepareClassesDir() {
> >> +        classesDir = Paths.get("build/classes/java/main");
> >> +        if (!Files.exists(classesDir)) {
> >> +            Debug.logWarning("Hot-reload: classes directory not found
> at " + classesDir.toAbsolutePath()
> >> +                    + ". Run './gradlew classes' first, then
> restart.", MODULE);
> >> +            return false;
> >> +        }
> >> +        return true;
> >> +    }
> >> +
> >> +    /**
> >> +     * Sets {@link #hotReloadOutputDir} and clears/recreates it.
> Returns {@code false} (after
> >> +     * logging) if the directory could not be prepared -- without a
> writable output directory,
> >> +     * in-process compilation cannot work at all, so the whole
> container is disabled up front
> >> +     * instead of continuing to a misleading "ready" log.
> >> +     */
> >> +    private boolean prepareHotReloadOutputDir() {
> >> +        // build.gradle passes this down as
> -Dofbiz.hotreload.outputDir, computed from the
> >> +        // same value it prepends to the classpath, so the path exists
> in exactly one place
> >> +        // rather than being hardcoded independently here too. The
> literal default below is
> >> +        // only a fallback for the (unsupported) case of starting this
> container without
> >> +        // going through the ofbizDev Gradle task.
> >> +        hotReloadOutputDir =
> Paths.get(System.getProperty("ofbiz.hotreload.outputDir",
> "build/devreload/classes"));
> >> +        try {
> >> + FileUtils.deleteDirectory(hotReloadOutputDir.toFile());
> >> +            Files.createDirectories(hotReloadOutputDir);
> >> +            return true;
> >> +        } catch (IOException e) {
> >> +            Debug.logWarning("Hot-reload: could not prepare this
> plugin's own output directory "
> >> +                    + hotReloadOutputDir.toAbsolutePath() + ": " +
> e.getMessage(), MODULE);
> >> +            return false;
> >> +        }
> >> +    }
> >> +
> >> +    /** Creates {@link #watchService} on {@link #classesDir}'s
> filesystem. */
> >> +    private void createWatchService() throws ContainerException {
> >> +        try {
> >> +            watchService =
> classesDir.getFileSystem().newWatchService();
> >> +        } catch (IOException e) {
> >> +            throw new ContainerException("DevReloadContainer: failed
> to initialise WatchService", e);
> >> +        }
> >> +    }
> >> +
> >> +    /**
> >> +     * Registers {@link #classesDir} with the WatchService when {@link
> #watchBuildOutput} is
> >> +     * on, so externally-produced {@code .class} files (e.g. {@code
> ./gradlew -t classes} in
> >> +     * a second terminal) are picked up too.
> >> +     */
> >> +    private void registerBuildOutputWatchIfEnabled() {
> >> +        if (watchBuildOutput) {
> >> +            try {
> >> +                registerAll(classesDir);
> >> +            } catch (IOException e) {
> >> +                // registerAll() already logs a warning and skips
> individual directories
> >> +                // that fail to register; reaching here means
> something more fundamental
> >> +                // broke walking the tree at all (e.g. can't even list
> classesDir).
> >> +                Debug.logWarning("Hot-reload: could not fully walk " +
> classesDir + ": " + e.getMessage(), MODULE);
> >> +            }
> >> +            Debug.logInfo("Hot-reload: watching compiled-output
> directory " + classesDir.toAbsolutePath()
> >> +                    + " for externally-produced .class files (set via
> -Dofbiz.hotreload.watchBuildOutput=true).",
> >> +                    MODULE);
> >> +        } else {
> >> +            Debug.logInfo("Hot-reload: not watching " +
> classesDir.toAbsolutePath() + " (this tree isn't "
> >> +                    + "narrowed by -Dofbiz.hotreload.components and
> roughly doubles the total directories "
> >> +                    + "watched). In-process edits still hot-swap
> normally; running './gradlew -t classes' in a "
> >> +                    + "second terminal will not be picked up unless
> you set "
> >> +                    + "-Dofbiz.hotreload.watchBuildOutput=true (or
> -Photreload.watchBuildOutput=true with the "
> >> +                    + "ofbizDev Gradle task).", MODULE);
> >> +        }
> >> +    }
> >> +
> >> +    /** Starts the daemon executor backing every {@link Debouncer}. */
> >> +    private void startDebounceExecutor() {
> >> +        debounceExecutor =
> Executors.newSingleThreadScheduledExecutor(r -> {
> >> +            Thread t = new Thread(r, "ofbiz-hot-reload-debouncer");
> >> +            t.setDaemon(true);
> >> +            return t;
> >> +        });
> >> +    }
> >> +
> >> +    /**
> >> +     * Picks whichever of this plugin's own compiled output ({@code
> overlay}, under
> >> +     * {@link #hotReloadOutputDir}) or Gradle's ({@code fallback},
> under {@link #classesDir})
> >> +     * is the right one to redefine from. {@code overlay} always wins
> when it exists,
> >> +     * matching its position ahead of {@code fallback} on the runtime
> classpath (see
> >> +     * {@code build.gradle}) — a class compiled here is always the one
> a caller would load.
> >> +     * Returns {@code null} if neither exists.
> >> +     */
> >> +    private Path resolveClassFile(Path overlay, Path fallback) {
> >> +        if (Files.exists(overlay)) {
> >> +            return overlay;
> >> +        }
> >> +        if (Files.exists(fallback)) {
> >> +            return fallback;
> >> +        }
> >> +        return null;
> >> +    }
> >> +
> >> +    @Override
> >> +    public boolean start() throws ContainerException {
> >> +        if (watchService == null) {
> >> +            return true; // disabled
> >> +        }
> >> +        registerServicedefDirs();
> >> +        registerSourceDirs();
> >> +        if (watchDirsFailed > 0) {
> >> +            Debug.logWarning("Hot-reload: " + watchDirsFailed + " of "
> + watchDirsAttempted + " directory watch "
> >> +                    + "registrations hit file-descriptor/watch
> exhaustion (see warnings above for which ones) "
> >> +                    + "and are NOT being watched — changes there will
> not hot-reload until you restart. Scope "
> >> +                    + "hot-reload to just the components you're
> working on with "
> >> +                    + "-Dofbiz.hotreload.components=compA,compB (or
> -Photreload.components=compA,compB with "
> >> +                    + "the ofbizDev Gradle task) to fit under the OS
> watch limit.", MODULE);
> >> +        }
> >> +        watchThread = new Thread(this::watchLoop,
> "ofbiz-hot-reload-watcher");
> >> +        watchThread.setDaemon(true);
> >> +        watchThread.start();
> >> +        Debug.logInfo("DevReloadContainer started. Edit any Java or
> services.xml file and changes go live without a restart.", MODULE);
> >> +        return true;
> >> +    }
> >> +
> >> +    /**
> >> +     * Registers every directory that contains a component
> service-definition XML file
> >> +     * (type="model") with the WatchService so that edits to those
> files are detected.
> >> +     * Called once from {@link #start()}, before the watch thread
> launches.
> >> +     */
> >> +    private void registerServicedefDirs() {
> >> +        for (ComponentConfig.ServiceResourceInfo sri :
> ComponentConfig.getAllServiceResourceInfos("model")) {
> >> +            if (allowedComponents != null &&
> !allowedComponents.contains(sri.getComponentConfig().getComponentName())) {
> >> +                continue;
> >> +            }
> >> +            try {
> >> +                URL url = sri.createResourceHandler().getURL();
> >> +                if (!"file".equals(url.getProtocol())) {
> >> +                    continue; // skip non-filesystem resources
> (classpath jars, etc.)
> >> +                }
> >> +                Path dir = Paths.get(new
> URI(url.toString())).getParent();
> >> +                if (dir != null && Files.isDirectory(dir) &&
> servicedefDirs.add(dir)) {
> >> +                    watchDirsAttempted++;
> >> +                    try {
> >> +                        dir.register(watchService,
> >> + StandardWatchEventKinds.ENTRY_CREATE,
> >> + StandardWatchEventKinds.ENTRY_MODIFY);
> >> +                        Debug.logInfo("Hot-reload: watching servicedef
> directory " + dir, MODULE);
> >> +                    } catch (IOException e) {
> >> +                        warnUnwatched(dir, e);
> >> +                    }
> >> +                }
> >> +            } catch (GenericConfigException | URISyntaxException e) {
> >> +                Debug.logWarning("Hot-reload: could not register
> servicedef dir for "
> >> +                        + sri.getLocation() + ": " + e.getMessage(),
> MODULE);
> >> +            } catch (Throwable t) {
> >> +                // Defensive: a single component's servicedef
> registration must not be able
> >> +                // to abort the loop and leave every subsequent
> component's servicedef
> >> +                // directory unwatched.
> >> +                Debug.logError(t, "Hot-reload: unexpected error
> registering servicedef dir for "
> >> +                        + sri.getLocation(), MODULE);
> >> +            }
> >> +        }
> >> +    }
> >> +
> >> +    /**
> >> +     * Registers every component's {@code src/main/java} directory
> with the WatchService
> >> +     * so that saving a {@code .java} file triggers in-process
> compilation via
> >> +     * {@link ToolProvider#getSystemJavaCompiler()}. Falls back
> gracefully when running
> >> +     * on a JRE (compiler unavailable) — source watching is simply
> skipped.
> >> +     */
> >> +    private void registerSourceDirs() {
> >> +        if (ToolProvider.getSystemJavaCompiler() == null) {
> >> +            Debug.logWarning("Hot-reload: javax.tools.JavaCompiler not
> available (JRE, not JDK?). "
> >> +                    + "Java source auto-compilation disabled — use
> './gradlew -t classes' in a second terminal.", MODULE);
> >> +            return;
> >> +        }
> >> +        for (ComponentConfig cc : ComponentConfig.getAllComponents()) {
> >> +            if (cc.rootLocation() == null) {
> >> +                continue;
> >> +            }
> >> +            if (allowedComponents != null &&
> !allowedComponents.contains(cc.getComponentName())) {
> >> +                continue;
> >> +            }
> >> +            Path srcDir = cc.rootLocation().resolve("src/main/java");
> >> +            if (Files.isDirectory(srcDir) &&
> sourceRootDirs.add(srcDir)) {
> >> +                try {
> >> +                    registerAll(srcDir);
> >> +                    Debug.logInfo("Hot-reload: watching source
> directory " + srcDir, MODULE);
> >> +                } catch (IOException e) {
> >> +                    // registerAll() already logs a warning and skips
> individual directories
> >> +                    // that fail to register; reaching here means
> something more fundamental
> >> +                    // broke walking the tree at all (e.g. can't list
> srcDir).
> >> +                    Debug.logWarning("Hot-reload: could not walk
> source dir " + srcDir + ": " + e.getMessage(), MODULE);
> >> +                }
> >> +            }
> >> +        }
> >> +        if (!sourceRootDirs.isEmpty()) {
> >> +            Debug.logInfo("Hot-reload: Java source auto-compilation
> active — save a .java file and it reloads automatically.", MODULE);
> >> +        }
> >> +    }
> >> +
> >> +    @Override
> >> +    public void stop() throws ContainerException {
> >> +        if (debounceExecutor != null) {
> >> +            debounceExecutor.shutdownNow();
> >> +        }
> >> +        if (watchService != null) {
> >> +            try {
> >> +                watchService.close();
> >> +            } catch (IOException e) {
> >> +                Debug.logError(e, "DevReloadContainer: error closing
> WatchService", MODULE);
> >> +            }
> >> +        }
> >> +        if (watchThread != null) {
> >> +            watchThread.interrupt();
> >> +        }
> >> +    }
> >> +
> >> +    @Override
> >> +    public String getName() {
> >> +        return name;
> >> +    }
> >> +
> >> +    //
> -------------------------------------------------------------------------
> >> +    // Watch loop
> >> +    //
> -------------------------------------------------------------------------
> >> +
> >> +    private void watchLoop() {
> >> +        while (!Thread.currentThread().isInterrupted()) {
> >> +            WatchKey key;
> >> +            try {
> >> +                key = watchService.take();
> >> +            } catch (InterruptedException e) {
> >> +                Thread.currentThread().interrupt();
> >> +                break;
> >> +            } catch (ClosedWatchServiceException e) {
> >> +                break;
> >> +            }
> >> +
> >> +            Path dir = (Path) key.watchable();
> >> +            for (WatchEvent<?> event : key.pollEvents()) {
> >> +                WatchEvent.Kind<?> kind = event.kind();
> >> +                if (kind == StandardWatchEventKinds.OVERFLOW) {
> >> +                    continue;
> >> +                }
> >> +
> >> +                @SuppressWarnings("unchecked")
> >> +                Path changed = dir.resolve(((WatchEvent<Path>)
> event).context());
> >> +
> >> +                if (kind == StandardWatchEventKinds.ENTRY_CREATE &&
> Files.isDirectory(changed)) {
> >> +                    // New package directory created during
> compilation — register it.
> >> +                    try {
> >> +                        registerAll(changed);
> >> +                    } catch (IOException e) {
> >> +                        Debug.logError(e, "DevReloadContainer: failed
> to register new directory: " + changed, MODULE);
> >> +                    }
> >> +                } else if ((kind ==
> StandardWatchEventKinds.ENTRY_CREATE || kind ==
> StandardWatchEventKinds.ENTRY_MODIFY)
> >> +                        && changed.toString().endsWith(".class")) {
> >> +                    // Only react to written/updated class files.
> Ignore ENTRY_DELETE so
> >> +                    // that removing a source file (and its .class
> output) does not cause
> >> +                    // a redefinition attempt against a now-missing
> file.
> >> +                    reloadClassFile(classesDir, changed);
> >> +                } else if ((kind ==
> StandardWatchEventKinds.ENTRY_CREATE || kind ==
> StandardWatchEventKinds.ENTRY_MODIFY)
> >> +                        && changed.toString().endsWith(".xml")
> >> +                        &&
> servicedefDirs.stream().anyMatch(dir::startsWith)) {
> >> +                    xmlReloadDebouncer.add(changed);
> >> +                } else if ((kind ==
> StandardWatchEventKinds.ENTRY_CREATE || kind ==
> StandardWatchEventKinds.ENTRY_MODIFY)
> >> +                        && changed.toString().endsWith(".java")
> >> +                        &&
> sourceRootDirs.stream().anyMatch(dir::startsWith)) {
> >> +                    compileDebouncer.add(changed);
> >> +                }
> >> +            }
> >> +            if (!key.reset()) {
> >> +                Debug.logWarning("Hot-reload: watch key became invalid
> (directory deleted?): "
> >> +                        + key.watchable() + ". WatchService will no
> longer detect changes in that directory.", MODULE);
> >> +            }
> >> +        }
> >> +    }
> >> +
> >> +    //
> -------------------------------------------------------------------------
> >> +    // Debounced batching
> >> +    //
> -------------------------------------------------------------------------
> >> +
> >> +    /**
> >> +     * Coalesces rapid-fire change notifications into one action, so a
> single compile run
> >> +     * that touches many files (e.g. one with inner/anonymous classes,
> or a Gradle build
> >> +     * writing several {@code .class} files at once) is handled as a
> single batch instead
> >> +     * of one action per file. Shared by all three change pipelines
> (class reload,
> >> +     * {@code services.xml} reload, Java compile) instead of each
> hand-rolling its own
> >> +     * pending-set/cancel/reschedule bookkeeping.
> >> +     */
> >> +    private final class Debouncer<T> {
> >> +        private final Set<T> pending = new HashSet<>();
> >> +        private final Consumer<Set<T>> action;
> >> +        private ScheduledFuture<?> scheduled;
> >> +
> >> +        Debouncer(Consumer<Set<T>> action) {
> >> +            this.action = action;
> >> +        }
> >> +
> >> +        synchronized void add(T item) {
> >> +            pending.add(item);
> >> +            if (scheduled != null) {
> >> +                scheduled.cancel(false);
> >> +            }
> >> +            try {
> >> +                // Wait 300 ms after the last change so a burst of
> related changes (e.g. a
> >> +                // single Gradle compile run writing multiple .class
> files) is handled as
> >> +                // one batch instead of one action per file.
> >> +                scheduled = debounceExecutor.schedule(this::fire, 300,
> TimeUnit.MILLISECONDS);
> >> +            } catch (RejectedExecutionException e) {
> >> +                // Container is shutting down; pending changes will
> not be applied.
> >> +            }
> >> +        }
> >> +
> >> +        private synchronized void fire() {
> >> +            if (pending.isEmpty()) {
> >> +                return;
> >> +            }
> >> +            Set<T> batch = new HashSet<>(pending);
> >> +            pending.clear();
> >> +            action.accept(batch);
> >> +        }
> >> +    }
> >> +
> >> +    /** Resolves {@code changed} to a class name relative to {@code
> baseDir} and, if valid, queues it for reload. */
> >> +    private void reloadClassFile(Path baseDir, Path changed) {
> >> +        String className = toClassName(baseDir, changed);
> >> +        if (className != null) {
> >> +            classReloadDebouncer.add(className);
> >> +        }
> >> +    }
> >> +
> >> +    private void applyReload(Set<String> batch) {
> >> +        Debug.logInfo("Hot-reload: detected changes in " + batch,
> MODULE);
> >> +
> >> +        if (instrumentation == null) {
> >> +            Debug.logWarning("Hot-reload: HotSwapAgent not attached —
> " + batch
> >> +                    + " compiled but not applied to the running JVM.
> Restart to pick it up.", MODULE);
> >> +            return;
> >> +        }
> >> +
> >> +        List<ClassDefinition> defs = new ArrayList<>();
> >> +        for (String className : batch) {
> >> +            Path relative = Paths.get(className.replace('.', '/') +
> ".class");
> >> +            Path overlayFile = hotReloadOutputDir.resolve(relative);
> >> +            Path gradleFile = classesDir.resolve(relative);
> >> +            Path classFile = resolveClassFile(overlayFile, gradleFile);
> >> +            if (classFile == null) {
> >> +                Debug.logWarning("Hot-reload: detected a change for "
> + className
> >> +                        + " but could not find its compiled output in
> either "
> >> +                        + hotReloadOutputDir.toAbsolutePath() + " or "
> + classesDir.toAbsolutePath(), MODULE);
> >> +                continue;
> >> +            }
> >> +            try {
> >> +                Class<?> loaded = findLoadedClass(className);
> >> +                if (loaded == null) {
> >> +                    // Never loaded yet in this JVM — nothing to
> redefine. It will simply
> >> +                    // load fresh, with the new bytecode, the first
> time something
> >> +                    // references it, from whichever directory
> resolveClassFile() would
> >> +                    // pick (overlay first on the classpath too, see
> build.gradle).
> >> +                    continue;
> >> +                }
> >> +                defs.add(new ClassDefinition(loaded,
> Files.readAllBytes(classFile)));
> >> +            } catch (IOException e) {
> >> +                Debug.logError(e, "Hot-reload: failed to read class
> file for " + className, MODULE);
> >> +            }
> >> +        }
> >> +
> >> +        if (defs.isEmpty()) {
> >> +            Debug.logInfo("Hot-reload: nothing already loaded to
> redefine for " + batch, MODULE);
> >> +            return;
> >> +        }
> >> +
> >> +        try {
> >> +            instrumentation.redefineClasses(defs.toArray(new
> ClassDefinition[0]));
> >> +            // Clear service definition cache so newly added service
> methods are discovered.
> >> +            // We deliberately do NOT clear webapp.Controller caches
> here — controller.xml
> >> +            // has not changed, only .class files have, and clearing
> those caches triggers
> >> +            // Groovy re-compilation of screen expressions which can
> fail unexpectedly.
> >> +            UtilCache.clearCache(SERVICE_MODEL_CACHE_NAME);
> >> +            Debug.logInfo("Hot-reload complete for: " + batch, MODULE);
> >> +        } catch (UnsupportedOperationException e) {
> >> +            // ./gradlew ofbizDev only ever runs on a DCEVM-patched
> JVM (see build.gradle),
> >> +            // which already lifts the plain-JVM restriction to method
> bodies only, so
> >> +            // add/remove method-or-field and signature changes
> normally succeed here. This
> >> +            // still fires for the narrower set of changes DCEVM
> itself can't apply either
> >> +            // (e.g. a changed class hierarchy) -- the same remaining
> limit an IDE debugger's
> >> +            // HotSwap has even on a capable JVM.
> >> +            Debug.logWarning("Hot-reload: " + batch + " contains a
> structural change (added/removed "
> >> +                    + "method or field, changed signature, changed
> hierarchy) that the JVM cannot "
> >> +                    + "hot-swap. Restart OFBiz to pick it up. (" +
> e.getMessage() + ")", MODULE);
> >> +        } catch (Throwable e) {
> >> +            Debug.logError(e, "Hot-reload failed for " + batch,
> MODULE);
> >> +        }
> >> +    }
> >> +
> >> +    /**
> >> +     * Best-effort detection of whether this JVM was launched with
> >> +     * {@code -XX:+AllowEnhancedClassRedefinition} (e.g. a JetBrains
> Runtime), which is
> >> +     * what allows {@link Instrumentation#redefineClasses} to also
> apply structural
> >> +     * changes instead of just method bodies. Purely informational —
> the actual
> >> +     * capability is exercised (and, if absent, reported) when a
> redefinition is
> >> +     * attempted in {@link #applyReload}.
> >> +     */
> >> +    private static boolean enhancedRedefinitionRequested() {
> >> +        return
> ManagementFactory.getRuntimeMXBean().getInputArguments().stream()
> >> +                .anyMatch(arg ->
> arg.contains("AllowEnhancedClassRedefinition"));
> >> +    }
> >> +
> >> +    /** Searches classes already loaded in the JVM for one matching
> {@code className}. */
> >> +    private Class<?> findLoadedClass(String className) {
> >> +        for (Class<?> c : instrumentation.getAllLoadedClasses()) {
> >> +            if (c.getName().equals(className)) {
> >> +                return c;
> >> +            }
> >> +        }
> >> +        return null;
> >> +    }
> >> +
> >> +    private void applyServiceXmlReload(Set<Path> batch) {
> >> +        Debug.logInfo("Hot-reload: service XML changed " + batch + " —
> clearing service model cache", MODULE);
> >> +        try {
> >> +            UtilCache.clearCache(SERVICE_MODEL_CACHE_NAME);
> >> +            Debug.logInfo("Hot-reload: service model cache cleared;
> definitions will be re-read on next service call", MODULE);
> >> +        } catch (Throwable e) {
> >> +            Debug.logError(e, "Hot-reload: failed to clear service
> model cache", MODULE);
> >> +        }
> >> +    }
> >> +
> >> +    private void applyCompile(Set<Path> batch) {
> >> +        Debug.logInfo("Hot-reload: compiling " + batch, MODULE);
> >> +        try {
> >> +            JavaCompiler compiler =
> ToolProvider.getSystemJavaCompiler();
> >> +            if (compiler == null) {
> >> +                return;
> >> +            }
> >> +            try (StandardJavaFileManager fm =
> compiler.getStandardFileManager(null, null, null)) {
> >> +                fm.setLocation(StandardLocation.CLASS_OUTPUT,
> >> + List.of(hotReloadOutputDir.toAbsolutePath().toFile()));
> >> +                // Reuse the running JVM's classpath — it already
> contains all OFBiz jars.
> >> +                List<String> options = Arrays.asList("-cp",
> System.getProperty("java.class.path"), "-proc:none");
> >> +                var units = fm.getJavaFileObjectsFromPaths(batch);
> >> +                boolean ok = compiler.getTask(null, fm, null, options,
> null, units).call();
> >> +                if (ok) {
> >> +                    Debug.logInfo("Hot-reload: compilation
> successful", MODULE);
> >> +
> >> +                    // Collect all .class files produced by this
> compilation round.
> >> +                    // Each source file can produce multiple .class
> files when it contains
> >> +                    // inner or anonymous classes (e.g. Foo$Bar.class,
> Foo$1.class).
> >> +                    // All of them must be redefined too — otherwise
> the inner class still
> >> +                    // resolves through its stale, previously-loaded
> bytecode.
> >> +                    for (Path src : batch) {
> >> +                        Path cf = sourceToClassFile(src); // relative
> path for the outer class
> >> +                        if (cf == null) {
> >> +                            continue;
> >> +                        }
> >> +                        String outerName =
> cf.getFileName().toString().replace(".class", "");
> >> +                        Path absOutputDir =
> cf.toAbsolutePath().getParent();
> >> +                        try (var dirStream = Files.list(absOutputDir))
> {
> >> +                            dirStream.filter(absFile -> {
> >> +                                String fn =
> absFile.getFileName().toString();
> >> +                                // Match Foo.class and Foo$Inner.class
> / Foo$1.class
> >> +                                return fn.endsWith(".class")
> >> +                                        && (fn.equals(outerName +
> ".class")
> >> +                                                ||
> fn.startsWith(outerName + "$"));
> >> +                            }).forEach(absFile -> {
> >> +                                // Convert absolute output path back
> to a relative path that
> >> +                                // is rooted at CWD (same type as
> hotReloadOutputDir) so that
> >> +                                // toClassName(hotReloadOutputDir,
> relPath) — which calls
> >> +                                // relativize — does not throw
> IllegalArgumentException.
> >> +                                Path rel = hotReloadOutputDir.resolve(
> >> + hotReloadOutputDir.toAbsolutePath().relativize(absFile));
> >> + reloadClassFile(hotReloadOutputDir, rel);
> >> +                            });
> >> +                        } catch (IOException e) {
> >> +                            // Output dir unreadable; fall back to the
> outer class only.
> >> +                            reloadClassFile(hotReloadOutputDir, cf);
> >> +                        }
> >> +                    }
> >> +
> >> +                    // Re-register class directories so external
> compilations (./gradlew classes
> >> +                    // run by a developer in a separate terminal)
> still reach the class watcher.
> >> +                    // Only relevant if that watch is enabled in the
> first place (watchBuildOutput);
> >> +                    // otherwise there is nothing registered under
> classesDir to refresh.
> >> +                    // Catch Exception (not just IOException) because
> ClosedWatchServiceException
> >> +                    // extends IllegalStateException, which is a
> RuntimeException — it can be
> >> +                    // thrown here if OFBiz is shutting down while a
> compile finishes.
> >> +                    if (watchBuildOutput) {
> >> +                        try {
> >> +                            registerAll(classesDir);
> >> +                        } catch (Exception e) {
> >> +                            Debug.logWarning("Hot-reload: could not
> re-register class dirs: " + e.getMessage(), MODULE);
> >> +                        }
> >> +                    }
> >> +                } else {
> >> +                    Debug.logWarning("Hot-reload: compilation failed —
> fix the error and save again", MODULE);
> >> +                }
> >> +            }
> >> +        } catch (Throwable e) {
> >> +            Debug.logError(e, "Hot-reload: compilation error", MODULE);
> >> +        }
> >> +    }
> >> +
> >> +    /**
> >> +     * Maps a {@code .java} source file to the corresponding {@code
> .class} output file
> >> +     * under {@link #hotReloadOutputDir}. Returns {@code null} if the
> source file is not
> >> +     * under any registered source root.
> >> +     */
> >> +    private Path sourceToClassFile(Path sourceFile) {
> >> +        for (Path srcRoot : sourceRootDirs) {
> >> +            if (sourceFile.startsWith(srcRoot)) {
> >> +                Path relative = srcRoot.relativize(sourceFile);
> >> +                String name = relative.toString();
> >> +                if (name.endsWith(".java")) {
> >> +                    String classRelative = name.substring(0,
> name.length() - ".java".length()) + ".class";
> >> +                    return hotReloadOutputDir.resolve(classRelative);
> >> +                }
> >> +            }
> >> +        }
> >> +        return null;
> >> +    }
> >> +
> >> +    //
> -------------------------------------------------------------------------
> >> +    // Helpers
> >> +    //
> -------------------------------------------------------------------------
> >> +
> >> +    /**
> >> +     * Recursively registers every directory under {@code start} with
> the WatchService.
> >> +     * A directory whose registration fails (e.g. watch/descriptor
> exhaustion) is logged
> >> +     * and left unwatched via {@link #warnUnwatched} instead of
> aborting the whole walk,
> >> +     * so one overloaded directory never leaves the rest of the tree
> unwatched.
> >> +     */
> >> +    private void registerAll(Path start) throws IOException {
> >> +        Files.walkFileTree(start, new SimpleFileVisitor<Path>() {
> >> +            @Override
> >> +            public FileVisitResult preVisitDirectory(Path dir,
> BasicFileAttributes attrs) {
> >> +                watchDirsAttempted++;
> >> +                try {
> >> +                    dir.register(watchService,
> >> + StandardWatchEventKinds.ENTRY_CREATE,
> >> + StandardWatchEventKinds.ENTRY_MODIFY);
> >> +                } catch (IOException e) {
> >> +                    warnUnwatched(dir, e);
> >> +                } catch (Throwable t) {
> >> +                    // register() itself should only throw
> IOException, but nothing here is
> >> +                    // worth crashing the whole startup over.
> >> +                    Debug.logError(t, "Hot-reload: unexpected error
> registering watch for " + dir
> >> +                            + " -- this directory will not be
> watched.", MODULE);
> >> +                }
> >> +                return FileVisitResult.CONTINUE;
> >> +            }
> >> +        });
> >> +    }
> >> +
> >> +    /**
> >> +     * Records that {@code dir} could not get a WatchService
> registration (most commonly
> >> +     * the OS's per-process watch ceiling, e.g. macOS's
> kqueue-per-directory cost) and
> >> +     * logs why. The directory is simply left unwatched: changes there
> require a restart
> >> +     * (or {@code -Dofbiz.hotreload.components}/{@code
> -Photreload.components} to narrow
> >> +     * the watched set below the ceiling) rather than falling back to
> some slower
> >> +     * alternate mechanism.
> >> +     */
> >> +    private void warnUnwatched(Path dir, IOException cause) {
> >> +        watchDirsFailed++;
> >> +        Debug.logWarning("Hot-reload: could not watch " + dir + " (" +
> cause.getMessage() + ") -- changes "
> >> +                + "there will not be picked up until OFBiz is
> restarted. Narrow the watched set with "
> >> +                + "-Dofbiz.hotreload.components=compA,compB (or
> -Photreload.components=compA,compB with the "
> >> +                + "ofbizDev Gradle task) to fit under the OS watch
> limit.", MODULE);
> >> +    }
> >> +
> >> +    /**
> >> +     * Converts a {@code .class} file path relative to {@code baseDir}
> into a
> >> +     * binary class name.
> >> +     *
> >> +     * <p>Example: {@code com/example/Foo.class} → {@code
> com.example.Foo}
> >> +     */
> >> +    private static String toClassName(Path baseDir, Path classFile) {
> >> +        Path relative = baseDir.relativize(classFile);
> >> +        String s = relative.toString();
> >> +        if (!s.endsWith(".class")) {
> >> +            return null;
> >> +        }
> >> +        // Normalise path separator to '.' and strip the '.class'
> suffix
> >> +        return s.substring(0, s.length() - ".class".length())
> >> + .replace(classFile.getFileSystem().getSeparator(), ".");
> >> +    }
> >> +}
> >> diff --git
> a/devreload/src/main/java/org/apache/ofbiz/devreload/HotSwapAgent.java
> >> b/devreload/src/main/java/org/apache/ofbiz/devreload/HotSwapAgent.java
> >> new file mode 100644
> >> index 000000000..f3542a5bf
> >> --- /dev/null
> >> +++
> b/devreload/src/main/java/org/apache/ofbiz/devreload/HotSwapAgent.java
> >> @@ -0,0 +1,109 @@
> >>
> +/*******************************************************************************
> >> + * Licensed to the Apache Software Foundation (ASF) under one
> >> + * or more contributor license agreements.  See the NOTICE file
> >> + * distributed with this work for additional information
> >> + * regarding copyright ownership.  The ASF licenses this file
> >> + * to you under the Apache License, Version 2.0 (the
> >> + * "License"); you may not use this file except in compliance
> >> + * with the License.  You may obtain a copy of the License at
> >> + *
> >> + *http://www.apache.org/licenses/LICENSE-2.0
> >> + *
> >> + * 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.
> >> +
> *******************************************************************************/
> >> +package org.apache.ofbiz.devreload;
> >> +
> >> +import java.io.IOException;
> >> +import java.io.InputStream;
> >> +import java.io.OutputStream;
> >> +import java.lang.instrument.Instrumentation;
> >> +import java.nio.file.Files;
> >> +import java.nio.file.Path;
> >> +import java.util.jar.Attributes;
> >> +import java.util.jar.JarOutputStream;
> >> +import java.util.jar.Manifest;
> >> +import java.util.zip.ZipEntry;
> >> +
> >> +import com.sun.tools.attach.VirtualMachine;
> >> +
> >> +/**
> >> + * Self-attaching Java agent that gives {@link DevReloadContainer}
> access to
> >> + * {@link Instrumentation#redefineClasses}, the same JVM mechanism an
> IDE debugger uses
> >> + * for HotSwap — it replaces the bytecode of an already-loaded {@link
> Class} object in
> >> + * place, so every existing reference to it (including cached {@code
> Class} objects in
> >> + * framework classes like {@code JavaEventHandler}) picks up the new
> method bodies on the
> >> + * very next call.
> >> + *
> >> + * <p>The agent jar is built on the fly from this class's own compiled
> bytecode and
> >> + * attached to the current process via the JDK Attach API — no {@code
> -javaagent} JVM
> >> + * flag, and no framework code changes. This class and {@link
> DevReloadContainer} are the
> >> + * only two moving parts.
> >> + *
> >> + * <p>Requires the JVM flag {@code -Djdk.attach.allowAttachSelf=true}
> (a JDK 9+ safeguard
> >> + * against a process attaching to itself). The {@code ofbizDev} Gradle
> task provided by
> >> + * this plugin sets it automatically.
> >> + */
> >> +public final class HotSwapAgent {
> >> +
> >> +    private static volatile Instrumentation instrumentation;
> >> +
> >> +    private HotSwapAgent() { }
> >> +
> >> +    /** Invoked by the JVM once {@link VirtualMachine#loadAgent} loads
> this class as an agent. */
> >> +    public static void agentmain(String agentArgs, Instrumentation
> inst) {
> >> +        instrumentation = inst;
> >> +    }
> >> +
> >> +    /**
> >> +     * Builds a minimal agent jar wrapping this class and
> self-attaches it to the current
> >> +     * JVM, returning the resulting {@link Instrumentation}. Safe to
> call more than once;
> >> +     * later calls return the instance obtained by the first
> successful attach.
> >> +     */
> >> +    static synchronized Instrumentation install() throws Exception {
> >> +        if (instrumentation != null) {
> >> +            return instrumentation;
> >> +        }
> >> +        Path agentJar = buildAgentJar();
> >> +        try {
> >> +            String pid = String.valueOf(ProcessHandle.current().pid());
> >> +            VirtualMachine vm = VirtualMachine.attach(pid);
> >> +            try {
> >> + vm.loadAgent(agentJar.toAbsolutePath().toString());
> >> +            } finally {
> >> +                vm.detach();
> >> +            }
> >> +        } finally {
> >> +            Files.deleteIfExists(agentJar);
> >> +        }
> >> +        return instrumentation;
> >> +    }
> >> +
> >> +    private static Path buildAgentJar() throws IOException {
> >> +        String resource = HotSwapAgent.class.getName().replace('.',
> '/') + ".class";
> >> +        Path jar = Files.createTempFile("devreload-hotswap-agent",
> ".jar");
> >> +
> >> +        Manifest manifest = new Manifest();
> >> +        Attributes attrs = manifest.getMainAttributes();
> >> +        attrs.put(Attributes.Name.MANIFEST_VERSION, "1.0");
> >> +        attrs.put(new Attributes.Name("Agent-Class"),
> HotSwapAgent.class.getName());
> >> +        attrs.put(new Attributes.Name("Can-Redefine-Classes"), "true");
> >> +        attrs.put(new Attributes.Name("Can-Retransform-Classes"),
> "true");
> >> +
> >> +        try (InputStream in =
> HotSwapAgent.class.getClassLoader().getResourceAsStream(resource);
> >> +                OutputStream fos = Files.newOutputStream(jar);
> >> +                JarOutputStream jos = new JarOutputStream(fos,
> manifest)) {
> >> +            if (in == null) {
> >> +                throw new IOException("Could not locate compiled class
> on classpath: " + resource);
> >> +            }
> >> +            jos.putNextEntry(new ZipEntry(resource));
> >> +            in.transferTo(jos);
> >> +            jos.closeEntry();
> >> +        }
> >> +        return jar;
> >> +    }
> >> +}
> >>
>
>

Reply via email to