ClassPath.java

package Hoot.Runtime.Maps;

import java.io.*;
import java.util.*;
import static java.util.Collections.*;
import org.eclipse.aether.resolution.ArtifactResult;

import Hoot.Runtime.Names.Name;
import Hoot.Runtime.Faces.Logging;
import static Hoot.Runtime.Functions.Utils.*;
import static Hoot.Runtime.Maps.Discovery.*;
import static Hoot.Runtime.Maps.Library.*;
import static Hoot.Runtime.Names.Operator.Dot;
import static Hoot.Runtime.Names.Primitive.*;

/**
 * Provides a directory of the classes located by the Java class path.
 * Locates packages by their directory names and provides a list of the classes contained in a package.
 *
 * @author nik <nikboyd@sonic.net>
 * @see "Copyright 1999,2021 Nikolas S Boyd."
 * @see "Permission is granted to copy this work provided this copyright statement is retained in all copies."
 */
public class ClassPath implements Logging {

    protected ClassPath() { }
    public static final ClassPath CurrentPath = new ClassPath();
    static String normalPath(String p) { return Package.normalPath(p); }

    static final String WorkPath = "user.dir";
    public static String workPath() { return systemValue(WorkPath); }

    static File CachedBase = new File(workPath());
    public static File cachedBase() { return CachedBase; }

    public static final String Workspace = "workspace";
    public static final String HootSmalltalk = "hoot-smalltalk";
    static { discoverBase(Workspace, HootSmalltalk); }

    static final String BaseReport = "using default base folder: %s";
    static void reportDefaultBase(File baseCache) {
        cachedDiscovery().report(String.format(BaseReport, baseCache.getAbsolutePath())); }

    static boolean found(File f) { return cachedDiscovery().found(f); }
    static File find(String name, File f) { return hasSome(f) && !found(name, f) ? find(name, f.getParentFile()) : f; }
    static boolean found(String baseName, File f) { return f.getAbsolutePath().endsWith(baseName); }
    static boolean foundAny(File f, String... baseNames) { return matchAny(wrap(baseNames), (n) -> found(n, f)); }
    public static File discoverBase(String... baseNames) {
        File workFolder = new File(workPath());
        File baseFolder = find(baseNames[0], workFolder);

        int index = 0;
        while (!found(baseFolder) && (++index) < baseNames.length)
        { baseFolder = find(baseNames[index], workFolder); }

        File baseCache = baseFolder;
        setSafely(() -> baseCache, (f) -> {
            if (!CachedBase.getAbsolutePath().equals(f.getAbsolutePath()) && !foundAny(f, baseNames)) {
                reportDefaultBase(f); // found a different default
            }
            CachedBase = f;
        });

        return cachedBase();
    }

    List<PathMap> contents = emptyList(PathMap.class);
    public void clear() { contents.clear(); }
    public List<PathMap> contents() { return this.contents; }
    private List<PathMap> reversedPath() { List<PathMap> results = copyList(contents); reverse(results); return results; }

    public List<String> listFaces() { return collectList(fs -> collectFaces(fs)); }
    void collectFaces(Collection<String> fs) { contents().forEach(pathMap -> fs.addAll(pathMap.listFaces())); }


    public void mapLibs(String... libNames) { mapLibs(wrap(libNames)); }
    public void mapLibs(List<String> libNames) { libNames.forEach(libName -> mapLibrary(libName)); }

    public void mapLibWhen(String codePath, String libName) { if (whenSource(codePath)) mapLibrary(libName); }
    public void mapLibWhenNot(String... codePaths) { mapLibWhenNot(wrap(codePaths)); }
    public void mapLibWhenNot(List<String> codePaths) { // when lib not the source
        if (!matchAny(codePaths, s -> whenSource(s))) mapLibrary(codePaths.get(0));
    }

    public boolean whenSource(String libName) { return CurrentLib.sourcePath().contains(libName); }
    public boolean whenTarget(String libName) { return CurrentLib.targetPath().contains(libName); }

    static final String Colon = ":";
    static final String LATEST = Colon + "LATEST";
    static String artifactNamed(String libName) { // default to Hoot if no group provided
        return libName.contains(Colon) ? libName : HootSmalltalk + Colon + libName + LATEST; }

