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