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 549591 2007-06-21 19:57: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 (NamingException e)
222            {
223                fireError(EVENT_READ_PROPERTY, null, null, e);
224                return new ArrayList().iterator();
225            }
226        }
227    
228        /**
229         * Because JNDI is based on a tree configuration, we need to filter down the
230         * tree, till we find the Context specified by the key to start from.
231         * Otherwise return null.
232         *
233         * @param path     the path of keys to traverse in order to find the context
234         * @param context  the context to start from
235         * @return The context at that key's location in the JNDI tree, or null if not found
236         * @throws NamingException if JNDI has an issue
237         */
238        private Context getContext(List path, Context context) throws NamingException
239        {
240            // return the current context if the path is empty
241            if (path == null || path.isEmpty())
242            {
243                return context;
244            }
245    
246            String key = (String) path.get(0);
247    
248            // search a context matching the key in the context's elements
249            NamingEnumeration elements = null;
250    
251            try
252            {
253                elements = context.list("");
254                while (elements.hasMore())
255                {
256                    NameClassPair nameClassPair = (NameClassPair) elements.next();
257                    String name = nameClassPair.getName();
258                    Object object = context.lookup(name);
259    
260                    if (object instanceof Context && name.equals(key))
261                    {
262                        Context subcontext = (Context) object;
263    
264                        // recursive search in the sub context
265                        return getContext(path.subList(1, path.size()), subcontext);
266                    }
267                }
268            }
269            finally
270            {
271                if (elements != null)
272                {
273                    elements.close();
274                }
275            }
276    
277            return null;
278        }
279    
280        /**
281         * Returns a flag whether this configuration is empty.
282         *
283         * @return the empty flag
284         */
285        public boolean isEmpty()
286        {
287            try
288            {
289                NamingEnumeration enumeration = null;
290    
291                try
292                {
293                    enumeration = getBaseContext().list("");
294                    return !enumeration.hasMore();
295                }
296                finally
297                {
298                    // close the enumeration
299                    if (enumeration != null)
300                    {
301                        enumeration.close();
302                    }
303                }
304            }
305            catch (NamingException e)
306            {
307                fireError(EVENT_READ_PROPERTY, null, null, e);
308                return true;
309            }
310        }
311    
312        /**
313         * <p><strong>This operation is not supported and will throw an
314         * UnsupportedOperationException.</strong></p>
315         *
316         * @param key the key
317         * @param value the value
318         * @throws UnsupportedOperationException
319         */
320        public void setProperty(String key, Object value)
321        {
322            throw new UnsupportedOperationException("This operation is not supported");
323        }
324    
325        /**
326         * Removes the specified property.
327         *
328         * @param key the key of the property to remove
329         */
330        public void clearProperty(String key)
331        {
332            clearedProperties.add(key);
333        }
334    
335        /**
336         * Checks whether the specified key is contained in this configuration.
337         *
338         * @param key the key to check
339         * @return a flag whether this key is stored in this configuration
340         */
341        public boolean containsKey(String key)
342        {
343            if (clearedProperties.contains(key))
344            {
345                return false;
346            }
347            key = StringUtils.replace(key, ".", "/");
348            try
349            {
350                // throws a NamingException if JNDI doesn't contain the key.
351                getBaseContext().lookup(key);
352                return true;
353            }
354            catch (NameNotFoundException e)
355            {
356                // expected exception, no need to log it
357                return false;
358            }
359            catch (NamingException e)
360            {
361                fireError(EVENT_READ_PROPERTY, key, null, e);
362                return false;
363            }
364        }
365    
366        /**
367         * Returns the prefix.
368         * @return the prefix
369         */
370        public String getPrefix()
371        {
372            return prefix;
373        }
374    
375        /**
376         * Sets the prefix.
377         *
378         * @param prefix The prefix to set
379         */
380        public void setPrefix(String prefix)
381        {
382            this.prefix = prefix;
383    
384            // clear the previous baseContext
385            baseContext = null;
386        }
387    
388        /**
389         * Returns the value of the specified property.
390         *
391         * @param key the key of the property
392         * @return the value of this property
393         */
394        public Object getProperty(String key)
395        {
396            if (clearedProperties.contains(key))
397            {
398                return null;
399            }
400    
401            try
402            {
403                key = StringUtils.replace(key, ".", "/");
404                return getBaseContext().lookup(key);
405            }
406            catch (NameNotFoundException e)
407            {
408                // expected exception, no need to log it
409                return null;
410            }
411            catch (NotContextException nctxex)
412            {
413                // expected exception, no need to log it
414                return null;
415            }
416            catch (NamingException e)
417            {
418                fireError(EVENT_READ_PROPERTY, key, null, e);
419                return null;
420            }
421        }
422    
423        /**
424         * <p><strong>This operation is not supported and will throw an
425         * UnsupportedOperationException.</strong></p>
426         *
427         * @param key the key
428         * @param obj the value
429         * @throws UnsupportedOperationException
430         */
431        protected void addPropertyDirect(String key, Object obj)
432        {
433            throw new UnsupportedOperationException("This operation is not supported");
434        }
435    
436        /**
437         * Return the base context with the prefix applied.
438         *
439         * @return the base context
440         * @throws NamingException if an error occurs
441         */
442        public Context getBaseContext() throws NamingException
443        {
444            if (baseContext == null)
445            {
446                baseContext = (Context) getContext().lookup(prefix == null ? "" : prefix);
447            }
448    
449            return baseContext;
450        }
451    
452        /**
453         * Return the initial context used by this configuration. This context is
454         * independent of the prefix specified.
455         *
456         * @return the initial context
457         */
458        public Context getContext()
459        {
460            return context;
461        }
462    
463        /**
464         * Set the initial context of the configuration.
465         *
466         * @param context the context
467         */
468        public void setContext(Context context)
469        {
470            // forget the removed properties
471            clearedProperties.clear();
472    
473            // change the context
474            this.context = context;
475        }
476    }