Oliver Heger wrote:
Hmm, navigating through a tree based on a configuration key, creating missing nodes if necessary and finally storing the value...

Similar code is contained in HierarchicalConfiguration and I suppose in XMLConfiguration, too, to update the internally used DOM tree. I wonder if this could be generalized.

I've been pondering the same thing for quite some time too. I started working on a WindowsConfiguration wrapping the windows registry and I'm reimplementing the same logic found in the other hierarchical configurations. And we will have the same issue if we write a LDAPConfiguration.

I tried a quick refactoring of JNDIConfiguration to test this idea, this is a rough design, let me know what do you think of it :

- a ConfigurationNode interface is introduced, it's an abstract node with basic navigation methods like getElements(), createNode(), getName(). Its implementations will wrap a real node, that's a JNDI Context or a DOM Element for example.

- JNDIConfiguration implements a TreeConfiguration interface. This interface exposes the getRootNode() method returning the root ConfigurationNode. We may put in this interface some methods defining the variations of behaviour between the hierarchical configurations (supportsXXX():boolean). For example with an XMLConfiguration, several child nodes can have the same name, this is not true for JNDIConfiguration or WindowsConfiguration, that means the search algorithm is slightly different.

- JNDIConfiguration has a private implementation of ConfigurationNode, that's JNDIConfigurationNode.

- the search methods of JNDIConfiguration have been moved to TreeConfigurationUtils and now use the ConfigurationNode instead of the JNDI context.

- JNDIConfiguration delegates to TreeConfigurationUtils its implementations for getKeys and isEmpty.

I'm attaching the resulting classes, the test cases still pass. I started looking at XMLConfiguration but it's a bit more complex.

Emmanuel Bourg
/*
 * Copyright 2004 The Apache Software Foundation.
 *
 * Licensed under the Apache 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.apache.org/licenses/LICENSE-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.apache.commons.configuration;

import java.util.Collection;

/**
 * An abstract node for hierarchical configurations.
 *
 * @author Emmanuel Bourg
 * @version $Revision$, $Date$
 */
public interface ConfigurationNode
{
    String getName();

    /**
     * Return the elements under this node (both sub nodes and objects)
     */
    Collection getElements();

    //ConfigurationNode getNode(String name);

    //void createNode(String name);

    //void destroyNode(String name);

}
/*
 * Copyright 2004 The Apache Software Foundation.
 *
 * Licensed under the Apache 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.apache.org/licenses/LICENSE-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.apache.commons.configuration;

/**
 * Configuration based on a hierarchical data structure.
 *
 * @author Emmanuel Bourg
 * @version $Revision$, $Date$
 */
public interface TreeConfiguration extends Configuration
{
    ConfigurationNode getRootNode();

    //boolean supportsNonUniqueNodes();

    //boolean supportsDirectNodeLookup();
}
/*
 * Copyright 2004 The Apache Software Foundation.
 *
 * Licensed under the Apache 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.apache.org/licenses/LICENSE-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.apache.commons.configuration;

/**
 * A configuration property, that's a pair key/value.
 *
 * @author Emmanuel Bourg
 * @version $Revision$, $Date$
 */
public class ConfigurationProperty
{
    private String key;
    private Object value;

    public ConfigurationProperty(String key, Object value)
    {
        this.key = key;
        this.value = value;
    }

    public String getKey()
    {
        return key;
    }

    public Object getValue()
    {
        return value;
    }
}
/*
 * Copyright 2001-2004 The Apache Software Foundation.
 *
 * Licensed under the Apache 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.apache.org/licenses/LICENSE-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.apache.commons.configuration;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Properties;
import java.util.Set;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NameClassPair;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * This Configuration class allows you to interface with a JNDI datasource.
 * A JNDIConfiguration is read-only, write operations will throw an
 * UnsupportedOperationException. The clear operations are supported but the
 * underlying JNDI data source is not changed.
 *
 * @author <a href="mailto:[EMAIL PROTECTED]">Eric Pugh</a>
 * @version $Id: JNDIConfiguration.java,v 1.20 2004/10/21 18:36:14 ebourg Exp $
 */
