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