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 * @version $Id: JNDIConfiguration.java 1842194 2018-09-27 22:24:23Z ggregory $
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 final Set<String> clearedProperties = new HashSet<>();
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(final 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(final 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(final Context context, final String prefix)
104    {
105        this.context = context;
106        this.prefix = prefix;
107        initLogger(new ConfigurationLogger(JNDIConfiguration.class));
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(final Set<String> keys, final Context context, final String prefix,
123            final 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                final NameClassPair nameClassPair = elements.next();
136                final String name = nameClassPair.getName();
137                final Object object = context.lookup(name);
138
139                // build the key
140                final 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                    final 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    @Override
181    protected Iterator<String> getKeysInternal()
182    {
183        return getKeysInternal("");
184    }
185
186    /**
187     * Returns an iterator with all property keys starting with the given
188     * prefix.
189     *
190     * @param prefix the prefix
191     * @return an iterator with the selected keys
192     */
193    @Override
194    protected Iterator<String> getKeysInternal(final String prefix)
195    {
196        // build the path
197        final String[] splitPath = StringUtils.split(prefix, ".");
198
199        final List<String> path = Arrays.asList(splitPath);
200
201        try
202        {
203            // find the context matching the specified path
204            final Context context = getContext(path, getBaseContext());
205
206            // return all the keys under the context found
207            final Set<String> keys = new HashSet<>();
208            if (context != null)
209            {
210                recursiveGetKeys(keys, context, prefix, new HashSet<Context>());
211            }
212            else if (containsKey(prefix))
213            {
214                // add the prefix if it matches exactly a property key
215                keys.add(prefix);
216            }
217
218            return keys.iterator();
219        }
220        catch (final NameNotFoundException e)
221        {
222            // expected exception, no need to log it
223            return new ArrayList<String>().iterator();
224        }
225        catch (final NamingException e)
226        {
227            fireError(ConfigurationErrorEvent.READ,
228                    ConfigurationErrorEvent.READ, null, null, e);
229            return new ArrayList<String>().iterator();
230        }
231    }
232
233    /**
234     * Because JNDI is based on a tree configuration, we need to filter down the
235     * tree, till we find the Context specified by the key to start from.
236     * Otherwise return null.
237     *
238     * @param path     the path of keys to traverse in order to find the context
239     * @param context  the context to start from
240     * @return The context at that key's location in the JNDI tree, or null if not found
241     * @throws NamingException if JNDI has an issue
242     */
243    private Context getContext(final List<String> path, final Context context) throws NamingException
244    {
245        // return the current context if the path is empty
246        if (path == null || path.isEmpty())
247        {
248            return context;
249        }
250
251        final String key = path.get(0);
252
253        // search a context matching the key in the context's elements
254        NamingEnumeration<NameClassPair> elements = null;
255
256        try
257        {
258            elements = context.list("");
259            while (elements.hasMore())
260            {
261                final NameClassPair nameClassPair = elements.next();
262                final String name = nameClassPair.getName();
263                final Object object = context.lookup(name);
264
265                if (object instanceof Context && name.equals(key))
266                {
267                    final Context subcontext = (Context) object;
268
269                    // recursive search in the sub context
270                    return getContext(path.subList(1, path.size()), subcontext);
271                }
272            }
273        }
274        finally
275        {
276            if (elements != null)
277            {
278                elements.close();
279            }
280        }
281
282        return null;
283    }
284
285    /**
286     * Returns a flag whether this configuration is empty.
287     *
288     * @return the empty flag
289     */
290    @Override
291    protected boolean isEmptyInternal()
292    {
293        try
294        {
295            NamingEnumeration<NameClassPair> enumeration = null;
296
297            try
298            {
299                enumeration = getBaseContext().list("");
300                return !enumeration.hasMore();
301            }
302            finally
303            {
304                // close the enumeration
305                if (enumeration != null)
306                {
307                    enumeration.close();
308                }
309            }
310        }
311        catch (final NamingException e)
312        {
313            fireError(ConfigurationErrorEvent.READ,
314                    ConfigurationErrorEvent.READ, null, null, e);
315            return true;
316        }
317    }
318
319    /**
320     * <p><strong>This operation is not supported and will throw an
321     * UnsupportedOperationException.</strong></p>
322     *
323     * @param key the key
324     * @param value the value
325     * @throws UnsupportedOperationException always thrown as this method is not supported
326     */
327    @Override
328    protected void setPropertyInternal(final String key, final Object value)
329    {
330        throw new UnsupportedOperationException("This operation is not supported");
331    }
332
333    /**
334     * Removes the specified property.
335     *
336     * @param key the key of the property to remove
337     */
338    @Override
339    protected void clearPropertyDirect(final String key)
340    {
341        clearedProperties.add(key);
342    }
343
344    /**
345     * Checks whether the specified key is contained in this configuration.
346     *
347     * @param key the key to check
348     * @return a flag whether this key is stored in this configuration
349     */
350    @Override
351    protected boolean containsKeyInternal(String key)
352    {
353        if (clearedProperties.contains(key))
354        {
355            return false;
356        }
357        key = key.replaceAll("\\.", "/");
358        try
359        {
360            // throws a NamingException if JNDI doesn't contain the key.
361            getBaseContext().lookup(key);
362            return true;
363        }
364        catch (final NameNotFoundException e)
365        {
366            // expected exception, no need to log it
367            return false;
368        }
369        catch (final NamingException e)
370        {
371            fireError(ConfigurationErrorEvent.READ,
372                    ConfigurationErrorEvent.READ, key, null, e);
373            return false;
374        }
375    }
376
377    /**
378     * Returns the prefix.
379     * @return the prefix
380     */
381    public String getPrefix()
382    {
383        return prefix;
384    }
385
386    /**
387     * Sets the prefix.
388     *
389     * @param prefix The prefix to set
390     */
391    public void setPrefix(final String prefix)
392    {
393        this.prefix = prefix;
394
395        // clear the previous baseContext
396        baseContext = null;
397    }
398
399    /**
400     * Returns the value of the specified property.
401     *
402     * @param key the key of the property
403     * @return the value of this property
404     */
405    @Override
406    protected Object getPropertyInternal(String key)
407    {
408        if (clearedProperties.contains(key))
409        {
410            return null;
411        }
412
413        try
414        {
415            key = key.replaceAll("\\.", "/");
416            return getBaseContext().lookup(key);
417        }
418        catch (final NameNotFoundException e)
419        {
420            // expected exception, no need to log it
421            return null;
422        }
423        catch (final NotContextException nctxex)
424        {
425            // expected exception, no need to log it
426            return null;
427        }
428        catch (final NamingException e)
429        {
430            fireError(ConfigurationErrorEvent.READ,
431                    ConfigurationErrorEvent.READ, key, null, e);
432            return null;
433        }
434    }
435
436    /**
437     * <p><strong>This operation is not supported and will throw an
438     * UnsupportedOperationException.</strong></p>
439     *
440     * @param key the key
441     * @param obj the value
442     * @throws UnsupportedOperationException always thrown as this method is not supported
443     */
444    @Override
445    protected void addPropertyDirect(final String key, final Object obj)
446    {
447        throw new UnsupportedOperationException("This operation is not supported");
448    }
449
450    /**
451     * Return the base context with the prefix applied.
452     *
453     * @return the base context
454     * @throws NamingException if an error occurs
455     */
456    public Context getBaseContext() throws NamingException
457    {
458        if (baseContext == null)
459        {
460            baseContext = (Context) getContext().lookup(prefix == null ? "" : prefix);
461        }
462
463        return baseContext;
464    }
465
466    /**
467     * Return the initial context used by this configuration. This context is
468     * independent of the prefix specified.
469     *
470     * @return the initial context
471     */
472    public Context getContext()
473    {
474        return context;
475    }
476
477    /**
478     * Set the initial context of the configuration.
479     *
480     * @param context the context
481     */
482    public void setContext(final Context context)
483    {
484        // forget the removed properties
485        clearedProperties.clear();
486
487        // change the context
488        this.context = context;
489    }
490}