Repository: OpenLiberty/open-liberty
Pull request #30945: Reduce URL creation and properly set seal base
Author: login: tjwatson
Labels: name: CLA Signed
name: performance
Created: 2025-03-06T16:46:22Z | Merged: 2025-03-27T15:17:59Z

- [x] I have considered the risk of behavior change or other zero migration impact (https://github.com/OpenLiberty/open-liberty/wiki/Behavior-Changes).
- [x] If this PR fixes an Issue, the description includes "Fixes #FILLMEIN" or "Resolves #FILLMEIN" (verify `release bug` label if applicable: https://github.com/OpenLiberty/open-liberty/wiki/Open-Liberty-Conventions).
- [x] If this PR resolves an external Known Issue (including APARS), the description includes "Fixes #FILLMEIN" or "Resolves #FILLMEIN".




Diff:
diff --git a/dev/com.ibm.ws.classloading/src/com/ibm/ws/classloading/internal/AppClassLoader.java b/dev/com.ibm.ws.classloading/src/com/ibm/ws/classloading/internal/AppClassLoader.java
index 92dbeab4cf4..013e0ded496 100644
--- a/dev/com.ibm.ws.classloading/src/com/ibm/ws/classloading/internal/AppClassLoader.java
+++ b/dev/com.ibm.ws.classloading/src/com/ibm/ws/classloading/internal/AppClassLoader.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2011, 2024 IBM Corporation and others.
+ * Copyright (c) 2011, 2025 IBM Corporation and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License 2.0
  * which accompanies this distribution, and is available at
@@ -24,9 +24,7 @@
 import java.io.InputStream;
 import java.lang.instrument.ClassFileTransformer;
 import java.lang.instrument.IllegalClassFormatException;
-import java.net.JarURLConnection;
 import java.net.URL;
-import java.net.URLConnection;
 import java.security.AccessController;
 import java.security.CodeSource;
 import java.security.PrivilegedAction;
@@ -46,23 +44,18 @@
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
-import java.util.jar.Manifest;
 
 import org.osgi.framework.Bundle;
 
 import com.ibm.websphere.ras.Tr;
 import com.ibm.websphere.ras.TraceComponent;
 import com.ibm.websphere.ras.annotation.Trivial;
-import com.ibm.ws.artifact.url.WSJarURLConnection;
 import com.ibm.ws.classloading.ClassGenerator;
 import com.ibm.ws.classloading.configuration.GlobalClassloadingConfiguration;
 import com.ibm.ws.classloading.internal.providers.Providers;
 import com.ibm.ws.classloading.internal.util.ClassRedefiner;
 import com.ibm.ws.classloading.internal.util.FeatureSuggestion;
 import com.ibm.ws.ffdc.annotation.FFDCIgnore;
-import com.ibm.ws.kernel.boot.classloader.ClassLoaderHook;
-import com.ibm.ws.kernel.boot.classloader.ClassLoaderHookFactory;
-import com.ibm.ws.kernel.productinfo.ProductInfo;
 import com.ibm.ws.kernel.security.thread.ThreadIdentityManager;
 import com.ibm.wsspi.adaptable.module.Container;
 import com.ibm.wsspi.classloading.ApiType;
@@ -130,8 +123,6 @@ enum SearchLocation {
         PARENT, SELF, DELEGATES
     };
 
-    private static final boolean disableSharedClassesCache = Boolean.getBoolean("liberty.disableApplicationClassSharing");
-
     static final List<SearchLocation> PARENT_FIRST_SEARCH_ORDER = freeze(list(PARENT, SELF, DELEGATES));
 
     private final Set<String> packagesDefined = Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>()); 
@@ -160,7 +151,6 @@ private TraceComponent getClassLoadingTraceComponent(String pkg) {
     private final DeclaredApiAccess apiAccess;
     private final ClassGenerator generator;
     private final ConcurrentHashMap<String, ProtectionDomain> protectionDomains = new ConcurrentHashMap<String, ProtectionDomain>();
-    private final ClassLoaderHook hook;
 
     AppClassLoader(ClassLoader parent, ClassLoaderConfiguration config, List<Container> containers, DeclaredApiAccess access, ClassRedefiner redefiner, ClassGenerator generator, GlobalClassloadingConfiguration globalConfig, List<ClassFileTransformer> systemTransformers) {
         super(containers, parent, redefiner, globalConfig);
@@ -172,7 +162,6 @@ private TraceComponent getClassLoadingTraceComponent(String pkg) {
         this.privateLibraries = Providers.getPrivateLibraries(config);
         this.delegateLoaders = Providers.getDelegateLoaders(config, apiAccess);
         this.generator = generator;
-        hook = disableSharedClassesCache ? null : ClassLoaderHookFactory.getClassLoaderHook(this);
     }
 
     /** Provides the delegate loaders so the {@link ShadowClassLoader} can mimic the structure. */
@@ -410,8 +399,7 @@ private Class<?> definePackageAndClass(final String name, String resourceName, f
             definePackage(byteResourceInformation, packageName);
         }
 
-        URL resourceURL = byteResourceInformation.getResourceUrl();
-        ProtectionDomain pd = getClassSpecificProtectionDomain(resourceName, resourceURL);
+        ProtectionDomain pd = getClassSpecificProtectionDomain(byteResourceInformation.getContainerURL());
 
         final ProtectionDomain fpd = pd;
         java.security.PermissionCollection pc = null;
@@ -437,45 +425,27 @@ public java.security.PermissionCollection run() {
         } finally {
             final TraceComponent cltc;
             if (TraceComponent.isAnyTracingEnabled() && (cltc = getClassLoadingTraceComponent(packageName)).isDebugEnabled()) {
-                String loc = "" + byteResourceInformation.getResourceUrl();
-                String path = byteResourceInformation.getResourcePath();
-                if (loc.endsWith(path))
-                    loc = loc.substring(0, loc.length() - path.length());
-                if (loc.endsWith("!/"))
-                    loc = loc.substring(0, loc.length() - 2);
+                String loc = byteResourceInformation.getContainerURL().toString();
                 String message = clazz == null ? "CLASS FAIL" : "CLASS LOAD";
                 Tr.debug(cltc, String.format("%s: [%s] [%s] [%s]", message, getKey(), loc, name));
             }
         }
-        if (!byteResourceInformation.foundInClassCache() && hook != null) {
-            URL sharedClassCacheURL = getSharedClassCacheURL(resourceURL, byteResourceInformation.getResourcePath());
-            if (sharedClassCacheURL != null && Arrays.equals(bytes, byteResourceInformation.getBytes())) {
-                hook.storeClass(sharedClassCacheURL, clazz);
-                if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) {
-                    Tr.debug(tc, "Called shared class cache to store class", new Object[] {clazz.getName(), sharedClassCacheURL});
-                }
-            } else {
-                if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) {
-                    if (sharedClassCacheURL == null) {
-                        Tr.debug(tc, "No shared class cache URL to store class", clazz.getName());
-                    } else {
-                        Tr.debug(tc, "Did not store class because defined bytes got modified", clazz.getName());
-                    }
-                }
-            }
-        }
-        
+        byteResourceInformation.storeInClassCache(clazz, bytes);
         return clazz;
     }
 
     @Trivial // injected trace calls ProtectedDomain.toString() which requires privileged access
