sourceafFormBean::Messages.fan

using afConcurrent::AtomicMap

** (Service) - 
** Creates a map of messages by merging together property files. 
** Property files are looked for in the following locations:
** 
**  - 'FormBean.props' in pod 'afFormBean'
**  - 'FormBean.props' in pod '<pod>'
**  - '<bean>.props' in pod '<pod>'
** 
** Messages override those defined previously.
** 
** Note that message maps are cached on the given bean type, so hit of collating '.props' files is 
** only taken once per bean type. 
** 
** 'Messages' also takes contributions of string Maps:
** 
** pre>
** syntax: fantom
** @Contribute { serviceType=Messages# }
** Void contributeMessages(Configuration config) {
**     config.add([
**         "loginDetails.username.label"  : "Username:",
**         "loginDetails.password.label"  : "Password:"
**     ])
** }
** <pre
const mixin Messages {

    ** Returns messages for the given bean.
    @Operator
    abstract Str:Str getMessages(Type beanType)
    
    ** Clears the message cache.
    abstract Void clear()
}

internal const class MessagesImpl : Messages {
    private const AtomicMap messages 
    private const Str:Str   defaultMsgs
    private const Str:Str   contributedMsgs

    new make([Str:Str][] msgs) {
        contributedMsgs := Str:Str[:] { caseInsensitive=true }
        msgs.each { contributedMsgs.setAll(it) }

        this.contributedMsgs = contributedMsgs
        this.defaultMsgs     = this.typeof.pod.files.find { matches(it, "FormBean") } .readProps
        this.messages        = AtomicMap { keyType=Type#; valType=[Str:Str]# }
    }
    
    @Operator
    override Str:Str getMessages(Type beanType) {
        ((Str:Str) messages.getOrAdd(beanType) |->Str:Str| {
            msgs        := Str:Str[:] { caseInsensitive = true }
            try {
                // TODO use Fantom's locale lookup... somehow, see http://fantom.org/doc/sys/Env#locale
                // can only get single props, not the whole file, so add locale to getMessage
                // or just look for more files using the same rules (better)
                // pass in Locale locale := Locale.cur()
                formBeanMsgs := beanType.pod.files.find { matches(it, "FormBean")   }?.readProps ?: [:]
                beanTypeMsgs := beanType.pod.files.find { matches(it, beanType.name)}?.readProps ?: [:]
                tempMsgs     := Str:Str[:] { caseInsensitive=true }
                    .setAll(defaultMsgs)
                    .setAll(contributedMsgs)
                    .setAll(formBeanMsgs)
                    .setAll(beanTypeMsgs)
                beanName     := beanType.name.lower + "."

                tempMsgs.each |Str v, Str k| {
                    if (k.lower.startsWith(beanName)) {
                        msgs[k[beanName.size..-1]] = v
                        return
                    }

                    // don't overwrite a keys set via beanName above
                    if (!msgs.containsKey(k))
                        msgs[k] = v
                }
                return msgs

            } catch (Err err) {
                // Guard against being run in a script - an Err is thrown when not backed by a pod file
                if (err.msg.startsWith("Not backed by pod file"))
                    return Str:Str[:] { caseInsensitive = true }.addAll(defaultMsgs)
                throw err
            }
        }).rw
    }
    
    override Void clear() {
        messages.clear
    }
    
    private Bool matches(File file, Str baseName) {
        file.basename.equalsIgnoreCase(baseName) && file.ext != null && (file.ext.equalsIgnoreCase("props") || file.ext.equalsIgnoreCase("properties"))
    }
}