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