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 */
045public class JNDIConfiguration extends AbstractConfiguration
046{
047    /** The prefix of the context. */
048    private String prefix;
049
050    /** The initial JNDI context. */
051    private Context context;
052
053    /** The base JNDI context. */
054    private Context baseContext;
055
056    /** The Set of keys that have been virtually cleared. */
057    private final Set<String> clearedProperties = new HashSet<>();
058
059    /**
060     * Creates a JNDIConfiguration using the default initial context as the
061     * root of the properties.
062     *
063     * @throws NamingException thrown if an error occurs when initializing the default context
064     */
065    public JNDIConfiguration() throws NamingException
066    {
067        this((String) null);
068    }
069
070    /**
071     * Creates a JNDIConfiguration using the default initial context, shifted
072     * with the specified prefix, as the root of the properties.
073     *
074     * @param prefix the prefix
075     *
076     * @throws NamingException thrown if an error occurs when initializing the default context
077     */
078    public JNDIConfiguration(final String prefix) throws NamingException
079    {
080        this(new InitialContext(), prefix);
081    }
082
083    /**
084     * Creates a JNDIConfiguration using the specified initial context as the
085     * root of the properties.
086     *
087     * @param context the initial context
088     */
089    public JNDIConfiguration(final Context context)
090    {
091        this(context, null);
092    }
093
094    /**
095     * Creates a JNDIConfiguration using the specified initial context shifted
096     * by the specified prefix as the root of the properties.
097     *
098     * @param context the initial context
099     * @param prefix the prefix
100     */
101    public JNDIConfiguration(final Context context, final String prefix)
102    {
103        this.context = context;
104        this.prefix = prefix;
105        initLogger(new ConfigurationLogger(JNDIConfiguration.class));
106        addErrorLogListener();
107    }
108
109    /**
110     * This method recursive traverse the JNDI tree, looking for Context objects.
111     * When it finds them, it traverses them as well.  Otherwise it just adds the
112     * values to the list of keys found.
113     *
114     * @param keys All the keys that have been found.
115     * @param context The parent context
116     * @param prefix What prefix we are building on.
117     * @param processedCtx a set with the so far processed objects
118     * @throws NamingException If JNDI has an issue.
119     */
120    private void recursiveGetKeys(final Set<String> keys, final Context context, final String prefix,
121            final Set<Context> processedCtx) throws NamingException
122    {
123        processedCtx.add(context);
124        NamingEnumeration<NameClassPair> elements = null;
125
126        try
127        {
128            elements = context.list("");
129
130            // iterates through the context's elements
131            while (elements.hasMore())
132            {
133                final NameClassPair nameClassPair = elements.next();
134                final String name = nameClassPair.getName();
135                final Object object = context.lookup(name);
136
137                // build the key
138                final StringBuilder key = new StringBuilder();
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                    final 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    @Override
179    protected Iterator<String> getKeysInternal()
180    {
181        return getKeysInternal("");
182    }
183
184    /**
185     * Returns an iterator with all property keys starting with the given
186     * prefix.
187     *
188     * @param prefix the prefix
189     * @return an iterator with the selected keys
190     */
191    @Override
192    protected Iterator<String> getKeysInternal(final String prefix)
193    {
194        // build the path
195        final String[] splitPath = StringUtils.split(prefix, ".");
196
197        final List<String> path = Arrays.asList(splitPath);
198
199        try
200        {
201            // find the context matching the specified path
202            final Context context = getContext(path, getBaseContext());
203
204            // return all the keys under the context found
205            final Set<String> keys = new HashSet<>();
206            if (context != null)
207            {
208                recursiveGetKeys(keys, context, prefix, new HashSet<Context>());
209            }
210            else if (containsKey(prefix))
211            {
212                // add the prefix if it matches exactly a property key
213                keys.add(prefix);
214            }
215
216            return keys.iterator();
217        }
218        catch (final NameNotFoundException e)
219        {
220            // expected exception, no need to log it
221            return new ArrayList<String>().iterator();
222        }
223        catch (final NamingException e)
224        {
225            fireError(ConfigurationErrorEvent.READ,
226                    ConfigurationErrorEvent.READ, 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(final List<String> path, final 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        final 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                final NameClassPair nameClassPair = elements.next();
260                final String name = nameClassPair.getName();
261                final Object object = context.lookup(name);
262
263                if (object instanceof Context && name.equals(key))
264                {
265                    final 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    @Override
289    protected boolean isEmptyInternal()
290    {
291        try
292        {
293            NamingEnumeration<NameClassPair> enumeration = null;
294
295            try
296            {
297                enumeration = getBaseContext().list("");
298                return !enumeration.hasMore();
299            }
300            finally
301            {
302                // close the enumeration
303                if (enumeration != null)
304                {
305                    enumeration.close();
306                }
307            }
308        }
309        catch (final NamingException e)
310        {
311            fireError(ConfigurationErrorEvent.READ,
312                    ConfigurationErrorEvent.READ, 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 always thrown as this method is not supported
324     */
325    @Override
326    protected void setPropertyInternal(final String key, final Object value)
327    {
328        throw new UnsupportedOperationException("This operation is not supported");
329    }
330
331    /**
332     * Removes the specified property.
333     *
334     * @param key the key of the property to remove
335     */
336    @Override
337    protected void clearPropertyDirect(final String key)
338    {
339        clearedProperties.add(key);
340    }
341
342    /**
343     * Checks whether the specified key is contained in this configuration.
344     *
345     * @param key the key to check
346     * @return a flag whether this key is stored in this configuration
347     */
348    @Override
349    protected boolean containsKeyInternal(String key)
350    {
351        if (clearedProperties.contains(key))
352        {
353            return false;
354        }
355        key = key.replaceAll("\\.", "/");
356        try
357        {
358            // throws a NamingException if JNDI doesn't contain the key.
359            getBaseContext().lookup(key);
360            return true;
361        }
362        catch (final NameNotFoundException e)
363        {
364            // expected exception, no need to log it
365            return false;
366        }
367        catch (final NamingException e)
368        {
369            fireError(ConfigurationErrorEvent.READ,
370                    ConfigurationErrorEvent.READ, key, null, e);
371            return false;
372        }
373    }
374
375    /**
376     * Returns the prefix.
377     * @return the prefix
378     */
379    public String getPrefix()
380    {
381        return prefix;
382    }
383
384    /**
385     * Sets the prefix.
386     *
387     * @param prefix The prefix to set
388     */
389    public void setPrefix(final String prefix)
390    {
391        this.prefix = prefix;
392
393        // clear the previous baseContext
394        baseContext = null;
395    }
396
397    /**
398     * Returns the value of the specified property.
399     *
400     * @param key the key of the property
401     * @return the value of this property
402     */
403    @Override
404    protected Object getPropertyInternal(String key)
405    {
406        if (clearedProperties.contains(key))
407        {
408            return null;
409        }
410
411        try
412        {
413            key = key.replaceAll("\\.", "/");
414            return getBaseContext().lookup(key);
415        }
416        catch (final NameNotFoundException e)
417        {
418            // expected exception, no need to log it
419            return null;
420        }
421        catch (final NotContextException nctxex)
422        {
423            // expected exception, no need to log it
424            return null;
425        }
426        catch (final NamingException e)
427        {
428            fireError(ConfigurationErrorEvent.READ,
429                    ConfigurationErrorEvent.READ, key, null, e);
430            return null;
431        }
432    }
433
434    /**
435     * <p><strong>This operation is not supported and will throw an
436     * UnsupportedOperationException.</strong></p>
437     *
438     * @param key the key
439     * @param obj the value
440     * @throws UnsupportedOperationException always thrown as this method is not supported
441     */
442    @Override
443    protected void addPropertyDirect(final String key, final Object obj)
444    {
445        throw new UnsupportedOperationException("This operation is not supported");
446    }
447
448    /**
449     * Return the base context with the prefix applied.
450     *
451     * @return the base context
452     * @throws NamingException if an error occurs
453     */
454    public Context getBaseContext() throws NamingException
455    {
456        if (baseContext == null)
457        {
458            baseContext = (Context) getContext().lookup(prefix == null ? "" : prefix);
459        }
460
461        return baseContext;
462    }
463
464    /**
465     * Return the initial context used by this configuration. This context is
466     * independent of the prefix specified.
467     *
468     * @return the initial context
469     */
470    public Context getContext()
471    {
472        return context;
473    }
474
475    /**
476     * Set the initial context of the configuration.
477     *
478     * @param context the context
479     */
480    public void setContext(final Context context)
481    {
482        // forget the removed properties
483        clearedProperties.clear();
484
485        // change the context
486        this.context = context;
487    }
488}