|  | 
|  | 1 | +package com.coditory.quark.context; | 
|  | 2 | + | 
|  | 3 | +import org.slf4j.Logger; | 
|  | 4 | +import org.slf4j.LoggerFactory; | 
|  | 5 | + | 
|  | 6 | +import java.io.File; | 
|  | 7 | +import java.io.IOException; | 
|  | 8 | +import java.net.MalformedURLException; | 
|  | 9 | +import java.net.URISyntaxException; | 
|  | 10 | +import java.net.URL; | 
|  | 11 | +import java.net.URLClassLoader; | 
|  | 12 | +import java.util.ArrayList; | 
|  | 13 | +import java.util.Arrays; | 
|  | 14 | +import java.util.Enumeration; | 
|  | 15 | +import java.util.HashSet; | 
|  | 16 | +import java.util.LinkedHashMap; | 
|  | 17 | +import java.util.LinkedHashSet; | 
|  | 18 | +import java.util.List; | 
|  | 19 | +import java.util.Map; | 
|  | 20 | +import java.util.Set; | 
|  | 21 | +import java.util.jar.Attributes; | 
|  | 22 | +import java.util.jar.JarEntry; | 
|  | 23 | +import java.util.jar.JarFile; | 
|  | 24 | +import java.util.jar.Manifest; | 
|  | 25 | + | 
|  | 26 | +import static java.util.Collections.unmodifiableList; | 
|  | 27 | +import static java.util.Collections.unmodifiableMap; | 
|  | 28 | +import static java.util.Collections.unmodifiableSet; | 
|  | 29 | +import static java.util.Objects.requireNonNull; | 
|  | 30 | +import static java.util.stream.Collectors.toSet; | 
|  | 31 | + | 
|  | 32 | +final class ClassPath { | 
|  | 33 | +    private static final Logger logger = LoggerFactory.getLogger(ClassPath.class.getName()); | 
|  | 34 | +    private static final String CLASS_FILE_NAME_EXTENSION = ".class"; | 
|  | 35 | +    private static final String PATH_SEPARATOR_SYS_PROP = System.getProperty("path.separator"); | 
|  | 36 | +    private static final String JAVA_CLASS_PATH_SYS_PROP = System.getProperty("java.class.path"); | 
|  | 37 | + | 
|  | 38 | +    private final Set<ResourceInfo> resources; | 
|  | 39 | + | 
|  | 40 | +    private ClassPath(Set<ResourceInfo> resources) { | 
|  | 41 | +        this.resources = resources; | 
|  | 42 | +    } | 
|  | 43 | + | 
|  | 44 | +    public static ClassPath from(ClassLoader classloader) throws IOException { | 
|  | 45 | +        requireNonNull(classloader); | 
|  | 46 | +        Set<LocationInfo> locations = locationsFrom(classloader); | 
|  | 47 | +        Set<File> scanned = new LinkedHashSet<>(); | 
|  | 48 | +        for (LocationInfo location : locations) { | 
|  | 49 | +            scanned.add(location.file()); | 
|  | 50 | +        } | 
|  | 51 | +        Set<ResourceInfo> resources = new LinkedHashSet<>(); | 
|  | 52 | +        for (LocationInfo location : locations) { | 
|  | 53 | +            resources.addAll(location.scanResources(scanned)); | 
|  | 54 | +        } | 
|  | 55 | +        return new ClassPath(resources); | 
|  | 56 | +    } | 
|  | 57 | + | 
|  | 58 | +    public Set<ClassInfo> getTopLevelClasses() { | 
|  | 59 | +        return resources.stream() | 
|  | 60 | +                .filter(r -> r instanceof ClassInfo) | 
|  | 61 | +                .map(r -> (ClassInfo) r) | 
|  | 62 | +                .filter(ClassInfo::isTopLevel) | 
|  | 63 | +                .collect(toSet()); | 
|  | 64 | +    } | 
|  | 65 | + | 
|  | 66 | +    public Set<ClassInfo> getTopLevelClassesRecursive(String packageName) { | 
|  | 67 | +        requireNonNull(packageName); | 
|  | 68 | +        String packagePrefix = packageName + '.'; | 
|  | 69 | +        Set<ClassInfo> classes = new LinkedHashSet<>(); | 
|  | 70 | +        for (ClassInfo classInfo : getTopLevelClasses()) { | 
|  | 71 | +            if (classInfo.getName().startsWith(packagePrefix)) { | 
|  | 72 | +                classes.add(classInfo); | 
|  | 73 | +            } | 
|  | 74 | +        } | 
|  | 75 | +        return unmodifiableSet(classes); | 
|  | 76 | +    } | 
|  | 77 | + | 
|  | 78 | +    public static class ResourceInfo { | 
|  | 79 | +        private final File file; | 
|  | 80 | +        private final String resourceName; | 
|  | 81 | + | 
|  | 82 | +        final ClassLoader loader; | 
|  | 83 | + | 
|  | 84 | +        static ResourceInfo of(File file, String resourceName, ClassLoader loader) { | 
|  | 85 | +            return resourceName.endsWith(CLASS_FILE_NAME_EXTENSION) | 
|  | 86 | +                    ? new ClassInfo(file, resourceName, loader) | 
|  | 87 | +                    : new ResourceInfo(file, resourceName, loader); | 
|  | 88 | +        } | 
|  | 89 | + | 
|  | 90 | +        ResourceInfo(File file, String resourceName, ClassLoader loader) { | 
|  | 91 | +            this.file = requireNonNull(file); | 
|  | 92 | +            this.resourceName = requireNonNull(resourceName); | 
|  | 93 | +            this.loader = requireNonNull(loader); | 
|  | 94 | +        } | 
|  | 95 | + | 
|  | 96 | +        public File getFile() { | 
|  | 97 | +            return file; | 
|  | 98 | +        } | 
|  | 99 | + | 
|  | 100 | +        @Override | 
|  | 101 | +        public int hashCode() { | 
|  | 102 | +            return resourceName.hashCode(); | 
|  | 103 | +        } | 
|  | 104 | + | 
|  | 105 | +        @Override | 
|  | 106 | +        public boolean equals(Object obj) { | 
|  | 107 | +            if (obj instanceof ResourceInfo that) { | 
|  | 108 | +                return resourceName.equals(that.resourceName) | 
|  | 109 | +                        && loader == that.loader; | 
|  | 110 | +            } | 
|  | 111 | +            return false; | 
|  | 112 | +        } | 
|  | 113 | + | 
|  | 114 | +        @Override | 
|  | 115 | +        public String toString() { | 
|  | 116 | +            return resourceName; | 
|  | 117 | +        } | 
|  | 118 | +    } | 
|  | 119 | + | 
|  | 120 | +    public static final class ClassInfo extends ResourceInfo { | 
|  | 121 | +        private final String className; | 
|  | 122 | + | 
|  | 123 | +        ClassInfo(File file, String resourceName, ClassLoader loader) { | 
|  | 124 | +            super(file, resourceName, loader); | 
|  | 125 | +            this.className = getClassName(resourceName); | 
|  | 126 | +        } | 
|  | 127 | + | 
|  | 128 | +        public String getName() { | 
|  | 129 | +            return className; | 
|  | 130 | +        } | 
|  | 131 | + | 
|  | 132 | +        public boolean isTopLevel() { | 
|  | 133 | +            return className.indexOf('$') == -1; | 
|  | 134 | +        } | 
|  | 135 | + | 
|  | 136 | +        @Override | 
|  | 137 | +        public String toString() { | 
|  | 138 | +            return className; | 
|  | 139 | +        } | 
|  | 140 | +    } | 
|  | 141 | + | 
|  | 142 | +    static Set<LocationInfo> locationsFrom(ClassLoader classloader) { | 
|  | 143 | +        Set<LocationInfo> locations = new LinkedHashSet<>(); | 
|  | 144 | +        for (Map.Entry<File, ClassLoader> entry : getClassPathEntries(classloader).entrySet()) { | 
|  | 145 | +            locations.add(new LocationInfo(entry.getKey(), entry.getValue())); | 
|  | 146 | +        } | 
|  | 147 | +        return unmodifiableSet(locations); | 
|  | 148 | +    } | 
|  | 149 | + | 
|  | 150 | +    static final class LocationInfo { | 
|  | 151 | +        final File home; | 
|  | 152 | +        private final ClassLoader classloader; | 
|  | 153 | + | 
|  | 154 | +        LocationInfo(File home, ClassLoader classloader) { | 
|  | 155 | +            this.home = requireNonNull(home); | 
|  | 156 | +            this.classloader = requireNonNull(classloader); | 
|  | 157 | +        } | 
|  | 158 | + | 
|  | 159 | +        public File file() { | 
|  | 160 | +            return home; | 
|  | 161 | +        } | 
|  | 162 | + | 
|  | 163 | +        public Set<ResourceInfo> scanResources(Set<File> scannedFiles) throws IOException { | 
|  | 164 | +            Set<ResourceInfo> resources = new LinkedHashSet<>(); | 
|  | 165 | +            scannedFiles.add(home); | 
|  | 166 | +            scan(home, scannedFiles, resources); | 
|  | 167 | +            return unmodifiableSet(resources); | 
|  | 168 | +        } | 
|  | 169 | + | 
|  | 170 | +        private void scan(File file, Set<File> scannedUris, Set<ResourceInfo> result) | 
|  | 171 | +                throws IOException { | 
|  | 172 | +            try { | 
|  | 173 | +                if (!file.exists()) { | 
|  | 174 | +                    return; | 
|  | 175 | +                } | 
|  | 176 | +            } catch (SecurityException e) { | 
|  | 177 | +                logger.warn("Cannot access " + file + ": " + e); | 
|  | 178 | +                return; | 
|  | 179 | +            } | 
|  | 180 | +            if (file.isDirectory()) { | 
|  | 181 | +                scanDirectory(file, result); | 
|  | 182 | +            } else { | 
|  | 183 | +                scanJar(file, scannedUris, result); | 
|  | 184 | +            } | 
|  | 185 | +        } | 
|  | 186 | + | 
|  | 187 | +        private void scanJar(File file, Set<File> scannedUris, Set<ResourceInfo> result) throws IOException { | 
|  | 188 | +            JarFile jarFile; | 
|  | 189 | +            try { | 
|  | 190 | +                jarFile = new JarFile(file); | 
|  | 191 | +            } catch (IOException e) { | 
|  | 192 | +                // Not a jar file | 
|  | 193 | +                return; | 
|  | 194 | +            } | 
|  | 195 | +            try { | 
|  | 196 | +                for (File path : getClassPathFromManifest(file, jarFile.getManifest())) { | 
|  | 197 | +                    // We only scan each file once independent of the classloader that file might be | 
|  | 198 | +                    // associated with. | 
|  | 199 | +                    if (scannedUris.add(path.getCanonicalFile())) { | 
|  | 200 | +                        scan(path, scannedUris, result); | 
|  | 201 | +                    } | 
|  | 202 | +                } | 
|  | 203 | +                scanJarFile(jarFile, result); | 
|  | 204 | +            } finally { | 
|  | 205 | +                try { | 
|  | 206 | +                    jarFile.close(); | 
|  | 207 | +                } catch (IOException ignored) { // similar to try-with-resources, but don't fail scanning | 
|  | 208 | +                } | 
|  | 209 | +            } | 
|  | 210 | +        } | 
|  | 211 | + | 
|  | 212 | +        private void scanJarFile(JarFile file, Set<ResourceInfo> result) { | 
|  | 213 | +            Enumeration<JarEntry> entries = file.entries(); | 
|  | 214 | +            while (entries.hasMoreElements()) { | 
|  | 215 | +                JarEntry entry = entries.nextElement(); | 
|  | 216 | +                if (entry.isDirectory() || entry.getName().equals(JarFile.MANIFEST_NAME)) { | 
|  | 217 | +                    continue; | 
|  | 218 | +                } | 
|  | 219 | +                result.add(ResourceInfo.of(new File(file.getName()), entry.getName(), classloader)); | 
|  | 220 | +            } | 
|  | 221 | +        } | 
|  | 222 | + | 
|  | 223 | +        private void scanDirectory(File directory, Set<ResourceInfo> result) | 
|  | 224 | +                throws IOException { | 
|  | 225 | +            Set<File> currentPath = new HashSet<>(); | 
|  | 226 | +            currentPath.add(directory.getCanonicalFile()); | 
|  | 227 | +            scanDirectory(directory, "", currentPath, result); | 
|  | 228 | +        } | 
|  | 229 | + | 
|  | 230 | +        private void scanDirectory( | 
|  | 231 | +                File directory, | 
|  | 232 | +                String packagePrefix, | 
|  | 233 | +                Set<File> currentPath, | 
|  | 234 | +                Set<ResourceInfo> builder | 
|  | 235 | +        ) throws IOException { | 
|  | 236 | +            File[] files = directory.listFiles(); | 
|  | 237 | +            if (files == null) { | 
|  | 238 | +                logger.warn("Cannot read directory " + directory); | 
|  | 239 | +                // IO error, just skip the directory | 
|  | 240 | +                return; | 
|  | 241 | +            } | 
|  | 242 | +            for (File f : files) { | 
|  | 243 | +                String name = f.getName(); | 
|  | 244 | +                if (f.isDirectory()) { | 
|  | 245 | +                    File deref = f.getCanonicalFile(); | 
|  | 246 | +                    if (currentPath.add(deref)) { | 
|  | 247 | +                        scanDirectory(deref, packagePrefix + name + "/", currentPath, builder); | 
|  | 248 | +                        currentPath.remove(deref); | 
|  | 249 | +                    } | 
|  | 250 | +                } else { | 
|  | 251 | +                    String resourceName = packagePrefix + name; | 
|  | 252 | +                    if (!resourceName.equals(JarFile.MANIFEST_NAME)) { | 
|  | 253 | +                        builder.add(ResourceInfo.of(f, resourceName, classloader)); | 
|  | 254 | +                    } | 
|  | 255 | +                } | 
|  | 256 | +            } | 
|  | 257 | +        } | 
|  | 258 | + | 
|  | 259 | +        @Override | 
|  | 260 | +        public boolean equals(Object obj) { | 
|  | 261 | +            if (obj instanceof LocationInfo that) { | 
|  | 262 | +                return home.equals(that.home) && classloader.equals(that.classloader); | 
|  | 263 | +            } | 
|  | 264 | +            return false; | 
|  | 265 | +        } | 
|  | 266 | + | 
|  | 267 | +        @Override | 
|  | 268 | +        public int hashCode() { | 
|  | 269 | +            return home.hashCode(); | 
|  | 270 | +        } | 
|  | 271 | + | 
|  | 272 | +        @Override | 
|  | 273 | +        public String toString() { | 
|  | 274 | +            return home.toString(); | 
|  | 275 | +        } | 
|  | 276 | +    } | 
|  | 277 | + | 
|  | 278 | +    static Set<File> getClassPathFromManifest(File jarFile, Manifest manifest) { | 
|  | 279 | +        if (manifest == null) { | 
|  | 280 | +            return Set.of(); | 
|  | 281 | +        } | 
|  | 282 | +        Set<File> result = new LinkedHashSet<>(); | 
|  | 283 | +        String classpathAttribute = manifest | 
|  | 284 | +                .getMainAttributes() | 
|  | 285 | +                .getValue(Attributes.Name.CLASS_PATH.toString()); | 
|  | 286 | +        if (classpathAttribute != null) { | 
|  | 287 | +            for (String path : classpathAttribute.split(" ")) { | 
|  | 288 | +                if (path.isBlank()) { | 
|  | 289 | +                    continue; | 
|  | 290 | +                } | 
|  | 291 | +                URL url; | 
|  | 292 | +                try { | 
|  | 293 | +                    url = getClassPathEntry(jarFile, path); | 
|  | 294 | +                } catch (MalformedURLException e) { | 
|  | 295 | +                    // Ignore bad entry | 
|  | 296 | +                    logger.warn("Invalid Class-Path entry: " + path); | 
|  | 297 | +                    continue; | 
|  | 298 | +                } | 
|  | 299 | +                if (url.getProtocol().equals("file")) { | 
|  | 300 | +                    result.add(toFile(url)); | 
|  | 301 | +                } | 
|  | 302 | +            } | 
|  | 303 | +        } | 
|  | 304 | +        return unmodifiableSet(result); | 
|  | 305 | +    } | 
|  | 306 | + | 
|  | 307 | +    static Map<File, ClassLoader> getClassPathEntries(ClassLoader classloader) { | 
|  | 308 | +        LinkedHashMap<File, ClassLoader> entries = new LinkedHashMap<>(); | 
|  | 309 | +        // Search parent first, since it's the order ClassLoader#loadClass() uses. | 
|  | 310 | +        ClassLoader parent = classloader.getParent(); | 
|  | 311 | +        if (parent != null) { | 
|  | 312 | +            entries.putAll(getClassPathEntries(parent)); | 
|  | 313 | +        } | 
|  | 314 | +        for (URL url : getClassLoaderUrls(classloader)) { | 
|  | 315 | +            if (url.getProtocol().equals("file")) { | 
|  | 316 | +                File file = toFile(url); | 
|  | 317 | +                if (!entries.containsKey(file)) { | 
|  | 318 | +                    entries.put(file, classloader); | 
|  | 319 | +                } | 
|  | 320 | +            } | 
|  | 321 | +        } | 
|  | 322 | +        return unmodifiableMap(entries); | 
|  | 323 | +    } | 
|  | 324 | + | 
|  | 325 | +    private static List<URL> getClassLoaderUrls(ClassLoader classloader) { | 
|  | 326 | +        if (classloader instanceof URLClassLoader) { | 
|  | 327 | +            return Arrays.asList(((URLClassLoader) classloader).getURLs()); | 
|  | 328 | +        } | 
|  | 329 | +        if (classloader.equals(ClassLoader.getSystemClassLoader())) { | 
|  | 330 | +            return parseJavaClassPath(); | 
|  | 331 | +        } | 
|  | 332 | +        return List.of(); | 
|  | 333 | +    } | 
|  | 334 | + | 
|  | 335 | +    private static List<URL> parseJavaClassPath() { | 
|  | 336 | +        List<URL> urls = new ArrayList<>(); | 
|  | 337 | +        for (String entry : JAVA_CLASS_PATH_SYS_PROP.split(PATH_SEPARATOR_SYS_PROP)) { | 
|  | 338 | +            try { | 
|  | 339 | +                try { | 
|  | 340 | +                    urls.add(new File(entry).toURI().toURL()); | 
|  | 341 | +                } catch (SecurityException e) { // File.toURI checks to see if the file is a directory | 
|  | 342 | +                    urls.add(new URL("file", null, new File(entry).getAbsolutePath())); | 
|  | 343 | +                } | 
|  | 344 | +            } catch (MalformedURLException e) { | 
|  | 345 | +                logger.warn("Malformed classpath entry: " + entry, e); | 
|  | 346 | +            } | 
|  | 347 | +        } | 
|  | 348 | +        return unmodifiableList(urls); | 
|  | 349 | +    } | 
|  | 350 | + | 
|  | 351 | +    private static URL getClassPathEntry(File jarFile, String path) throws MalformedURLException { | 
|  | 352 | +        return new URL(jarFile.toURI().toURL(), path); | 
|  | 353 | +    } | 
|  | 354 | + | 
|  | 355 | +    private static String getClassName(String filename) { | 
|  | 356 | +        int classNameEnd = filename.length() - CLASS_FILE_NAME_EXTENSION.length(); | 
|  | 357 | +        return filename.substring(0, classNameEnd).replace('/', '.'); | 
|  | 358 | +    } | 
|  | 359 | + | 
|  | 360 | +    private static File toFile(URL url) { | 
|  | 361 | +        try { | 
|  | 362 | +            return new File(url.toURI()); // Accepts escaped characters like %20. | 
|  | 363 | +        } catch (URISyntaxException e) { // URL.toURI() doesn't escape chars. | 
|  | 364 | +            return new File(url.getPath()); // Accepts non-escaped chars like space. | 
|  | 365 | +        } | 
|  | 366 | +    } | 
|  | 367 | +} | 
0 commit comments