    static final String TargetClasses = "target/classes";
    public void mapLibrary(String libName) {
        String artifactName = artifactNamed(libName);
        ArtifactResult result = Discovery.lookup(artifactName);
        if (hasSome(result)) { // try maven resolution first
            if (Package.ReportLoads) reportMapping(result, artifactName);
            File file = result.getArtifact().getFile();
            if (file.exists()) {
                PathMap m = mapPath(file);
                if (Package.ReportLoads) printLine();
                m.getPackages().forEach(p -> p.loadFaces());
                return; // done with found library
            }
        }

        // try local library class resolution
        File libFolder = new File(cachedBase(), libName);
        if (libFolder.exists()) {
            File classFolder = new File(libFolder, normalPath(TargetClasses));
            if (classFolder.exists()) {
                if (Package.ReportLoads) reportMapping(libName, classFolder);
                PathMap m = mapPath(classFolder);
                if (Package.ReportLoads) printLine();
                m.getPackages().forEach(p -> p.loadFaces());
            }
        }
    }

    public PathMap mapPath(File folder) { boolean appended = true; return addMapped(folder, appended); }
    public PathMap addMapped(File folder, boolean appended) {
        if (hasNo(folder) || !folder.exists()) {
            if (hasSome(folder)) report(folder.getName() + " not loaded");
            return null; // bail out, can't load from non-existent folder
        }

        PathMap map = buildMap(folder);
        if (appended) contents.add(map);
                 else contents.add(0, map);

        map.load();
        reportMapped(); // while mapping
        if (Package.ReportLoads) reportCount(map.listFaces().size());
        return map;
    }

    private PathMap buildMap(File folder) {
        // NOTE: folder may actually locate
        // a ZIP or JAR file that contains a class library!
        return ZipMap.supports(folder.getAbsolutePath()) ?
                new ZipMap(folder.getAbsolutePath()) :
                new PathMap(folder.getAbsolutePath()) ; }

    boolean classPathMapping = false;
    public boolean showDots() { return this.classPathMapping; }
    public boolean showOnlyDots(boolean value) { this.classPathMapping = value; return value; }

    private boolean locatesFace(String faceName, String packageName) {
        return matchAny(classesInPackage(packageName), faceNames ->
            hasSome(faceNames) && faceNames.contains(faceName)); }

    private boolean anyPackageHasFaceNamed(String faceName) {
        return matchAny(reversedPath(), pathMap ->
            hasSome(pathMap.packageContaining(faceName)));
    }

    public boolean canLocateFaceNamed(String fullName) {
        String faceName = Name.typeName(fullName);
        String packageName = Name.packageName(fullName);
        return locatesFace(faceName, packageName) || anyPackageHasFaceNamed(faceName); }

    public boolean canLocatePackage(Package aPackage) { return classesExistInFolder(aPackage.pathname()); }
    private boolean classesExistInFolder(String packagePath) {
        return matchAny(reversedPath(), pathMap ->
            hasSome(pathMap.classesInFolder(packagePath))); }

    public Set<String> classesInPackage(Package aPackage) { return classesInPackage(aPackage.pathname()); }
    private Set<String> classesInPackage(String packageName) {
        return collectSet(results ->
            reversedPath().forEach(pathMap ->
                results.addAll(pathMap.classesInFolder(packageName)))); }

    public java.io.File locate(String folder) {
        for (PathMap map : reversedPath()) {
            File result = map.locate(folder);
            if (hasSome(result)) return result;
        }
        return null;
    }

    static final String StandardPath = "java.class.path";
    String[] standardPaths() { return systemValue(StandardPath).split(Separator); }
    public static String buildPath(String... basePaths) { return joinWith(Separator, wrap(basePaths)); }
    public void mapStandardPaths() {
        reportMapping("CLASSPATH", null); wrap(standardPaths()).forEach(p -> mapPath(new File(p))); }

    static final String ArtReport = "mapping %s = %s ";
    private void reportMapping(ArtifactResult r, String name) {
        printLine(); print(format(ArtReport, name, r.getArtifact().getVersion())); }

    static final String CountReport = " mapped %d faces";
    private void reportCount(int count) { if (!showDots()) print(format(CountReport, count)); }

    static final String MappingReport = "mapping %s ";
    public void reportMapping(String name, File folder) { printLine();
        print(hasNo(folder) ? format(MappingReport, name) : format(ArtReport, name, folder.getAbsolutePath())); }

    public void reportMapped() { if (showDots()) print(Dot); }
    public void println(int count) { while(count-- > 0) printLine(); }
    public void println() { System.out.println(); }

    public static final String PathSeparator = "path.separator";
    public static final String Separator = systemValue(PathSeparator);

    static final String[] Hoots = { "Hoot", "Smalltalk", };
    static final List<String> HootBases = wrap(Hoots);
    public static boolean matchHoots(String faceName) { return matchAny(HootBases, (h) -> faceName.startsWith(h)); }

} // ClassPath