This is an automated email from the ASF dual-hosted git repository.

jamesbognar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/juneau.git


The following commit(s) were added to refs/heads/master by this push:
     new b7b4b615b2 Shorts collection aliases, 
ChecksumUtils/ClasspathAssetCache extraction, Sonar+Eclipse cleanup
b7b4b615b2 is described below

commit b7b4b615b23cafc25157b0ee00d2310c2dffffcb
Author: James Bognar <[email protected]>
AuthorDate: Wed Aug 19 13:08:17 2026 -0400

    Shorts collection aliases, ChecksumUtils/ClasspathAssetCache extraction, 
Sonar+Eclipse cleanup
    
    - Add Shorts collection aliases (tl/ts/los/ist/im) and
      CollectionUtils.immutableSet/immutableMap
    - Extract ChecksumUtils and ClasspathAssetCache (de-dup asset caching logic
      shared by console-ui and views modules) with tests
    - Sonar and Eclipse-warning cleanup across juneau-rest-server-console-ui,
      juneau-rest-server-views, and CI workflow comments (keychain ->
      macos-keychain rename follow-ups, supply-chain pinning policy notes)
    - Add headless scripts/eclipse-warnings.py and minor todo-tracker script 
fixes
---
 .github/workflows/codeql-analysis.yml              |   6 +
 .github/workflows/maven.yml                        |   6 +
 .github/workflows/sonarcloud.yml                   |   6 +
 .../apache/juneau/commons/utils/ChecksumUtils.java |  45 ++++
 .../juneau/commons/utils/CollectionUtils.java      |  14 ++
 .../org/apache/juneau/commons/utils/Shorts.java    | 235 +++++++++++++++++++++
 .../juneau/commons/utils/ChecksumUtils_Test.java   |  61 ++++++
 .../juneau/commons/utils/CollectionUtils_Test.java |  33 +++
 .../apache/juneau/commons/utils/Shorts_Test.java   |  78 ++++++-
 .../rest/server/console/ConsoleChromeMixin.java    |  77 ++-----
 .../juneau/rest/server/views/ViewsMixin.java       |  65 +-----
 .../rest/server/views/ViewsMixin_Serving_Test.java |  19 ++
 .../rest/server/util/ClasspathAssetCache.java      | 160 ++++++++++++++
 .../rest/server/util/ClasspathAssetCache_Test.java | 185 ++++++++++++++++
 scripts/eclipse-warnings.py                        | 204 ++++++++++++++++++
 scripts/todo-next-id.py                            |   4 +-
 scripts/todo-status-audit.py                       |  30 ++-
 17 files changed, 1100 insertions(+), 128 deletions(-)

diff --git a/.github/workflows/codeql-analysis.yml 
b/.github/workflows/codeql-analysis.yml
index dd46600ef8..31d6824bb2 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -14,6 +14,12 @@
 # * specific language governing permissions and limitations under the License. 
                                             *
 # 
***************************************************************************************************************************
 #
+# Supply-chain pinning policy: every third-party Action below is pinned to a 
full commit SHA,
+# not a floating tag, because a tag is mutable and a SHA pin is what makes 
"what code ran in CI"
+# part of this project's provenance story. To bump: resolve the new tag to its 
full commit SHA
+# and update the trailing version comment to match. Same policy applies to 
maven.yml and
+# sonarcloud.yml.
+#
 name: "CodeQL"
 
 on:
diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml
index 6cab9b15f7..781d9c9533 100644
--- a/.github/workflows/maven.yml
+++ b/.github/workflows/maven.yml
@@ -12,6 +12,12 @@
 # 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.
+#
+# Supply-chain pinning policy: every third-party Action below is pinned to a 
full commit SHA,
+# not a floating tag, because a tag is mutable and a SHA pin is what makes 
"what code ran in CI"
+# part of this project's provenance story. To bump: resolve the new tag to its 
full commit SHA
+# and update the trailing "# vX.Y.Z" comment to match. Same policy applies to 
codeql-analysis.yml
+# and sonarcloud.yml.
 
 name: Java CI
 
diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml
index 55368ab86e..c3131c4c64 100644
--- a/.github/workflows/sonarcloud.yml
+++ b/.github/workflows/sonarcloud.yml
@@ -12,6 +12,12 @@
 # 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.
+#
+# Supply-chain pinning policy: every third-party Action below is pinned to a 
full commit SHA,
+# not a floating tag, because a tag is mutable and a SHA pin is what makes 
"what code ran in CI"
+# part of this project's provenance story. To bump: resolve the new tag to its 
full commit SHA
+# and update the trailing "# vX.Y.Z" comment to match. Same policy applies to 
maven.yml and
+# codeql-analysis.yml.
 name: SonarQube
 
 on:
diff --git 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/utils/ChecksumUtils.java
 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/utils/ChecksumUtils.java
new file mode 100644
index 0000000000..558feef951
--- /dev/null
+++ 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/utils/ChecksumUtils.java
@@ -0,0 +1,45 @@
+/*
+ * 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.juneau.commons.utils;
+
+import java.util.zip.*;
+
+/**
+ * Content-checksum utility methods.
+ */
+public class ChecksumUtils {
+
+       /** Prevents instantiation. */
+       private ChecksumUtils() {}
+
+       /**
+        * Formats a CRC32 checksum of the specified bytes as a zero-padded, 
lower-case, 8-hex-char content hash.
+        *
+        * <p>
+        * Suitable as a compact, content-sensitive cache-buster for 
classpath-shipped assets (JS/CSS/images, etc.):
+        * the hash changes the instant the underlying bytes do, without 
relying on a build/version string alone
+        * (which stays stable across {@code -SNAPSHOT} dev rebuilds and so 
cannot bust a browser's cache on its own).
+        *
+        * @param bytes The bytes to checksum.  Must not be <jk>null</jk>.
+        * @return The zero-padded 8-hex-char CRC32 checksum (e.g. {@code 
"1a2b3c4d"}).
+        */
+       public static String hash8(byte[] bytes) {
+               var crc = new CRC32();
+               crc.update(bytes);
+               return String.format("%08x", crc.getValue());
+       }
+}
diff --git 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/utils/CollectionUtils.java
 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/utils/CollectionUtils.java
index 14d1d42cb8..e9b2681730 100644
--- 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/utils/CollectionUtils.java
+++ 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/utils/CollectionUtils.java
@@ -1016,6 +1016,20 @@ public class CollectionUtils {
                return new SimpleMap<>(array(k1, k2, k3, k4, k5, k6, k7, k8, 
k9, k10), array(v1, v2, v3, v4, v5, v6, v7, v8, v9, v10));
        }
 
