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.configuration2;
019
020import javax.naming.Context;
021import javax.naming.InitialContext;
022import javax.naming.NameClassPair;
023import javax.naming.NameNotFoundException;
024import javax.naming.NamingEnumeration;
025import javax.naming.NamingException;
026import javax.naming.NotContextException;
027import java.util.ArrayList;
028import java.util.Arrays;
029import java.util.HashSet;
030import java.util.Iterator;
031import java.util.List;
032import java.util.Set;
033
034import org.apache.commons.configuration2.event.ConfigurationErrorEvent;
035import org.apache.commons.configuration2.io.ConfigurationLogger;
036import org.apache.commons.lang3.StringUtils;
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 */
046public 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 final Set<String> 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(final 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(final 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(final Context context, final String prefix)
103    {
104        this.context = context;
105        this.prefix = prefix;
106        initLogger(new ConfigurationLogger(JNDIConfiguration.class));
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(final Set<String> keys, final Context context, final String prefix,
122            final Set<Context> processedCtx) throws NamingException
123    {
124        processedCtx.add(context);
125        NamingEnumeration<NameClassPair> elements = null;
126
127        try
128        {
129            elements = context.list("");
130
131            // iterates through the context's elements
132            while (elements.hasMore())
133            {
134                final NameClassPair nameClassPair = elements.next();
135                final String name = nameClassPair.getName();
136                final Object object = context.lookup(name);
137
138                // build the key
139                final StringBuilder key = new StringBuilder();
140                key.append(prefix);
141                if (key.length() > 0)
142                {
143                    key.append(".");
144                }
145                key.append(name);
146
147                if (object instanceof Context)
148                {
149                    // add the keys of the sub context
150                    final Context subcontext = (Context) object;
151                    if (!processedCtx.contains(subcontext))
152                    {
153                        recursiveGetKeys(keys, subcontext, key.toString(),
154                                processedCtx);
155                    }
156                }
157                else
158                {
159                    // add the key
160                    keys.add(key.toString());
161                }
162            }
163        }
164        finally
165        {
166            // close the enumeration
167            if (elements != null)
168            {
169                elements.close();
170            }
171        }
172    }
173
174    /**
175     * Returns an iterator with all property keys stored in this configuration.
176     *
177     * @return an iterator with all keys
178     */
179    @Override
180    protected Iterator<String> getKeysInternal()
181    {
182        return getKeysInternal("");
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    protected Iterator<String> getKeysInternal(final String prefix)
194    {
195        // build the path
196        final String[] splitPath = StringUtils.split(prefix, ".");
197
198        final List<String> path = Arrays.asList(splitPath);
199
200        try
201        {
202            // find the context matching the specified path
203            final Context context = getContext(path, getBaseContext());
204
205            // return all the keys under the context found
206            final Set<String> keys = new HashSet<>();
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 (final NameNotFoundException e)
220        {
221            // expected exception, no need to log it
222            return new ArrayList<String>().iterator();
223        }
224        catch (final NamingException e)
225        {
226            fireError(ConfigurationErrorEvent.READ,
227                    ConfigurationErrorEvent.READ, null, null, e);
228            return new ArrayList<String>().iterator();
229        }
230    }
231
232    /**
233     * Because JNDI is based on a tree configuration, we need to filter down the
234     * tree, till we find the Context specified by the key to start from.
235     * Otherwise return null.
236     *
237     * @param path     the path of keys to traverse in order to find the context
238     * @param context  the context to start from
239     * @return The context at that key's location in the JNDI tree, or null if not found
240     * @throws NamingException if JNDI has an issue
241     */
242    private Context getContext(final List<String> path, final Context context) throws NamingException
243    {
244        // return the current context if the path is empty
245        if (path == null || path.isEmpty())
246        {
247            return context;
248        }
249
250        final String key = path.get(0);
251
252        // search a context matching the key in the context's elements
253        NamingEnumeration<NameClassPair> elements = null;
254
255        try
256        {
257            elements = context.list("");
258            while (elements.hasMore())
259            {
260                final NameClassPair nameClassPair = elements.next();
261                final String name = nameClassPair.getName();
262                final Object object = context.lookup(name);
263
264                if (object instanceof Context && name.equals(key))
265                {
266                    final Context subcontext = (Context) object;
267
268                    // recursive search in the sub context
269                    return getContext(path.subList(1, path.size()), subcontext);
270                }
271            }
272        }
273        finally
274        {
275            if (elements != null)
276            {
277                elements.close();
278            }
279        }
280
281        return null;
282    }
283
284    /**
285     * Returns a flag whether this configuration is empty.
286     *
287     * @return the empty flag
288     */
289    @Override
290    protected boolean isEmptyInternal()
291    {
292        try
293        {
294            NamingEnumeration<NameClassPair> 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 (final NamingException e)
311        {
312            fireError(ConfigurationErrorEvent.READ,
313                    ConfigurationErrorEvent.READ, null, null, e);
314            return true;
315        }
316    }
317
318    /**
319     * <p><strong>This operation is not supported and will throw an
320     * UnsupportedOperationException.</strong></p>
321     *
322     * @param key the key
323     * @param value the value
324     * @throws UnsupportedOperationException always thrown as this method is not supported
325     */
326    @Override
327    protected void setPropertyInternal(final String key, final Object value)
328    {
329        throw new UnsupportedOperationException("This operation is not supported");
330    }
331
332    /**
333     * Removes the specified property.
334     *
335     * @param key the key of the property to remove
336     */
337    @Override
338    protected void clearPropertyDirect(final String key)
339    {
340        clearedProperties.add(key);
341    }
342
343    /**
344     * Checks whether the specified key is contained in this configuration.
345     *
346     * @param key the key to check
347     * @return a flag whether this key is stored in this configuration
348     */
349    @Override
350    protected boolean containsKeyInternal(String key)
351    {
352        if (clearedProperties.contains(key))
353        {
354            return false;
355        }
356        key = key.replaceAll("\\.", "/");
357        try
358        {
359            // throws a NamingException if JNDI doesn't contain the key.
360            getBaseContext().lookup(key);
361            return true;
362        }
363        catch (final NameNotFoundException e)
364        {
365            // expected exception, no need to log it
366            return false;
367        }
368        catch (final NamingException e)
369        {
370            fireError(ConfigurationErrorEvent.READ,
371                    ConfigurationErrorEvent.READ, key, null, e);
372            return false;
373        }
374    }
375
376    /**
377     * Returns the prefix.
378     * @return the prefix
379     */
380    public String getPrefix()
381    {
382        return prefix;
383    }
384
385    /**
386     * Sets the prefix.
387     *
388     * @param prefix The prefix to set
389     */
390    public void setPrefix(final String prefix)
391    {
392        this.prefix = prefix;
393
394        // clear the previous baseContext
395        baseContext = null;
396    }
397
398    /**
399     * Returns the value of the specified property.
400     *
401     * @param key the key of the property
402     * @return the value of this property
403     */
404    @Override
405    protected Object getPropertyInternal(String key)
406    {
407        if (clearedProperties.contains(key))
408        {
409            return null;
410        }
411
412        try
413        {
414            key = key.replaceAll("\\.", "/");
415            return getBaseContext().lookup(key);
416        }
417        catch (final NameNotFoundException e)
418        {
419            // expected exception, no need to log it
420            return null;
421        }
422        catch (final NotContextException nctxex)
423        {
424            // expected exception, no need to log it
425            return null;
426        }
427        catch (final NamingException e)
428        {
429            fireError(ConfigurationErrorEvent.READ,
430                    ConfigurationErrorEvent.READ, key, null, e);
431            return null;
432        }
433    }
434
435    /**
436     * <p><strong>This operation is not supported and will throw an
437     * UnsupportedOperationException.</strong></p>
438     *
439     * @param key the key
440     * @param obj the value
441     * @throws UnsupportedOperationException always thrown as this method is not supported
442     */
443    @Override
444    protected void addPropertyDirect(final String key, final Object obj)
445    {
446        throw new UnsupportedOperationException("This operation is not supported");
447    }
448
449    /**
450     * Return the base context with the prefix applied.
451     *
452     * @return the base context
453     * @throws NamingException if an error occurs
454     */
455    public Context getBaseContext() throws NamingException
456    {
457        if (baseContext == null)
458        {
459            baseContext = (Context) getContext().lookup(prefix == null ? "" : prefix);
460        }
461
462        return baseContext;
463    }
464
465    /**
466     * Return the initial context used by this configuration. This context is
467     * independent of the prefix specified.
468     *
469     * @return the initial context
470     */
471    public Context getContext()
472    {
473        return context;
474    }
475
476    /**
477     * Set the initial context of the configuration.
478     *
479     * @param context the context
480     */
481    public void setContext(final Context context)
482    {
483        // forget the removed properties
484        clearedProperties.clear();
485
486        // change the context
487        this.context = context;
488    }
489}