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