+       /**
+        * Convenience method for creating an unmodifiable set.
+        * Unlike Set.of(...), supports a null element and preserves insertion 
order.
+        *
+        * @param <E> The element type.
+        * @param values The values to initialize the set with.  Must not be 
<jk>null</jk> (a <jk>null</jk> array throws {@link IllegalArgumentException}).
+        * @return A new unmodifiable set.
+        */
+       @SafeVarargs
+       public static <E> Set<E> immutableSet(E...values) {
+               assertArgNotNull(ARG_values, values);
+               return Collections.unmodifiableSet(new 
LinkedHashSet<>(Arrays.asList(values)));
+       }
+
        /**
         * Convenience method for creating a {@link LinkedHashMap}.
         *
diff --git 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/utils/Shorts.java
 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/utils/Shorts.java
index a81633282b..5698b08af7 100644
--- 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/utils/Shorts.java
+++ 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/utils/Shorts.java
@@ -1045,6 +1045,241 @@ public class Shorts {
        @SafeVarargs
        public static <E> SortedSet<E> ss(E...v) { return 
CollectionUtils.sortedSet(v); }
 
+       /**
+        * Creates a modifiable {@link ArrayList} copy of the specified 
collection.
+        *
+        * @param <E> The element type.
+        * @param v The collection to copy from.  Can be <jk>null</jk> (returns 
an empty list, not <jk>null</jk>).
+        * @return A new modifiable list.
+        * @see CollectionUtils#toList(Collection)
+        */
+       public static <E> List<E> tl(Collection<E> v) { return 
CollectionUtils.toList(v); }
+
+       /**
+        * Creates a modifiable {@link LinkedHashSet} copy of the specified 
collection.
+        *
+        * @param <E> The element type.
+        * @param v The collection to copy from.  Can be <jk>null</jk> (returns 
<jk>null</jk>).
+        * @return A new modifiable set, or <jk>null</jk> if the input was 
<jk>null</jk>.
+        * @see CollectionUtils#toSet(Collection)
+        */
+       public static <E> Set<E> ts(Collection<E> v) { return 
CollectionUtils.toSet(v); }
+
+       /**
+        * Creates a modifiable, empty {@link ArrayList} presized to the 
specified capacity.
+        *
+        * @param <E> The element type.
+        * @param n The initial capacity.
+        * @return A new modifiable, empty list.
+        * @see CollectionUtils#listOfSize(int)
+        */
+       public static <E> List<E> los(int n) { return 
CollectionUtils.listOfSize(n); }
+
+       /**
+        * Creates an unmodifiable set from the specified elements.  Unlike 
{@link Set#of(Object...)},
+        * permits a <jk>null</jk> element and preserves insertion order.
+        *
+        * @param <E> The element type.
+        * @param v The elements.  Must not be <jk>null</jk> (a <jk>null</jk> 
array throws {@link IllegalArgumentException}).
+        * @return A new unmodifiable set.
+        * @see CollectionUtils#immutableSet(Object...)
+        */
+       @SafeVarargs
+       public static <E> Set<E> ist(E...v) { return 
CollectionUtils.immutableSet(v); }
+
+       /**
+        * Creates an unmodifiable, empty map.
+        *
+        * @param <K> The key type.
+        * @param <V> The value type.
+        * @return A new unmodifiable empty map.
+        * @see CollectionUtils#immutableMap()
+        */
+       public static <K,V> Map<K,V> im() { return 
CollectionUtils.immutableMap(); }
+
+       /**
+        * Creates an unmodifiable map with 1 entry.  Unlike {@link 
Map#of(Object,Object)}, permits a
+        * <jk>null</jk> key or value and preserves insertion order.  Mutating 
the returned map (e.g.
+        * <jk>put</jk>/<jk>remove</jk>/<jk>clear</jk>) throws {@link 
UnsupportedOperationException}.
+        *
+        * @param <K> The key type.
+        * @param <V> The value type.
+        * @param k1 The key.
+        * @param v1 The value.
+        * @return A new unmodifiable map.
+        * @see CollectionUtils#immutableMap(Object,Object)
+        */
+       public static <K,V> Map<K,V> im(K k1, V v1) { return 
CollectionUtils.immutableMap(k1, v1); }
+
+       /**
+        * Creates an unmodifiable map with 2 entries.  Unlike {@link 
Map#of(Object,Object,Object,Object)}, permits
+        * <jk>null</jk> keys/values and preserves insertion order.  Mutating 
the returned map (e.g.
+        * <jk>put</jk>/<jk>remove</jk>/<jk>clear</jk>) throws {@link 
UnsupportedOperationException}.
+        *
+        * @param <K> The key type.
+        * @param <V> The value type.
+        * @param k1 The first key.
+        * @param v1 The first value.
+        * @param k2 The second key.
+        * @param v2 The second value.
+        * @return A new unmodifiable map.
+        * @throws IllegalArgumentException if <jv>k1</jv> and <jv>k2</jv> are 
duplicate keys.
+        * @see CollectionUtils#immutableMap(Object,Object,Object,Object)
+        */
+       public static <K,V> Map<K,V> im(K k1, V v1, K k2, V v2) { return 
CollectionUtils.immutableMap(k1, v1, k2, v2); }
+
+       /**
+        * Creates an unmodifiable map with 3 entries.  Unlike {@link 
Map#of(Object,Object,Object,Object,Object,Object)},
+        * permits <jk>null</jk> keys/values and preserves insertion order.  
Mutating the returned map (e.g.
+        * <jk>put</jk>/<jk>remove</jk>/<jk>clear</jk>) throws {@link 
UnsupportedOperationException}.
+        *
+        * @param <K> The key type.
+        * @param <V> The value type.
+        * @param k1 The first key.
+        * @param v1 The first value.
+        * @param k2 The second key.
+        * @param v2 The second value.
+        * @param k3 The third key.
+        * @param v3 The third value.
+        * @return A new unmodifiable map.
+        * @throws IllegalArgumentException if any of the keys are duplicated.
+        * @see 
CollectionUtils#immutableMap(Object,Object,Object,Object,Object,Object)
+        */
+       public static <K,V> Map<K,V> im(K k1, V v1, K k2, V v2, K k3, V v3) { 
return CollectionUtils.immutableMap(k1, v1, k2, v2, k3, v3); }
+
+       /**
+        * Creates an unmodifiable map with 4 entries (keys/values in 
alternating order).  Unlike
+        * {@link 
Map#of(Object,Object,Object,Object,Object,Object,Object,Object)}, permits 
<jk>null</jk> keys/values
+        * and preserves insertion order.  Mutating the returned map (e.g. 
<jk>put</jk>/<jk>remove</jk>/<jk>clear</jk>)
+        * throws {@link UnsupportedOperationException}.
+        *
+        * @param <K> The key type.
+        * @param <V> The value type.
+        * @param k1 Key 1. @param v1 Value 1. @param k2 Key 2. @param v2 Value 
2.
+        * @param k3 Key 3. @param v3 Value 3. @param k4 Key 4. @param v4 Value 
4.
+        * @return A new unmodifiable map.
+        * @throws IllegalArgumentException if any of the keys are duplicated.
+        * @see 
CollectionUtils#immutableMap(Object,Object,Object,Object,Object,Object,Object,Object)
+        */
+       @SuppressWarnings({
+               "java:S107" // Fixed-arity terse map-factory overload; the many 
parameters are intentional alternating key/value pairs mirroring map entries.
+       })
+       public static <K,V> Map<K,V> im(K k1, V v1, K k2, V v2, K k3, V v3, K 
k4, V v4) { return CollectionUtils.immutableMap(k1, v1, k2, v2, k3, v3, k4, 
v4); }
+
+       /**
+        * Creates an unmodifiable map with 5 entries (keys/values in 
alternating order).  Unlike
+        * {@link 
Map#of(Object,Object,Object,Object,Object,Object,Object,Object,Object,Object)}, 
permits <jk>null</jk>
+        * keys/values and preserves insertion order.  Mutating the returned 
map (e.g.
+        * <jk>put</jk>/<jk>remove</jk>/<jk>clear</jk>) throws {@link 
UnsupportedOperationException}.
+        *
+        * @param <K> The key type.
+        * @param <V> The value type.
+        * @param k1 Key 1. @param v1 Value 1. @param k2 Key 2. @param v2 Value 
2. @param k3 Key 3. @param v3 Value 3.
+        * @param k4 Key 4. @param v4 Value 4. @param k5 Key 5. @param v5 Value 
5.
+        * @return A new unmodifiable map.
+        * @throws IllegalArgumentException if any of the keys are duplicated.
+        * @see 
CollectionUtils#immutableMap(Object,Object,Object,Object,Object,Object,Object,Object,Object,Object)
+        */
+       @SuppressWarnings({
+               "java:S107" // Fixed-arity terse map-factory overload; the many 
parameters are intentional alternating key/value pairs mirroring map entries.
+       })
+       public static <K,V> Map<K,V> im(K k1, V v1, K k2, V v2, K k3, V v3, K 
k4, V v4, K k5, V v5) { return CollectionUtils.immutableMap(k1, v1, k2, v2, k3, 
v3, k4, v4, k5, v5); }
+
+       /**
+        * Creates an unmodifiable map with 6 entries (keys/values in 
alternating order).  Unlike
+        * {@link 
Map#of(Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object)},
 permits
+        * <jk>null</jk> keys/values and preserves insertion order.  Mutating 
the returned map (e.g.
+        * <jk>put</jk>/<jk>remove</jk>/<jk>clear</jk>) throws {@link 
UnsupportedOperationException}.
+        *
+        * @param <K> The key type.
+        * @param <V> The value type.
+        * @param k1 Key 1. @param v1 Value 1. @param k2 Key 2. @param v2 Value 
2. @param k3 Key 3. @param v3 Value 3.
+        * @param k4 Key 4. @param v4 Value 4. @param k5 Key 5. @param v5 Value 
5. @param k6 Key 6. @param v6 Value 6.
+        * @return A new unmodifiable map.
+        * @throws IllegalArgumentException if any of the keys are duplicated.
+        * @see 
CollectionUtils#immutableMap(Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object)
+        */
+       @SuppressWarnings({
+               "java:S107" // Fixed-arity terse map-factory overload; the many 
parameters are intentional alternating key/value pairs mirroring map entries.
+       })
+       public static <K,V> Map<K,V> im(K k1, V v1, K k2, V v2, K k3, V v3, K 
k4, V v4, K k5, V v5, K k6, V v6) { return CollectionUtils.immutableMap(k1, v1, 
k2, v2, k3, v3, k4, v4, k5, v5, k6, v6); }
+
+       /**
+        * Creates an unmodifiable map with 7 entries (keys/values in 
alternating order).  Unlike
+        * {@link 
Map#of(Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object)},
+        * permits <jk>null</jk> keys/values and preserves insertion order.  
Mutating the returned map (e.g.
+        * <jk>put</jk>/<jk>remove</jk>/<jk>clear</jk>) throws {@link 
UnsupportedOperationException}.
+        *
+        * @param <K> The key type.
+        * @param <V> The value type.
+        * @param k1 Key 1. @param v1 Value 1. @param k2 Key 2. @param v2 Value 
2. @param k3 Key 3. @param v3 Value 3. @param k4 Key 4. @param v4 Value 4.
+        * @param k5 Key 5. @param v5 Value 5. @param k6 Key 6. @param v6 Value 
6. @param k7 Key 7. @param v7 Value 7.
+        * @return A new unmodifiable map.
+        * @throws IllegalArgumentException if any of the keys are duplicated.
+        * @see 
CollectionUtils#immutableMap(Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object)
+        */
+       @SuppressWarnings({
+               "java:S107" // Fixed-arity terse map-factory overload; the many 
parameters are intentional alternating key/value pairs mirroring map entries.
+       })
+       public static <K,V> Map<K,V> im(K k1, V v1, K k2, V v2, K k3, V v3, K 
k4, V v4, K k5, V v5, K k6, V v6, K k7, V v7) { return 
CollectionUtils.immutableMap(k1, v1, k2, v2, k3, v3, k4, v4, k5, v5, k6, v6, 
k7, v7); }
+
+       /**
+        * Creates an unmodifiable map with 8 entries (keys/values in 
alternating order).  Unlike
+        * {@link 
Map#of(Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object)},
+        * permits <jk>null</jk> keys/values and preserves insertion order.  
Mutating the returned map (e.g.
+        * <jk>put</jk>/<jk>remove</jk>/<jk>clear</jk>) throws {@link 
UnsupportedOperationException}.
+        *
+        * @param <K> The key type.
+        * @param <V> The value type.
+        * @param k1 Key 1. @param v1 Value 1. @param k2 Key 2. @param v2 Value 
2. @param k3 Key 3. @param v3 Value 3. @param k4 Key 4. @param v4 Value 4.
+        * @param k5 Key 5. @param v5 Value 5. @param k6 Key 6. @param v6 Value 
6. @param k7 Key 7. @param v7 Value 7. @param k8 Key 8. @param v8 Value 8.
+        * @return A new unmodifiable map.
+        * @throws IllegalArgumentException if any of the keys are duplicated.
+        * @see 
CollectionUtils#immutableMap(Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object)
+        */
+       @SuppressWarnings({
+               "java:S107" // Fixed-arity terse map-factory overload; the many 
parameters are intentional alternating key/value pairs mirroring map entries.
+       })
+       public static <K,V> Map<K,V> im(K k1, V v1, K k2, V v2, K k3, V v3, K 
k4, V v4, K k5, V v5, K k6, V v6, K k7, V v7, K k8, V v8) { return 
CollectionUtils.immutableMap(k1, v1, k2, v2, k3, v3, k4, v4, k5, v5, k6, v6, 
k7, v7, k8, v8); }
+
+       /**
+        * Creates an unmodifiable map with 9 entries (keys/values in 
alternating order).  Unlike
+        * {@link 
Map#of(Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object)},
+        * permits <jk>null</jk> keys/values and preserves insertion order.  
Mutating the returned map (e.g.
+        * <jk>put</jk>/<jk>remove</jk>/<jk>clear</jk>) throws {@link 
UnsupportedOperationException}.
+        *
+        * @param <K> The key type.
+        * @param <V> The value type.
+        * @param k1 Key 1. @param v1 Value 1. @param k2 Key 2. @param v2 Value 
2. @param k3 Key 3. @param v3 Value 3. @param k4 Key 4. @param v4 Value 4. 
@param k5 Key 5. @param v5 Value 5.
+        * @param k6 Key 6. @param v6 Value 6. @param k7 Key 7. @param v7 Value 
7. @param k8 Key 8. @param v8 Value 8. @param k9 Key 9. @param v9 Value 9.
+        * @return A new unmodifiable map.
+        * @throws IllegalArgumentException if any of the keys are duplicated.
+        * @see 
CollectionUtils#immutableMap(Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object)
+        */
+       @SuppressWarnings({
+               "java:S107" // Fixed-arity terse map-factory overload; the many 
parameters are intentional alternating key/value pairs mirroring map entries.
+       })
+       public static <K,V> Map<K,V> im(K k1, V v1, K k2, V v2, K k3, V v3, K 
k4, V v4, K k5, V v5, K k6, V v6, K k7, V v7, K k8, V v8, K k9, V v9) { return 
CollectionUtils.immutableMap(k1, v1, k2, v2, k3, v3, k4, v4, k5, v5, k6, v6, 
k7, v7, k8, v8, k9, v9); }
+
+       /**
+        * Creates an unmodifiable map with 10 entries (keys/values in 
alternating order).  Unlike
+        * {@link 
Map#of(Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object)},
+        * permits <jk>null</jk> keys/values and preserves insertion order.  
Mutating the returned map (e.g.
+        * <jk>put</jk>/<jk>remove</jk>/<jk>clear</jk>) throws {@link 
UnsupportedOperationException}.
+        *
+        * @param <K> The key type.
+        * @param <V> The value type.
+        * @param k1 Key 1. @param v1 Value 1. @param k2 Key 2. @param v2 Value 
2. @param k3 Key 3. @param v3 Value 3. @param k4 Key 4. @param v4 Value 4. 
@param k5 Key 5. @param v5 Value 5.
+        * @param k6 Key 6. @param v6 Value 6. @param k7 Key 7. @param v7 Value 
7. @param k8 Key 8. @param v8 Value 8. @param k9 Key 9. @param v9 Value 9. 
@param k10 Key 10. @param v10 Value 10.
+        * @return A new unmodifiable map.
+        * @throws IllegalArgumentException if any of the keys are duplicated.
+        * @see 
CollectionUtils#immutableMap(Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object)
+        */
+       @SuppressWarnings({
+               "java:S107" // Fixed-arity terse map-factory overload; the many 
parameters are intentional alternating key/value pairs mirroring map entries.
+       })
+       public static <K,V> Map<K,V> im(K k1, V v1, K k2, V v2, K k3, V v3, K 
k4, V v4, K k5, V v5, K k6, V v6, K k7, V v7, K k8, V v8, K k9, V v9, K k10, V 
v10) { return CollectionUtils.immutableMap(k1, v1, k2, v2, k3, v3, k4, v4, k5, 
v5, k6, v6, k7, v7, k8, v8, k9, v9, k10, v10); }
+
        /**
         * Returns an unmodifiable view of the specified list.
         *
diff --git 
a/juneau-core/juneau-commons/src/test/java/org/apache/juneau/commons/utils/ChecksumUtils_Test.java
 
b/juneau-core/juneau-commons/src/test/java/org/apache/juneau/commons/utils/ChecksumUtils_Test.java
new file mode 100644
index 0000000000..ae05a7b79e
--- /dev/null
+++ 
b/juneau-core/juneau-commons/src/test/java/org/apache/juneau/commons/utils/ChecksumUtils_Test.java
@@ -0,0 +1,61 @@
+/*
+ * 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.juneau.commons.utils;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.nio.charset.*;
+import java.util.zip.*;
+
+import org.apache.juneau.commons.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Tests {@link ChecksumUtils}.
+ */
+class ChecksumUtils_Test extends TestBase {
+
+       @Test void a01_hash8_emptyInput_isAllZeros() {
+               // CRC32 of zero bytes is 0, so the zero-padded hex form is all 
zeros.
+               assertEquals("00000000", ChecksumUtils.hash8(new byte[0]));
+       }
+
+       @Test void a02_hash8_knownVector_matchesJavaUtilZipCrc32() {
+               var bytes = "The quick brown fox jumps over the lazy 
dog".getBytes(StandardCharsets.UTF_8);
+               var crc = new CRC32();
+               crc.update(bytes);
+               assertEquals(String.format("%08x", crc.getValue()), 
ChecksumUtils.hash8(bytes));
+               // Widely-published CRC32 test vector for this exact string - 
pins the value, not just the delegation.
+               assertEquals("414fa339", ChecksumUtils.hash8(bytes));
+       }
+
+       @Test void a03_hash8_leadingZeroPadding_isNotTruncated() {
+               // CRC32("62") == 0x0012d20a - fewer than 8 significant hex 
digits, so this pins that hash8 zero-pads
+               // rather than truncates (a raw 
Long.toHexString(...)/Integer.toHexString(...) would drop the leading
+               // zeros and return a shorter string).
+               assertEquals("0012d20a", 
ChecksumUtils.hash8("62".getBytes(StandardCharsets.UTF_8)));
+       }
+
+       @Test void a04_hash8_distinctInputs_produceDifferentHashes() {
+               
assertNotEquals(ChecksumUtils.hash8("a".getBytes(StandardCharsets.UTF_8)), 
ChecksumUtils.hash8("b".getBytes(StandardCharsets.UTF_8)));
+       }
+
+       @Test void a05_hash8_sameInput_isDeterministic() {
+               var bytes = "deterministic".getBytes(StandardCharsets.UTF_8);
+               assertEquals(ChecksumUtils.hash8(bytes), 
ChecksumUtils.hash8(bytes));
+       }
+}
diff --git 
a/juneau-core/juneau-commons/src/test/java/org/apache/juneau/commons/utils/CollectionUtils_Test.java
 
