blob: 6f14645d1fc2020bc64dc7adcc2273a93a47c5e3 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.configuration2;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NameClassPair;
import javax.naming.NameNotFoundException;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.NotContextException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import org.apache.commons.configuration2.event.ConfigurationErrorEvent;
import org.apache.commons.configuration2.io.ConfigurationLogger;
import org.apache.commons.lang3.StringUtils;
/**
* 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.
*
*/
public class JNDIConfiguration extends AbstractConfiguration
{
/** 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 final Set<String> 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 the prefix
*
* @throws NamingException thrown if an error occurs when initializing the default context
*/
public JNDIConfiguration(final 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(final 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 the prefix
*/
public JNDIConfiguration(final Context context, final String prefix)
{
this.context = context;
this.prefix = prefix;
initLogger(new ConfigurationLogger(JNDIConfiguration.class));
addErrorLogListener();
}
/**
* 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 context The parent context
* @param prefix What prefix we are building on.
* @param processedCtx a set with the so far processed objects
* @throws NamingException If JNDI has an issue.
*/
private void recursiveGetKeys(final Set<String> keys, final Context context, final String prefix,
final Set<Context> processedCtx) throws NamingException
{
processedCtx.add(context);
NamingEnumeration<NameClassPair> elements = null;
try
{
elements = context.list("");
// iterates through the context's elements
while (elements.hasMore())
{
final NameClassPair nameClassPair = elements.next();
final String name = nameClassPair.getName();
final Object object = context.lookup(name);
// build the key
final StringBuilder key = new StringBuilder();
key.append(prefix);
if (key.length() > 0)
{
key.append(".");
}
key.append(name);
if (object instanceof Context)
{
// add the keys of the sub context
final Context subcontext = (Context) object;
if (!processedCtx.contains(subcontext))
{
recursiveGetKeys(keys, subcontext, key.toString(),
processedCtx);
}
}
else
{
// add the key
keys.add(key.toString());
}
}
}
finally
{
// close the enumeration
if (elements != null)
{
elements.close();
}
}
}
/**
* Returns an iterator with all property keys stored in this configuration.
*
* @return an iterator with all keys
*/
@Override
protected Iterator<String> getKeysInternal()
{
return getKeysInternal("");
}
/**
* Returns an iterator with all property keys starting with the given
* prefix.
*
* @param prefix the prefix
* @return an iterator with the selected keys
*/
@Override
protected Iterator<String> getKeysInternal(final String prefix)
{
// build the path
final String[] splitPath = StringUtils.split(prefix, ".");
final List<String> path = Arrays.asList(splitPath);
try
{
// find the context matching the specified path
final Context context = getContext(path, getBaseContext());
// return all the keys under the context found
final Set<String> keys = new HashSet<>();
if (context != null)
{
recursiveGetKeys(keys, context, prefix, new HashSet<Context>());
}
else if (containsKey(prefix))
{
// add the prefix if it matches exactly a property key
keys.add(prefix);
}
return keys.iterator();
}
catch (final NameNotFoundException e)
{
// expected exception, no need to log it
return new ArrayList<String>().iterator();
}
catch (final NamingException e)
{
fireError(ConfigurationErrorEvent.READ,
ConfigurationErrorEvent.READ, null, null, e);
return new ArrayList<String>().iterator();
}
}
/**
* 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 context the context to start from
* @return The context at that key's location in the JNDI tree, or null if not found
* @throws NamingException if JNDI has an issue
*/
private Context getContext(final List<String> path, final Context context) throws NamingException
{
// return the current context if the path is empty
if (path == null || path.isEmpty())
{
return context;
}
final String key = path.get(0);
// search a context matching the key in the context's elements
NamingEnumeration<NameClassPair> elements = null;
try
{
elements = context.list("");
while (elements.hasMore())
{
final NameClassPair nameClassPair = elements.next();
final String name = nameClassPair.getName();
final Object object = context.lookup(name);
if (object instanceof Context && name.equals(key))
{
final Context subcontext = (Context) object;
// recursive search in the sub context
return getContext(path.subList(1, path.size()), subcontext);
}
}
}
finally
{
if (elements != null)
{
elements.close();
}
}
return null;
}
/**
* Returns a flag whether this configuration is empty.
*
* @return the empty flag
*/
@Override
protected boolean isEmptyInternal()
{
try
{
NamingEnumeration<NameClassPair> enumeration = null;
try
{
enumeration = getBaseContext().list("");
return !enumeration.hasMore();
}
finally
{
// close the enumeration
if (enumeration != null)
{
enumeration.close();
}
}
}
catch (final NamingException e)
{
fireError(ConfigurationErrorEvent.READ,
ConfigurationErrorEvent.READ, null, null, e);
return true;
}
}
/**
* <p><strong>This operation is not supported and will throw an
* UnsupportedOperationException.</strong></p>
*
* @param key the key
* @param value the value
* @throws UnsupportedOperationException always thrown as this method is not supported
*/
@Override
protected void setPropertyInternal(final String key, final Object value)
{
throw new UnsupportedOperationException("This operation is not supported");
}
/**
* Removes the specified property.
*
* @param key the key of the property to remove
*/
@Override
protected void clearPropertyDirect(final String key)
{
clearedProperties.add(key);
}
/**
* Checks whether the specified key is contained in this configuration.
*
* @param key the key to check
* @return a flag whether this key is stored in this configuration
*/
@Override
protected boolean containsKeyInternal(String key)
{
if (clearedProperties.contains(key))
{
return false;
}
key = key.replaceAll("\\.", "/");
try
{
// throws a NamingException if JNDI doesn't contain the key.
getBaseContext().lookup(key);
return true;
}
catch (final NameNotFoundException e)
{
// expected exception, no need to log it
return false;
}
catch (final NamingException e)
{
fireError(ConfigurationErrorEvent.READ,
ConfigurationErrorEvent.READ, key, null, e);
return false;
}
}
/**
* Returns the prefix.
* @return the prefix
*/
public String getPrefix()
{
return prefix;
}
/**
* Sets the prefix.
*
* @param prefix The prefix to set
*/
public void setPrefix(final String prefix)
{
this.prefix = prefix;
// clear the previous baseContext
baseContext = null;
}
/**
* Returns the value of the specified property.
*
* @param key the key of the property
* @return the value of this property
*/
@Override
protected Object getPropertyInternal(String key)
{
if (clearedProperties.contains(key))
{
return null;
}
try
{
key = key.replaceAll("\\.", "/");
return getBaseContext().lookup(key);
}
catch (final NameNotFoundException | NotContextException nctxex)
{
// expected exception, no need to log it
return null;
}
catch (final NamingException e)
{
fireError(ConfigurationErrorEvent.READ,
ConfigurationErrorEvent.READ, key, null, e);
return null;
}
}
/**
* <p><strong>This operation is not supported and will throw an
* UnsupportedOperationException.</strong></p>
*
* @param key the key
* @param obj the value
* @throws UnsupportedOperationException always thrown as this method is not supported
*/
@Override
protected void addPropertyDirect(final String key, final Object obj)
{
throw new UnsupportedOperationException("This operation is not supported");
}
/**
* Return the base context with the prefix applied.
*
* @return the base context
* @throws NamingException if an error occurs
*/
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.
*
* @return the initial context
*/
public Context getContext()
{
return context;
}
/**
* Set the initial context of the configuration.
*
* @param context the context
*/
public void setContext(final Context context)
{
// forget the removed properties
clearedProperties.clear();
// change the context
this.context = context;
}
}