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.configuration;
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.lang.StringUtils;
036import 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 */
047public 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}