b/juneau-core/juneau-commons/src/test/java/org/apache/juneau/commons/utils/CollectionUtils_Test.java
index 9bdd1fc9b8..6d12cd46c9 100644
--- 
a/juneau-core/juneau-commons/src/test/java/org/apache/juneau/commons/utils/CollectionUtils_Test.java
+++ 
b/juneau-core/juneau-commons/src/test/java/org/apache/juneau/commons/utils/CollectionUtils_Test.java
@@ -1750,4 +1750,37 @@ class CollectionUtils_Test extends TestBase {
                List<Object> deepNested = deepStream((Object[]) 
nested).toList();
                assertEquals(list("a", "b", "c"), deepNested); // fully 
flattened
        }
+
+       
//====================================================================================================
+       // immutableSet(E...)
+       
//====================================================================================================
+       @Test
+       void a102_immutableSet() {
+               // Empty
+               Set<String> empty = immutableSet();
+               assertNotNull(empty);
+               assertTrue(empty.isEmpty());
+
+               // Multiple elements, insertion order preserved
+               Set<String> s1 = immutableSet("z", "a", "m");
+               assertEquals(3, s1.size());
+               assertEquals(list("z", "a", "m"), new ArrayList<>(s1));
+
+               // Null element permitted (unlike Set.of)
+               Set<String> withNull = immutableSet("a", null, "b");
+               assertEquals(3, withNull.size());
+               assertTrue(withNull.contains(null));
+
+               // Duplicate elements de-duplicated like a normal Set
+               Set<String> deduped = immutableSet("a", "b", "a");
+               assertEquals(2, deduped.size());
+
+               // Null array rejected
+               assertThrows(IllegalArgumentException.class, () -> 
immutableSet((String[]) null));
+
+               // Immutability
+               assertThrows(UnsupportedOperationException.class, () -> 
s1.add("x"));
+               assertThrows(UnsupportedOperationException.class, () -> 
s1.remove("z"));
+               assertThrows(UnsupportedOperationException.class, s1::clear);
+       }
 }
diff --git 
a/juneau-core/juneau-commons/src/test/java/org/apache/juneau/commons/utils/Shorts_Test.java
 