public class JNDIConfiguration extends AbstractConfiguration implements 
TreeConfiguration
{
    /** Logger. */
    private static Log log = LogFactory.getLog(JNDIConfiguration.class);

    /** The prefix of the context. */
    private String prefix;

    /** The initial JNDI context. */
    private Context context;

    /** The base JNDI context. */
    private Context baseContext;

    /** The Set of keys that have been virtually cleared. */
    private Set clearedProperties = new HashSet();

    /**
     * Creates a JNDIConfiguration using the default initial context as the
     * root of the properties.
     *
     * @throws NamingException thrown if an error occurs when initializing the default 
context
     */
    public JNDIConfiguration() throws NamingException
    {
        this((String) null);
    }

    /**
     * Creates a JNDIConfiguration using the default initial context, shifted
     * with the specified prefix, as the root of the properties.
     *
     * @param prefix
     *
     * @throws NamingException thrown if an error occurs when initializing the default 
context
     */
    public JNDIConfiguration(String prefix) throws NamingException
    {
        this(new InitialContext(), prefix);
    }

    /**
     * Creates a JNDIConfiguration using the specified initial context as the
     * root of the properties.
     *
     * @param context the initial context
     */
    public JNDIConfiguration(Context context)
    {
        this(context, null);
    }

    /**
     * Creates a JNDIConfiguration using the specified initial context shifted
     * by the specified prefix as the root of the properties.
     *
     * @param context the initial context
     * @param prefix
     */
    public JNDIConfiguration(Context context, String prefix)
    {
        this.context = context;
        this.prefix = prefix;
    }

    /**
     * <p><strong>This operation is not supported and will throw an
     * UnsupportedOperationException.</strong></p>
     *
     * @throws UnsupportedOperationException
     */
    public void addProperty(String key, Object token)
    {
        throw new UnsupportedOperationException("This operation is not supported");
    }

    /**
     * [EMAIL PROTECTED]
     */
    public Iterator getKeys()
    {
        return getKeys("");
    }

    /**
     * [EMAIL PROTECTED]
     */
    public Iterator getKeys(String prefix)
    {
        return TreeConfigurationUtils.getKeys(this, prefix);
    }

    /**
     * <p><strong>This operation is not supported and will throw an
     * UnsupportedOperationException.</strong></p>
     *
     * @throws UnsupportedOperationException
     */
    public Properties getProperties(String key)
    {
        throw new UnsupportedOperationException("This operation is not supported");
    }

    /**
     * [EMAIL PROTECTED]
     */
    public boolean isEmpty()
    {
        return TreeConfigurationUtils.isEmpty(this);
    }

    /**
     * [EMAIL PROTECTED]
     */
    public Object getProperty(String key)
    {
        return getPropertyDirect(key);
    }

    /**
     * <p><strong>This operation is not supported and will throw an
     * UnsupportedOperationException.</strong></p>
     *
     * @throws UnsupportedOperationException
     */
    public void setProperty(String key, Object value)
    {
        throw new UnsupportedOperationException("This operation is not supported");
    }

    /**
     * [EMAIL PROTECTED]
     */
    public void clearProperty(String key)
    {
        clearedProperties.add(key);
    }

    /**
     * [EMAIL PROTECTED]
     */
    public boolean containsKey(String key)
    {
        if (clearedProperties.contains(key))
        {
            return false;
        }
        key = StringUtils.replace(key, ".", "/");
        try
        {
            // throws a NamingException if JNDI doesn't contain the key.
            getBaseContext().lookup(key);
            return true;
        }
        catch (NamingException e)
        {
            log.error(e.getMessage(), e);
            return false;
        }
    }

    /**
     * @return String
     */
    public String getPrefix()
    {
        return prefix;
    }

    /**
     * Sets the prefix.
     *
     * @param prefix The prefix to set
     */
    public void setPrefix(String prefix)
    {
        this.prefix = prefix;

        // clear the previous baseContext
        baseContext = null;
    }

    /**
     * [EMAIL PROTECTED]
     */
    protected Object getPropertyDirect(String key)
    {
        if (clearedProperties.contains(key))
        {
            return null;
        }

        try
        {
            key = StringUtils.replace(key, ".", "/");
            return getBaseContext().lookup(key);
        }
        catch (NamingException e)
        {
            log.error(e.getMessage(), e);
            return null;
        }
    }

    /**
     * <p><strong>This operation is not supported and will throw an
     * UnsupportedOperationException.</strong></p>
     *
     * @throws UnsupportedOperationException
     */
    protected void addPropertyDirect(String key, Object obj)
    {
        throw new UnsupportedOperationException("This operation is not supported");
    }

    /**
     * Return the base context with the prefix applied.
     */
    public Context getBaseContext() throws NamingException
    {
        if (baseContext == null)
        {
            baseContext = (Context) getContext().lookup(prefix == null ? "" : prefix);
        }

        return baseContext;
    }

    /**
     * Return the initial context used by this configuration. This context is
     * independent of the prefix specified.
     */
    public Context getContext()
    {
        return context;
    }

    /**
     * Set the initial context of the configuration.
     */
    public void setContext(Context context)
    {
        // forget the removed properties
        clearedProperties.clear();

        // change the context
        this.context = context;
    }

    public ConfigurationNode getRootNode()
    {
        ConfigurationNode root = null;

        try
        {
            root = new JNDIConfigurationNode(null, getBaseContext());
        }
        catch (NamingException e)
        {
            log.error(e.getMessage(), e);
        }

        return root;
    }

    /**
     * A ConfigurationNode wrapping a JNDI Context
     */
    private class JNDIConfigurationNode implements ConfigurationNode
    {
        private String name;
        private Context context;

        public JNDIConfigurationNode(String name, Context context)
        {
            this.name = name;
            this.context = context;
        }

        public String getName()
        {
            return name;
        }

        public Collection getElements()
        {
            Collection elements = new ArrayList();

            try
            {
                NamingEnumeration objects = context.list(""); // todo close the 
enumeration
                while (objects.hasMore())
                {
                    NameClassPair nameClassPair = (NameClassPair) objects.next();
                    String name = nameClassPair.getName();
                    Object object = context.lookup(name);

                    if (object instanceof Context)
                    {
                        elements.add(new JNDIConfigurationNode(name, (Context) 
object));
                    }
                    else
                    {
                        elements.add(new ConfigurationProperty(name, object));
                    }
                }
            }
            catch (NamingException e)
            {
                log.error(e.getMessage(), e);
            }

            return elements;
        }
    }

}
/*
 * Copyright 2004 The Apache Software Foundation.
 *
 * Licensed under the Apache 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.apache.org/licenses/LICENSE-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.apache.commons.configuration;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.apache.commons.lang.StringUtils;

/**
 * Utility methods for configurations implementing the TreeConfiguration interface.
 *
 * @author Emmanuel Bourg
 * @version $Revision$, $Date$
 */