-    private ProtectionDomain getClassSpecificProtectionDomain(final String resourceName, final URL resourceUrl) {
+    private ProtectionDomain getClassSpecificProtectionDomain(final URL containerUrl) {
+        if (containerUrl == null) {
+            // not expected; there will have been some FFDCs if this is null
+            return config.getProtectionDomain();
+        }
         ProtectionDomain pd = config.getProtectionDomain();
         try {
             pd = AccessController.doPrivileged(new PrivilegedExceptionAction<ProtectionDomain>() {
                 @Override
                 public ProtectionDomain run() {
-                    return getClassSpecificProtectionDomainPrivileged(resourceName, resourceUrl);
+                    return getClassSpecificProtectionDomainPrivileged(containerUrl);
                 }
             });
         } catch (PrivilegedActionException paex) {
@@ -486,39 +456,12 @@ public ProtectionDomain run() {
 
     }
 
-    ProtectionDomain getClassSpecificProtectionDomainPrivileged(String resourceName, URL resourceUrl) {
-        ProtectionDomain pd;
-
-        try {
-            URLConnection conn = resourceUrl.openConnection();
-            URL containerUrl;
-            if (conn instanceof JarURLConnection) {
-                containerUrl = ((JarURLConnection) conn).getJarFileURL();
-            } else if (conn instanceof WSJarURLConnection) {
-                containerUrl = ((WSJarURLConnection) conn).getFile().toURI().toURL();
-            } else {
-                // this is most likely a file URL - i.e. the contents of the classes are expanded on the disk.
-                // so a path like:  .../myServer/dropins/myWar.war/WEB-INF/classes/com/myPkg/MyClass.class
-                // should convert to: .../myServer/dropins/myWar.war/WEB-INF/classes/
-                containerUrl = new URL(resourceUrl.toString().replace(resourceName, ""));
-            }
-            String containerUrlString = containerUrl.toString();
-            pd = protectionDomains.get(containerUrlString);
-            
-            if (pd == null) {
-                ProtectionDomain pdFromConfig = config.getProtectionDomain();
-                CodeSource cs = new CodeSource(containerUrl, pdFromConfig.getCodeSource().getCertificates());
-                pd = new ProtectionDomain(cs, pdFromConfig.getPermissions());
-                ProtectionDomain oldPD = protectionDomains.putIfAbsent(containerUrlString, pd);                
-                if (oldPD != null) {
-                    pd = oldPD;
-                }
-            } 
-        } catch (IOException ex) {
-            // Auto-FFDC - and then use the protection domain from the classloader configuration
-            pd = config.getProtectionDomain();
-        }
-        return pd;
+    ProtectionDomain getClassSpecificProtectionDomainPrivileged(URL containerUrl) {
+        return protectionDomains.computeIfAbsent(containerUrl.toString(), (c) -> {
+            ProtectionDomain pdFromConfig = config.getProtectionDomain();
+            CodeSource cs = new CodeSource(containerUrl, pdFromConfig.getCodeSource().getCertificates());
+            return new ProtectionDomain(cs, pdFromConfig.getPermissions());
+        });
     }
 
     /**
@@ -545,7 +488,7 @@ private void definePackage(ByteResourceInformation byteResourceInformation, Stri
 
     final ByteResourceInformation findClassBytes(String className, String resourceName) {
         try {
-            return findClassBytes(className, resourceName, hook);
+            return findClassBytesImpl(className, resourceName);
         } catch (IOException e) {
             Tr.error(tc, "cls.class.file.not.readable", className, resourceName);
             String message = String.format("Could not read class '%s' as resource '%s'", className, resourceName);
diff --git a/dev/com.ibm.ws.classloading/src/com/ibm/ws/classloading/internal/ContainerClassLoader.java b/dev/com.ibm.ws.classloading/src/com/ibm/ws/classloading/internal/ContainerClassLoader.java
index 4febe1bba25..9d2b6d66cd6 100644
--- a/dev/com.ibm.ws.classloading/src/com/ibm/ws/classloading/internal/ContainerClassLoader.java
+++ b/dev/com.ibm.ws.classloading/src/com/ibm/ws/classloading/internal/ContainerClassLoader.java
@@ -18,13 +18,17 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.lang.instrument.ClassDefinition;
+import java.net.JarURLConnection;
 import java.net.MalformedURLException;
+import java.net.URISyntaxException;
 import java.net.URL;
 import java.net.URLConnection;
 import java.security.AccessController;
+import java.security.PrivilegedAction;
 import java.security.PrivilegedActionException;
 import java.security.PrivilegedExceptionAction;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
@@ -60,12 +64,14 @@
 import com.ibm.websphere.ras.Tr;
 import com.ibm.websphere.ras.TraceComponent;
 import com.ibm.websphere.ras.annotation.Trivial;
+import com.ibm.ws.artifact.url.WSJarURLConnection;
 import com.ibm.ws.classloading.configuration.GlobalClassloadingConfiguration;
 import com.ibm.ws.classloading.internal.util.ClassRedefiner;
 import com.ibm.ws.classloading.internal.util.Keyed;
 import com.ibm.ws.ffdc.FFDCFilter;
 import com.ibm.ws.ffdc.annotation.FFDCIgnore;
 import com.ibm.ws.kernel.boot.classloader.ClassLoaderHook;
+import com.ibm.ws.kernel.boot.classloader.ClassLoaderHookFactory;
 import com.ibm.ws.kernel.feature.ServerStarted;
 import com.ibm.ws.kernel.security.thread.ThreadIdentityManager;
 import com.ibm.ws.kernel.service.util.ServiceCaller;
@@ -84,6 +90,7 @@
 import io.openliberty.checkpoint.spi.CheckpointPhase;
 
 abstract class ContainerClassLoader extends LibertyLoader implements Keyed<ClassLoaderIdentity> {
+    private static final boolean disableSharedClassesCache = Boolean.getBoolean("liberty.disableApplicationClassSharing");
     static final CheckpointPhase checkpointPhase = CheckpointPhase.getPhase();
     static {
         ClassLoader.registerAsParallelCapable();
@@ -126,6 +133,8 @@ public static void disableJarCaching() {
 
     final String jarProtocol;
 
+    private final ClassLoaderHook hook;
+
     /**
      * Util method to totally read an input stream into a byte array.
      * Used for class definition.
@@ -194,6 +203,8 @@ interface UniversalResource {
              * it to the file system as necessary.
              */
             public String getNativeLibraryPath();
+
+            public String getResourceName();
         }
 
         /**
@@ -226,78 +237,24 @@ interface UniversalResource {
          * Defines a package using the provided <code>LibertyLoader</code>
          */
         void definePackage(String packageName, LibertyLoader loader, URL sealBase);
-    }
 
-    /**
-     * Computes the shared class cache URL from the resource URL.
-     * 
-     * If the URL is a jar protocol URL, then use it as is.
-     * If it is a wsjar protocol URL, then change it to a jar protocol URL.
-     * If it is a file protocol URL, confirm that the URL ends with the
-     * class file name, and return the directory before the package
-     * qualified class file name.
-     * 
-     * @param resourceURL The URL of the location of the class file.
-     * @param resourceName The resource path of the class file. i.e. package/sub/MyClass.class
-     * @return the URL to pass to the shared class cache, or null if protocol is wrong,
-     *         or path doesn't include resourceName.
-     */
-    static URL getSharedClassCacheURL(URL resourceURL, String resourceName) {
-        URL sharedClassCacheURL;
-        if (resourceURL == null) {
-            sharedClassCacheURL = null;
-        } else {
-            String protocol = resourceURL.getProtocol();
-            // Doing the conversion that the shared class cache logic does for jar
-            // URLs in order to do less work while holding a shared class cache monitor.
-            if ("jar".equals(protocol) || "wsjar".equals(protocol)) {
-                String path = resourceURL.getPath();
-                // Can only do this for jar files.  Shared class cache logic
-                // cannot handle a file reference that is a war for instance.
-                // Need to use the full path for war files.
-                if (path.endsWith(resourceName)) {
-                    path = path.substring(0, path.length() - resourceName.length());
-                    if (path.endsWith(".jar!/") || path.endsWith(".zip!/")) {
-                        path = path.substring(0, path.length() - 2);
-                    } else {
-                        // If the archive file name does not end with jar or zip file extension and the URL ends with !/, 
-                        // the !/ will get stripped off by the shared classes cache logic and will not be recognized 
-                        // correctly as a jar file when it is a RAR for instance so add an extra character to the end of the URL.
-                        // Without this extra character, RAR files were not being recognized as being updated leading to 
-                        // stale classes being returned after the RAR file was updated.
-                        if (path.endsWith("!/")) {
-                            path += "l";
-                        }
-                    }
-                }
-                try {
-                    sharedClassCacheURL = new URL(path);
-                } catch (MalformedURLException e) {
-                    sharedClassCacheURL = null;
-                }
-            } else if (!"file".equals(protocol)) {
-                sharedClassCacheURL = null;
-            } else {
-                String externalForm = resourceURL.toExternalForm();
-                if (externalForm.endsWith(resourceName)) {
-                    try {
-                        sharedClassCacheURL = new URL(externalForm.substring(0, externalForm.length() - resourceName.length()));
-                    } catch (MalformedURLException e) {
-                        sharedClassCacheURL = null;
-                    }
-                } else {
-                    sharedClassCacheURL = null;
-                }
-            }
-        }
-        return sharedClassCacheURL;
+        /**
+         * Returns the container URL where the content of the resource is located for this container
+         * @return the container URL
+         */
+        URL getContainerURL(UniversalResource resource);
+
+        /**
+         * @return
+         */
+        URL getSharedClassCacheURL(UniversalResource resource);
     }
 
-    static byte[] getClassBytesFromHook(UniversalContainer.UniversalResource resource, String className, String resourceName, ClassLoaderHook hook) {
+    static ByteResourceInformation getClassBytesFromHook(UniversalContainer container, UniversalContainer.UniversalResource resource, String className, ClassLoaderHook hook, Supplier<byte[]> actualBytes) {
         byte[] bytes = null;
+        URL sharedClassCacheURL = null;
         if (hook != null) {
-            final URL resourceURL = resource.getResourceURL("jar");
-            URL sharedClassCacheURL = getSharedClassCacheURL(resourceURL, resourceName);
+            sharedClassCacheURL = container.getSharedClassCacheURL(resource);
             if (sharedClassCacheURL != null) {
                 bytes = hook.loadClass(sharedClassCacheURL, className);
                 if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) {
@@ -313,7 +270,11 @@ static byte[] getClassBytesFromHook(UniversalContainer.UniversalResource resourc
                 }
             }
         }
-        return bytes;
+        boolean foundInClassCache = bytes != null;
+        if (!foundInClassCache) {
+            bytes = actualBytes.get();
+        }
+        return new ByteResourceInformation(bytes, container, container.getContainerURL(resource), sharedClassCacheURL, foundInClassCache, actualBytes, hook);
     }
 
     @SuppressWarnings("unchecked")
@@ -364,13 +325,7 @@ public URL getResourceURL(String jarProtocol) {
 
         @Override
         public ByteResourceInformation getByteResourceInformation(String className, ClassLoaderHook hook) throws IOException {
-            byte[] bytes = ContainerClassLoader.getClassBytesFromHook(this, className, resourceName, hook);
-
-            boolean foundInClassCache = bytes != null;
-            if (!foundInClassCache) {
-                bytes = getActualBytes();
-            }
-            return new ByteResourceInformation(bytes, this.entry.getResource(), this.container, resourceName, foundInClassCache, this::getActualBytes);
+            return ContainerClassLoader.getClassBytesFromHook(container, this, className, hook, this::getActualBytes);
         }
 
         private byte[] getActualBytes() {
@@ -398,6 +353,11 @@ public String getNativeLibraryPath() {
             }
             return null;
         }
+
+        @Override
+        public String getResourceName() {
+            return resourceName;
+        }
     }
 
     private static class ContainerUniversalResource implements UniversalContainer.UniversalResource {
@@ -435,6 +395,11 @@ public ByteResourceInformation getByteResourceInformation(String className, Clas
         public String getNativeLibraryPath() {
             return null;
         }
+
+        @Override
+        public String getResourceName() {
+            return null;
+        }
     }
 
     private static abstract class AbstractUniversalContainer<E> implements UniversalContainer {
@@ -461,6 +426,217 @@ private static abstract class AbstractUniversalContainer<E> implements Universal
         private volatile Map<Name, String> manifestMainAttributes = null;
         private volatile Map<String, Map<Name, String>> manifestEntryAttributes = null;
 
+        private final URL resourceContainerURL;
+        private final URL resourceSharedClassCacheURL;
+        private final File resourceContainerDir;
+        public AbstractUniversalContainer(Collection<URL> containerURLs, ClassLoaderHook hook) {
+            URL originalRoot = null;
+            URL convertedRoot = null;
+            boolean multiple = false;
+            for (URL url : containerURLs) {
+                URL converted = createContainerURL(url);
+                if (converted != null) {
+                    String path = converted.getPath();
+                    if (!path.endsWith(".overlay/")) {
+                        if (convertedRoot == null) {
+                            convertedRoot = converted;
+                            originalRoot = url;
+                        } else {
+                            multiple = true;
+                        }
+                    }
+                }
+            }
+            if (multiple || convertedRoot == null) {
+                resourceContainerURL = null;
+                resourceContainerDir = null;
+                resourceSharedClassCacheURL = null;
+            } else {
+                resourceContainerURL = convertedRoot;
+                File containerFile = null;
+                try {
+                    containerFile = new File(resourceContainerURL.toURI());
+                } catch (URISyntaxException e) {
+                    // Auto-FFDC
+                }
+                resourceContainerDir = containerFile != null && isDirectory(containerFile) ? containerFile : null;
+                resourceSharedClassCacheURL = hook != null ? createSharedClassCacheURL(resourceContainerURL, originalRoot, resourceContainerDir) : null;
+            }
+        }
+
+        URL createContainerURL(URL base) {
+            try {
+                if ("file".equals(base.getProtocol())) {
+                    // use file URLs as-is
+                    return base;
+                }
+
+                URLConnection conn = base.openConnection();
+                if (conn instanceof JarURLConnection) {
+                    return ((JarURLConnection) conn).getJarFileURL();
+                } else if (conn instanceof WSJarURLConnection) {
+                    return ((WSJarURLConnection) conn).getFile().toURI().toURL();
+                }
+                throw new UnsupportedOperationException(base.getProtocol());
+            } catch (IOException err) {
+                throw new RuntimeException(err);
+            }
+        }
+
+        URL createSharedClassCacheURL(URL containerURL, URL originalRoot, File containerDir) {
+            if (containerURL == null) {
+                return null;
+            }
+            if (containerDir != null) {
+                return containerURL;
+            }
+            try {
+                String containerPath = containerURL.getPath();
+                if (containerPath.endsWith(".jar") || containerPath.endsWith(".zip")) {
+                    // use containerURL as-is if it is a jar or zip extension
+                    return containerURL;
+                }
+
+                String basePath = originalRoot.getPath();
+                int bangSlash = basePath.lastIndexOf("!/");
+                if (bangSlash >= 0) {
+                    // append the original !/ path (likely !/WEB-INF/classes)
+                    String bangSlashPath = basePath.substring(bangSlash);
+                    // If it is not a jar or zip and we add !/ only to the URL it will not be treated as a valid
+                    // archive and should fall to the code below to handle that case.
+                    if (bangSlashPath.length() > 2) {
+                        return new URL(containerURL.toExternalForm() + bangSlashPath);
+                    }
+                }
+                // If the URL is not to a directory and does not end with jar or zip file extension then
+                // we assume it is still some type of archive (e.g. rar).
+                // The Semeru shared classes cache logic will not recognize the URL as a valid archive
+                // if it does not end with jar or zip file extension.
+                // The URL will get recognized as a valid archive if it does contain '!/' with any path after.
+                // Here we append '!/l' to the URL so that the Semeru shared classes cache logic will treat it
+                // as a valid archive.  For example: file://path/to/myResourceAdaptor.rar!/l
+                return new URL(containerURL.toExternalForm() + "!/l");
+            } catch (MalformedURLException e) {
+                return null;
+            }
+
+        }
+
+        @Override
+        public URL getContainerURL(UniversalResource resource) {
+            if (resourceContainerURL != null) {
+                if (resourceContainerDir != null) {
+                    // need to make sure the resource is really in this directory
+                    if (exists(new File(resourceContainerDir, resource.getResourceName()))) {
+                        return resourceContainerURL;
+                    }
+                } else {
+                    return resourceContainerURL;
+                }
+            }
+            // TODO asking for "jar" but that is not honored in all cases so still need to handle "wsjar" being returned
+            URL resourceUrl = resource.getResourceURL("jar");
+            if (resourceUrl != null) {
+                String protocol = resourceUrl.getProtocol();
+                try {
+                    if ("jar".equals(protocol)) {
+                        URLConnection conn = resourceUrl.openConnection();
+                        if (conn instanceof JarURLConnection) {
+                            return((JarURLConnection) conn).getJarFileURL();
+                        }
+                        // unexpected; throw exception for FFDC indicating the connection class
+                        throw new IOException(conn.getClass().getName());
+                    } else if ("wsjar".equals(protocol)) {
+                        URLConnection conn = resourceUrl.openConnection();
+                        if (conn instanceof WSJarURLConnection) {
+                            return ((WSJarURLConnection) conn).getFile().toURI().toURL();
+                        }
+                        // unexpected; throw exception for FFDC indicating the connection class
+                        throw new IOException(conn.getClass().getName());
+                    } else if ("file".equals(protocol)) {
+                        // A file URL - i.e. the contents of the classes are expanded on the disk.
+                        // so a path like:  .../myServer/dropins/myWar.war/WEB-INF/classes/com/myPkg/MyClass.class
+                        // should convert to: .../myServer/dropins/myWar.war/WEB-INF/classes/
+                        return new URL(resourceUrl.toString().replace(resource.getResourceName(), ""));
+                    }
+                    // unexpected; throw exception for FFDC indicating the unexpected protocol
+                    throw new IOException(protocol);
+                } catch (IOException e) {
+                    // auto-FFDC
+                }
+            }
+            // TODO it is questionable to allow null here; currently the code handles null.
+            return null;
+        }
+
+        @Override
+        public URL getSharedClassCacheURL(UniversalResource resource) {
+            if (resourceSharedClassCacheURL != null) {
+                if (resourceContainerDir != null) {
+                    // need to make sure the resource is really in this directory
+                    if (exists(new File(resourceContainerDir, resource.getResourceName()))) {
+                        return resourceSharedClassCacheURL;
+                    }
+                } else {
+                    return resourceSharedClassCacheURL;
+                }
+            }
+            return getSharedClassCacheURLFromResource(resource);
+        }
+
+        private URL getSharedClassCacheURLFromResource(UniversalResource resource) {
+            // TODO asking for "jar" but that is not honored in all cases so still need to handle "wsjar" being returned
+            URL resourceURL = resource.getResourceURL("jar");
+            String resourceName = resource.getResourceName();
+            URL sharedClassCacheURL;
+            if (resourceURL == null) {
+                return null;
+            }
+            String protocol = resourceURL.getProtocol();
+            // Doing the conversion that the shared class cache logic does for jar
+            // URLs in order to do less work while holding a shared class cache monitor.
+            if ("jar".equals(protocol) || "wsjar".equals(protocol)) {
+                String path = resourceURL.getPath();
+                // Can only do this for jar files.  Shared class cache logic
+                // cannot handle a file reference that is a war for instance.
+                // Need to use the full path for war files.
+                if (path.endsWith(resourceName)) {
+                    path = path.substring(0, path.length() - resourceName.length());
+                    if (path.endsWith(".jar!/") || path.endsWith(".zip!/")) {
+                        path = path.substring(0, path.length() - 2);
+                    } else {
+                        // If the archive file name does not end with jar or zip file extension and the URL ends with !/, 
+                        // the !/ will get stripped off by the shared classes cache logic and will not be recognized 
+                        // correctly as a jar file when it is a RAR for instance so add an extra character to the end of the URL.
+                        // Without this extra character, RAR files were not being recognized as being updated leading to 
+                        // stale classes being returned after the RAR file was updated.
+                        if (path.endsWith("!/")) {
+                            path += "l";
+                        }
+                    }
+                }
+                try {
+                    sharedClassCacheURL = new URL(path);
+                } catch (MalformedURLException e) {
+                    sharedClassCacheURL = null;
+                }
+            } else if (!"file".equals(protocol)) {
+                sharedClassCacheURL = null;
+            } else {
+                String externalForm = resourceURL.toExternalForm();
+                if (externalForm.endsWith(resourceName)) {
+                    try {
+                        sharedClassCacheURL = new URL(externalForm.substring(0, externalForm.length() - resourceName.length()));
+                    } catch (MalformedURLException e) {
+                        sharedClassCacheURL = null;
+                    }
+                } else {
+                    sharedClassCacheURL = null;
+                }
+            }
+            return sharedClassCacheURL;
+        }
+
         @Override
         @FFDCIgnore(value = { IllegalArgumentException.class })
         public final void definePackage(String packageName, LibertyLoader loader, URL sealBase) {
@@ -617,7 +793,7 @@ private static Map<Name, String> filterAttributes(Attributes attributes) {
 
         abstract URL getResourceURL(E entry);
     }
-    
+
     /**
      * Implementation of a UniversalContainer, backed by an adaptable Container.
      */
@@ -626,7 +802,8 @@ private static class ContainerUniversalContainer extends AbstractUniversalContai
         private final boolean isRoot;
         private String debugString;
 
-        public ContainerUniversalContainer(Container container) {
+        public ContainerUniversalContainer(Container container, ClassLoaderHook hook) {
+            super(container.getURLs(), hook);
             this.container = container;
             this.isRoot = container.isRoot();
             // If we are doing checkpoint, process the manifest file when the container is created.
@@ -786,13 +963,7 @@ public URL getResourceURL(String jarProtocol) {
 
         @Override
         public ByteResourceInformation getByteResourceInformation(String className, ClassLoaderHook hook) throws IOException {
-            byte[] bytes = ContainerClassLoader.getClassBytesFromHook(this, className, resourceName, hook);
-
-            boolean foundInClassCache = bytes != null;
-            if (!foundInClassCache) {
-                bytes = getActualBytes();
-            }
-            return new ByteResourceInformation(bytes, this.entry.getResource(), this.container, resourceName, foundInClassCache, this::getActualBytes);
+            return ContainerClassLoader.getClassBytesFromHook(container, this, className, hook, this::getActualBytes);
         }
 
         byte[] getActualBytes() {
@@ -815,6 +986,11 @@ public String getNativeLibraryPath() {
             }
             return null;
         }
+
+        @Override
+        public String getResourceName() {
+            return resourceName;
+        }
     }
 
     /**
@@ -824,7 +1000,8 @@ private static class ArtifactContainerUniversalContainer extends AbstractUnivers
         final ArtifactContainer container;
         final boolean isRoot;
 
-        public ArtifactContainerUniversalContainer(ArtifactContainer container) {
+        public ArtifactContainerUniversalContainer(ArtifactContainer container, ClassLoaderHook hook) {
+            super(container.getURLs(), hook);
             this.container = container;
             this.isRoot = container.isRoot();
             // If we are doing checkpoint, process the manifest file when the container is created.
@@ -952,6 +1129,11 @@ public ByteResourceInformation getByteResourceInformation(String className, Clas
         public String getNativeLibraryPath() {
             return null;
         }
+
+        @Override
+        public String getResourceName() {
+            return null;
+        }
     }
 
     private interface SmartClassPath {
@@ -1089,6 +1271,12 @@ private static class SmartClassPathImpl implements SmartClassPath {
 
         final Set<Container> containers = Collections.newSetFromMap(new WeakHashMap<Container, Boolean>());
 
+        final ClassLoaderHook hook;
+
+        SmartClassPathImpl(ClassLoaderHook hook) {
+            this.hook = hook;
+        }
+
         /**
          * Internal method to add a new UniversalContainer to the list.
          *
@@ -1147,12 +1335,12 @@ public void run() {
         @Override
         public void addContainer(Container container) {
             containers.add(container);
-            addUniversalContainers(new ContainerUniversalContainer(container));
+            addUniversalContainers(new ContainerUniversalContainer(container, hook));
         }
 
         @Override
         public void addArtifactContainer(ArtifactContainer container) {
-            addUniversalContainers(new ArtifactContainerUniversalContainer(container));
+            addUniversalContainers(new ArtifactContainerUniversalContainer(container, hook));
         }
 
         private List<UniversalContainer> getUniversalContainersForPath(String path, List<UniversalContainer> classpath) {
@@ -1394,8 +1582,8 @@ public Collection<Collection<URL>> getClassPath() {
     private class UnreadSmartClassPath implements SmartClassPath {
         SmartClassPathImpl delegate;
 
-        UnreadSmartClassPath() {
-            delegate = new SmartClassPathImpl();
+        UnreadSmartClassPath(ClassLoaderHook hook) {
+            delegate = new SmartClassPathImpl(hook);
         }
 
         @Override
@@ -1474,23 +1662,25 @@ public Collection<Collection<URL>> getClassPath() {
      */
     static final class ByteResourceInformation {
         private final byte[] bytes;
-        private final URL resourceEntry;
         private final UniversalContainer resourceContainer;
-        private final String resourcePath;
+        private final URL containerURL;
+        private final URL sharedClassCacheURL;
         private final boolean fromClassCache;
         private final Supplier<byte[]> actualBytes;
+        private final ClassLoaderHook hook;
 
         /**
          * @param bytes
          * @param resourceUrl
          */
-        ByteResourceInformation(byte[] bytes, URL resourceUrl, UniversalContainer root, String resourcePath, boolean fromClassCache, Supplier<byte[]> actualBytes) {
+        ByteResourceInformation(byte[] bytes, UniversalContainer root, URL containerURL, URL sharedClassCacheURL, boolean fromClassCache, Supplier<byte[]> actualBytes, ClassLoaderHook hook) {
             this.bytes = bytes;
-            this.resourceEntry = resourceUrl;
             this.resourceContainer = root;
-            this.resourcePath = resourcePath;
+            this.containerURL = containerURL;
+            this.sharedClassCacheURL = sharedClassCacheURL;
             this.fromClassCache = fromClassCache;
             this.actualBytes = actualBytes;
+            this.hook = hook;
         }
 
         /**
@@ -1503,25 +1693,16 @@ public byte[] getBytes() {
         }
 
         void definePackage(String packageName, LibertyLoader loader) {
-            resourceContainer.definePackage(packageName, loader, resourceEntry);
+            resourceContainer.definePackage(packageName, loader, getContainerURL());
         }
 
         /**
-         * Returns the resource URL for this resource
+         * Returns the container URL for this resource
          *
          * @return
          */
-        public URL getResourceUrl() {
-            return this.resourceEntry;
-        }
-
-        /**
-         * Returns the resource style path to this resource, this will be in the form "a/b/c" rather than a . notation.
-         *
-         * @return The resource path
-         */
-        public String getResourcePath() {
-            return this.resourcePath;
+        public URL getContainerURL() {
+            return containerURL;
         }
 
         public boolean foundInClassCache() {
@@ -1531,6 +1712,28 @@ public boolean foundInClassCache() {
         public byte[] getActualBytes() throws IOException {
             return actualBytes.get();
         }
+
+        public void storeInClassCache(Class<?> clazz, byte[] definedBytes ) {
+            if (foundInClassCache() || hook == null) {
+                return;
+            }
+            if (sharedClassCacheURL == null) {
+                if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) {
+                    Tr.debug(tc, "No shared class cache URL to store class", clazz.getName());
+                }
+                return;
+            }
+            if (!Arrays.equals(definedBytes, getBytes())) {
+                if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) {
+                    Tr.debug(tc, "Did not store class because defined bytes got modified", clazz.getName());
+                }
+                return;
+            }
+            hook.storeClass(sharedClassCacheURL, clazz);
+            if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) {
+                Tr.debug(tc, "Called shared class cache to store class", new Object[] {clazz.getName(), sharedClassCacheURL});
+            }
+        }
     }
 
     /**
@@ -1546,7 +1749,8 @@ public ContainerClassLoader(List<Container> classpath, ClassLoader parent, Class
         //Temporary, reintroduced until WSJAR is implemented.
         JarCacheDisabler.disableJarCaching();
 
-        smartClassPath = new UnreadSmartClassPath();
+        hook = disableSharedClassesCache ? null : ClassLoaderHookFactory.getClassLoaderHook(this);
+        smartClassPath = new UnreadSmartClassPath(hook);
 
         if (classpath != null) {
             for (Container c : classpath) {
@@ -1557,6 +1761,10 @@ public ContainerClassLoader(List<Container> classpath, ClassLoader parent, Class
         this.redefiner = redefiner;
     }
 
+    ClassLoaderHook getClassLoaderHook() {
+        return hook;
+    }
+
     @Override
     public URL findResource(String name) {
         //check super first, which checks parent, if any.
@@ -1628,7 +1836,11 @@ protected String findLibrary(String libName) {
         return null;
     }
 
-    protected ByteResourceInformation findClassBytes(String className, String resourceName, ClassLoaderHook hook) throws IOException {
+    // Renamed this method with an Impl name so we can continue to test what happens when this method throws
+    // an IOException in the unit tests with a method override. If it wasn't for the unit test it would be nice to
+    // put the IOException handling directly into this method from AppClassLoader.
+    // TODO consider mocking smartClassPath.getByteResourceInformation to throw an IOException for the test instead
+    ByteResourceInformation findClassBytesImpl(String className, String resourceName) throws IOException {
         Object token = ThreadIdentityManager.runAsServer();
         try {
             return smartClassPath.getByteResourceInformation(className, resourceName, hook);
@@ -1656,7 +1868,7 @@ protected void addToClassPath(Iterable<ArtifactContainer> artifacts) {
     @FFDCIgnore(IllegalStateException.class)
     protected void addLibraryFile(final File f) {
 
-        if (!!!f.exists()) {
+        if (!!!exists(f)) {
             if (tc.isWarningEnabled()) {
                 Tr.warning(tc, "cls.library.archive", f, new FileNotFoundException(f.getName()));
             }
@@ -1664,7 +1876,7 @@ protected void addLibraryFile(final File f) {
         }
 
         // Skip files that are not archives of some sort.
-        if (!f.isDirectory() && !isArchive(f))
+        if (!isDirectory(f) && !isArchive(f))
             return;
 
         //this area subject to refactor following shared lib rework..
@@ -1698,7 +1910,7 @@ public void accept(ArtifactContainerFactory factory) {
     }
 
     protected void addNativeLibraryContainer(Container container) {
-        nativeLibraryContainers.add(new ContainerUniversalContainer(container));
+        nativeLibraryContainers.add(new ContainerUniversalContainer(container, hook));
     }
 
     /**
@@ -1905,4 +2117,22 @@ public byte[] run() throws IOException {
     Collection<Collection<URL>> getClassPath() {
         return smartClassPath.getClassPath();
     }
+
+    static boolean exists(File f) {
+        return System.getSecurityManager() == null ? f.exists() : AccessController.doPrivileged(new PrivilegedAction<Boolean>() {
+            @Override
+            public Boolean run() {
+                return f.exists();
+            }
+        });
+    }
+
+    static boolean isDirectory(File f) {
+        return System.getSecurityManager() == null ? f.isDirectory() : AccessController.doPrivileged(new PrivilegedAction<Boolean>() {
+            @Override
+            public Boolean run() {
+                return f.isDirectory();
+            }
+        });
+    }
 }
diff --git a/dev/com.ibm.ws.classloading_test/test/com/ibm/ws/classloading/internal/LibertyClassLoaderTest.java b/dev/com.ibm.ws.classloading_test/test/com/ibm/ws/classloading/internal/LibertyClassLoaderTest.java
index 7ec3c04f7e8..f0437e94547 100644
--- a/dev/com.ibm.ws.classloading_test/test/com/ibm/ws/classloading/internal/LibertyClassLoaderTest.java
+++ b/dev/com.ibm.ws.classloading_test/test/com/ibm/ws/classloading/internal/LibertyClassLoaderTest.java
@@ -1,10 +1,10 @@
 /*******************************************************************************
- * Copyright (c) 2011, 2022 IBM Corporation and others.
+ * Copyright (c) 2011, 2025 IBM Corporation and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License 2.0
  * which accompanies this distribution, and is available at
  * http://www.eclipse.org/legal/epl-2.0/
- * 
+ *
  * SPDX-License-Identifier: EPL-2.0
  *
  * Contributors:
@@ -35,7 +35,6 @@
 
 import com.ibm.ws.classloading.configuration.GlobalClassloadingConfiguration;
 import com.ibm.ws.classloading.internal.TestUtil.ClassSource;
-import com.ibm.ws.kernel.boot.classloader.ClassLoaderHook;
 import com.ibm.wsspi.adaptable.module.Container;
 import com.ibm.wsspi.classloading.ClassLoaderConfiguration;
 import com.ibm.wsspi.classloading.ClassLoaderIdentity;
@@ -66,8 +65,7 @@ public void testClassFormatError() throws Exception {
 
         loader = new AppClassLoader(loader.getParent(), loader.config, Arrays.asList(c), (DeclaredApiAccess) (loader.getParent()), null, null, new GlobalClassloadingConfiguration(), Collections.emptyList()) {
             @Override
-            protected com.ibm.ws.classloading.internal.AppClassLoader.ByteResourceInformation findClassBytes(String className, String resourceName,
-                                                                                                             ClassLoaderHook hook) throws IOException {
+            protected com.ibm.ws.classloading.internal.AppClassLoader.ByteResourceInformation findClassBytesImpl(String className, String resourceName) throws IOException {
                 throw new IOException();
             }
         };
diff --git a/dev/com.ibm.ws.classloading_test/test/com/ibm/ws/classloading/internal/TransformerTest.java b/dev/com.ibm.ws.classloading_test/test/com/ibm/ws/classloading/internal/TransformerTest.java
index b065f79e074..49ca1c24d9e 100644
--- a/dev/com.ibm.ws.classloading_test/test/com/ibm/ws/classloading/internal/TransformerTest.java
+++ b/dev/com.ibm.ws.classloading_test/test/com/ibm/ws/classloading/internal/TransformerTest.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2012, 2021 IBM Corporation and others.
+ * Copyright (c) 2012, 2025 IBM Corporation and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License 2.0
  * which accompanies this distribution, and is available at
@@ -97,7 +97,7 @@ public byte[] transform(ClassLoader loader, String className, Class<?> classBein
         AppClassLoader loader = createAppClassloaderTransformer(transformer, systemTransformer);
 
         byte[] originalBytes = "Hello!".getBytes();
-        ByteResourceInformation toTransform = new ByteResourceInformation(originalBytes, null, null, null, false, () -> originalBytes);
+        ByteResourceInformation toTransform = new ByteResourceInformation(originalBytes, null, null, null, false, () -> originalBytes, null);
         byte[] transformedBytes = loader.transformClassBytes("hello", toTransform);
 
         assertTrue(transformerInvoked.get());
@@ -143,7 +143,7 @@ public byte[] transform(ClassLoader loader, String className, Class<?> classBein
         AppClassLoader loader = createAppClassloaderTransformer(transformer, systemTransformer);
 
         byte[] originalBytes = "Goodbye!".getBytes();
-        ByteResourceInformation toTransform = new ByteResourceInformation(originalBytes, null, null, null, false, () -> originalBytes);
+        ByteResourceInformation toTransform = new ByteResourceInformation(originalBytes, null, null, null, false, () -> originalBytes, null);
         byte[] transformedBytes = loader.transformClassBytes("goodbye", toTransform);
 
         assertTrue(transformerInvoked.get());
@@ -181,7 +181,7 @@ public byte[] transform(ClassLoader loader, String className, Class<?> classBein
         AppClassLoader loader = createAppClassloaderTransformer(transformer, systemTransformer);
 
         byte[] originalBytes = "Greetings".getBytes();
-        ByteResourceInformation toTransform = new ByteResourceInformation(originalBytes, null, null, null, false, () -> originalBytes);
+        ByteResourceInformation toTransform = new ByteResourceInformation(originalBytes, null, null, null, false, () -> originalBytes, null);
         byte[] transformedBytes = loader.transformClassBytes("greetings", toTransform);
 
         assertTrue(transformerInvoked.get());
@@ -216,7 +216,7 @@ public byte[] transform(ClassLoader loader, String className, Class<?> classBein
 
         byte[] originalBytes = "Greetings".getBytes();
         final boolean fromCached = true;
-        ByteResourceInformation toTransform = new ByteResourceInformation(originalBytes, null, null, null, fromCached, () -> originalBytes);
+        ByteResourceInformation toTransform = new ByteResourceInformation(originalBytes, null, null, null, fromCached, () -> originalBytes, null);
         byte[] transformedBytes = loader.transformClassBytes("greetings", toTransform);
 
         if (systemTransformer) {


Reviews:
user: login: jhanders34
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-26T18:07:33Z
user: login: tjwatson
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-26T18:40:36Z
user: login: tjwatson
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-26T18:54:38Z
user: login: jhanders34
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-26T19:15:09Z
user: login: tjwatson
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-26T19:17:38Z
user: login: tjwatson
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-26T19:21:48Z
user: login: tjwatson
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-26T19:27:36Z
user: login: tjwatson
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-26T19:42:19Z
user: login: tjwatson
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-26T19:43:01Z
user: login: tjwatson
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-26T19:43:13Z
user: login: tjwatson
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-26T19:44:48Z
user: login: jhanders34
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-26T19:50:22Z
user: login: jhanders34
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-26T19:50:54Z

Review comments:
diff_hunk: @@ -97,7 +97,7 @@ public byte[] transform(ClassLoader loader, String className, Class<?> classBein
         AppClassLoader loader = createAppClassloaderTransformer(transformer, systemTransformer);
 
         byte[] originalBytes = "Hello!".getBytes();
-        ByteResourceInformation toTransform = new ByteResourceInformation(originalBytes, null, null, null, false, () -> originalBytes);
+        ByteResourceInformation toTransform = new ByteResourceInformation(originalBytes, null, null, null, false, () -> originalBytes, null);
path: dev/com.ibm.ws.classloading_test/test/com/ibm/ws/classloading/internal/TransformerTest.java
user: login: jhanders34
body: Copyright date update
created_at: 2025-03-26T17:02:42Z
author_association: MEMBER
diff_hunk: @@ -70,9 +70,7 @@ public class MockContainer implements Container {
 
     private Set<String> getImmediateChildren() {
         URL url = ucl.getURLs()[0];
-        return (url.toString().endsWith(".jar"))
-                        ? getChildrenFromJar(url, path)
-                        : getChildrenFromDir(url, path);
+        return (url.toString().endsWith(".jar")) ? getChildrenFromJar(url, path) : getChildrenFromDir(url, path);
path: dev/com.ibm.ws.classloading_test/test/com/ibm/ws/classloading/internal/MockContainer.java
user: login: jhanders34
body: Any reason not to undo this file's changes completely since they are just formatting changes?
created_at: 2025-03-26T17:03:47Z
author_association: MEMBER
diff_hunk: @@ -66,8 +65,7 @@ public void testClassFormatError() throws Exception {
 
         loader = new AppClassLoader(loader.getParent(), loader.config, Arrays.asList(c), (DeclaredApiAccess) (loader.getParent()), null, null, new GlobalClassloadingConfiguration(), Collections.emptyList()) {
             @Override
-            protected com.ibm.ws.classloading.internal.AppClassLoader.ByteResourceInformation findClassBytes(String className, String resourceName,
-                                                                                                             ClassLoaderHook hook) throws IOException {
+            protected com.ibm.ws.classloading.internal.AppClassLoader.ByteResourceInformation findClassBytesImpl(String className, String resourceName) throws IOException {
path: dev/com.ibm.ws.classloading_test/test/com/ibm/ws/classloading/internal/LibertyClassLoaderTest.java
user: login: jhanders34
body: Copyright date update?
created_at: 2025-03-26T17:04:21Z
author_association: MEMBER
diff_hunk: @@ -24,9 +24,7 @@
 import java.io.InputStream;
 import java.lang.instrument.ClassFileTransformer;
 import java.lang.instrument.IllegalClassFormatException;
-import java.net.JarURLConnection;
path: dev/com.ibm.ws.classloading/src/com/ibm/ws/classloading/internal/AppClassLoader.java
user: login: jhanders34
body: Copyright date update
created_at: 2025-03-26T17:18:36Z
author_association: MEMBER
diff_hunk: @@ -1628,7 +1831,10 @@ protected String findLibrary(String libName) {
         return null;
     }
 
-    protected ByteResourceInformation findClassBytes(String className, String resourceName, ClassLoaderHook hook) throws IOException {
+    // TODO renamed this method with Impl so we can continue to test what happens when this method throws
path: dev/com.ibm.ws.classloading/src/com/ibm/ws/classloading/internal/ContainerClassLoader.java
user: login: jhanders34
body: Not sure if the `TODO` should still be in the comment.  What is the todo?  The comment doesn't say what is left to do, just what was done.
created_at: 2025-03-26T17:34:50Z
author_association: MEMBER
diff_hunk: @@ -461,6 +426,212 @@ private static abstract class AbstractUniversalContainer<E> implements Universal
         private volatile Map<Name, String> manifestMainAttributes = null;
         private volatile Map<String, Map<Name, String>> manifestEntryAttributes = null;
 
+        private final URL resourceContainerURL;
+        private final URL resourceSharedClassCacheURL;
+        private final File resourceContainerDir;
+        public AbstractUniversalContainer(Collection<URL> containerURLs, ClassLoaderHook hook) {
+            URL originalRoot = null;
+            URL convertedRoot = null;
+            boolean multiple = false;
+            for (URL url : containerURLs) {
+                URL converted = createContainerURL(url);
+                if (converted != null) {
+                    String path = converted.getPath();
+                    if (!path.endsWith(".overlay/")) {
+                        if (convertedRoot == null) {
+                            convertedRoot = converted;
+                            originalRoot = url;
+                        } else {
+                            multiple = true;
+                        }
+                    }
+                }
+            }
+            if (multiple) {
+                resourceContainerURL = null;
+                resourceContainerDir = null;
+                resourceSharedClassCacheURL = null;
+            } else {
+                resourceContainerURL = convertedRoot;
+                File containerFile = null;
+                try {
+                    containerFile = resourceContainerURL != null ? new File(resourceContainerURL.toURI()) : null;
path: dev/com.ibm.ws.classloading/src/com/ibm/ws/classloading/internal/ContainerClassLoader.java
user: login: jhanders34
body: ```suggestion
            if (multiple || convertedRoot == null) {
                resourceContainerURL = null;
                resourceContainerDir = null;
                resourceSharedClassCacheURL = null;
            } else {
                resourceContainerURL = convertedRoot;
                File containerFile = null;
                try {
                    containerFile = new File(resourceContainerURL.toURI());
```
created_at: 2025-03-26T17:41:52Z
author_association: MEMBER
diff_hunk: @@ -461,6 +426,212 @@ private static abstract class AbstractUniversalContainer<E> implements Universal
         private volatile Map<Name, String> manifestMainAttributes = null;
         private volatile Map<String, Map<Name, String>> manifestEntryAttributes = null;
 
+        private final URL resourceContainerURL;
+        private final URL resourceSharedClassCacheURL;
+        private final File resourceContainerDir;
+        public AbstractUniversalContainer(Collection<URL> containerURLs, ClassLoaderHook hook) {
+            URL originalRoot = null;
+            URL convertedRoot = null;
+            boolean multiple = false;
+            for (URL url : containerURLs) {
+                URL converted = createContainerURL(url);
+                if (converted != null) {
+                    String path = converted.getPath();
+                    if (!path.endsWith(".overlay/")) {
+                        if (convertedRoot == null) {
+                            convertedRoot = converted;
+                            originalRoot = url;
+                        } else {
+                            multiple = true;
+                        }
+                    }
+                }
+            }
+            if (multiple) {
+                resourceContainerURL = null;
+                resourceContainerDir = null;
+                resourceSharedClassCacheURL = null;
+            } else {
+                resourceContainerURL = convertedRoot;
+                File containerFile = null;
+                try {
+                    containerFile = resourceContainerURL != null ? new File(resourceContainerURL.toURI()) : null;
+                } catch (URISyntaxException e) {
+                    // Auto-FFDC
+                }
+                resourceContainerDir = containerFile != null && isDirectory(containerFile) ? containerFile : null;
+                resourceSharedClassCacheURL = hook != null ? createSharedClassCacheURL(resourceContainerURL, originalRoot, resourceContainerDir) : null;
+            }
+        }
+
+        URL createContainerURL(URL base) {
+            try {
+                if ("file".equals(base.getProtocol())) {
+                    // use file URLs as-is
+                    return base;
+                }
+
+                URLConnection conn = base.openConnection();
+                if (conn instanceof JarURLConnection) {
+                    return ((JarURLConnection) conn).getJarFileURL();
+                } else if (conn instanceof WSJarURLConnection) {
+                    return ((WSJarURLConnection) conn).getFile().toURI().toURL();
+                }
+                throw new UnsupportedOperationException(base.getProtocol());
+            } catch (IOException err) {
+                throw new RuntimeException(err);
+            }
+        }
+
+        URL createSharedClassCacheURL(URL containerURL, URL originalRoot, File containerDir) {
+            if (containerURL == null) {
+                return null;
+            }
+            if (containerDir != null) {
+                return containerURL;
+            }
+            try {
+                String containerPath = containerURL.getPath();
+                if (containerPath.endsWith(".jar") || containerPath.endsWith(".zip")) {
+                    // use containerURL as-is if it is a jar or zip extension
+                    return containerURL;
+                }
+
+                String basePath = originalRoot.getPath();
+                int bangSlash = basePath.lastIndexOf("!/");
+                if (bangSlash >= 0) {
+                    // append the original !/ path (likely !/WEB-INF/classes)
+                    return new URL(containerURL.toExternalForm() + basePath.substring(bangSlash));
+                }
+                // If the URL is not to a directory and does not end ith jar or zip file extension then
path: dev/com.ibm.ws.classloading/src/com/ibm/ws/classloading/internal/ContainerClassLoader.java
user: login: jhanders34
body: ```suggestion
                // If the URL is not to a directory and does not end with jar or zip file extension then
```
created_at: 2025-03-26T17:46:19Z
author_association: MEMBER
diff_hunk: @@ -461,6 +426,212 @@ private static abstract class AbstractUniversalContainer<E> implements Universal
         private volatile Map<Name, String> manifestMainAttributes = null;
         private volatile Map<String, Map<Name, String>> manifestEntryAttributes = null;
 
+        private final URL resourceContainerURL;
+        private final URL resourceSharedClassCacheURL;
+        private final File resourceContainerDir;
+        public AbstractUniversalContainer(Collection<URL> containerURLs, ClassLoaderHook hook) {
+            URL originalRoot = null;
+            URL convertedRoot = null;
+            boolean multiple = false;
+            for (URL url : containerURLs) {
+                URL converted = createContainerURL(url);
+                if (converted != null) {
+                    String path = converted.getPath();
+                    if (!path.endsWith(".overlay/")) {
+                        if (convertedRoot == null) {
+                            convertedRoot = converted;
+                            originalRoot = url;
+                        } else {
+                            multiple = true;
+                        }
+                    }
+                }
+            }
+            if (multiple) {
+                resourceContainerURL = null;
+                resourceContainerDir = null;
+                resourceSharedClassCacheURL = null;
+            } else {
+                resourceContainerURL = convertedRoot;
+                File containerFile = null;
+                try {
+                    containerFile = resourceContainerURL != null ? new File(resourceContainerURL.toURI()) : null;
+                } catch (URISyntaxException e) {
+                    // Auto-FFDC
+                }
+                resourceContainerDir = containerFile != null && isDirectory(containerFile) ? containerFile : null;
+                resourceSharedClassCacheURL = hook != null ? createSharedClassCacheURL(resourceContainerURL, originalRoot, resourceContainerDir) : null;
+            }
+        }
+
+        URL createContainerURL(URL base) {
+            try {
+                if ("file".equals(base.getProtocol())) {
+                    // use file URLs as-is
+                    return base;
+                }
+
+                URLConnection conn = base.openConnection();
+                if (conn instanceof JarURLConnection) {
+                    return ((JarURLConnection) conn).getJarFileURL();
+                } else if (conn instanceof WSJarURLConnection) {
+                    return ((WSJarURLConnection) conn).getFile().toURI().toURL();
+                }
+                throw new UnsupportedOperationException(base.getProtocol());
+            } catch (IOException err) {
+                throw new RuntimeException(err);
+            }
+        }
+
+        URL createSharedClassCacheURL(URL containerURL, URL originalRoot, File containerDir) {
+            if (containerURL == null) {
+                return null;
+            }
+            if (containerDir != null) {
+                return containerURL;
+            }
+            try {
+                String containerPath = containerURL.getPath();
+                if (containerPath.endsWith(".jar") || containerPath.endsWith(".zip")) {
+                    // use containerURL as-is if it is a jar or zip extension
+                    return containerURL;
+                }
+
+                String basePath = originalRoot.getPath();
+                int bangSlash = basePath.lastIndexOf("!/");
+                if (bangSlash >= 0) {
+                    // append the original !/ path (likely !/WEB-INF/classes)
+                    return new URL(containerURL.toExternalForm() + basePath.substring(bangSlash));
path: dev/com.ibm.ws.classloading/src/com/ibm/ws/classloading/internal/ContainerClassLoader.java
user: login: jhanders34
body: ```suggestion
                    // append the original !/ path (likely !/WEB-INF/classes)
                    String bangSlashPath = basePath.substring(bangSlash);
                    // If it is not a jar or zip and we add !/ only to the URL it will not be treated as a valid
                    // archive and should fall to the code below to handle that case.
                    if (bangSlashPath.length() > 2) {
                        return new URL(containerURL.toExternalForm() + bangSlashPath);
                    }
```
created_at: 2025-03-26T17:50:39Z
author_association: MEMBER
diff_hunk: @@ -461,6 +426,212 @@ private static abstract class AbstractUniversalContainer<E> implements Universal
         private volatile Map<Name, String> manifestMainAttributes = null;
         private volatile Map<String, Map<Name, String>> manifestEntryAttributes = null;
 
+        private final URL resourceContainerURL;
+        private final URL resourceSharedClassCacheURL;
+        private final File resourceContainerDir;
+        public AbstractUniversalContainer(Collection<URL> containerURLs, ClassLoaderHook hook) {
+            URL originalRoot = null;
+            URL convertedRoot = null;
+            boolean multiple = false;
+            for (URL url : containerURLs) {
+                URL converted = createContainerURL(url);
+                if (converted != null) {
+                    String path = converted.getPath();
+                    if (!path.endsWith(".overlay/")) {
+                        if (convertedRoot == null) {
+                            convertedRoot = converted;
+                            originalRoot = url;
+                        } else {
+                            multiple = true;
+                        }
+                    }
+                }
+            }
+            if (multiple) {
+                resourceContainerURL = null;
+                resourceContainerDir = null;
+                resourceSharedClassCacheURL = null;
+            } else {
+                resourceContainerURL = convertedRoot;
+                File containerFile = null;
+                try {
+                    containerFile = resourceContainerURL != null ? new File(resourceContainerURL.toURI()) : null;
+                } catch (URISyntaxException e) {
+                    // Auto-FFDC
+                }
+                resourceContainerDir = containerFile != null && isDirectory(containerFile) ? containerFile : null;
+                resourceSharedClassCacheURL = hook != null ? createSharedClassCacheURL(resourceContainerURL, originalRoot, resourceContainerDir) : null;
+            }
+        }
+
+        URL createContainerURL(URL base) {
+            try {
+                if ("file".equals(base.getProtocol())) {
+                    // use file URLs as-is
+                    return base;
+                }
+
+                URLConnection conn = base.openConnection();
+                if (conn instanceof JarURLConnection) {
+                    return ((JarURLConnection) conn).getJarFileURL();
+                } else if (conn instanceof WSJarURLConnection) {
+                    return ((WSJarURLConnection) conn).getFile().toURI().toURL();
+                }
+                throw new UnsupportedOperationException(base.getProtocol());
+            } catch (IOException err) {
+                throw new RuntimeException(err);
+            }
+        }
+
+        URL createSharedClassCacheURL(URL containerURL, URL originalRoot, File containerDir) {
+            if (containerURL == null) {
+                return null;
+            }
+            if (containerDir != null) {
+                return containerURL;
+            }
+            try {
+                String containerPath = containerURL.getPath();
+                if (containerPath.endsWith(".jar") || containerPath.endsWith(".zip")) {
+                    // use containerURL as-is if it is a jar or zip extension
+                    return containerURL;
+                }
+
+                String basePath = originalRoot.getPath();
+                int bangSlash = basePath.lastIndexOf("!/");
+                if (bangSlash >= 0) {
+                    // append the original !/ path (likely !/WEB-INF/classes)
+                    return new URL(containerURL.toExternalForm() + basePath.substring(bangSlash));
+                }
+                // If the URL is not to a directory and does not end ith jar or zip file extension then
+                // we assume it is still some type of archive (e.g. rar).
+                // The Semeru shared classes cache logic will not recognize the URL as a valid archive
+                // if it does not end with jar or zip file extension.
+                // The URL will get recognized as a valid archive if it does contain '!/' with any path after.
+                // Here we append '!/l' to the URL so that the Semeru shared classes cache logic will treat it
+                // as a valid archive.  For example: file://path/to/myResourceAdaptor.rar!/l
+                return new URL(containerURL + "!/l");
path: dev/com.ibm.ws.classloading/src/com/ibm/ws/classloading/internal/ContainerClassLoader.java
user: login: jhanders34
body: ```suggestion
                return new URL(containerURL.toExternalForm() + "!/l");
```
To be consistent with the code above this section.
created_at: 2025-03-26T17:52:53Z
author_association: MEMBER
diff_hunk: @@ -461,6 +426,212 @@ private static abstract class AbstractUniversalContainer<E> implements Universal
         private volatile Map<Name, String> manifestMainAttributes = null;
         private volatile Map<String, Map<Name, String>> manifestEntryAttributes = null;
 
+        private final URL resourceContainerURL;
+        private final URL resourceSharedClassCacheURL;
+        private final File resourceContainerDir;
+        public AbstractUniversalContainer(Collection<URL> containerURLs, ClassLoaderHook hook) {
+            URL originalRoot = null;
+            URL convertedRoot = null;
+            boolean multiple = false;
+            for (URL url : containerURLs) {
+                URL converted = createContainerURL(url);
+                if (converted != null) {
+                    String path = converted.getPath();
+                    if (!path.endsWith(".overlay/")) {
+                        if (convertedRoot == null) {
+                            convertedRoot = converted;
+                            originalRoot = url;
+                        } else {
+                            multiple = true;
+                        }
+                    }
+                }
+            }
+            if (multiple) {
+                resourceContainerURL = null;
+                resourceContainerDir = null;
+                resourceSharedClassCacheURL = null;
+            } else {
+                resourceContainerURL = convertedRoot;
+                File containerFile = null;
+                try {
+                    containerFile = resourceContainerURL != null ? new File(resourceContainerURL.toURI()) : null;
+                } catch (URISyntaxException e) {
+                    // Auto-FFDC
+                }
+                resourceContainerDir = containerFile != null && isDirectory(containerFile) ? containerFile : null;
+                resourceSharedClassCacheURL = hook != null ? createSharedClassCacheURL(resourceContainerURL, originalRoot, resourceContainerDir) : null;
+            }
+        }
+
+        URL createContainerURL(URL base) {
+            try {
+                if ("file".equals(base.getProtocol())) {
+                    // use file URLs as-is
+                    return base;
+                }
+
+                URLConnection conn = base.openConnection();
+                if (conn instanceof JarURLConnection) {
+                    return ((JarURLConnection) conn).getJarFileURL();
+                } else if (conn instanceof WSJarURLConnection) {
+                    return ((WSJarURLConnection) conn).getFile().toURI().toURL();
+                }
+                throw new UnsupportedOperationException(base.getProtocol());
+            } catch (IOException err) {
+                throw new RuntimeException(err);
+            }
+        }
+
+        URL createSharedClassCacheURL(URL containerURL, URL originalRoot, File containerDir) {
+            if (containerURL == null) {
+                return null;
+            }
+            if (containerDir != null) {
+                return containerURL;
+            }
+            try {
+                String containerPath = containerURL.getPath();
+                if (containerPath.endsWith(".jar") || containerPath.endsWith(".zip")) {
+                    // use containerURL as-is if it is a jar or zip extension
+                    return containerURL;
+                }
+
+                String basePath = originalRoot.getPath();
+                int bangSlash = basePath.lastIndexOf("!/");
+                if (bangSlash >= 0) {
+                    // append the original !/ path (likely !/WEB-INF/classes)
+                    return new URL(containerURL.toExternalForm() + basePath.substring(bangSlash));
+                }
+                // If the URL is not to a directory and does not end ith jar or zip file extension then
+                // we assume it is still some type of archive (e.g. rar).
+                // The Semeru shared classes cache logic will not recognize the URL as a valid archive
+                // if it does not end with jar or zip file extension.
+                // The URL will get recognized as a valid archive if it does contain '!/' with any path after.
+                // Here we append '!/l' to the URL so that the Semeru shared classes cache logic will treat it
+                // as a valid archive.  For example: file://path/to/myResourceAdaptor.rar!/l
+                return new URL(containerURL + "!/l");
+            } catch (MalformedURLException e) {
+                return null;
+            }
+
+        }
+
+        @Override
+        public URL getContainerURL(UniversalResource resource) {
+            if (resourceContainerURL != null) {
+                if (resourceContainerDir != null) {
+                    // need to make sure the resource is really in this directory
+                    if (exists(new File(resourceContainerDir, resource.getResourceName()))) {
+                        return resourceContainerURL;
+                    }
+                } else {
+                    return resourceContainerURL;
+                }
+            }
+            // TODO asking for "jar" but that is not honored in all cases so still need to handle "wsjar" being returned
+            URL resourceUrl = resource.getResourceURL("jar");
+            if (resourceUrl != null) {
+                String protocol = resourceUrl.getProtocol();
+                try {
+                    if ("jar".equals(protocol)) {
+                        URLConnection conn = resourceUrl.openConnection();
+                        if (conn instanceof JarURLConnection) {
+                            return((JarURLConnection) conn).getJarFileURL();
+                        }
+                        // unexpected; throw exception for FFDC indicating the connection class
+                        throw new IOException(conn.getClass().getName());
+                    } else if ("wsjar".equals(protocol)) {
+                        URLConnection conn = resourceUrl.openConnection();
+                        if (conn instanceof WSJarURLConnection) {
+                            return ((WSJarURLConnection) conn).getFile().toURI().toURL();
+                        }
+                        // unexpected; throw exception for FFDC indicating the connection class
+                        throw new IOException(conn.getClass().getName());
+                    } else if ("file".equals(protocol)) {
+                        // A file URL - i.e. the contents of the classes are expanded on the disk.
+                        // so a path like:  .../myServer/dropins/myWar.war/WEB-INF/classes/com/myPkg/MyClass.class
+                        // should convert to: .../myServer/dropins/myWar.war/WEB-INF/classes/
+                        return new URL(resourceUrl.toString().replace(resource.getResourceName(), ""));
path: dev/com.ibm.ws.classloading/src/com/ibm/ws/classloading/internal/ContainerClassLoader.java
user: login: jhanders34
body: So if the resource URL doesn't end with the resource Name, the container URL will be the resource URL.  Is that what we want?
created_at: 2025-03-26T17:58:44Z
author_association: MEMBER
diff_hunk: @@ -1628,7 +1831,10 @@ protected String findLibrary(String libName) {
         return null;
     }
 
-    protected ByteResourceInformation findClassBytes(String className, String resourceName, ClassLoaderHook hook) throws IOException {
+    // TODO renamed this method with Impl so we can continue to test what happens when this method throws
path: dev/com.ibm.ws.classloading/src/com/ibm/ws/classloading/internal/ContainerClassLoader.java
user: login: tjwatson
body: If I had my way we would not have to do this at all and we would have all the logic of `findClassBytes` in this class instead of spread between AppClassLoader and this class simply so AppClassLoader can catch the possible IOException from this method and log an error.

The unit tests depend on this so they can override this method to force an IOException.  I didn't spend the time to figure out if I could mock up the `smartClassPath.getByteResourceInformation` method in the unit test to throw an IOException instead.  But that would be more clean than the way it is now.
created_at: 2025-03-26T18:40:36Z
author_association: MEMBER
diff_hunk: @@ -461,6 +426,212 @@ private static abstract class AbstractUniversalContainer<E> implements Universal
         private volatile Map<Name, String> manifestMainAttributes = null;
         private volatile Map<String, Map<Name, String>> manifestEntryAttributes = null;
 
+        private final URL resourceContainerURL;
+        private final URL resourceSharedClassCacheURL;
+        private final File resourceContainerDir;
+        public AbstractUniversalContainer(Collection<URL> containerURLs, ClassLoaderHook hook) {
+            URL originalRoot = null;
+            URL convertedRoot = null;
+            boolean multiple = false;
+            for (URL url : containerURLs) {
+                URL converted = createContainerURL(url);
+                if (converted != null) {
+                    String path = converted.getPath();
+                    if (!path.endsWith(".overlay/")) {
+                        if (convertedRoot == null) {
+                            convertedRoot = converted;
+                            originalRoot = url;
+                        } else {
+                            multiple = true;
+                        }
+                    }
+                }
+            }
+            if (multiple) {
+                resourceContainerURL = null;
+                resourceContainerDir = null;
+                resourceSharedClassCacheURL = null;
+            } else {
+                resourceContainerURL = convertedRoot;
+                File containerFile = null;
+                try {
+                    containerFile = resourceContainerURL != null ? new File(resourceContainerURL.toURI()) : null;
+                } catch (URISyntaxException e) {
+                    // Auto-FFDC
+                }
+                resourceContainerDir = containerFile != null && isDirectory(containerFile) ? containerFile : null;
+                resourceSharedClassCacheURL = hook != null ? createSharedClassCacheURL(resourceContainerURL, originalRoot, resourceContainerDir) : null;
+            }
+        }
+
+        URL createContainerURL(URL base) {
+            try {
+                if ("file".equals(base.getProtocol())) {
+                    // use file URLs as-is
+                    return base;
+                }
+
+                URLConnection conn = base.openConnection();
+                if (conn instanceof JarURLConnection) {
+                    return ((JarURLConnection) conn).getJarFileURL();
+                } else if (conn instanceof WSJarURLConnection) {
+                    return ((WSJarURLConnection) conn).getFile().toURI().toURL();
+                }
+                throw new UnsupportedOperationException(base.getProtocol());
+            } catch (IOException err) {
+                throw new RuntimeException(err);
+            }
+        }
+
+        URL createSharedClassCacheURL(URL containerURL, URL originalRoot, File containerDir) {
+            if (containerURL == null) {
+                return null;
+            }
+            if (containerDir != null) {
+                return containerURL;
+            }
+            try {
+                String containerPath = containerURL.getPath();
+                if (containerPath.endsWith(".jar") || containerPath.endsWith(".zip")) {
+                    // use containerURL as-is if it is a jar or zip extension
+                    return containerURL;
+                }
+
+                String basePath = originalRoot.getPath();
+                int bangSlash = basePath.lastIndexOf("!/");
+                if (bangSlash >= 0) {
+                    // append the original !/ path (likely !/WEB-INF/classes)
+                    return new URL(containerURL.toExternalForm() + basePath.substring(bangSlash));
path: dev/com.ibm.ws.classloading/src/com/ibm/ws/classloading/internal/ContainerClassLoader.java
user: login: tjwatson
body: I will apply this suggestion because it is not technically wrong, but I am unsure it is necessary.  I don't believe there should be a case where we will see an original container URL that looks like `jar:file://path/to/myRA.rar!/` where it points to the root of the RAR.  Instead I think we will just get a `file://path/to/myRA.rar` URL there.

The testcase does have a `.rar` which contains classes itself (instead of just having classes in an embedded JAR) and I never saw a URL come like `jar:file://path/to/myRA.rar!/` to load classes from the RAR file.
created_at: 2025-03-26T18:54:38Z
author_association: MEMBER
diff_hunk: @@ -461,6 +426,212 @@ private static abstract class AbstractUniversalContainer<E> implements Universal
         private volatile Map<Name, String> manifestMainAttributes = null;
         private volatile Map<String, Map<Name, String>> manifestEntryAttributes = null;
 
+        private final URL resourceContainerURL;
+        private final URL resourceSharedClassCacheURL;
+        private final File resourceContainerDir;
+        public AbstractUniversalContainer(Collection<URL> containerURLs, ClassLoaderHook hook) {
+            URL originalRoot = null;
+            URL convertedRoot = null;
+            boolean multiple = false;
+            for (URL url : containerURLs) {
+                URL converted = createContainerURL(url);
+                if (converted != null) {
+                    String path = converted.getPath();
+                    if (!path.endsWith(".overlay/")) {
+                        if (convertedRoot == null) {
+                            convertedRoot = converted;
+                            originalRoot = url;
+                        } else {
+                            multiple = true;
+                        }
+                    }
+                }
+            }
+            if (multiple) {
+                resourceContainerURL = null;
+                resourceContainerDir = null;
+                resourceSharedClassCacheURL = null;
+            } else {
+                resourceContainerURL = convertedRoot;
+                File containerFile = null;
+                try {
+                    containerFile = resourceContainerURL != null ? new File(resourceContainerURL.toURI()) : null;
+                } catch (URISyntaxException e) {
+                    // Auto-FFDC
+                }
+                resourceContainerDir = containerFile != null && isDirectory(containerFile) ? containerFile : null;
+                resourceSharedClassCacheURL = hook != null ? createSharedClassCacheURL(resourceContainerURL, originalRoot, resourceContainerDir) : null;
+            }
+        }
+
+        URL createContainerURL(URL base) {
+            try {
+                if ("file".equals(base.getProtocol())) {
+                    // use file URLs as-is
+                    return base;
+                }
+
+                URLConnection conn = base.openConnection();
+                if (conn instanceof JarURLConnection) {
+                    return ((JarURLConnection) conn).getJarFileURL();
+                } else if (conn instanceof WSJarURLConnection) {
+                    return ((WSJarURLConnection) conn).getFile().toURI().toURL();
+                }
+                throw new UnsupportedOperationException(base.getProtocol());
+            } catch (IOException err) {
+                throw new RuntimeException(err);
+            }
+        }
+
+        URL createSharedClassCacheURL(URL containerURL, URL originalRoot, File containerDir) {
+            if (containerURL == null) {
+                return null;
+            }
+            if (containerDir != null) {
+                return containerURL;
+            }
+            try {
+                String containerPath = containerURL.getPath();
+                if (containerPath.endsWith(".jar") || containerPath.endsWith(".zip")) {
+                    // use containerURL as-is if it is a jar or zip extension
+                    return containerURL;
+                }
+
+                String basePath = originalRoot.getPath();
+                int bangSlash = basePath.lastIndexOf("!/");
+                if (bangSlash >= 0) {
+                    // append the original !/ path (likely !/WEB-INF/classes)
+                    return new URL(containerURL.toExternalForm() + basePath.substring(bangSlash));
path: dev/com.ibm.ws.classloading/src/com/ibm/ws/classloading/internal/ContainerClassLoader.java
user: login: jhanders34
body: Yeah likely it won't happen, but if it ever did we would end up getting an unusable shared class cache URL which would not be helpful.  
created_at: 2025-03-26T19:15:09Z
author_association: MEMBER
diff_hunk: @@ -461,6 +426,212 @@ private static abstract class AbstractUniversalContainer<E> implements Universal
         private volatile Map<Name, String> manifestMainAttributes = null;
         private volatile Map<String, Map<Name, String>> manifestEntryAttributes = null;
 
+        private final URL resourceContainerURL;
+        private final URL resourceSharedClassCacheURL;
+        private final File resourceContainerDir;
+        public AbstractUniversalContainer(Collection<URL> containerURLs, ClassLoaderHook hook) {
+            URL originalRoot = null;
+            URL convertedRoot = null;
+            boolean multiple = false;
+            for (URL url : containerURLs) {
+                URL converted = createContainerURL(url);
+                if (converted != null) {
+                    String path = converted.getPath();
+                    if (!path.endsWith(".overlay/")) {
+                        if (convertedRoot == null) {
+                            convertedRoot = converted;
+                            originalRoot = url;
+                        } else {
+                            multiple = true;
+                        }
+                    }
+                }
+            }
+            if (multiple) {
+                resourceContainerURL = null;
+                resourceContainerDir = null;
+                resourceSharedClassCacheURL = null;
+            } else {
+                resourceContainerURL = convertedRoot;
+                File containerFile = null;
+                try {
+                    containerFile = resourceContainerURL != null ? new File(resourceContainerURL.toURI()) : null;
+                } catch (URISyntaxException e) {
+                    // Auto-FFDC
+                }
+                resourceContainerDir = containerFile != null && isDirectory(containerFile) ? containerFile : null;
+                resourceSharedClassCacheURL = hook != null ? createSharedClassCacheURL(resourceContainerURL, originalRoot, resourceContainerDir) : null;
+            }
+        }
+
+        URL createContainerURL(URL base) {
+            try {
+                if ("file".equals(base.getProtocol())) {
+                    // use file URLs as-is
+                    return base;
+                }
+
+                URLConnection conn = base.openConnection();
+                if (conn instanceof JarURLConnection) {
+                    return ((JarURLConnection) conn).getJarFileURL();
+                } else if (conn instanceof WSJarURLConnection) {
+                    return ((WSJarURLConnection) conn).getFile().toURI().toURL();
+                }
+                throw new UnsupportedOperationException(base.getProtocol());
+            } catch (IOException err) {
+                throw new RuntimeException(err);
+            }
+        }
+
+        URL createSharedClassCacheURL(URL containerURL, URL originalRoot, File containerDir) {
+            if (containerURL == null) {
+                return null;
+            }
+            if (containerDir != null) {
+                return containerURL;
+            }
+            try {
+                String containerPath = containerURL.getPath();
+                if (containerPath.endsWith(".jar") || containerPath.endsWith(".zip")) {
+                    // use containerURL as-is if it is a jar or zip extension
+                    return containerURL;
+                }
+
+                String basePath = originalRoot.getPath();
+                int bangSlash = basePath.lastIndexOf("!/");
+                if (bangSlash >= 0) {
+                    // append the original !/ path (likely !/WEB-INF/classes)
+                    return new URL(containerURL.toExternalForm() + basePath.substring(bangSlash));
+                }
+                // If the URL is not to a directory and does not end ith jar or zip file extension then
+                // we assume it is still some type of archive (e.g. rar).
+                // The Semeru shared classes cache logic will not recognize the URL as a valid archive
+                // if it does not end with jar or zip file extension.
+                // The URL will get recognized as a valid archive if it does contain '!/' with any path after.
+                // Here we append '!/l' to the URL so that the Semeru shared classes cache logic will treat it
+                // as a valid archive.  For example: file://path/to/myResourceAdaptor.rar!/l
+                return new URL(containerURL + "!/l");
+            } catch (MalformedURLException e) {
+                return null;
+            }
+
+        }
+
+        @Override
+        public URL getContainerURL(UniversalResource resource) {
+            if (resourceContainerURL != null) {
+                if (resourceContainerDir != null) {
+                    // need to make sure the resource is really in this directory
+                    if (exists(new File(resourceContainerDir, resource.getResourceName()))) {
+                        return resourceContainerURL;
+                    }
+                } else {
+                    return resourceContainerURL;
+                }
+            }
+            // TODO asking for "jar" but that is not honored in all cases so still need to handle "wsjar" being returned
+            URL resourceUrl = resource.getResourceURL("jar");
+            if (resourceUrl != null) {
+                String protocol = resourceUrl.getProtocol();
+                try {
+                    if ("jar".equals(protocol)) {
+                        URLConnection conn = resourceUrl.openConnection();
+                        if (conn instanceof JarURLConnection) {
+                            return((JarURLConnection) conn).getJarFileURL();
+                        }
+                        // unexpected; throw exception for FFDC indicating the connection class
+                        throw new IOException(conn.getClass().getName());
+                    } else if ("wsjar".equals(protocol)) {
+                        URLConnection conn = resourceUrl.openConnection();
+                        if (conn instanceof WSJarURLConnection) {
+                            return ((WSJarURLConnection) conn).getFile().toURI().toURL();
+                        }
+                        // unexpected; throw exception for FFDC indicating the connection class
+                        throw new IOException(conn.getClass().getName());
+                    } else if ("file".equals(protocol)) {
+                        // A file URL - i.e. the contents of the classes are expanded on the disk.
+                        // so a path like:  .../myServer/dropins/myWar.war/WEB-INF/classes/com/myPkg/MyClass.class
+                        // should convert to: .../myServer/dropins/myWar.war/WEB-INF/classes/
+                        return new URL(resourceUrl.toString().replace(resource.getResourceName(), ""));
path: dev/com.ibm.ws.classloading/src/com/ibm/ws/classloading/internal/ContainerClassLoader.java
user: login: tjwatson
body: This is a good point.

That bit of code was copied from https://github.com/OpenLiberty/open-liberty/blob/0c1a92f874350a219a0f5caf1d8266f697147407/dev/com.ibm.ws.classloading/src/com/ibm/ws/classloading/internal/AppClassLoader.java#L495-L504

The container URL from `ByteResourceInformation.getContainerURL()` is only used in two places.

1. From this original location to create the CodeSource of the ProtectionDomain.  So the behavior is unchanged from before with respect to this.
2. To set the sealBase which used to always use the full resource URL

I see three options here.

1. Keep the current behavior for mapped directories that don't have matching package directory structure for the resource, it is no worse that it was before.
2. Have `ByteResourceInformation.getContainerURL()` return null if the path doesn't end in the resource name and have the callers deal with possible `null`.
3. Add logic to go up the directory structure of the path until it doesn't match.  For example a class `com.foo.bar.Some.class` the URL `file://prefix/directory/foo/bar/Some.class` would return `file://prefix/directory`, the URL `file://prefix/directory/bar/Some.class` would return `file://prefix/directory/` and so on.  This is the most complicated option and I don't think it really gets us much in terms of improving the behavior.
created_at: 2025-03-26T19:17:37Z
author_association: MEMBER
diff_hunk: @@ -1628,7 +1831,10 @@ protected String findLibrary(String libName) {
         return null;
     }
 
-    protected ByteResourceInformation findClassBytes(String className, String resourceName, ClassLoaderHook hook) throws IOException {
+    // TODO renamed this method with Impl so we can continue to test what happens when this method throws
+    // an IOException in the unit tests with a method override. If it wasn't for the unit test I would
+    // have put the IOException handling directly into this method from AppClassLoader
path: dev/com.ibm.ws.classloading/src/com/ibm/ws/classloading/internal/ContainerClassLoader.java
user: login: tjwatson
body: ```suggestion
    // Renamed this method with an Impl name so we can continue to test what happens when this method throws
    // an IOException in the unit tests with a method override. If it wasn't for the unit test it would be nice to
    // put the IOException handling directly into this method from AppClassLoader.
    // TODO consider mocking smartClassPath.getByteResourceInformation to throw an IOException for the test instead
```
created_at: 2025-03-26T19:21:36Z
author_association: MEMBER
diff_hunk: @@ -1628,7 +1831,10 @@ protected String findLibrary(String libName) {
         return null;
     }
 
-    protected ByteResourceInformation findClassBytes(String className, String resourceName, ClassLoaderHook hook) throws IOException {
+    // TODO renamed this method with Impl so we can continue to test what happens when this method throws
path: dev/com.ibm.ws.classloading/src/com/ibm/ws/classloading/internal/ContainerClassLoader.java
user: login: tjwatson
body: I reworded the TODO in terms of mocking smartClassPath.getByteResourceInformation
created_at: 2025-03-26T19:27:36Z
author_association: MEMBER
diff_hunk: @@ -24,9 +24,7 @@
 import java.io.InputStream;
 import java.lang.instrument.ClassFileTransformer;
 import java.lang.instrument.IllegalClassFormatException;
-import java.net.JarURLConnection;
path: dev/com.ibm.ws.classloading/src/com/ibm/ws/classloading/internal/AppClassLoader.java
user: login: tjwatson
body: Done
created_at: 2025-03-26T19:42:19Z
author_association: MEMBER
diff_hunk: @@ -66,8 +65,7 @@ public void testClassFormatError() throws Exception {
 
         loader = new AppClassLoader(loader.getParent(), loader.config, Arrays.asList(c), (DeclaredApiAccess) (loader.getParent()), null, null, new GlobalClassloadingConfiguration(), Collections.emptyList()) {
             @Override
-            protected com.ibm.ws.classloading.internal.AppClassLoader.ByteResourceInformation findClassBytes(String className, String resourceName,
-                                                                                                             ClassLoaderHook hook) throws IOException {
+            protected com.ibm.ws.classloading.internal.AppClassLoader.ByteResourceInformation findClassBytesImpl(String className, String resourceName) throws IOException {
path: dev/com.ibm.ws.classloading_test/test/com/ibm/ws/classloading/internal/LibertyClassLoaderTest.java
user: login: tjwatson
body: Done
created_at: 2025-03-26T19:43:01Z
author_association: MEMBER
diff_hunk: @@ -97,7 +97,7 @@ public byte[] transform(ClassLoader loader, String className, Class<?> classBein
         AppClassLoader loader = createAppClassloaderTransformer(transformer, systemTransformer);
 
         byte[] originalBytes = "Hello!".getBytes();
-        ByteResourceInformation toTransform = new ByteResourceInformation(originalBytes, null, null, null, false, () -> originalBytes);
+        ByteResourceInformation toTransform = new ByteResourceInformation(originalBytes, null, null, null, false, () -> originalBytes, null);
path: dev/com.ibm.ws.classloading_test/test/com/ibm/ws/classloading/internal/TransformerTest.java
user: login: tjwatson
body: Done
created_at: 2025-03-26T19:43:12Z
author_association: MEMBER
diff_hunk: @@ -70,9 +70,7 @@ public class MockContainer implements Container {
 
     private Set<String> getImmediateChildren() {
         URL url = ucl.getURLs()[0];
-        return (url.toString().endsWith(".jar"))
-                        ? getChildrenFromJar(url, path)
-                        : getChildrenFromDir(url, path);
+        return (url.toString().endsWith(".jar")) ? getChildrenFromJar(url, path) : getChildrenFromDir(url, path);
path: dev/com.ibm.ws.classloading_test/test/com/ibm/ws/classloading/internal/MockContainer.java
user: login: tjwatson
body: Done
created_at: 2025-03-26T19:44:48Z
author_association: MEMBER
diff_hunk: @@ -461,6 +426,212 @@ private static abstract class AbstractUniversalContainer<E> implements Universal
         private volatile Map<Name, String> manifestMainAttributes = null;
         private volatile Map<String, Map<Name, String>> manifestEntryAttributes = null;
 
+        private final URL resourceContainerURL;
+        private final URL resourceSharedClassCacheURL;
+        private final File resourceContainerDir;
+        public AbstractUniversalContainer(Collection<URL> containerURLs, ClassLoaderHook hook) {
+            URL originalRoot = null;
+            URL convertedRoot = null;
+            boolean multiple = false;
+            for (URL url : containerURLs) {
+                URL converted = createContainerURL(url);
+                if (converted != null) {
+                    String path = converted.getPath();
+                    if (!path.endsWith(".overlay/")) {
+                        if (convertedRoot == null) {
+                            convertedRoot = converted;
+                            originalRoot = url;
+                        } else {
+                            multiple = true;
+                        }
+                    }
+                }
+            }
+            if (multiple) {
+                resourceContainerURL = null;
+                resourceContainerDir = null;
+                resourceSharedClassCacheURL = null;
+            } else {
+                resourceContainerURL = convertedRoot;
+                File containerFile = null;
+                try {
+                    containerFile = resourceContainerURL != null ? new File(resourceContainerURL.toURI()) : null;
+                } catch (URISyntaxException e) {
+                    // Auto-FFDC
+                }
+                resourceContainerDir = containerFile != null && isDirectory(containerFile) ? containerFile : null;
+                resourceSharedClassCacheURL = hook != null ? createSharedClassCacheURL(resourceContainerURL, originalRoot, resourceContainerDir) : null;
+            }
+        }
+
+        URL createContainerURL(URL base) {
+            try {
+                if ("file".equals(base.getProtocol())) {
+                    // use file URLs as-is
+                    return base;
+                }
+
+                URLConnection conn = base.openConnection();
+                if (conn instanceof JarURLConnection) {
+                    return ((JarURLConnection) conn).getJarFileURL();
+                } else if (conn instanceof WSJarURLConnection) {
+                    return ((WSJarURLConnection) conn).getFile().toURI().toURL();
+                }
+                throw new UnsupportedOperationException(base.getProtocol());
+            } catch (IOException err) {
+                throw new RuntimeException(err);
+            }
+        }
+
+        URL createSharedClassCacheURL(URL containerURL, URL originalRoot, File containerDir) {
+            if (containerURL == null) {
+                return null;
+            }
+            if (containerDir != null) {
+                return containerURL;
+            }
+            try {
+                String containerPath = containerURL.getPath();
+                if (containerPath.endsWith(".jar") || containerPath.endsWith(".zip")) {
+                    // use containerURL as-is if it is a jar or zip extension
+                    return containerURL;
+                }
+
+                String basePath = originalRoot.getPath();
+                int bangSlash = basePath.lastIndexOf("!/");
+                if (bangSlash >= 0) {
+                    // append the original !/ path (likely !/WEB-INF/classes)
+                    return new URL(containerURL.toExternalForm() + basePath.substring(bangSlash));
+                }
+                // If the URL is not to a directory and does not end ith jar or zip file extension then
+                // we assume it is still some type of archive (e.g. rar).
+                // The Semeru shared classes cache logic will not recognize the URL as a valid archive
+                // if it does not end with jar or zip file extension.
+                // The URL will get recognized as a valid archive if it does contain '!/' with any path after.
+                // Here we append '!/l' to the URL so that the Semeru shared classes cache logic will treat it
+                // as a valid archive.  For example: file://path/to/myResourceAdaptor.rar!/l
+                return new URL(containerURL + "!/l");
+            } catch (MalformedURLException e) {
+                return null;
+            }
+
+        }
+
+        @Override
+        public URL getContainerURL(UniversalResource resource) {
+            if (resourceContainerURL != null) {
+                if (resourceContainerDir != null) {
+                    // need to make sure the resource is really in this directory
+                    if (exists(new File(resourceContainerDir, resource.getResourceName()))) {
+                        return resourceContainerURL;
+                    }
+                } else {
+                    return resourceContainerURL;
+                }
+            }
+            // TODO asking for "jar" but that is not honored in all cases so still need to handle "wsjar" being returned
+            URL resourceUrl = resource.getResourceURL("jar");
+            if (resourceUrl != null) {
+                String protocol = resourceUrl.getProtocol();
+                try {
+                    if ("jar".equals(protocol)) {
+                        URLConnection conn = resourceUrl.openConnection();
+                        if (conn instanceof JarURLConnection) {
+                            return((JarURLConnection) conn).getJarFileURL();
+                        }
+                        // unexpected; throw exception for FFDC indicating the connection class
+                        throw new IOException(conn.getClass().getName());
+                    } else if ("wsjar".equals(protocol)) {
+                        URLConnection conn = resourceUrl.openConnection();
+                        if (conn instanceof WSJarURLConnection) {
+                            return ((WSJarURLConnection) conn).getFile().toURI().toURL();
+                        }
+                        // unexpected; throw exception for FFDC indicating the connection class
+                        throw new IOException(conn.getClass().getName());
+                    } else if ("file".equals(protocol)) {
+                        // A file URL - i.e. the contents of the classes are expanded on the disk.
+                        // so a path like:  .../myServer/dropins/myWar.war/WEB-INF/classes/com/myPkg/MyClass.class
+                        // should convert to: .../myServer/dropins/myWar.war/WEB-INF/classes/
+                        return new URL(resourceUrl.toString().replace(resource.getResourceName(), ""));
path: dev/com.ibm.ws.classloading/src/com/ibm/ws/classloading/internal/ContainerClassLoader.java
user: login: jhanders34
body: I am fine with option 1.
created_at: 2025-03-26T19:50:22Z
author_association: MEMBER

PR comments:
user: login: tjwatson
created_at: 2025-03-06T16:46:44Z
body: #build ([view Open Liberty Personal Build](https://libh-proxy1.fyre.ibm.com/cognitive/pipelineAnalysis.html?pipelineId=cbf7606b-081b-4901-ad66-aeb91d2ee217) - :x: completed with errors/failures.)

_Note: Target locations of links might be accessible only to IBM employees._
author_association: MEMBER
user: login: LibbyBot
created_at: 2025-03-06T16:53:22Z
body: <h3>Code analysis and actions</h3>
DO NOT DELETE THIS COMMENT.

- 2 unit test files were changed.
 - Check that the build did not break the affected unit test suite(s).

- 2 product code files were changed.
- **Please describe in a separate comment how you tested your changes.**
author_association: MEMBER
user: login: tjwatson
created_at: 2025-03-07T19:54:30Z
body: #build ([view Open Liberty Personal Build](https://libh-proxy1.fyre.ibm.com/cognitive/pipelineAnalysis.html?pipelineId=e826f5cb-8a2a-4712-8a69-d569cd3f3133) - :white_check_mark: completed successfully!)

_Note: Target locations of links might be accessible only to IBM employees._
author_association: MEMBER
user: login: LibbyBot
created_at: 2025-03-07T20:02:17Z
body: <h3>Code analysis and actions</h3>
DO NOT DELETE THIS COMMENT.

- 2 unit test files were changed.
 - Check that the build did not break the affected unit test suite(s).

- 2 product code files were changed.
- **Please describe in a separate comment how you tested your changes.**
author_association: MEMBER
user: login: tjwatson
created_at: 2025-03-24T22:32:17Z
body: !build ([view Open Liberty Personal Build](https://libh-proxy1.fyre.ibm.com/cognitive/pipelineAnalysis.html?pipelineId=d03ecab1-c9ca-42ce-918c-7f3b34c19e21) - :x: completed with errors/failures.)

_Note: Target locations of links might be accessible only to IBM employees._
author_association: MEMBER
user: login: LibbyBot
created_at: 2025-03-24T22:47:44Z
body: <h3>Code analysis and actions</h3>
DO NOT DELETE THIS COMMENT.

- 2 unit test files were changed.
 - Check that the build did not break the affected unit test suite(s).

- 2 product code files were changed.
- **Please describe in a separate comment how you tested your changes.**
author_association: MEMBER
user: login: tjwatson
created_at: 2025-03-25T17:30:39Z
body: !build ([view Open Liberty Personal Build](https://libh-proxy1.fyre.ibm.com/cognitive/pipelineAnalysis.html?pipelineId=6fd24df1-cfeb-4dc2-8a38-55d36ceda2d5) - :x: completed with errors/failures.)

_Note: Target locations of links might be accessible only to IBM employees._
author_association: MEMBER
user: login: LibbyBot
created_at: 2025-03-25T17:37:54Z
body: <h3>Code analysis and actions</h3>
DO NOT DELETE THIS COMMENT.

- 3 unit test files were changed.
 - Check that the build did not break the affected unit test suite(s).

- 2 product code files were changed.
- **Please describe in a separate comment how you tested your changes.**
author_association: MEMBER
user: login: tjwatson
created_at: 2025-03-26T20:08:20Z
body: !build ([view Open Liberty Personal Build](https://libh-proxy1.fyre.ibm.com/cognitive/pipelineAnalysis.html?pipelineId=48a44f32-8e40-4d79-9a5f-0cfb8fe15142) - :x: completed with errors/failures.)

_Note: Target locations of links might be accessible only to IBM employees._
author_association: MEMBER
user: login: LibbyBot
created_at: 2025-03-26T20:16:03Z
body: <h3>Code analysis and actions</h3>
DO NOT DELETE THIS COMMENT.

- 2 unit test files were changed.
 - Check that the build did not break the affected unit test suite(s).

- 2 product code files were changed.
- **Please describe in a separate comment how you tested your changes.**
author_association: MEMBER