Index: kernel-impl/src/test/java/org/sakaiproject/component/impl/BasicConfigurationServiceTest.java =================================================================== --- kernel-impl/src/test/java/org/sakaiproject/component/impl/BasicConfigurationServiceTest.java (revision 0) +++ kernel-impl/src/test/java/org/sakaiproject/component/impl/BasicConfigurationServiceTest.java (revision 0) @@ -0,0 +1,103 @@ +/********************************************************************************** +* +* $Id: BasicConfigurationServiceTest.java 107118 2012-04-16 14:41:07Z azeckoski@unicon.net $ +* +*********************************************************************************** +* + * Copyright (c) 2007, 2008 Sakai Foundation + * + * Licensed under the Educational Community License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.opensource.org/licenses/ECL-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +* +**********************************************************************************/ + +package org.sakaiproject.component.impl; + +import junit.framework.TestCase; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.sakaiproject.component.api.ServerConfigurationService.ConfigData; + +/** + * Used for testing protected methods in the BasicConfigurationService + */ +public class BasicConfigurationServiceTest extends TestCase { + + private static Log log = LogFactory.getLog(BasicConfigurationServiceTest.class); + + private BasicConfigurationService basicConfigurationService; + private String SOURCE = "TEST"; + + public void setUp() throws Exception { + basicConfigurationService = new BasicConfigurationService(); + basicConfigurationService.addConfigItem( new ConfigItemImpl("name", "Aaron"), SOURCE); + basicConfigurationService.addConfigItem( new ConfigItemImpl("AZ", "Aaron Zeckoski"), SOURCE); + basicConfigurationService.addConfigItem( new ConfigItemImpl("testKeyEmpty", ""), SOURCE); + basicConfigurationService.addConfigItem( new ConfigItemImpl("testKeyInvalid", "testing invalid=${invalid} testing"), SOURCE); + basicConfigurationService.addConfigItem( new ConfigItemImpl("testKeyNested", "testing name=${name} testing"), SOURCE); + basicConfigurationService.addConfigItem( new ConfigItemImpl("testKeyNestedMulti", "testing az=${AZ} nested=${testKeyNested} invalid=${invalid}"), SOURCE); + basicConfigurationService.addConfigItem( new ConfigItemImpl("test1", "test1"), SOURCE); + basicConfigurationService.addConfigItem( new ConfigItemImpl("test2", "test2"), SOURCE); + basicConfigurationService.addConfigItem( new ConfigItemImpl("test3", "test3"), SOURCE); + basicConfigurationService.addConfigItem( new ConfigItemImpl("test4", "test4"), SOURCE); + basicConfigurationService.addConfigItem( new ConfigItemImpl("test5", "test5"), SOURCE); + basicConfigurationService.addConfigItem( new ConfigItemImpl("test6", "test6"), SOURCE); + basicConfigurationService.addConfigItem( new ConfigItemImpl("test7", "${AZ}"), SOURCE); + log.info(basicConfigurationService.getConfigData()); + } + + public void testDereferenceString() throws Exception { + // https://jira.sakaiproject.org/browse/SAK-22148 + String value = null; + // testing for - String dereferenceValue(String value) + value = basicConfigurationService.dereferenceValue(null); + assertNull(value); + value = basicConfigurationService.dereferenceValue(""); + assertNotNull(value); + assertEquals("", value); + value = basicConfigurationService.dereferenceValue(" "); + assertNotNull(value); + assertEquals(" ", value); + value = basicConfigurationService.dereferenceValue("hello world"); + assertNotNull(value); + assertEquals("hello world", value); + value = basicConfigurationService.dereferenceValue("hello ${name}"); + assertNotNull(value); + assertEquals("hello Aaron", value); + value = basicConfigurationService.dereferenceValue("hello ${testKeyInvalid}"); + assertNotNull(value); + assertEquals("hello testing invalid=${invalid} testing", value); + value = basicConfigurationService.dereferenceValue("hello ${testKeyEmpty}"); + assertNotNull(value); + assertEquals("hello ", value); + value = basicConfigurationService.dereferenceValue("hello ${testKeyNested}"); + assertNotNull(value); + assertEquals("hello testing name=Aaron testing", value); + value = basicConfigurationService.dereferenceValue("hello ${testKeyNestedMulti}"); + assertNotNull(value); + assertEquals("hello testing az=Aaron Zeckoski nested=testing name=Aaron testing invalid=${invalid}", value); + } + + public void testDereferenceAll() throws Exception { + // https://jira.sakaiproject.org/browse/SAK-22148 + int changed = basicConfigurationService.dereferenceConfig(); + ConfigData cd = basicConfigurationService.getConfigData(); + assertEquals(14, cd.getTotalConfigItems()); + assertEquals(3, changed); // 4 of them have keys but 1 key is invalid so it will not be replaced + assertEquals("Aaron", basicConfigurationService.getConfig("name", "default") ); + assertEquals("testing name=Aaron testing", basicConfigurationService.getConfig("testKeyNested", "default") ); + assertEquals("testing az=Aaron Zeckoski nested=testing name=Aaron testing invalid=${invalid}", basicConfigurationService.getConfig("testKeyNestedMulti", "default") ); + assertEquals("Aaron Zeckoski", basicConfigurationService.getConfig("test7", "default") ); + } + +} Property changes on: kernel-impl/src/test/java/org/sakaiproject/component/impl/BasicConfigurationServiceTest.java ___________________________________________________________________ Added: svn:eol-style + native Index: kernel-impl/src/main/java/org/sakaiproject/component/impl/BasicConfigurationService.java =================================================================== --- kernel-impl/src/main/java/org/sakaiproject/component/impl/BasicConfigurationService.java (revision 108224) +++ kernel-impl/src/main/java/org/sakaiproject/component/impl/BasicConfigurationService.java (working copy) @@ -36,6 +36,8 @@ import java.util.Properties; import java.util.Vector; import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; @@ -149,6 +151,8 @@ { // can enable the output of the complete set of configuration items using: config.dump.to.log this.rawProperties = sakaiProperties.getRawProperties(); + M_log.error(this.rawProperties); + M_log.error(this.sakaiProperties.getProperties()); // populate the security keys set this.secureConfigurationKeys.add("password@javax.sql.BaseDataSource"); @@ -171,6 +175,11 @@ } M_log.info("Loaded "+configurationItems.size()+" config items from all initial sources"); + if (this.getBoolean("config.dereference.on.load.initial", true)) { + int changed = dereferenceConfig(); + M_log.info("Dereference Initial: Changed (dereferenced) "+changed+" item values out of "+configurationItems.size()+" initial config items"); + } + // load all the providers which are known (the rest have to manually register their configs), must be singleton without lazy init Map providerBeans = this.applicationContext.getBeansOfType(ConfigurationProvider.class, false, false); if (providerBeans != null) { @@ -188,6 +197,11 @@ M_log.info("Found and loaded "+configCounter+" config values from "+providerBeans.size()+" configuration providers"); } + if (this.getBoolean("config.dereference.on.load.all", false)) { + int changed = dereferenceConfig(); + M_log.info("Dereference All: Changed (dereferenced) "+changed+" item values out of all "+configurationItems.size()+" config items"); + } + // OTHER STUFF try { @@ -447,7 +461,17 @@ * {@inheritDoc} */ public String getRawProperty(String name) { - String rv = StringUtils.trimToNull((String) this.rawProperties.get(name)); + String rv = null; + if (this.rawProperties.containsKey(name)) { + // NOTE: raw properties ONLY contains the data read in from the properties files + rv = StringUtils.trimToNull((String) this.rawProperties.get(name)); + } else { + // check the config storage since it is not in the raw props + ConfigItem ci = getConfigItem(name); + if (ci != null && ci.getValue() != null) { + rv = ci.getValue().toString(); + } + } if (rv == null) rv = ""; return rv; } @@ -463,6 +487,11 @@ * {@inheritDoc} */ public String getString(String name, String dflt) { + /** + * NOTE: everything calls this in order to resolve a configuration setting, + * we take advantage of that by doing the heavy lifting in this method + * (which includes variable replacement) + */ //return getString(name, dflt, properties); String value = dflt; // retrieve a registered config item for this name @@ -470,6 +499,8 @@ if (ci != null) { if (ci.getValue() != null) { value = StringUtils.trimToNull(ci.getValue().toString()); + // check if we need to do any variable replacement + value = dereferenceValue(value); } else { // if the default value is set then we will return that instead if (ci.getDefaultValue() != null) { @@ -482,6 +513,94 @@ return value; } + /** + * Pattern to find "${var}" in strings (should match "key" from ${key}) + */ + static Pattern referencePattern = Pattern.compile("\\$\\{(.+?)\\}"); + /** + * This will search for any values in the string which need to be replaced + * with actual values from the current known set of properties which are + * available to the config service + * + * @param value any string (might have a reference like: "thing-${suffix}") + * @return the value with all matched ${vars} replaced (unmatched ones are left as is), only returns null if the input is null + */ + protected String dereferenceValue(String value) { + if (M_log.isDebugEnabled()) M_log.debug("dereferenceValue("+value+")"); + /* + * NOTE: if the performance of this becomes an issue then the right way to handle it is + * to place a flag on the ConfigItem to indicate if there is replaceable refs in it + * (probably when this runs the first time) and if there are none then skip this + * until the value is changed and then reset the flag so it will be checked again, + * if there are still issues then adding a "lastChecked" timestamp and + * a cache of the processed value to the ConfigItem which is used as long as the timestamp + * has not expired (maybe 15 mins or something), with automatic expiration when the value changes + */ + String drValue = value; + if (value != null && value.length() >= 4) { // min length of a replaceable value - "${a}" + Matcher matcher = referencePattern.matcher(value); + if (matcher.find()) { + if (M_log.isDebugEnabled()) M_log.debug("dereferenceValue("+value+"), found refs to replace"); + matcher.reset(); + StringBuilder sb = new StringBuilder(); + // loop through and find the vars to replace and write out the new string + int pointer = 0; + while (matcher.find()) { + String name = matcher.group(1); + if (name != null && StringUtils.isNotBlank(name)) { + // look up the value + String replacementValue = null; + ConfigItemImpl ci = findConfigItem(name, null); + if (ci != null) { + // found the config name so we will at least replace with empty string + replacementValue = ""; + if (ci.getValue() != null) { + replacementValue = StringUtils.trimToEmpty(ci.getValue().toString()); + } + } + sb.append(value.substring(pointer, matcher.start())); + if (replacementValue == null) { + replacementValue = matcher.group(); // just put the ${name} back in + } else { + // need to recurse in the case of nested refs + replacementValue = dereferenceValue(replacementValue); + } + sb.append(replacementValue); + pointer = matcher.end(); + } + } + sb.append(value.substring(pointer, value.length())); // get the remainder of the value + drValue = sb.toString(); + } + } + if (M_log.isDebugEnabled()) M_log.debug("dereferenceValue("+value+"): return="+drValue); + return drValue; + } + + /** + * Goes through the entire config and resolves all references and updates the actual + * stored values in the config. + * NOTE: this is destructive and probably not a good idea to run generally + * + * @return the number of config items which were changed + */ + protected int dereferenceConfig() { + int counter = 0; + for (Entry entry : configurationItems.entrySet()) { + ConfigItemImpl configItem = entry.getValue(); + if (configItem.getValue() != null) { + String currentValue = configItem.getValue().toString(); + String newValue = dereferenceValue(currentValue); + if (!currentValue.equals(newValue)) { + configItem.setValue( newValue ); + counter++; + } + } + } + return counter; + } + + /* private String getString(String name, Properties fromProperties) { return getString(name, "", fromProperties);