sourceafFpm::FpmConfig.fan


** FpmConfig is gathered from a hierarchy of 'fpm.props' files. These files are looked for in the following locations:
** 
** - '<FAN_HOME>/etc/afFpm/fpm.props'
** - '<WORK_DIR>/etc/afFpm/fpm.props'
** - './fpm.props'
** - './../fpm.props'
** - './../../fpm.props' *(...and so on...)*
** 
** Note that the config files are additive but the values are not. If all 3 files exist, then all 3 files are merged together, 
** with config values from a more specific file replacing (or overriding) values found in less specific one.
** 
** '<WORK_DIR>' may be specified with the 'FPM_ENV_PATH' environment variable. This means that **ALL** the config for FPM may 
** live outside of the Fantom installation. The only FPM file that needs to live in the Fantom installation is the 'afFpm.pod' 
** file itself.
** 
** Config may be removed by specifying an empty string as a value. For example, to remove the eggbox repository:
** 
**   fanrRepo.eggbox = 
** 
** Read the comments in the actual 'fpm.props' file itself for more details.
const class FpmConfig {

    ** The directory used to resolve relative files.
    const File      baseDir

    ** The Fantom installation.
    const File      homeDir

    ** The temp directory.
    const File      tempDir

    ** A list of working directories.
    ** The 'workDir' as returned by 'FpmEnv' is is always first item in this list.
    ** 'homeDir' is always the last entry in the list, so it is never empty.
    const File[]    workDirs
    
    ** A map of named directory repositories. These are essentially just dirs of pods. 
    const Str:File  dirRepos
    
    ** A map of named fanr repositories, may be local or remote.
    const Str:Uri   fanrRepos
    
    ** A list of extra pods to be resolved alongside the target pod.
    const Depend[]  extraPods

    ** A list of libraries used to launch applications
    const Str[]     launchPods
    
    ** The 'fpm.props' files used to generate this class.
    const File[]    configFiles

    ** The macros applied to repository paths.
    const Str:Str   macros

    ** The properties gleaned from the 'configFiles' (after macros have been applied).
    const Str:Str   props

    private static const Str _pathSepStr    := ";"
    private static const Int _pathSepInt    := ';'
    
    @NoDoc
    static new make() {
        fromDirs(File(`./`), Env.cur.homeDir, Env.cur.vars["FAN_ENV_PATH"])
    }

    ** This ctor used by f4afFpm::FpmCompileEnv
    @NoDoc
    static new fromDirs(File baseDir, File homeDir, Str? fanEnvPath) {
        baseDir = baseDir.normalize
        homeDir = homeDir.normalize

        // grab the config filename from an env var, but only if the version matches
        // this is useful for testing and development
        configFilename := `fpm.props`
        t1 := Env.cur.vars["FPM_CONFIG_FILENAME"]   // = fpm.props/2.0.1
        if (t1 != null) {
            t2 := Uri(t1, false)
            if (t2 != null) {
                t3 := t2.path
                if (t3.getSafe(0) == FpmEnv#.pod.version.toStr) {
                    t4 := t3.getSafe(1)
                    if (t4 != null) {
                        t5 := Uri(t4, false)
                        if (t5 != null)
                            configFilename = t5
                    }
                }
            }
        }
        
        // find all fpm.props in parent directories
        fpmFiles := File[,]
        curFile2 := (File?) baseDir.plus(configFilename)
        while (curFile2 != null) {
            if (curFile2.exists)
                fpmFiles.add(curFile2)
            curFile2 = curFile2.parent.parent?.plus(configFilename)
        }

        // this is a little bit chicken and egg - we use the workDir to find fpm.props to find the workDir! 
        workStrs := StrBuf()
        workStrs.join(fanEnvPath?.replace(File.pathSep, _pathSepStr) ?: "", _pathSepStr)
        workStrs.join(homeDir.osPath, _pathSepStr)
        workStrs.toStr.split(_pathSepInt).exclude { it.isEmpty }.each |workDir| {
            file := toRelDir(workDir, baseDir) + `etc/afFpm/` + configFilename
            if (file.exists)
                fpmFiles.add(file)
        }

        // convert files to RawProps
        rawProps := null as RawProps
        fpmFiles.unique.reverse.each |fpmFile| {
            rawProps = RawProps(fpmFile.readProps, fpmFile, rawProps)
        }
        
        // cater for having NO fpm.prop files (which may well happen!)
        if (rawProps == null)
            rawProps = RawProps.defVal
        
        return makeFromProps(rawProps, baseDir, homeDir, fanEnvPath)
    }

    ** 'fpmProps' has the least significant first, so it may be overridden by the later entries.
    @NoDoc
    internal new makeFromProps(RawProps rawProps, File baseDir, File homeDir, Str? fanEnvPath := null) {
        homeDir = homeDir.normalize
        if (homeDir.isDir.not || homeDir.exists.not)
            throw ArgErr("Home directory is not valid: ${homeDir.osPath}")

        baseDir = baseDir.normalize
        if (baseDir.isDir.not || baseDir.exists.not)
            throw ArgErr("Base directory is not valid: ${baseDir.osPath}")

        macros      := rawProps.allMacros
        macros["baseDir"] = baseDir.uri.toStr
        macros["fanHome"] = homeDir.uri.toStr

        // todo - maybe add a second round of macro replacement here for workDirs?
        
        allProps    := rawProps.allProps.map |pval, pkey| {
            macros.each |mval, mkey| { pval = pval.replace("\${${mkey}}", mval) }
            return pval
        }
        fpmProps    := FpmProps(allProps)
        tempDir     := rawProps.toRelDir("tempDir", fpmProps.tempDir) ?: homeDir + `temp/`
        workDirs    := fpmProps.workDirs .map |dir     ->File?| { rawProps.toRelDir("workDirs", dir)            }.exclude { it == null } as File[]
        dirRepos    := fpmProps.dirRepos .map |dir, key->File?| { rawProps.toRelDir("dirRepo.${key}", dir)      }.exclude { it == null } as Str:File
        fanrRepos   := fpmProps.fanrRepos.map |dir, key->Uri? | {
            url  := Uri(dir, false)
            if (url != null && (url.scheme == "http" || url.scheme == "https"))
                return url
            return rawProps.toRelDir("fanrRepo.${key}", dir)?.uri
        }.exclude { it == null } as Str:Uri

        // add FAN_ENV_PATH dirs to workDirs
        if (fanEnvPath != null)
            fanEnvPath.trim.split(File.pathSep.chars.first).each |workDir| {
                if (workDir.size > 0)
                    workDirs.add(toRelDir(workDir, baseDir))
            }

        // add workDirs to dirRepos
        if (workDirs.size > 0)
            if (!dirRepos.containsKey("workDir"))
                dirRepos["workDir"] = workDirs.first + `lib/fan/`
        if (workDirs.size > 1)
            workDirs.eachRange(1..-1) |dir, i| {
                if (!dirRepos.containsKey("workDir[$i]"))
                    dirRepos["workDir[$i]"] = dir + `lib/fan/`
            }

        // workDirs should always have fanHome as a backup
        workDirs.add(homeDir)
        
        // if not defined, add "fanHome" as a new directory repo
        // add fanHome last as these pods are _least_ important when resolving environment runtime pods
        if (!dirRepos.containsKey("fanHome"))
            dirRepos["fanHome"] = homeDir + `lib/fan/`

        // if "default" is not defined, set it to fanHome so FPM becomes a drop in replacement for fanr
        // this allows people to update their fanHome env by default
        if (!fanrRepos.containsKey("default") && !dirRepos.containsKey("default"))
            dirRepos["default"] = homeDir + `lib/fan/`

        both := dirRepos.keys.intersection(fanrRepos.keys)
        if (both.size > 0)
            throw Err("Repository '" + both.join(", ") + "' is defined as both a dirRepo AND a fanrRepo")

        this.baseDir        = baseDir
        this.homeDir        = homeDir
        this.tempDir        = tempDir
        this.workDirs       = workDirs
        this.dirRepos       = dirRepos
        this.fanrRepos      = fanrRepos
        this.extraPods      = fpmProps.extraPods
        this.launchPods     = fpmProps.launchPods
        this.configFiles    = rawProps.files.unique
        this.props          = allProps
        this.macros         = macros
    }
    
    ** Returns a 'Repository' instance for the named repository. 
    ** 'repoName' may be either a 'dirRepo' or a 'fanrRepo'. 
    Repository? repository(Str repoName, Str? username := null, Str? password := null) {
        
        if (dirRepos.containsKey(repoName))
            return LocalDirRepository(repoName, dirRepos[repoName])
        
        if (fanrRepos.containsKey(repoName)) {
            if (username == null)
                username = props["fanrRepo.${repoName}.username"]
            if (password == null)
                password = props["fanrRepo.${repoName}.password"]
            url := fanrRepos[repoName]
            if (url.scheme == null   || url.scheme == "file")
                return LocalFanrRepository(repoName, url.toFile)
            if (url.scheme == "http" || url.scheme == "https")
                return RemoteFanrRepository(repoName, url, username, password)
            throw ArgErr("Unknown scheme '${url.scheme}' in $url")
        }

        return null
    }
    
    ** Returns a list of all repositories.
    ** Note that some repositories may point to the same directory / URL.
    Repository[] repositories() {
        repos1 :=  dirRepos.keys.map { repository(it) }
        repos2 := fanrRepos.keys.map { repository(it) }
        // note that default, workDir, and fanHome may be the same
        return repos1.addAll(repos2)
    }

    ** Dumps debug output to a string. The string will look similar to:
    ** 
    ** pre>
    ** FPM Environment:
    **    Target Pod : afIoc 3.0+
    **      Base Dir : C:\
    **  Fan Home Dir : C:\Apps\fantom-1.0.70
    **     Work Dirs : C:\Repositories\Fantom
    **                 C:\Apps\fantom-1.0.70
    **      Temp Dir : C:\Repositories\Fantom\temp
    **  Config Files : C:\Apps\fantom-1.0.70\etc\afFpm\config.props
    ** 
    **     Dir Repos :
    **       workDir = C:\Repositories\Fantom
    **       fanHome = C:\Apps\fantom-1.0.70/lib/fan/
    ** 
    **    Fanr Repos :
    **       default = C:\Repositories\Fantom\repo-default
    **        eggbox = http://eggbox.fantomfactory.org/fanr/
    **       release = C:\Repositories\Fantom\repo-release
    **       repo302 = http://repo.status302.com/fanr/
    ** <pre
    Str dump() {
        str := ""
        str += "      Base Dir : " + dumpFiles([baseDir])
        str += "  Fan Home Dir : " + dumpFiles([homeDir])
        str += "     Work Dirs : " + dumpFiles(workDirs)
        str += "      Temp Dir : " + dumpFiles([tempDir])
        str += "    Extra Pods : " + dumpList(extraPods)
        str += "  Config Files : " + dumpFiles(configFiles)

        str += "\n"
        str += "     Dir Repos : " + (dirRepos.isEmpty ? "(none)" : "") + "\n"
        max := dirRepos.keys.reduce(14) |Int size, name| { size.max(name.size) } as Int
        dirRepos.each |file, name| {
            exists := file.exists ? "" : " (does not exist)"
            str += name.justr(max) + " = ${file.osPath}${exists}\n"
        }

        str += "\n"
        str += "    Fanr Repos : " + (fanrRepos.isEmpty ? "(none)" : "") + "\n"
        max = fanrRepos.keys.reduce(14) |Int size, name| { size.max(name.size) } as Int
        fanrRepos.each |repoUrl, name| {
            usr     := repoUrl.userInfo == null ? "" : repoUrl.userInfo + "@"
            url     := repoUrl.toStr.replace(usr, "")
            exists  := (repoUrl.scheme == "file" && repoUrl.toFile.exists == false) ? " (does not exist)" : ""
            str += name.justr(max) + " = ${url}${exists}\n"
        }

        // I've got too many Macros for this to still be useful 
//      str += "\n"
//      str += "        Macros : " + (macros.isEmpty ? "(none)" : "") + "\n"
//      max = macros.keys.reduce(14) |Int size, name| { size.max(name.size) } as Int
//      macros.each |value, name| {
//          str += name.justr(max) + " = ${value}\n"
//      }

        return str
    }

    private Str dumpFiles(File[] files) {
        if (files.isEmpty)
            return "(none)\n"

        ext := files.first.exists ? "" : " (does not exist)"
        str := "${files.first.osPath}${ext}\n"
        if (files.size > 1)
            files[1..-1].each {
                exists := it.exists ? "" : " (does not exist)"
                str += "".justr(14) + "   ${it.osPath}${exists}\n"
            }
        return str
    }

    private Str dumpList(Obj[] objs) {
        if (objs.isEmpty)
            return "(none)\n"

        str := "${objs.first}\n"
        if (objs.size > 1)
            objs[1..-1].each {
                str += "".justr(14) + "   ${it}\n"
            }
        return str
    }

    private static File toRelDir(Str dirPath, File base) {
        FileUtils.toAbsDir(dirPath, base)
    }
}