Search code examples
grailsconfigurationexternalweb-inf

Copying default external configuration on first run of Grails web app


In our Grails web applications, we'd like to use external configuration files so that we can change the configuration without releasing a new version. We'd also like these files to be outside of the application directory so that they stay unchanged during continuous integration.

The last thing we need to do is to make sure the external configuration files exist. If they don't, then we'd like to create them, fill them with predefined content (production environment defaults) and then use them as if they existed before. This allows any administrator to change settings of the application without detailed knowledge of the options actually available.

For this purpose, there's a couple of files within web-app/WEB-INF/conf ready to be copied to the external configuration location upon the first run of the application.

So far so good. But we need to do this before the application is initialized so that production-related modifications to data sources definitions are taken into account.

I can do the copy-and-load operation inside the Config.groovy file, but I don't know the absolute location of the WEB-INF/conf directory at the moment.

How can I get the location during this early phase of initialization? Is there any other solution to the problem?


Solution

  • I finally managed to solve this myself by using the Java's ability to locate resources placed on the classpath.

    I took the .groovy files later to be copied outside, placed them into the grails-app/conf directory (which is on the classpath) and appended a suffix to their name so that they wouldn't get compiled upon packaging the application. So now I have *Config.groovy files containing configuration defaults (for all environments) and *Config.groovy.production files containing defaults for production environment (overriding the precompiled defaults).

    Now - Config.groovy starts like this:

    grails.config.defaults.locations = [ EmailConfig, AccessConfig, LogConfig, SecurityConfig ]
    
    environments {
        production {
            grails.config.locations = ConfigUtils.getExternalConfigFiles(
                '.production',
                "${userHome}${File.separator}.config${File.separator}${appName}",
                'AccessConfig.groovy',
                'Config.groovy',
                'DataSource.groovy',
                'EmailConfig.groovy',
                'LogConfig.groovy',
                'SecurityConfig.groovy'
            )
        }
    }
    

    Then the ConfigUtils class:

    public class ConfigUtils {
    
        // Log4j may not be initialized yet 
        private static final Logger LOG = Logger.getGlobal()
    
        public static def getExternalConfigFiles(final String defaultSuffix, final String externalConfigFilesLocation, final String... externalConfigFiles) {
    
            final def externalConfigFilesDir = new File(externalConfigFilesLocation)
    
            LOG.info "Loading configuration from ${externalConfigFilesDir}"
    
            if (!externalConfigFilesDir.exists()) {
                LOG.warning "${externalConfigFilesDir} not found. Creating..."
                try {
                    externalConfigFilesDir.mkdirs()
                } catch (e) {
                    LOG.severe "Failed to create external configuration storage. Default configuration will be used."
                    e.printStackTrace()
                    return []
                }
            }
    
            final def cl = ConfigUtils.class.getClassLoader()
            def result = []
    
            externalConfigFiles.each {
    
                final def file = new File(externalConfigFilesDir, it)
                if (file.exists()) {
                    result << file.toURI().toURL()
                    return
                }
    
                final def error = false
                final def defaultFileURL = cl.getResource(it + defaultSuffix)
                final def defaultFile
    
                if (defaultFileURL) {
                    defaultFile = new File(defaultFileURL.toURI())
                    error = !defaultFile.exists();
                } else {
                    error = true
                }
    
                if (error) {
                    LOG.severe "Neither of ${file} or ${defaultFile} exists. Skipping..."
                    return
                }
    
                LOG.warning "${file} does not exist. Copying ${defaultFile} -> ${file}..."
    
                try {
                    FileUtils.copyFile(defaultFile, file)
                } catch (e) {
                    LOG.severe "Couldn't copy ${defaultFile} -> ${file}. Skipping..."
                    e.printStackTrace()
                    return
                }
    
                result << file.toURI().toURL()
            }
    
            return result
        }
    
    }