    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
    018    package org.apache.commons.configuration;
    019
    020    import java.util.ArrayList;
    021    import java.util.Arrays;
    022    import java.util.HashSet;
    023    import java.util.Iterator;
    024    import java.util.List;
    025    import java.util.Set;
    026
    027    import javax.naming.Context;
    028    import javax.naming.InitialContext;
    029    import javax.naming.NameClassPair;
    030    import javax.naming.NameNotFoundException;
    031    import javax.naming.NamingEnumeration;
    032    import javax.naming.NamingException;
    033    import javax.naming.NotContextException;
    034
    035    import org.apache.commons.lang.StringUtils;
    036    import org.apache.commons.logging.LogFactory;
    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 1234985 2012-01-23 21:09:09Z oheger $
    046     */
    047    public 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 Set<String> clearedProperties = new HashSet<String>();
    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(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(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(Context context, String prefix)
    104        {
    105            this.context = context;
    106            this.prefix = prefix;
    107            setLogger(LogFactory.getLog(getClass()));
    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(Set<String> keys, Context context, String prefix,
    123                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                    NameClassPair nameClassPair = elements.next();
    136                    String name = nameClassPair.getName();
    137                    Object object = context.lookup(name);
    138
    139                    // build the key
    140                    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                        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        public Iterator<String> getKeys()
    181        {
    182            return getKeys("");
    183        }
    184
    185        /**
    186         * Returns an iterator with all property keys starting with the given
    187         * prefix.
    188         *
    189         * @param prefix the prefix
    190         * @return an iterator with the selected keys
    191         */
    192        @Override
    193        public Iterator<String> getKeys(String prefix)
    194        {
    195            // build the path
    196            String[] splitPath = StringUtils.split(prefix, ".");
    197
    198            List<String> path = Arrays.asList(splitPath);
    199
    200            try
    201            {
    202                // find the context matching the specified path
    203                Context context = getContext(path, getBaseContext());
    204
    205                // return all the keys under the context found
    206                Set<String> keys = new HashSet<String>();
    207                if (context != null)
    208                {
    209                    recursiveGetKeys(keys, context, prefix, new HashSet<Context>());
    210                }
    211                else if (containsKey(prefix))
    212                {
    213                    // add the prefix if it matches exactly a property key
    214                    keys.add(prefix);
    215                }
    216
    217                return keys.iterator();
    218            }
    219            catch (NameNotFoundException e)
    220            {
    221                // expected exception, no need to log it
    222                return new ArrayList<String>().iterator();
    223            }
    224            catch (NamingException e)
    225            {
    226                fireError(EVENT_READ_PROPERTY, null, null, e);
    227                return new ArrayList<String>().iterator();
    228            }
    229        }
    230
    231        /**
    232         * Because JNDI is based on a tree configuration, we need to filter down the
    233         * tree, till we find the Context specified by the key to start from.
    234         * Otherwise return null.
    235         *
    236         * @param path     the path of keys to traverse in order to find the context
    237         * @param context  the context to start from
    238         * @return The context at that key's location in the JNDI tree, or null if not found
    239         * @throws NamingException if JNDI has an issue
    240         */
    241        private Context getContext(List<String> path, Context context) throws NamingException
    242        {
    243            // return the current context if the path is empty
    244            if (path == null || path.isEmpty())
    245            {
    246                return context;
    247            }
    248
    249            String key = path.get(0);
    250
    251            // search a context matching the key in the context's elements
    252            NamingEnumeration<NameClassPair> elements = null;
    253
    254            try
    255            {
    256                elements = context.list("");
    257                while (elements.hasMore())
    258                {
    259                    NameClassPair nameClassPair = elements.next();
    260                    String name = nameClassPair.getName();
    261                    Object object = context.lookup(name);
    262
    263                    if (object instanceof Context && name.equals(key))
    264                    {
    265                        Context subcontext = (Context) object;
    266
    267                        // recursive search in the sub context
    268                        return getContext(path.subList(1, path.size()), subcontext);
    269                    }
    270                }
    271            }
    272            finally
    273            {
    274                if (elements != null)
    275                {
    276                    elements.close();
    277                }
    278            }
    279
    280            return null;
    281        }
    282
    283        /**
    284         * Returns a flag whether this configuration is empty.
    285         *
    286         * @return the empty flag
    287         */
    288        public boolean isEmpty()
    289        {
    290            try
    291            {
    292                NamingEnumeration<NameClassPair> enumeration = null;
    293
    294                try
    295                {
    296                    enumeration = getBaseContext().list("");
    297                    return !enumeration.hasMore();
    298                }
    299                finally
    300                {
    301                    // close the enumeration
    302                    if (enumeration != null)
    303                    {
    304                        enumeration.close();
    305                    }
    306                }
    307            }
    308            catch (NamingException e)
    309            {
    310                fireError(EVENT_READ_PROPERTY, null, null, e);
    311                return true;
    312            }
    313        }
    314
    315        /**
    316         * <p><strong>This operation is not supported and will throw an
    317         * UnsupportedOperationException.</strong></p>
    318         *
    319         * @param key the key
    320         * @param value the value
    321         * @throws UnsupportedOperationException
    322         */
    323        @Override
    324        public void setProperty(String key, Object value)
    325        {
    326            throw new UnsupportedOperationException("This operation is not supported");
    327        }
    328
    329        /**
    330         * Removes the specified property.
    331         *
    332         * @param key the key of the property to remove
    333         */
    334        @Override
    335        public void clearProperty(String key)
    336        {
    337            clearedProperties.add(key);
    338        }
    339
    340        /**
    341         * Checks whether the specified key is contained in this configuration.
    342         *
    343         * @param key the key to check
    344         * @return a flag whether this key is stored in this configuration
    345         */
    346        public boolean containsKey(String key)
    347        {
    348            if (clearedProperties.contains(key))
    349            {
    350                return false;
    351            }
    352            key = key.replaceAll("\\.", "/");
    353            try
    354            {
    355                // throws a NamingException if JNDI doesn't contain the key.
    356                getBaseContext().lookup(key);
    357                return true;
    358            }
    359            catch (NameNotFoundException e)
    360            {
    361                // expected exception, no need to log it
    362                return false;
    363            }
    364            catch (NamingException e)
    365            {
    366                fireError(EVENT_READ_PROPERTY, key, null, e);
    367                return false;
    368            }
    369        }
    370
    371        /**
    372         * Returns the prefix.
    373         * @return the prefix
    374         */
    375        public String getPrefix()
    376        {
    377            return prefix;
    378        }
    379
    380        /**
    381         * Sets the prefix.
    382         *
    383         * @param prefix The prefix to set
    384         */
    385        public void setPrefix(String prefix)
    386        {
    387            this.prefix = prefix;
    388
    389            // clear the previous baseContext
    390            baseContext = null;
    391        }
    392
    393        /**
    394         * Returns the value of the specified property.
    395         *
    396         * @param key the key of the property
    397         * @return the value of this property
    398         */
    399        public Object getProperty(String key)
    400        {
    401            if (clearedProperties.contains(key))
    402            {
    403                return null;
    404            }
    405
    406            try
    407            {
    408                key = key.replaceAll("\\.", "/");
    409                return getBaseContext().lookup(key);
    410            }
    411            catch (NameNotFoundException e)
    412            {
    413                // expected exception, no need to log it
    414                return null;
    415            }
    416            catch (NotContextException nctxex)
    417            {
    418                // expected exception, no need to log it
    419                return null;
    420            }
    421            catch (NamingException e)
    422            {
    423                fireError(EVENT_READ_PROPERTY, key, null, e);
    424                return null;
    425            }
    426        }
    427
    428        /**
    429         * <p><strong>This operation is not supported and will throw an
    430         * UnsupportedOperationException.</strong></p>
    431         *
    432         * @param key the key
    433         * @param obj the value
    434         * @throws UnsupportedOperationException
    435         */
    436        @Override
    437        protected void addPropertyDirect(String key, Object obj)
    438        {
    439            throw new UnsupportedOperationException("This operation is not supported");
    440        }
    441
    442        /**
    443         * Return the base context with the prefix applied.
    444         *
    445         * @return the base context
    446         * @throws NamingException if an error occurs
    447         */
    448        public Context getBaseContext() throws NamingException
    449        {
    450            if (baseContext == null)
    451            {
    452                baseContext = (Context) getContext().lookup(prefix == null ? "" : prefix);
    453            }
    454
    455            return baseContext;
    456        }
    457
    458        /**
    459         * Return the initial context used by this configuration. This context is
    460         * independent of the prefix specified.
    461         *
    462         * @return the initial context
    463         */
    464        public Context getContext()
    465        {
    466            return context;
    467        }
    468
    469        /**
    470         * Set the initial context of the configuration.
    471         *
    472         * @param context the context
    473         */
    474        public void setContext(Context context)
    475        {
    476            // forget the removed properties
    477            clearedProperties.clear();
    478
    479            // change the context
    480            this.context = context;
    481        }
    482    }