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