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    }