public class TreeConfigurationUtils
{
    /**
     * Because JNDI is based on a tree configuration, we need to filter down the
     * tree, till we find the Context specified by the key to start from.
     * Otherwise return null.
     *
     * @param path the path of keys to traverse in order to find the context
     * @param node the node to start from
     * @return The context at that key's location in the JNDI tree, or null if not 
found
     */
    public static ConfigurationNode getNode(List path, ConfigurationNode node)
    {
        // return the current node if the path is empty
        if (path == null || path.isEmpty())
        {
            return node;
        }

        String key = (String) path.get(0);

        // search a node matching the key in the subnodes
        Iterator elements = node.getElements().iterator();
        while (elements.hasNext())
        {
            Object object = elements.next();

            if (object instanceof ConfigurationNode)
            {
                ConfigurationNode subnode = (ConfigurationNode) object;

                if (subnode.getName().equals(key))
                {
                    // recursive search in the sub node
                    return getNode(path.subList(1, path.size()), subnode);
                }
            }
        }

        return null;
    }

    public static Iterator getKeys(TreeConfiguration config, String prefix)
    {
        // build the path
        String[] splitPath = StringUtils.split(prefix, ".");

        List path = new ArrayList();

        for (int i = 0; i < splitPath.length; i++)
        {
            path.add(splitPath[i]);
        }

        // find the node matching the specified path
        ConfigurationNode node = TreeConfigurationUtils.getNode(path, 
config.getRootNode());

        // return all the keys under the node found
        Set keys = new HashSet();
        if (node != null)
        {
            recursiveGetKeys(keys, node, prefix);
        }
        else if (config.containsKey(prefix))
        {
            // add the prefix if it matches exactly a property key
            keys.add(prefix);
        }

        return keys.iterator();
    }

    /**
     * This method recursive traverse the JNDI tree, looking for Context objects.
     * When it finds them, it traverses them as well.  Otherwise it just adds the
     * values to the list of keys found.
     *
     * @param keys All the keys that have been found.
     * @param node The parent context
     * @param prefix What prefix we are building on.
     */
    private static void recursiveGetKeys(Set keys, ConfigurationNode node, String 
prefix)
    {
        Iterator elements = node.getElements().iterator();

        // iterates through the node's elements
        while (elements.hasNext())
        {
            Object object = elements.next();

            // build the key
            StringBuffer key = new StringBuffer();
            key.append(prefix);
            if (key.length() > 0)
            {
                key.append(".");
            }

            if (object instanceof ConfigurationNode)
            {
                // add the keys of the sub node
                ConfigurationNode subnode = (ConfigurationNode) object;
                key.append(subnode.getName());
                recursiveGetKeys(keys, subnode, key.toString());
            }
            else
            {
                // add the key
                ConfigurationProperty property = (ConfigurationProperty) object;
                key.append(property.getKey());

                keys.add(key.toString());
            }
        }
    }

    public static boolean isEmpty(TreeConfiguration config)
    {
        Collection elements = config.getRootNode().getElements();
        return elements.isEmpty();
    }
}

Attachment: smime.p7s
Description: S/MIME Cryptographic Signature

Reply via email to