    001/*
    002 * Licensed to the Apache Software Foundation (ASF) under one or more
    003 * contributor license agreements.  See the NOTICE file distributed with
    004 * this work for additional information regarding copyright ownership.
    005 * The ASF licenses this file to You under the Apache License, Version 2.0
    006 * (the "License"); you may not use this file except in compliance with
    007 * the License.  You may obtain a copy of the License at
    008 *
    009 *     http://www.apache.org/licenses/LICENSE-2.0
    010 *
    011 * Unless required by applicable law or agreed to in writing, software
    012 * distributed under the License is distributed on an "AS IS" BASIS,
    013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014 * See the License for the specific language governing permissions and
    015 * limitations under the License.
    016 */
    017
    018package org.apache.commons.configuration2;
    019
    020import javax.naming.Context;
    021import javax.naming.InitialContext;
    022import javax.naming.NameClassPair;
    023import javax.naming.NameNotFoundException;
    024import javax.naming.NamingEnumeration;
    025import javax.naming.NamingException;
    026import javax.naming.NotContextException;
    027import java.util.ArrayList;
    028import java.util.Arrays;
    029import java.util.HashSet;
    030import java.util.Iterator;
    031import java.util.List;
    032import java.util.Set;
    033
    034import org.apache.commons.configuration2.event.ConfigurationErrorEvent;
    035import org.apache.commons.configuration2.io.ConfigurationLogger;
    036import org.apache.commons.lang3.StringUtils;
    037
    038/**
    039 * This Configuration class allows you to interface with a JNDI datasource.
    040 * A JNDIConfiguration is read-only, write operations will throw an
    041 * UnsupportedOperationException. The clear operations are supported but the
    042 * underlying JNDI data source is not changed.
    043 *
    044 */
    045public class JNDIConfiguration extends AbstractConfiguration
    046{
    047    /** The prefix of the context. */
    048    private String prefix;
    049
    050    /** The initial JNDI context. */
    051    private Context context;
    052
    053    /** The base JNDI context. */
    054    private Context baseContext;
    055
    056    /** The Set of keys that have been virtually cleared. */
    057    private final Set<String> clearedProperties = new HashSet<>();
    058
    059    /**
    060     * Creates a JNDIConfiguration using the default initial context as the
    061     * root of the properties.
    062     *
    063     * @throws NamingException thrown if an error occurs when initializing the default context
    064     */
    065    public JNDIConfiguration() throws NamingException
    066    {
    067        this((String) null);
    068    }
    069
    070    /**
    071     * Creates a JNDIConfiguration using the default initial context, shifted
    072     * with the specified prefix, as the root of the properties.
    073     *
    074     * @param prefix the prefix
    075     *
    076     * @throws NamingException thrown if an error occurs when initializing the default context
    077     */
    078    public JNDIConfiguration(final String prefix) throws NamingException
    079    {
    080        this(new InitialContext(), prefix);
    081    }
    082
    083    /**
    084     * Creates a JNDIConfiguration using the specified initial context as the
    085     * root of the properties.
    086     *
    087     * @param context the initial context
    088     */
    089    public JNDIConfiguration(final Context context)
    090    {
    091        this(context, null);
    092    }
    093
    094    /**
    095     * Creates a JNDIConfiguration using the specified initial context shifted
    096     * by the specified prefix as the root of the properties.
    097     *
    098     * @param context the initial context
    099     * @param prefix the prefix
    100     */
    101    public JNDIConfiguration(final Context context, final String prefix)
    102    {
    103        this.context = context;
    104        this.prefix = prefix;
    105        initLogger(new ConfigurationLogger(JNDIConfiguration.class));
    106        addErrorLogListener();
    107    }
    108
    109    /**
    110     * This method recursive traverse the JNDI tree, looking for Context objects.
    111     * When it finds them, it traverses them as well.  Otherwise it just adds the
    112     * values to the list of keys found.
    113     *
    114     * @param keys All the keys that have been found.
    115     * @param context The parent context
    116     * @param prefix What prefix we are building on.
    117     * @param processedCtx a set with the so far processed objects
    118     * @throws NamingException If JNDI has an issue.
    119     */
    120    private void recursiveGetKeys(final Set<String> keys, final Context context, final String prefix,
    121            final Set<Context> processedCtx) throws NamingException
    122    {
    123        processedCtx.add(context);
    124        NamingEnumeration<NameClassPair> elements = null;
    125
    126        try
    127        {
    128            elements = context.list("");
    129
    130            // iterates through the context's elements
    131            while (elements.hasMore())
    132            {
    133                final NameClassPair nameClassPair = elements.next();
    134                final String name = nameClassPair.getName();
    135                final Object object = context.lookup(name);
    136
    137                // build the key
    138                final StringBuilder key = new StringBuilder();
    139                key.append(prefix);
    140                if (key.length() > 0)
    141                {
    142                    key.append(".");
    143                }
    144                key.append(name);
    145
    146                if (object instanceof Context)
    147                {
    148                    // add the keys of the sub context
    149                    final Context subcontext = (Context) object;
    150                    if (!processedCtx.contains(subcontext))
    151                    {
    152                        recursiveGetKeys(keys, subcontext, key.toString(),
    153                                processedCtx);
    154                    }
    155                }
    156                else
    157                {
    158                    // add the key
    159                    keys.add(key.toString());
    160                }
    161            }
    162        }
    163        finally
    164        {
    165            // close the enumeration
    166            if (elements != null)
    167            {
    168                elements.close();
    169            }
    170        }
    171    }
    172
    173    /**
    174     * Returns an iterator with all property keys stored in this configuration.
    175     *
    176     * @return an iterator with all keys
    177     */
    178    @Override
    179    protected Iterator<String> getKeysInternal()
    180    {
    181        return getKeysInternal("");
    182    }
    183
    184    /**
    185     * Returns an iterator with all property keys starting with the given
    186     * prefix.
    187     *
    188     * @param prefix the prefix
    189     * @return an iterator with the selected keys
    190     */
    191    @Override
    192    protected Iterator<String> getKeysInternal(final String prefix)
    193    {
    194        // build the path
    195        final String[] splitPath = StringUtils.split(prefix, ".");
    196
    197        final List<String> path = Arrays.asList(splitPath);
    198
    199        try
    200        {
    201            // find the context matching the specified path
    202            final Context context = getContext(path, getBaseContext());
    203
    204            // return all the keys under the context found
    205            final Set<String> keys = new HashSet<>();
    206            if (context != null)
    207            {
    208                recursiveGetKeys(keys, context, prefix, new HashSet<Context>());
    209            }
    210            else if (containsKey(prefix))
    211            {
    212                // add the prefix if it matches exactly a property key
    213                keys.add(prefix);
    214            }
    215
    216            return keys.iterator();
    217        }
    218        catch (final NameNotFoundException e)
    219        {
    220            // expected exception, no need to log it
    221            return new ArrayList<String>().iterator();
    222        }
    223        catch (final NamingException e)
    224        {
    225            fireError(ConfigurationErrorEvent.READ,
    226                    ConfigurationErrorEvent.READ, null, null, e);
    227            return new ArrayList<String>().iterator();
    228        }
    229    }
    230
    231    /**
    232     * Because JNDI is based on a tree configuration, we need to filter down the
    233     * tree, till we find the Context specified by the key to start from.
    234     * Otherwise return null.
    235     *
    236     * @param path     the path of keys to traverse in order to find the context
    237     * @param context  the context to start from
    238     * @return The context at that key's location in the JNDI tree, or null if not found
    239     * @throws NamingException if JNDI has an issue
    240     */
    241    private Context getContext(final List<String> path, final Context context) throws NamingException
    242    {
    243        // return the current context if the path is empty
    244        if (path == null || path.isEmpty())
    245        {
    246            return context;
    247        }
    248
    249        final String key = path.get(0);
    250
    251        // search a context matching the key in the context's elements
    252        NamingEnumeration<NameClassPair> elements = null;
    253
    254        try
    255        {
    256            elements = context.list("");
    257            while (elements.hasMore())
    258            {
    259                final NameClassPair nameClassPair = elements.next();
    260                final String name = nameClassPair.getName();
    261                final Object object = context.lookup(name);
    262
    263                if (object instanceof Context && name.equals(key))
    264                {
    265                    final Context subcontext = (Context) object;
    266
    267                    // recursive search in the sub context
    268                    return getContext(path.subList(1, path.size()), subcontext);
    269                }
    270            }
    271        }
    272        finally
    273        {
    274            if (elements != null)
    275            {
    276                elements.close();
    277            }
    278        }
    279
    280        return null;
    281    }
    282
    283    /**
    284     * Returns a flag whether this configuration is empty.
    285     *
    286     * @return the empty flag
    287     */
    288    @Override
    289    protected boolean isEmptyInternal()
    290    {
    291        try
    292        {
    293            NamingEnumeration<NameClassPair> enumeration = null;
    294
    295            try
    296            {
    297                enumeration = getBaseContext().list("");
    298                return !enumeration.hasMore();
    299            }
    300            finally
    301            {
    302                // close the enumeration
    303                if (enumeration != null)
    304                {
    305                    enumeration.close();
    306                }
    307            }
    308        }
    309        catch (final NamingException e)
    310        {
    311            fireError(ConfigurationErrorEvent.READ,
    312                    ConfigurationErrorEvent.READ, null, null, e);
    313            return true;
    314        }
    315    }
    316
    317    /**
    318     * <p><strong>This operation is not supported and will throw an
    319     * UnsupportedOperationException.</strong></p>
    320     *
    321     * @param key the key
    322     * @param value the value
    323     * @throws UnsupportedOperationException always thrown as this method is not supported
    324     */
    325    @Override
    326    protected void setPropertyInternal(final String key, final Object value)
    327    {
    328        throw new UnsupportedOperationException("This operation is not supported");
    329    }
    330
    331    /**
    332     * Removes the specified property.
    333     *
    334     * @param key the key of the property to remove
    335     */
    336    @Override
    337    protected void clearPropertyDirect(final String key)
    338    {
    339        clearedProperties.add(key);
    340    }
    341
    342    /**
    343     * Checks whether the specified key is contained in this configuration.
    344     *
    345     * @param key the key to check
    346     * @return a flag whether this key is stored in this configuration
    347     */
    348    @Override
    349    protected boolean containsKeyInternal(String key)
    350    {
    351        if (clearedProperties.contains(key))
    352        {
    353            return false;
    354        }
    355        key = key.replaceAll("\\.", "/");
    356        try
    357        {
    358            // throws a NamingException if JNDI doesn't contain the key.
    359            getBaseContext().lookup(key);
    360            return true;
    361        }
    362        catch (final NameNotFoundException e)
    363        {
    364            // expected exception, no need to log it
    365            return false;
    366        }
    367        catch (final NamingException e)
    368        {
    369            fireError(ConfigurationErrorEvent.READ,
    370                    ConfigurationErrorEvent.READ, key, null, e);
    371            return false;
    372        }
    373    }
    374
    375    /**
    376     * Returns the prefix.
    377     * @return the prefix
    378     */
    379    public String getPrefix()
    380    {
    381        return prefix;
    382    }
    383
    384    /**
    385     * Sets the prefix.
    386     *
    387     * @param prefix The prefix to set
    388     */
    389    public void setPrefix(final String prefix)
    390    {
    391        this.prefix = prefix;
    392
    393        // clear the previous baseContext
    394        baseContext = null;
    395    }
    396
    397    /**
    398     * Returns the value of the specified property.
    399     *
    400     * @param key the key of the property
    401     * @return the value of this property
    402     */
    403    @Override
    404    protected Object getPropertyInternal(String key)
    405    {
    406        if (clearedProperties.contains(key))
    407        {
    408            return null;
    409        }
    410
    411        try
    412        {
    413            key = key.replaceAll("\\.", "/");
    414            return getBaseContext().lookup(key);
    415        }
    416        catch (final NameNotFoundException e)
    417        {
    418            // expected exception, no need to log it
    419            return null;
    420        }
    421        catch (final NotContextException nctxex)
    422        {
    423            // expected exception, no need to log it
    424            return null;
    425        }
    426        catch (final NamingException e)
    427        {
    428            fireError(ConfigurationErrorEvent.READ,
    429                    ConfigurationErrorEvent.READ, key, null, e);
    430            return null;
    431        }
    432    }
    433
    434    /**
    435     * <p><strong>This operation is not supported and will throw an
    436     * UnsupportedOperationException.</strong></p>
    437     *
    438     * @param key the key
    439     * @param obj the value
    440     * @throws UnsupportedOperationException always thrown as this method is not supported
    441     */
    442    @Override
    443    protected void addPropertyDirect(final String key, final Object obj)
    444    {
    445        throw new UnsupportedOperationException("This operation is not supported");
    446    }
    447
    448    /**
    449     * Return the base context with the prefix applied.
    450     *
    451     * @return the base context
    452     * @throws NamingException if an error occurs
    453     */
    454    public Context getBaseContext() throws NamingException
    455    {
    456        if (baseContext == null)
    457        {
    458            baseContext = (Context) getContext().lookup(prefix == null ? "" : prefix);
    459        }
    460
    461        return baseContext;
    462    }
    463
    464    /**
    465     * Return the initial context used by this configuration. This context is
    466     * independent of the prefix specified.
    467     *
    468     * @return the initial context
    469     */
    470    public Context getContext()
    471    {
    472        return context;
    473    }
    474
    475    /**
    476     * Set the initial context of the configuration.
    477     *
    478     * @param context the context
    479     */
    480    public void setContext(final Context context)
    481    {
    482        // forget the removed properties
    483        clearedProperties.clear();
    484
    485        // change the context
    486        this.context = context;
    487    }
    488}