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.HashSet;
022    import java.util.Iterator;
023    import java.util.List;
024    import java.util.Set;
025    
026    import javax.naming.Context;
027    import javax.naming.InitialContext;
028    import javax.naming.NameClassPair;
029    import javax.naming.NameNotFoundException;
030    import javax.naming.NamingEnumeration;
031    import javax.naming.NamingException;
032    import javax.naming.NotContextException;
033    
034    import org.apache.commons.lang.StringUtils;
035    import org.apache.commons.logging.LogFactory;
036    
037    /**
038     * This Configuration class allows you to interface with a JNDI datasource.
039     * A JNDIConfiguration is read-only, write operations will throw an
040     * UnsupportedOperationException. The clear operations are supported but the
041     * underlying JNDI data source is not changed.
042     *
043     * @author <a href="mailto:epugh@upstate.com">Eric Pugh</a>
044     * @version $Id: JNDIConfiguration.java 1081926 2011-03-15 20:20:25Z oheger $
045     */
046    public class JNDIConfiguration extends AbstractConfiguration
047    {
048        /** The prefix of the context. */
049        private String prefix;
050    
051        /** The initial JNDI context. */
052        private Context context;
053    
054        /** The base JNDI context. */
055        private Context baseContext;
056    
057        /** The Set of keys that have been virtually cleared. */
058        private Set clearedProperties = new HashSet();
059    
060        /**
061         * Creates a JNDIConfiguration using the default initial context as the
062         * root of the properties.
063         *
064         * @throws NamingException thrown if an error occurs when initializing the default context
065         */
066        public JNDIConfiguration() throws NamingException
067        {
068            this((String) null);
069        }
070    
071        /**
072         * Creates a JNDIConfiguration using the default initial context, shifted
073         * with the specified prefix, as the root of the properties.
074         *
075         * @param prefix the prefix
076         *
077         * @throws NamingException thrown if an error occurs when initializing the default context
078         */
079        public JNDIConfiguration(String prefix) throws NamingException
080        {
081            this(new InitialContext(), prefix);
082        }
083    
084        /**
085         * Creates a JNDIConfiguration using the specified initial context as the
086         * root of the properties.
087         *
088         * @param context the initial context
089         */
090        public JNDIConfiguration(Context context)
091        {
092            this(context, null);
093        }
094    
095        /**
096         * Creates a JNDIConfiguration using the specified initial context shifted
097         * by the specified prefix as the root of the properties.
098         *
099         * @param context the initial context
100         * @param prefix the prefix
101         */
102        public JNDIConfiguration(Context context, String prefix)
103        {
104            this.context = context;
105            this.prefix = prefix;
106            setLogger(LogFactory.getLog(getClass()));
107            addErrorLogListener();
108        }
109    
110        /**
111         * This method recursive traverse the JNDI tree, looking for Context objects.
112         * When it finds them, it traverses them as well.  Otherwise it just adds the
113         * values to the list of keys found.
114         *
115         * @param keys All the keys that have been found.
116         * @param context The parent context
117         * @param prefix What prefix we are building on.
118         * @param processedCtx a set with the so far processed objects
119         * @throws NamingException If JNDI has an issue.
120         */
121        private void recursiveGetKeys(Set keys, Context context, String prefix, Set processedCtx) throws NamingException
122        {
123            processedCtx.add(context);
124            NamingEnumeration elements = null;
125    
126            try
127            {
128                elements = context.list("");
129    
130                // iterates through the context's elements
131                while (elements.hasMore())
132                {
133                    NameClassPair nameClassPair = (NameClassPair) elements.next();
134                    String name = nameClassPair.getName();
135                    Object object = context.lookup(name);
136    
137                    // build the key
138                    StringBuffer key = new StringBuffer();
139                    key.append(prefix);
140                    if (key.length() > 0)
141                    {
142                        key.append(".");
143                    }
144                    key.append(name);
145    
146                    if (object instanceof Context)
147                    {
148                        // add the keys of the sub context
149                        Context subcontext = (Context) object;
150                        if (!processedCtx.contains(subcontext))
151                        {
152                            recursiveGetKeys(keys, subcontext, key.toString(),
153                                    processedCtx);
154                        }
155                    }
156                    else
157                    {
158                        // add the key
159                        keys.add(key.toString());
160                    }
161                }
162            }
163            finally
164            {
165                // close the enumeration
166                if (elements != null)
167                {
168                    elements.close();
169                }
170            }
171        }
172    
173        /**
174         * Returns an iterator with all property keys stored in this configuration.
175         *
176         * @return an iterator with all keys
177         */
178        public Iterator getKeys()
179        {
180            return getKeys("");
181        }
182    
183        /**
184         * Returns an iterator with all property keys starting with the given
185         * prefix.
186         *
187         * @param prefix the prefix
188         * @return an iterator with the selected keys
189         */
190        public Iterator getKeys(String prefix)
191        {
192            // build the path
193            String[] splitPath = StringUtils.split(prefix, ".");
194    
195            List path = new ArrayList();
196    
197            for (int i = 0; i < splitPath.length; i++)
198            {
199                path.add(splitPath[i]);
200            }
201    
202            try
203            {
204                // find the context matching the specified path
205                Context context = getContext(path, getBaseContext());
206    
207                // return all the keys under the context found
208                Set keys = new HashSet();
209                if (context != null)
210                {
211                    recursiveGetKeys(keys, context, prefix, new HashSet());
212                }
213                else if (containsKey(prefix))
214                {
215                    // add the prefix if it matches exactly a property key
216                    keys.add(prefix);
217                }
218    
219                return keys.iterator();
220            }
221            catch (NameNotFoundException e)
222            {
223                // expected exception, no need to log it
224                return new ArrayList().iterator();
225            }
226            catch (NamingException e)
227            {
228                fireError(EVENT_READ_PROPERTY, null, null, e);
229                return new ArrayList().iterator();
230            }
231        }
232    
233        /**
234         * Because JNDI is based on a tree configuration, we need to filter down the
235         * tree, till we find the Context specified by the key to start from.
236         * Otherwise return null.
237         *
238         * @param path     the path of keys to traverse in order to find the context
239         * @param context  the context to start from
240         * @return The context at that key's location in the JNDI tree, or null if not found
241         * @throws NamingException if JNDI has an issue
242         */
243        private Context getContext(List path, Context context) throws NamingException
244        {
245            // return the current context if the path is empty
246            if (path == null || path.isEmpty())
247            {
248                return context;
249            }
250    
251            String key = (String) path.get(0);
252    
253            // search a context matching the key in the context's elements
254            NamingEnumeration elements = null;
255    
256            try
257            {
258                elements = context.list("");
259                while (elements.hasMore())
260                {
261                    NameClassPair nameClassPair = (NameClassPair) elements.next();
262                    String name = nameClassPair.getName();
263                    Object object = context.lookup(name);
264    
265                    if (object instanceof Context && name.equals(key))
266                    {
267                        Context subcontext = (Context) object;
268    
269                        // recursive search in the sub context
270                        return getContext(path.subList(1, path.size()), subcontext);
271                    }
272                }
273            }
274            finally
275            {
276                if (elements != null)
277                {
278                    elements.close();
279                }
280            }
281    
282            return null;
283        }
284    
285        /**
286         * Returns a flag whether this configuration is empty.
287         *
288         * @return the empty flag
289         */
290        public boolean isEmpty()
291        {
292            try
293            {
294                NamingEnumeration enumeration = null;
295    
296                try
297                {
298                    enumeration = getBaseContext().list("");
299                    return !enumeration.hasMore();
300                }
301                finally
302                {
303                    // close the enumeration
304                    if (enumeration != null)
305                    {
306                        enumeration.close();
307                    }
308                }
309            }
310            catch (NamingException e)
311            {
312                fireError(EVENT_READ_PROPERTY, null, null, e);
313                return true;
314            }
315        }
316    
317        /**
318         * <p><strong>This operation is not supported and will throw an
319         * UnsupportedOperationException.</strong></p>
320         *
321         * @param key the key
322         * @param value the value
323         * @throws UnsupportedOperationException
324         */
325        public void setProperty(String key, Object value)
326        {
327            throw new UnsupportedOperationException("This operation is not supported");
328        }
329    
330        /**
331         * Removes the specified property.
332         *
333         * @param key the key of the property to remove
334         */
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 = StringUtils.replace(key, ".", "/");
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 = StringUtils.replace(key, ".", "/");
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        protected void addPropertyDirect(String key, Object obj)
437        {
438            throw new UnsupportedOperationException("This operation is not supported");
439        }
440    
441        /**
442         * Return the base context with the prefix applied.
443         *
444         * @return the base context
445         * @throws NamingException if an error occurs
446         */
447        public Context getBaseContext() throws NamingException
448        {
449            if (baseContext == null)
450            {
451                baseContext = (Context) getContext().lookup(prefix == null ? "" : prefix);
452            }
453    
454            return baseContext;
455        }
456    
457        /**
458         * Return the initial context used by this configuration. This context is
459         * independent of the prefix specified.
460         *
461         * @return the initial context
462         */
463        public Context getContext()
464        {
465            return context;
466        }
467    
468        /**
469         * Set the initial context of the configuration.
470         *
471         * @param context the context
472         */
473        public void setContext(Context context)
474        {
475            // forget the removed properties
476            clearedProperties.clear();
477    
478            // change the context
479            this.context = context;
480        }
481    }