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