b/juneau-core/juneau-commons/src/test/java/org/apache/juneau/commons/utils/Shorts_Test.java
index b3ad163f56..7c870c2145 100644
--- 
a/juneau-core/juneau-commons/src/test/java/org/apache/juneau/commons/utils/Shorts_Test.java
+++ 
b/juneau-core/juneau-commons/src/test/java/org/apache/juneau/commons/utils/Shorts_Test.java
@@ -227,7 +227,83 @@ class Shorts_Test extends TestBase {
        void c008_ints_intArray() { assertArrayEquals(new int[]{1, 2}, ints(1, 
2)); }
 
        @Test
-       void c009_tl_toList() { assertEquals(List.of("a"), 
CollectionUtils.toList(Set.of("a"))); }
+       void c009_tl_toList() { assertEquals(List.of("a"), tl(Set.of("a"))); }
+
+       @Test
+       void c010_tl_toList_nullReturnsEmpty() { assertEquals(List.of(), 
tl(null)); }
+
+       @Test
+       void c011_ts_toSet() { assertEquals(Set.of("a"), ts(List.of("a", 
"a"))); }
+
+       @Test
+       void c012_ts_toSet_nullReturnsNull() { assertNull(ts(null)); }
+
+       @Test
+       void c013_los_listOfSize() {
+               List<String> l = los(5);
+               assertTrue(l.isEmpty());
+               l.add("x");
+               assertEquals(List.of("x"), l);
+       }
+
+       @Test
+       void c014_ist_immutableSet() {
+               Set<String> s = ist("a", null, "b");
+               assertEquals(3, s.size());
+               assertTrue(s.contains(null));
+               assertThrows(UnsupportedOperationException.class, () -> 
s.add("c"));
+       }
+
+       @Test
+       void c015_im_immutableMap_empty() {
+               Map<String,Integer> m = im();
+               assertTrue(m.isEmpty());
+               assertThrows(UnsupportedOperationException.class, () -> 
m.put("x", 1));
+       }
+
+       @Test
+       void c016_im_immutableMap_insertionOrder() {
+               Map<String,Integer> m = im("z", 1, "a", 2, "m", 3, "b", 4);
+               assertEquals(List.of("z", "a", "m", "b"), new 
ArrayList<>(m.keySet()));
+       }
+
+       @Test
+       void c017_im_immutableMap_nullKeyAndValue() {
+               Map<String,Integer> nullKey = im(null, 1);
+               assertEquals(1, nullKey.get(null));
+               assertTrue(nullKey.containsKey(null));
+
+               Map<String,Integer> nullValue = im("a", null);
+               assertNull(nullValue.get("a"));
+               assertTrue(nullValue.containsKey("a"));
+       }
+
+       @Test
+       void c018_im_immutableMap_duplicateKeyThrows() {
+               assertThrows(IllegalArgumentException.class, () -> im("a", 1, 
"b", 2, "a", 3));
+       }
+
+       @Test
+       void c019_im_immutableMap_immutability() {
+               Map<String,Integer> m = im("a", 1, "b", 2);
+               assertThrows(UnsupportedOperationException.class, () -> 
m.put("c", 3));
+               assertThrows(UnsupportedOperationException.class, () -> 
m.remove("a"));
+               assertThrows(UnsupportedOperationException.class, m::clear);
+       }
+
+       @Test
+       void c020_im_immutableMap_allArities() {
+               assertEquals(1, im("a", 1).size());
+               assertEquals(2, im("a", 1, "b", 2).size());
+               assertEquals(3, im("a", 1, "b", 2, "c", 3).size());
+               assertEquals(4, im("a", 1, "b", 2, "c", 3, "d", 4).size());
+               assertEquals(5, im("a", 1, "b", 2, "c", 3, "d", 4, "e", 
5).size());
+               assertEquals(6, im("a", 1, "b", 2, "c", 3, "d", 4, "e", 5, "f", 
6).size());
+               assertEquals(7, im("a", 1, "b", 2, "c", 3, "d", 4, "e", 5, "f", 
6, "g", 7).size());
+               assertEquals(8, im("a", 1, "b", 2, "c", 3, "d", 4, "e", 5, "f", 
6, "g", 7, "h", 8).size());
+               assertEquals(9, im("a", 1, "b", 2, "c", 3, "d", 4, "e", 5, "f", 
6, "g", 7, "h", 8, "i", 9).size());
+               assertEquals(10, im("a", 1, "b", 2, "c", 3, "d", 4, "e", 5, 
"f", 6, "g", 7, "h", 8, "i", 9, "j", 10).size());
+       }
 
        // ---- ClassUtils aliases ----
 
diff --git 
a/juneau-rest/juneau-rest-server-console-ui/src/main/java/org/apache/juneau/rest/server/console/ConsoleChromeMixin.java
 
b/juneau-rest/juneau-rest-server-console-ui/src/main/java/org/apache/juneau/rest/server/console/ConsoleChromeMixin.java
index 2ae17522f3..a913a84473 100644
--- 
a/juneau-rest/juneau-rest-server-console-ui/src/main/java/org/apache/juneau/rest/server/console/ConsoleChromeMixin.java
+++ 
b/juneau-rest/juneau-rest-server-console-ui/src/main/java/org/apache/juneau/rest/server/console/ConsoleChromeMixin.java
@@ -23,16 +23,13 @@ import java.nio.charset.*;
 import java.util.*;
 import java.util.concurrent.*;
 import java.util.concurrent.atomic.*;
-import java.util.zip.*;
 
 import org.apache.juneau.commons.io.*;
 import org.apache.juneau.commons.utils.*;
 import org.apache.juneau.http.*;
-import org.apache.juneau.http.entity.*;
-import org.apache.juneau.http.header.*;
-import org.apache.juneau.http.resource.*;
 import org.apache.juneau.http.response.*;
 import org.apache.juneau.rest.server.*;
+import org.apache.juneau.rest.server.util.*;
 
 /**
  * Mixin that serves the admin-console chrome stylesheet at {@code 
/juneau-console/chrome.css}.
@@ -162,8 +159,12 @@ public class ConsoleChromeMixin {
        /** The shipped static chrome.css bytes, read once from the classpath 
(shared - the static file never varies by theme). */
        private static volatile String staticCss;
 
-       /** Per-resource content-hash cache for the configured 
logo/page-background assets (populated on first request). */
-       private static final Map<String,String> ASSET_HASH_CACHE = new 
ConcurrentHashMap<>();
+       /**
+        * Read+cache+hash+serve helper for the configured logo/page-background 
assets, anchored on this class so
+        * {@link ClasspathAssetCache#buildVersion()} resolves this module's 
own implementation version (see that
+        * class's javadoc's version-anchor section).
+        */
+       private static final ClasspathAssetCache ASSET_CACHE = new 
ClasspathAssetCache(ConsoleChromeMixin.class);
 
        private final boolean cacheAssets;
        private final Theme theme;
@@ -221,7 +222,7 @@ public class ConsoleChromeMixin {
        )
        public HttpResource getChromeCss(RestRequest req) throws IOException {
                var body = cacheAssets ? cachedBody(req) : buildBody(req);
-               return httpResource(body, CONTENT_TYPE);
+               return ASSET_CACHE.wrap(body, CONTENT_TYPE, CACHE_CONTROL);
        }
 
        /**
@@ -238,7 +239,7 @@ public class ConsoleChromeMixin {
        public HttpResource getLogoAsset() throws IOException {
                if (logoResource == null)
                        throw new NotFound("No logo asset configured.");
-               return serveAsset(logoResource);
+               return ASSET_CACHE.serve(logoResource, 
MimeTypeDetector.DEFAULT.getContentType(logoResource), CACHE_CONTROL);
        }
 
        /**
@@ -255,24 +256,7 @@ public class ConsoleChromeMixin {
        public HttpResource getPageBackgroundAsset() throws IOException {
                if (pageBackgroundResource == null)
                        throw new NotFound("No page-background asset 
configured.");
-               return serveAsset(pageBackgroundResource);
-       }
-
-       /** Reads and wraps a validated, already-configured classpath resource 
as a cacheable {@link HttpResource}. */
-       private static HttpResource serveAsset(String classpathResource) throws 
IOException {
-               byte[] bytes;
-               try (var in = 
ConsoleChromeMixin.class.getResourceAsStream(classpathResource)) {
-                       bytes = IoUtils.readBytes(in);
-               }
-               return httpResource(bytes, 
MimeTypeDetector.DEFAULT.getContentType(classpathResource));
-       }
-
-       /** Wraps pre-computed bytes as a cacheable {@link HttpResource} 
carrying the given content type. */
-       private static HttpResource httpResource(byte[] bytes, String 
contentType) {
-               return HttpResourceBean.of(
-                       ByteArrayBody.of(bytes, contentType),
-                       CollectionUtils.list(ContentType.of(contentType), 
CacheControl.of(CACHE_CONTROL))
-               );
+               return ASSET_CACHE.serve(pageBackgroundResource, 
MimeTypeDetector.DEFAULT.getContentType(pageBackgroundResource), CACHE_CONTROL);
        }
 
        /**
@@ -305,9 +289,10 @@ public class ConsoleChromeMixin {
        /**
         * Builds the response body: the static structural CSS, then 
Theme.OPEN's block, then (if different) the active
         * theme's override block, then (if configured) the 
logo/page-background asset override rules. Each override
-        * rule's {@code ?v=<buildVersion>-<hash8>} cache-buster is 
content-sensitive (see {@link #assetContentHash},
-        * mirroring {@code ViewsMixin}) so a {@code -SNAPSHOT} rebuild of the 
configured asset busts the browser cache
-        * without relying on {@code buildVersion} (stable across dev rebuilds) 
alone.
+        * rule's {@code ?v=<buildVersion>-<hash8>} cache-buster is 
content-sensitive (see
+        * {@link ClasspathAssetCache#cacheBuster}, mirroring {@code 
ViewsMixin}) so a {@code -SNAPSHOT} rebuild of the
+        * configured asset busts the browser cache without relying on {@code 
buildVersion} (stable across dev rebuilds)
+        * alone.
         */
        private byte[] buildBody(RestRequest req) throws IOException {
                buildCount.incrementAndGet();
@@ -318,11 +303,11 @@ public class ConsoleChromeMixin {
                        sb.append('\n').append(rootBlock(active));
                if (pageBackgroundResource != null)
                        sb.append('\n').append("html, 
body{background-image:url(\"").append(assetUrl(req, PAGE_BG_ASSET_PATH, 
PAGE_BG_ASSET_PATH_UNPREFIXED))
-                               
.append("?v=").append(buildVersion()).append('-').append(assetContentHash(pageBackgroundResource))
+                               
.append(ASSET_CACHE.cacheBuster(pageBackgroundResource))
                                .append("\"), var(--jc-page-bg);}");
                if (logoResource != null)
                        
sb.append('\n').append(".jc-logo{background-image:url(\"").append(assetUrl(req, 
LOGO_ASSET_PATH, LOGO_ASSET_PATH_UNPREFIXED))
-                               
.append("?v=").append(buildVersion()).append('-').append(assetContentHash(logoResource))
+                               .append(ASSET_CACHE.cacheBuster(logoResource))
                                .append("\");}");
                return sb.toString().getBytes(StandardCharsets.UTF_8);
        }
@@ -348,36 +333,6 @@ public class ConsoleChromeMixin {
                return req.getUriResolver().resolve("servlet:" + (standalone ? 
unprefixedPath : prefixedPath));
        }
 
-       /**
-        * Resolves the framework build version for asset cache-busting, 
falling back to {@code "dev"} when unset
-        * (e.g. running from IDE/test classpath rather than a packaged jar).
-        */
-       private static String buildVersion() {
-               var v = 
ConsoleChromeMixin.class.getPackage().getImplementationVersion();
-               return v == null ? "dev" : v;  // HTT: the non-null branch only 
fires when running from a packaged jar with a manifest Implementation-Version - 
unreachable when tests run against unpackaged target/classes.
-       }
-
-       /** Computes (and caches) the given classpath resource's 8-hex-char 
content hash, read once per resource. */
-       private static String assetContentHash(String classpathResource) {
-               return ASSET_HASH_CACHE.computeIfAbsent(classpathResource, 
ConsoleChromeMixin::readAndHash);
-       }
-
-       /** Reads a validated, already-configured classpath resource and hashes 
its bytes. */
-       private static String readAndHash(String classpathResource) {
-               try (var in = 
ConsoleChromeMixin.class.getResourceAsStream(classpathResource)) {
-                       return hash8(IoUtils.readBytes(in));
-               } catch (IOException e) {  // HTT: unreachable - 
validateAssetResource already confirmed the resource exists.
-                       throw new UncheckedIOException(e);
-               }
-       }
-
-       /** Formats a CRC32 checksum of {@code bytes} as a zero-padded 
8-hex-char content hash. */
-       private static String hash8(byte[] bytes) {
-               var crc = new CRC32();
-               crc.update(bytes);
-               return String.format("%08x", crc.getValue());
-       }
-
        /**
         * Test-only diagnostic: the number of times this instance has 
(re)assembled its response body.
         *
diff --git 
a/juneau-rest/juneau-rest-server-views/src/main/java/org/apache/juneau/rest/server/views/ViewsMixin.java
 
b/juneau-rest/juneau-rest-server-views/src/main/java/org/apache/juneau/rest/server/views/ViewsMixin.java
index 6b3a594455..524527f45a 100644
--- 
a/juneau-rest/juneau-rest-server-views/src/main/java/org/apache/juneau/rest/server/views/ViewsMixin.java
+++ 
b/juneau-rest/juneau-rest-server-views/src/main/java/org/apache/juneau/rest/server/views/ViewsMixin.java
@@ -16,19 +16,9 @@
  */
 package org.apache.juneau.rest.server.views;
 
-import static org.apache.juneau.commons.utils.CollectionUtils.*;
-
-import java.io.*;
-import java.util.*;
-import java.util.concurrent.*;
-import java.util.zip.*;
-
-import org.apache.juneau.commons.utils.*;
 import org.apache.juneau.http.*;
-import org.apache.juneau.http.entity.*;
-import org.apache.juneau.http.header.*;
-import org.apache.juneau.http.resource.*;
 import org.apache.juneau.rest.server.*;
+import org.apache.juneau.rest.server.util.*;
 
 /**
  * Mixin that serves the first-party rich-view runtime assets &mdash; {@code 
juneau-views.js},
@@ -142,11 +132,12 @@ public class ViewsMixin {
        /** {@code Cache-Control} header emitted for every asset (1 day). */
        static final String CACHE_CONTROL = "max-age=86400, public";
 
-       /** Per-resource byte cache, populated on first request (the shipped 
files never vary). */
-       private static final Map<String,byte[]> CACHE = new 
ConcurrentHashMap<>();
-
-       /** Per-path content-hash cache, populated on first {@link 
#viewAssetUrl(String)} call (mirrors {@link #CACHE}). */
-       private static final Map<String,String> HASH_CACHE = new 
ConcurrentHashMap<>();
+       /**
+        * Read+cache+hash+serve helper for this mixin's shipped assets, 
anchored on this class so
+        * {@link ClasspathAssetCache#buildVersion()} resolves this module's 
own implementation version (see that
+        * class's javadoc's version-anchor section).
+        */
+       private static final ClasspathAssetCache ASSET_CACHE = new 
ClasspathAssetCache(ViewsMixin.class);
 
        /**
         * [GET /juneau-views.js] &mdash; serve the client initializer.
@@ -248,7 +239,7 @@ public class ViewsMixin {
         * @return The servlet-relative asset URL with the version+content-hash 
cache-buster appended.
         */
        public static String viewAssetUrl(String path) {
-               return "servlet:" + path + "?v=" + buildVersion() + "-" + 
contentHash(path);
+               return "servlet:" + path + 
ASSET_CACHE.cacheBuster(resourceFor(path));
        }
 
        /**
@@ -270,41 +261,12 @@ public class ViewsMixin {
         * @return The absolute asset URL with the version+content-hash 
cache-buster appended.
         */
        public static String viewAssetUrl(RestRequest req, String path) {
-               return req.getUriResolver().resolve("servlet:" + path) + "?v=" 
+ buildVersion() + "-" + contentHash(path);
+               return req.getUriResolver().resolve("servlet:" + path) + 
ASSET_CACHE.cacheBuster(resourceFor(path));
        }
 
        /** Reads (and caches) the classpath asset and wraps it as a cacheable 
{@link HttpResource}. */
        private static HttpResource serve(String resource, String contentType) {
-               var bytes = CACHE.computeIfAbsent(resource, ViewsMixin::load);
-               return HttpResourceBean.of(
-                       ByteArrayBody.of(bytes, contentType),
-                       list(ContentType.of(contentType), 
CacheControl.of(CACHE_CONTROL))
-               );
-       }
-
-       /** Reads a shipped classpath asset into a byte array, wrapping the 
(effectively unreachable) IO failure. */
-       private static byte[] load(String resource) {
-               try (var in = ViewsMixin.class.getResourceAsStream(resource)) {
-                       if (in == null)
-                               throw new IOException("Classpath resource not 
found: " + resource);
-                       return IoUtils.readBytes(in);
-               } catch (IOException e) {  // HTT: unreachable - the asset 
ships in the same jar as this class.
-                       throw new UncheckedIOException(e);
-               }
-       }
-
-       /**
-        * Resolves the framework build version for asset cache-busting, 
falling back to {@code "dev"} when unset (e.g.
-        * running from an IDE/test classpath rather than a packaged jar).
-        */
-       private static String buildVersion() {
-               var v = 
ViewsMixin.class.getPackage().getImplementationVersion();
-               return v == null ? "dev" : v;  // HTT: the non-null branch only 
fires from a packaged jar manifest - unreachable against unpackaged 
target/classes.
-       }
-
-       /** Computes (and caches) the given asset path's 8-hex-char content 
hash from its served bytes. */
-       private static String contentHash(String path) {
-               return HASH_CACHE.computeIfAbsent(path, p -> 
hash8(CACHE.computeIfAbsent(resourceFor(p), ViewsMixin::load)));
+               return ASSET_CACHE.serve(resource, contentType, CACHE_CONTROL);
        }
 
        /** Maps a public asset path constant to its classpath resource 
constant (content-hashing only; routing itself is by {@code 
@RestGet(path=...)}). */
@@ -317,11 +279,4 @@ public class ViewsMixin {
                if (PAGES_JS_PATH.equals(path)) return PAGES_JS_RESOURCE;
                throw new IllegalArgumentException("Unknown asset path: " + 
path);
        }
-
-       /** Formats a CRC32 checksum of {@code bytes} as a zero-padded 
8-hex-char content hash. */
-       private static String hash8(byte[] bytes) {
-               var crc = new CRC32();
-               crc.update(bytes);
-               return String.format("%08x", crc.getValue());
-       }
 }
diff --git 
a/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/ViewsMixin_Serving_Test.java
 
b/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/ViewsMixin_Serving_Test.java
index d1887ae3a7..5014699b3a 100644
--- 
a/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/ViewsMixin_Serving_Test.java
+++ 
b/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/ViewsMixin_Serving_Test.java
@@ -24,6 +24,7 @@ import java.nio.charset.*;
 
 import org.apache.juneau.*;
 import org.apache.juneau.bean.html5.Div;
+import org.apache.juneau.commons.utils.*;
 import org.apache.juneau.http.*;
 import org.apache.juneau.http.entity.*;
 import org.apache.juneau.http.header.*;
@@ -223,6 +224,24 @@ class ViewsMixin_Serving_Test extends TestBase {
                assertNotEquals(url1, 
ViewsMixin.viewAssetUrl(ViewsMixin.RIBBON_JS_PATH), "distinct assets must not 
collide on their content hash");
        }
 
+       /**
+        * Pins {@link ViewsMixin#viewAssetUrl(String)}'s content-hash suffix 
against an independently-computed
+        * {@link ChecksumUtils#hash8} of each served asset's actual bytes 
(fetched over the mock client, not read off
+        * the classpath directly) - a regression guard for the {@code 
ClasspathAssetCache} extraction (TODO-443):
+        * proves the shared helper still produces byte-for-byte the same hash 
the hand-rolled pre-refactor
+        * {@code hash8}/{@code contentHash} pair did.
+        */
+       @Test void 
c04_viewAssetUrl_contentHash_matchesIndependentlyComputedHash8OfServedBytes() 
throws Exception {
+               for (var path : new String[]{
+                               ViewsMixin.VIEWS_JS_PATH, 
ViewsMixin.RIBBON_JS_PATH, ViewsMixin.RENDERS_JS_PATH,
+                               ViewsMixin.VIEWS_CSS_PATH, 
ViewsMixin.ICONS_JS_PATH, ViewsMixin.PAGES_JS_PATH}) {
+                       var servedBytes = 
cWithMixin.get(path).run().assertStatus(200).getContent().asBytes();
+                       var expectedHash = ChecksumUtils.hash8(servedBytes);
+                       var url = ViewsMixin.viewAssetUrl(path);
+                       assertTrue(url.endsWith("-" + expectedHash), () -> path 
+ ": expected suffix '-" + expectedHash + "' in '" + url + "'");
+               }
+       }
+
        
//------------------------------------------------------------------------------------------------------------------
        // d) Contract-version handshake constant (single source of truth = 
ViewDef.CONTRACT_VERSION)
        
//------------------------------------------------------------------------------------------------------------------
diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/util/ClasspathAssetCache.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/util/ClasspathAssetCache.java
new file mode 100644
index 0000000000..1b86e4a1cb
--- /dev/null
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/util/ClasspathAssetCache.java
@@ -0,0 +1,160 @@
+/*
+ * 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.juneau.rest.server.util;
+
+import static org.apache.juneau.commons.utils.CollectionUtils.*;
+import static org.apache.juneau.commons.utils.Shorts.*;
+
+import java.io.*;
+import java.util.concurrent.*;
+
+import org.apache.juneau.commons.utils.*;
+import org.apache.juneau.http.*;
+import org.apache.juneau.http.entity.*;
+import org.apache.juneau.http.header.*;
+import org.apache.juneau.http.resource.*;
+
+/**
+ * A themeable/configurable asset-serving mixin's cache of classpath-shipped 
assets: reads each configured
+ * resource's bytes once, computes and caches its {@link 
ChecksumUtils#hash8(byte[]) content hash}, and wraps
+ * bytes as a cacheable {@link HttpResource} - the 
read+cache+hash+cache-buster-URL+serve bundle that
+ * {@code ConsoleChromeMixin} (juneau-rest-server-console-ui) and {@code 
ViewsMixin} (juneau-rest-server-views)
+ * previously each hand-rolled as their own private statics and static cache 
maps.
+ *
+ * <h5 class='section'>Version-anchor:</h5>
+ * <p>
+ * {@link #buildVersion()} resolves {@link Package#getImplementationVersion()} 
from the constructor-supplied
+ * <b>anchor</b> class, not from this class's own class/package. A mixin 
composes one {@link ClasspathAssetCache}
+ * per mixin class (mirroring the static caches this replaces) and anchors it 
on <b>its own</b> class, so the
+ * resolved implementation version - and therefore every {@code 
?v=<buildVersion>-<hash8>} cache-buster URL this
+ * cache builds - matches what that mixin's own jar shipped with, exactly as 
it did before extraction. Anchoring
+ * on this class instead would resolve {@code juneau-rest-server}'s own 
implementation version for every
+ * consuming mixin, silently changing every consumer's cache-buster URLs.
+ *
+ * <h5 class='section'>Thread-safety:</h5>
+ * <p>
+ * Backed by {@link ConcurrentHashMap}s; safe to share across concurrent 
requests. Classpath-shipped resources
+ * are assumed immutable for the lifetime of the JVM, so bytes and hashes are 
computed at most once per resource.
+ *
+ * @since 10.0.0
+ */
+public class ClasspathAssetCache {
+
+       private final Class<?> anchor;
+       private final ConcurrentHashMap<String,byte[]> byteCache = new 
ConcurrentHashMap<>();
+       private final ConcurrentHashMap<String,String> hashCache = new 
ConcurrentHashMap<>();
+
+       /**
+        * Constructor.
+        *
+        * @param anchor
+        *      The class that anchors both classpath resource resolution 
({@link Class#getResourceAsStream(String)}) and
+        *      {@link #buildVersion()}'s {@link 
Package#getImplementationVersion()} resolution. Typically the owning
+        *      mixin's own class (see the class javadoc's version-anchor 
section). Must not be <jk>null</jk>.
+        */
+       public ClasspathAssetCache(Class<?> anchor) {
+               this.anchor = rnn(anchor);
+       }
+
+       /** Test-only accessor for the constructor-supplied anchor class. */
+       Class<?> anchor() {
+               return anchor;
+       }
+
+       /**
+        * Reads (and caches) the given classpath resource's bytes, resolved 
relative to this cache's anchor class.
+        *
+        * @param classpathResource A classpath-root-absolute resource path 
(e.g. {@code "/org/apache/juneau/foo/bar.js"}).
+        *      Must not be <jk>null</jk> and must resolve to an existing 
resource on the anchor's classpath.
+        * @return The resource's bytes (served from cache on every call after 
the first for that resource).
+        */
+       public byte[] bytes(String classpathResource) {
+               return byteCache.computeIfAbsent(classpathResource, this::read);
+       }
+
+       private byte[] read(String classpathResource) {
+               try (var in = anchor.getResourceAsStream(classpathResource)) {
+                       if (in == null)
+                               throw new IOException("Classpath resource not 
found: " + classpathResource);
+                       return IoUtils.readBytes(in);
+               } catch (IOException e) {  // HTT: unreachable - callers only 
pass already-validated, jar-shipped classpath resources.
+                       throw new UncheckedIOException(e);
+               }
+       }
+
+       /**
+        * Computes (and caches) the given classpath resource's 8-hex-char 
content hash.
+        *
+        * @param classpathResource A classpath-root-absolute resource path.  
Same constraints as {@link #bytes(String)}.
+        * @return The resource's {@link ChecksumUtils#hash8(byte[])} content 
hash.
+        */
+       public String hash(String classpathResource) {
+               return hashCache.computeIfAbsent(classpathResource, r -> 
ChecksumUtils.hash8(bytes(r)));
+       }
+
+       /**
+        * Resolves the framework build version for asset cache-busting from 
this cache's anchor class's package,
+        * falling back to {@code "dev"} when unset (e.g. running from an 
IDE/test classpath rather than a packaged
+        * jar) - see the class javadoc's version-anchor section.
+        *
+        * @return The anchor's {@link Package#getImplementationVersion()}, or 
{@code "dev"} if unset.
+        */
+       public String buildVersion() {
+               var v = anchor.getPackage().getImplementationVersion();
+               return v == null ? "dev" : v;  // HTT: the non-null branch only 
fires when running from a packaged jar with a manifest Implementation-Version - 
unreachable when tests run against unpackaged target/classes.
+       }
+
+       /**
+        * Builds the {@code ?v=<buildVersion>-<hash8>} content-sensitive 
cache-buster query suffix for the given
+        * classpath resource, suitable for appending to that resource's served 
URL.
+        *
+        * @param classpathResource A classpath-root-absolute resource path.  
Same constraints as {@link #bytes(String)}.
+        * @return The {@code "?v=" + buildVersion() + "-" + 
hash(classpathResource)} suffix.
+        */
+       public String cacheBuster(String classpathResource) {
+               return "?v=" + buildVersion() + "-" + hash(classpathResource);
+       }
+
+       /**
+        * Wraps pre-computed bytes as a cacheable {@link HttpResource} 
carrying the given content type and
+        * {@code Cache-Control} header.
+        *
+        * @param bytes The response body bytes.  Must not be <jk>null</jk>.
+        * @param contentType The {@code Content-Type} header value (e.g. 
{@code "text/css;charset=utf-8"}).
+        * @param cacheControl The {@code Cache-Control} header value (e.g. 
{@code "max-age=86400, public"}).
+        * @return The wrapped, cacheable resource.
+        */
+       public HttpResource wrap(byte[] bytes, String contentType, String 
cacheControl) {
+               return HttpResourceBean.of(
+                       ByteArrayBody.of(bytes, contentType),
+                       list(ContentType.of(contentType), 
CacheControl.of(cacheControl)));
+       }
+
+       /**
+        * Reads (and caches) the given classpath resource and wraps it as a 
cacheable {@link HttpResource} - the
+        * {@link #bytes(String)} + {@link #wrap(byte[],String,String)} bundle 
every classpath-asset-serving endpoint
+        * needs.
+        *
+        * @param classpathResource A classpath-root-absolute resource path.  
Same constraints as {@link #bytes(String)}.
+        * @param contentType The {@code Content-Type} header value.
+        * @param cacheControl The {@code Cache-Control} header value.
+        * @return The wrapped, cacheable resource.
+        */
+       public HttpResource serve(String classpathResource, String contentType, 
String cacheControl) {
+               return wrap(bytes(classpathResource), contentType, 
cacheControl);
+       }
+}
diff --git 
a/juneau-rest/juneau-rest-server/src/test/java/org/apache/juneau/rest/server/util/ClasspathAssetCache_Test.java
 
b/juneau-rest/juneau-rest-server/src/test/java/org/apache/juneau/rest/server/util/ClasspathAssetCache_Test.java
new file mode 100644
index 0000000000..4d88f7966a
--- /dev/null
+++ 
b/juneau-rest/juneau-rest-server/src/test/java/org/apache/juneau/rest/server/util/ClasspathAssetCache_Test.java
@@ -0,0 +1,185 @@
+/*
+ * 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.juneau.rest.server.util;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.*;
+
+import org.apache.juneau.commons.utils.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Tests {@link ClasspathAssetCache}.
+ */
+class ClasspathAssetCache_Test {
+
+       /** A resource guaranteed present on this module's own test classpath 
(this very test's compiled class file). */
+       private static final String OWN_CLASS_RESOURCE = 
"/org/apache/juneau/rest/server/util/ClasspathAssetCache_Test.class";
+
+       /** A second, distinct resource on the same classpath, for 
cross-resource hash-collision checks. */
+       private static final String OTHER_CLASS_RESOURCE = 
"/org/apache/juneau/rest/server/util/ClasspathAssetCache.class";
+
+       
//------------------------------------------------------------------------------------------------------------------
+       // a) bytes(...): read-and-cache
+       
//------------------------------------------------------------------------------------------------------------------
+
+       @Test void a01_bytes_returnsTheResourcesActualContent() {
+               var cache = new 
ClasspathAssetCache(ClasspathAssetCache_Test.class);
+               var bytes = cache.bytes(OWN_CLASS_RESOURCE);
+               assertTrue(bytes.length > 0);
+               // A compiled .class file always starts with the JVM class-file 
magic number.
+               assertEquals((byte) 0xCA, bytes[0]);
+               assertEquals((byte) 0xFE, bytes[1]);
+       }
+
+       @Test void a02_bytes_sameResource_isServedFromCacheOnSecondCall() {
+               // computeIfAbsent returns the SAME array instance on a cache 
hit; a fresh read would return a distinct
+               // (if equal-content) array, so reference equality pins that 
the resource is read at most once.
+               var cache = new 
ClasspathAssetCache(ClasspathAssetCache_Test.class);
+               var first = cache.bytes(OWN_CLASS_RESOURCE);
+               var second = cache.bytes(OWN_CLASS_RESOURCE);
+               assertSame(first, second, "expected the cached byte[] instance, 
not a freshly-read copy");
+       }
+
+       @Test void a03_bytes_distinctResources_areNotConflated() {
+               var cache = new 
ClasspathAssetCache(ClasspathAssetCache_Test.class);
+               
assertFalse(java.util.Arrays.equals(cache.bytes(OWN_CLASS_RESOURCE), 
cache.bytes(OTHER_CLASS_RESOURCE)));
+       }
+
+       @Test void a04_bytes_missingResource_throwsUncheckedIOException() {
+               var cache = new 
ClasspathAssetCache(ClasspathAssetCache_Test.class);
+               assertThrows(UncheckedIOException.class, () -> 
cache.bytes("/no/such/resource.bin"));
+       }
+
+       
//------------------------------------------------------------------------------------------------------------------
+       // b) hash(...): content-hash cache, backed by ChecksumUtils.hash8
+       
//------------------------------------------------------------------------------------------------------------------
+
+       @Test void b01_hash_matchesChecksumUtilsHash8OfTheSameBytes() {
+               var cache = new 
ClasspathAssetCache(ClasspathAssetCache_Test.class);
+               var bytes = cache.bytes(OWN_CLASS_RESOURCE);
+               assertEquals(ChecksumUtils.hash8(bytes), 
cache.hash(OWN_CLASS_RESOURCE));
+       }
+
+       @Test void b02_hash_isStableAcrossCalls() {
+               var cache = new 
ClasspathAssetCache(ClasspathAssetCache_Test.class);
+               assertEquals(cache.hash(OWN_CLASS_RESOURCE), 
cache.hash(OWN_CLASS_RESOURCE));
+       }
+
+       @Test void b03_hash_distinctResources_doNotCollide() {
+               var cache = new 
ClasspathAssetCache(ClasspathAssetCache_Test.class);
+               assertNotEquals(cache.hash(OWN_CLASS_RESOURCE), 
cache.hash(OTHER_CLASS_RESOURCE));
+       }
+
+       
//------------------------------------------------------------------------------------------------------------------
+       // c) buildVersion(): resolved from the constructor-supplied anchor, 
"dev" fallback
+       
//------------------------------------------------------------------------------------------------------------------
+
+       @Test void 
c01_buildVersion_fallsBackToDev_whenAnchorPackageHasNoImplementationVersion() {
+               // Every anchor in this unpackaged-classes test run has a null 
Package#getImplementationVersion() (no
+               // manifest), so this pins the documented "dev" fallback 
directly.
+               
assertNull(ClasspathAssetCache_Test.class.getPackage().getImplementationVersion(),
 "test precondition: expected an unpackaged (manifest-less) test run");
+               var cache = new 
ClasspathAssetCache(ClasspathAssetCache_Test.class);
+               assertEquals("dev", cache.buildVersion());
+       }
+
+       @Test void 
c02_buildVersion_isConsistentWithDirectPackageLookupOnTheAnchor() {
+               // Same anchor, computed two ways: cache.buildVersion() and 
Package#getImplementationVersion() read
+               // directly off the SAME anchor class - equal in either branch 
(real version or "dev" fallback), so this
+               // stays valid even if a future packaged run gives the anchor a 
real Implementation-Version.
+               var cache = new 
ClasspathAssetCache(ClasspathAssetCache_Test.class);
+               var direct = 
ClasspathAssetCache_Test.class.getPackage().getImplementationVersion();
+               assertEquals(direct == null ? "dev" : direct, 
cache.buildVersion());
+       }
+
+       @Test void 
c03_constructor_storesTheSuppliedAnchor_notTheCachesOwnClass() {
+               // Guards the version-anchor design point directly: the cache 
must resolve buildVersion() from the
+               // MIXIN-supplied anchor, not from ClasspathAssetCache's own 
class/package - a hardcode of `getClass()`
+               // (or similar) in place of the constructor argument would flip 
this to ClasspathAssetCache.class, and
+               // this assertion catches that even though both currently 
resolve to "dev".
+               var cache = new 
ClasspathAssetCache(ClasspathAssetCache_Test.class);
+               assertSame(ClasspathAssetCache_Test.class, cache.anchor());
+       }
+
+       @Test void c04_constructor_rejectsNullAnchor() {
+               assertThrows(NullPointerException.class, () -> new 
ClasspathAssetCache(null));
+       }
+
+       
//------------------------------------------------------------------------------------------------------------------
+       // d) cacheBuster(...): "?v=<buildVersion>-<hash8>"
+       
//------------------------------------------------------------------------------------------------------------------
+
+       @Test void d01_cacheBuster_isVEqualsBuildVersionDashHash8() {
+               var cache = new 
ClasspathAssetCache(ClasspathAssetCache_Test.class);
+               assertEquals("?v=" + cache.buildVersion() + "-" + 
cache.hash(OWN_CLASS_RESOURCE), cache.cacheBuster(OWN_CLASS_RESOURCE));
+       }
+
+       
//------------------------------------------------------------------------------------------------------------------
+       // e) wrap(...) / serve(...): cacheable HttpResource with the requested 
Content-Type/Cache-Control
+       
//------------------------------------------------------------------------------------------------------------------
+
+       @Test void e01_wrap_setsContentTypeHeader() {
+               var cache = new 
ClasspathAssetCache(ClasspathAssetCache_Test.class);
+               var resource = cache.wrap(new byte[]{1, 2, 3}, "image/svg+xml", 
"max-age=86400, public");
+               assertEquals("image/svg+xml", resource.getContentType());
+       }
+
+       @Test void e02_wrap_setsCacheControlHeader() {
+               var cache = new 
ClasspathAssetCache(ClasspathAssetCache_Test.class);
+               var resource = cache.wrap(new byte[]{1, 2, 3}, "image/svg+xml", 
"max-age=86400, public");
+               assertEquals("max-age=86400, public", cacheControlOf(resource));
+       }
+
+       @Test void e03_wrap_bodyBytes_matchTheSuppliedBytes() throws 
IOException {
+               var cache = new 
ClasspathAssetCache(ClasspathAssetCache_Test.class);
+               var expected = new byte[]{1, 2, 3, 4};
+               var resource = cache.wrap(expected, "application/octet-stream", 
"max-age=86400, public");
+               assertArrayEquals(expected, writtenBytes(resource));
+       }
+
+       @Test void e04_serve_bodyBytes_matchTheClasspathResourcesActualBytes() 
throws IOException {
+               var cache = new 
ClasspathAssetCache(ClasspathAssetCache_Test.class);
+               var resource = cache.serve(OWN_CLASS_RESOURCE, 
"application/octet-stream", "max-age=86400, public");
+               assertArrayEquals(cache.bytes(OWN_CLASS_RESOURCE), 
writtenBytes(resource));
+       }
+
+       @Test void e05_serve_setsContentTypeAndCacheControl() {
+               var cache = new 
ClasspathAssetCache(ClasspathAssetCache_Test.class);
+               var resource = cache.serve(OWN_CLASS_RESOURCE, 
"application/octet-stream", "max-age=3600, public");
+               assertEquals("application/octet-stream", 
resource.getContentType());
+               assertEquals("max-age=3600, public", cacheControlOf(resource));
+       }
+
+       
//------------------------------------------------------------------------------------------------------------------
+       // Test helpers
+       
//------------------------------------------------------------------------------------------------------------------
+
+       private static byte[] writtenBytes(org.apache.juneau.http.HttpBody 
body) throws IOException {
+               var out = new ByteArrayOutputStream();
+               body.writeTo(out);
+               return out.toByteArray();
+       }
+
+       private static String 
cacheControlOf(org.apache.juneau.http.HttpResource resource) {
+               return resource.getHeaders().stream()
+                       .filter(h -> 
h.getName().equalsIgnoreCase("Cache-Control"))
+                       .findFirst()
+                       .map(org.apache.juneau.http.HttpHeader::getValue)
+                       .orElseThrow(() -> new AssertionError("no Cache-Control 
header found"));
+       }
+}
diff --git a/scripts/eclipse-warnings.py b/scripts/eclipse-warnings.py
new file mode 100644
index 0000000000..13b18c9ee8
--- /dev/null
+++ b/scripts/eclipse-warnings.py
@@ -0,0 +1,204 @@
+#!/usr/bin/env python3
+#
+# 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.
+
+"""
+Headless reproduction of Eclipse JDT compiler warnings via `ecj`.
+
+`ecj` is the exact JDT compiler Eclipse embeds. Driven by a module's own
+.settings/org.eclipse.jdt.core.prefs (the file scripts/apply-eclipse-prefs.py
+seeds/syncs), it reproduces Eclipse's Problems-view diagnostics verbatim,
+including exact message strings -- without a live Eclipse session or the
+AssistAI Eclipse MCP server. See the `eclipse-warnings` skill's "Headless
+fallback" section for the full technique writeup and when to prefer it.
+
+Usage (run from the repo root):
+    python3 scripts/eclipse-warnings.py <module-path>   # e.g. 
juneau-rest/juneau-rest-server
+    python3 scripts/eclipse-warnings.py --all           # every source module 
apply-eclipse-prefs.py would touch
+
+Requires:
+    - `ecj` 3.45.0. Resolved automatically from 
~/.m2/repository/org/eclipse/jdt/ecj/3.45.0/
+      (already present if the project has ever built; the ecj Maven coordinate 
is
+      org.eclipse.jdt:ecj:3.45.0). Override with the ECJ_JAR env var if it 
lives elsewhere.
+    - the module's own .settings/org.eclipse.jdt.core.prefs to exist (run
+      apply-eclipse-prefs.py first if missing/stale).
+    - `mvn` on PATH, to resolve the module's runtime+test classpath.
+
+Javadoc diagnostics additionally need -enableJavadoc plus explicit javadoc
+severities (passed below unconditionally) because the repo's tracked .prefs
+files set no javadoc options at all -- those particular diagnostics come from
+workspace-level Eclipse settings that no tracked file controls. Omitting the
+flags means a javadoc warning Eclipse reports simply won't reproduce here.
+
+Caveat -- classpath uses INSTALLED reactor jars, not live sources: the runtime
+classpath comes from `mvn dependency:build-classpath`, which resolves other
+reactor modules' jars from ~/.m2 as of their last `mvn install`, not their
+current on-disk source. A class added/changed in an upstream module since the
+last install won't be visible here ("X cannot be resolved" for a genuinely
+new/renamed type is the tell) -- `mvn install` the affected upstream module(s)
+first. Also expect occasional "invalid Class-Path header in manifest of jar
+file ..." lines from ecj scanning a third-party dependency's own bundled
+manifest; that's noise unrelated to Juneau's code and safe to ignore.
+"""
+
+import glob
+import importlib.util
+import os
+import subprocess
+import sys
+import tempfile
+from pathlib import Path
+
+DEFAULT_ECJ_JAR = Path.home() / 
".m2/repository/org/eclipse/jdt/ecj/3.45.0/ecj-3.45.0.jar"
+
+
+def find_ecj_jar():
+    override = os.environ.get("ECJ_JAR")
+    if override:
+        return Path(override)
+    return DEFAULT_ECJ_JAR
+
+
+def load_apply_prefs_module(script_dir):
+    """Reuse apply-eclipse-prefs.py's module discovery so both scripts agree
+    on what counts as a module and how it's classified source vs. test."""
+    spec = importlib.util.spec_from_file_location(
+        "apply_eclipse_prefs", script_dir / "apply-eclipse-prefs.py"
+    )
+    module = importlib.util.module_from_spec(spec)
+    spec.loader.exec_module(module)
+    return module
+
+
+def build_classpath(module_dir):
+    """Run `mvn dependency:build-classpath` from inside the module dir itself
+    (not `-pl <module>` from the reactor root). `-pl` still triggers a
+    multi-module reactor pass, and multiple modules' plugin executions racing
+    to write the same `-Dmdep.outputFile` target corrupts the output (jar
+    paths get concatenated with missing `:` separators). Running from the
+    module's own directory keeps this to a single-module build."""
+    with tempfile.NamedTemporaryFile(prefix="ecj-cp-", suffix=".txt", 
delete=False) as tmp:
+        cp_file = tmp.name
+    try:
+        result = subprocess.run(
+            ["mvn", "-q", "dependency:build-classpath", 
f"-Dmdep.outputFile={cp_file}"],
+            cwd=module_dir, capture_output=True, text=True, timeout=180,
+        )
+        if result.returncode != 0:
+            print(f"Warning: `mvn dependency:build-classpath` failed for 
{module_dir}:", file=sys.stderr)
+            print(result.stderr, file=sys.stderr)
+            return ""
+        return Path(cp_file).read_text().strip()
+    finally:
+        os.unlink(cp_file)
+
+
+def java_sources(module_dir):
+    files = []
+    for sub in ("src/main/java", "src/test/java"):
+        src_dir = module_dir / sub
+        if src_dir.exists():
+            files.extend(glob.glob(str(src_dir / "**" / "*.java"), 
recursive=True))
+    return files
+
+
+def run_ecj(ecj_jar, module_dir, module_path, classpath):
+    prefs = module_dir / ".settings" / "org.eclipse.jdt.core.prefs"
+    if not prefs.exists():
+        print(f"Skipping {module_path}: no 
.settings/org.eclipse.jdt.core.prefs "
+              f"(run apply-eclipse-prefs.py first)", file=sys.stderr)
+        return None
+
+    sources = java_sources(module_dir)
+    if not sources:
+        print(f"Skipping {module_path}: no .java sources found", 
file=sys.stderr)
+        return None
+
+    target_classes = module_dir / "target" / "classes"
+    test_classes = module_dir / "target" / "test-classes"
+    cp_parts = [classpath] if classpath else []
+    for extra in (target_classes, test_classes):
+        if extra.exists():
+            cp_parts.append(str(extra))
+    full_cp = os.pathsep.join(p for p in cp_parts if p)
+
+    with tempfile.TemporaryDirectory(prefix="ecj-out-") as out_dir:
+        cmd = [
+            "java", "-jar", str(ecj_jar),
+            "-properties", str(prefs),
+            "-enableJavadoc", "-warn:+allJavadoc,invalidJavadoc,javadoc",
+            "-cp", full_cp,
+            "-d", out_dir,
+        ] + sources
+        result = subprocess.run(cmd, cwd=module_dir, capture_output=True, 
text=True, timeout=300)
+        return result.stdout + result.stderr
+
+
+def summarize(output):
+    """Pull ecj's trailing '<n> problems (<e> errors, <w> warnings)' line, if 
present."""
+    for line in reversed(output.strip().splitlines()):
+        if "problem" in line and ("error" in line or "warning" in line):
+            return line.strip()
+    return None
+
+
+def main():
+    script_dir = Path(__file__).parent
+    root_dir = script_dir.parent
+
+    if len(sys.argv) != 2:
+        print(__doc__)
+        sys.exit(1)
+
+    ecj_jar = find_ecj_jar()
+    if not ecj_jar.exists():
+        print(f"ecj jar not found at {ecj_jar}.", file=sys.stderr)
+        print("Resolve it first, e.g.: mvn dependency:get 
-Dartifact=org.eclipse.jdt:ecj:3.45.0", file=sys.stderr)
+        print("or set ECJ_JAR to point at an existing copy.", file=sys.stderr)
+        sys.exit(2)
+
+    if sys.argv[1] == "--all":
+        apply_prefs = load_apply_prefs_module(script_dir)
+        source_modules, test_modules = apply_prefs.discover_projects(root_dir)
+        module_paths = source_modules + test_modules
+    else:
+        module_paths = [sys.argv[1]]
+
+    any_problems = False
+    for module_path in module_paths:
+        module_dir = root_dir / module_path
+        if not module_dir.exists():
+            print(f"Warning: module not found: {module_path}", file=sys.stderr)
+            continue
+
+        print(f"\n{'=' * 60}\n{module_path}\n{'=' * 60}")
+        classpath = build_classpath(module_dir)
+        output = run_ecj(ecj_jar, module_dir, module_path, classpath)
+        if output is None:
+            continue
+
+        print(output)
+        summary = summarize(output)
+        if summary:
+            print(f">>> {module_path}: {summary}")
+            if "0 errors, 0 warnings" not in summary:
+                any_problems = True
+
+    sys.exit(1 if any_problems else 0)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/scripts/todo-next-id.py b/scripts/todo-next-id.py
index 5a74ec73f7..485a2ee5c6 100755
--- a/scripts/todo-next-id.py
+++ b/scripts/todo-next-id.py
@@ -34,7 +34,7 @@ Mirrors the exact scan scope documented in this repo's 
TODO-management skill, in
      Note that this scan cannot distinguish an illustrative id from a live 
one: writing
      "for example, TODO-5" anywhere in TODO.md permanently consumes id 5. 
Write "TODO-<n>" in
      prose.
-  2. Every 
"TODO-"/"READY-"/"MAYBE-"/"FINISHED-"/"CANCELLED-<n>[<letter>]-*.md" filename
+  2. Every 
"TODO-"/"READY-"/"MAYBE-"/"HOLD-"/"FINISHED-"/"CANCELLED-<n>[<letter>]-*.md" 
filename
      directly under .work/todo/ and .work/todo/finished/.
 
 next = 1 + max(all numeric ids found). A child's letter suffix (TODO-174a, 
FINISHED-337f, ...)
@@ -96,7 +96,7 @@ DEFAULT_REPO_ROOT = Path(__file__).resolve().parent.parent
 TODO_TOKEN_RE = re.compile(r"(?<![\w:])TODO-(\d+)([a-z]*)\b")
 
 # Every lifecycle-state filename directly under .work/todo/ or 
.work/todo/finished/.
-FILENAME_RE = 
re.compile(r"^(?:TODO|READY|MAYBE|FINISHED|CANCELLED)-(\d+)([a-z]*)-.*\.md$")
+FILENAME_RE = 
re.compile(r"^(?:TODO|READY|MAYBE|HOLD|FINISHED|CANCELLED)-(\d+)([a-z]*)-.*\.md$")
 
 
 def collect_ids(todo_dir: Path) -> tuple[set, set]:
diff --git a/scripts/todo-status-audit.py b/scripts/todo-status-audit.py
index 177260d4a9..6b6e37cdbc 100755
--- a/scripts/todo-status-audit.py
+++ b/scripts/todo-status-audit.py
@@ -19,9 +19,9 @@ Repo-agnostic: the repository root is derived from this 
file's own location
 every repository that adopts the convention. Only the REPO_LABEL / SKILL_NAME 
constants
 below and the license header differ between copies.
 
-Checks every TODO-<id>-*.md / READY-<id>-*.md / MAYBE-<id>-*.md file directly 
under .work/todo/
-(FINISHED-/CANCELLED-*.md archives are explicitly out of scope -- per this 
repo's
-TODO-management skill, "status line is not required in FINISHED archives") 
against that
+Checks every TODO-<id>-*.md / READY-<id>-*.md / MAYBE-<id>-*.md / 
HOLD-<id>-*.md file directly
+under .work/todo/ (FINISHED-/CANCELLED-*.md archives are explicitly out of 
scope -- per this
+repo's TODO-management skill, "status line is not required in FINISHED 
archives") against that
 skill's "Per-file `Current status:` and `Complexity:` header" rules, and flags 
candidate
 inconsistencies. This is a PRE-FILTER, not a validator: it flags candidates 
for a human
 (or agent) to look at, and will not catch everything on format-drifted files 
-- tolerant,
@@ -35,10 +35,10 @@ Checks performed (each file may accumulate multiple flags):
   - unrecognized_status_phrase  The status text doesn't start with one of the 
skill's documented
                                  phrases for this file's prefix (TODO/READY: 
"Waiting for user
                                  input on open questions.", "Ready to 
execute.", "In progress.";
-                                 MAYBE: must start with "Parked"). Free-form 
variants that legitimately
-                                 extend a recognized prefix (e.g. "Ready to 
execute (all items
-                                 independently actionable)." ) are NOT flagged 
-- only prefix
-                                 mismatches are.
+                                 MAYBE: must start with "Parked"; HOLD: must 
start with "On hold").
+                                 Free-form variants that legitimately extend a 
recognized prefix
+                                 (e.g. "Ready to execute (all items 
independently actionable)." )
+                                 are NOT flagged -- only prefix mismatches are.
   - ready_but_has_open_questions
                                  Status starts with "Ready to execute" but the 
file still has a
                                  "## Open questions" section containing at 
least one numbered item
@@ -51,6 +51,9 @@ Checks performed (each file may accumulate multiple flags):
   - parked_status_wrong_prefix  A TODO-*.md/READY-*.md file whose status 
starts with "Parked" (that
                                  wording is reserved for MAYBE-*.md files).
   - maybe_prefix_non_parked     A MAYBE-*.md file whose status does NOT start 
with "Parked".
+  - on_hold_status_wrong_prefix A TODO-*.md/READY-*.md file whose status 
starts with "On hold" (that
+                                 wording is reserved for HOLD-*.md files).
+  - hold_prefix_non_on_hold     A HOLD-*.md file whose status does NOT start 
with "On hold".
 
 A MISSING scan directory is a hard error (exit 2). An EMPTY-but-present one is 
a clean pass
 (exit 0). The original version conflated the two and returned 0 for both, so 
pointing the
@@ -93,7 +96,7 @@ SKILL_NAME = "juneau-todo-management"
 
 DEFAULT_REPO_ROOT = Path(__file__).resolve().parent.parent
 
-FILENAME_RE = re.compile(r"^(TODO|READY|MAYBE)-\d+[a-z]*-.*\.md$")
+FILENAME_RE = re.compile(r"^(TODO|READY|MAYBE|HOLD)-\d+[a-z]*-.*\.md$")
 
 STATUS_LINE_RE = re.compile(r"^\s*Current status:\s*(.*)$", re.IGNORECASE | 
re.MULTILINE)
 COMPLEXITY_LINE_RE = re.compile(r"^\s*Complexity:\s*(.*)$", re.IGNORECASE | 
re.MULTILINE)
@@ -115,6 +118,7 @@ TODO_READY_STATUS_PREFIXES = (
     "in progress",
 )
 MAYBE_STATUS_PREFIX = "parked"
+HOLD_STATUS_PREFIX = "on hold"
 
 
 def find_plan_files(todo_dir: Path) -> list:
@@ -159,10 +163,12 @@ def open_questions_are_unresolved(section_body: str) -> 
bool:
 
 
 def status_prefix_ok(prefix: str, status: str) -> bool:
-    """True if status's wording matches one of the recognized phrases for this 
file's TODO/READY/MAYBE prefix."""
+    """True if status's wording matches one of the recognized phrases for this 
file's TODO/READY/MAYBE/HOLD prefix."""
     normalized = status.strip().lower()
     if prefix == "MAYBE":
         return normalized.startswith(MAYBE_STATUS_PREFIX)
+    if prefix == "HOLD":
+        return normalized.startswith(HOLD_STATUS_PREFIX)
     return any(normalized.startswith(p) for p in TODO_READY_STATUS_PREFIXES)
 
 
@@ -207,6 +213,12 @@ def audit_file(path: Path) -> list:
         if prefix == "MAYBE" and not normalized.startswith("parked"):
             flags.append(("maybe_prefix_non_parked", f"MAYBE-prefixed file but 
status doesn't start with 'Parked': '{status}'"))
 
+        if prefix in ("TODO", "READY") and 
normalized.startswith(HOLD_STATUS_PREFIX):
+            flags.append(("on_hold_status_wrong_prefix", "Status says 'On 
hold...' but filename is not HOLD-prefixed."))
+
+        if prefix == "HOLD" and not normalized.startswith(HOLD_STATUS_PREFIX):
+            flags.append(("hold_prefix_non_on_hold", f"HOLD-prefixed file but 
status doesn't start with 'On hold': '{status}'"))
+
     return flags
 
 

Reply via email to