View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.commons.configuration2;
19  
20  import java.util.ArrayList;
21  import java.util.Arrays;
22  import java.util.HashSet;
23  import java.util.Iterator;
24  import java.util.List;
25  import java.util.Set;
26  
27  import javax.naming.Context;
28  import javax.naming.InitialContext;
29  import javax.naming.NameClassPair;
30  import javax.naming.NameNotFoundException;
31  import javax.naming.NamingEnumeration;
32  import javax.naming.NamingException;
33  import javax.naming.NotContextException;
34  
35  import org.apache.commons.configuration2.event.ConfigurationErrorEvent;
36  import org.apache.commons.configuration2.io.ConfigurationLogger;
37  import org.apache.commons.lang3.StringUtils;
38  
39  /**
40   * This Configuration class allows you to interface with a JNDI datasource. A JNDIConfiguration is read-only, write
41   * operations will throw an UnsupportedOperationException. The clear operations are supported but the underlying JNDI
42   * data source is not changed.
43   */
44  public class JNDIConfiguration extends AbstractConfiguration {
45      /** The prefix of the context. */
46      private String prefix;
47  
48      /** The initial JNDI context. */
49      private Context context;
50  
51      /** The base JNDI context. */
52      private Context baseContext;
53  
54      /** The Set of keys that have been virtually cleared. */
55      private final Set<String> clearedProperties = new HashSet<>();
56  
57      /**
58       * Creates a JNDIConfiguration using the default initial context as the root of the properties.
59       *
60       * @throws NamingException thrown if an error occurs when initializing the default context
61       */
62      public JNDIConfiguration() throws NamingException {
63          this((String) null);
64      }
65  
66      /**
67       * Creates a JNDIConfiguration using the default initial context, shifted with the specified prefix, as the root of the
68       * properties.
69       *
70       * @param prefix the prefix
71       *
72       * @throws NamingException thrown if an error occurs when initializing the default context
73       */
74      public JNDIConfiguration(final String prefix) throws NamingException {
75          this(new InitialContext(), prefix);
76      }
77  
78      /**
79       * Creates a JNDIConfiguration using the specified initial context as the root of the properties.
80       *
81       * @param context the initial context
82       */
83      public JNDIConfiguration(final Context context) {
84          this(context, null);
85      }
86  
87      /**
88       * Creates a JNDIConfiguration using the specified initial context shifted by the specified prefix as the root of the
89       * properties.
90       *
91       * @param context the initial context
92       * @param prefix the prefix
93       */
94      public JNDIConfiguration(final Context context, final String prefix) {
95          this.context = context;
96          this.prefix = prefix;
97          initLogger(new ConfigurationLogger(JNDIConfiguration.class));
98          addErrorLogListener();
99      }
100 
101     /**
102      * This method recursive traverse the JNDI tree, looking for Context objects. When it finds them, it traverses them as
103      * well. Otherwise it just adds the values to the list of keys found.
104      *
105      * @param keys All the keys that have been found.
106      * @param context The parent context
107      * @param prefix What prefix we are building on.
108      * @param processedCtx a set with the so far processed objects
109      * @throws NamingException If JNDI has an issue.
110      */
111     private void recursiveGetKeys(final Set<String> keys, final Context context, final String prefix, final Set<Context> processedCtx) throws NamingException {
112         processedCtx.add(context);
113         NamingEnumeration<NameClassPair> elements = null;
114 
115         try {
116             elements = context.list("");
117 
118             // iterates through the context's elements
119             while (elements.hasMore()) {
120                 final NameClassPair nameClassPair = elements.next();
121                 final String name = nameClassPair.getName();
122                 final Object object = context.lookup(name);
123 
124                 // build the key
125                 final StringBuilder keyBuilder = new StringBuilder();
126                 keyBuilder.append(prefix);
127                 if (keyBuilder.length() > 0) {
128                     keyBuilder.append(".");
129                 }
130                 keyBuilder.append(name);
131 
132                 if (object instanceof Context) {
133                     // add the keys of the sub context
134                     final Context subcontext = (Context) object;
135                     if (!processedCtx.contains(subcontext)) {
136                         recursiveGetKeys(keys, subcontext, keyBuilder.toString(), processedCtx);
137                     }
138                 } else {
139                     // add the key
140                     keys.add(keyBuilder.toString());
141                 }
142             }
143         } finally {
144             // close the enumeration
145             if (elements != null) {
146                 elements.close();
147             }
148         }
149     }
150 
151     /**
152      * Gets an iterator with all property keys stored in this configuration.
153      *
154      * @return an iterator with all keys
155      */
156     @Override
157     protected Iterator<String> getKeysInternal() {
158         return getKeysInternal("");
159     }
160 
161     /**
162      * Gets an iterator with all property keys starting with the given prefix.
163      *
164      * @param prefix the prefix
165      * @return an iterator with the selected keys
166      */
167     @Override
168     protected Iterator<String> getKeysInternal(final String prefix) {
169         // build the path
170         final String[] splitPath = StringUtils.split(prefix, ".");
171 
172         final List<String> path = Arrays.asList(splitPath);
173 
174         try {
175             // find the context matching the specified path
176             final Context context = getContext(path, getBaseContext());
177 
178             // return all the keys under the context found
179             final Set<String> keys = new HashSet<>();
180             if (context != null) {
181                 recursiveGetKeys(keys, context, prefix, new HashSet<>());
182             } else if (containsKey(prefix)) {
183                 // add the prefix if it matches exactly a property key
184                 keys.add(prefix);
185             }
186 
187             return keys.iterator();
188         } catch (final NameNotFoundException e) {
189             // expected exception, no need to log it
190             return new ArrayList<String>().iterator();
191         } catch (final NamingException e) {
192             fireError(ConfigurationErrorEvent.READ, ConfigurationErrorEvent.READ, null, null, e);
193             return new ArrayList<String>().iterator();
194         }
195     }
196 
197     /**
198      * Because JNDI is based on a tree configuration, we need to filter down the tree, till we find the Context specified by
199      * the key to start from. Otherwise return null.
200      *
201      * @param path the path of keys to traverse in order to find the context
202      * @param context the context to start from
203      * @return The context at that key's location in the JNDI tree, or null if not found
204      * @throws NamingException if JNDI has an issue
205      */
206     private Context getContext(final List<String> path, final Context context) throws NamingException {
207         // return the current context if the path is empty
208         if (path == null || path.isEmpty()) {
209             return context;
210         }
211 
212         final String key = path.get(0);
213 
214         // search a context matching the key in the context's elements
215         NamingEnumeration<NameClassPair> elements = null;
216 
217         try {
218             elements = context.list("");
219             while (elements.hasMore()) {
220                 final NameClassPair nameClassPair = elements.next();
221                 final String name = nameClassPair.getName();
222                 final Object object = context.lookup(name);
223 
224                 if (object instanceof Context && name.equals(key)) {
225                     final Context subcontext = (Context) object;
226 
227                     // recursive search in the sub context
228                     return getContext(path.subList(1, path.size()), subcontext);
229                 }
230             }
231         } finally {
232             if (elements != null) {
233                 elements.close();
234             }
235         }
236 
237         return null;
238     }
239 
240     /**
241      * Returns a flag whether this configuration is empty.
242      *
243      * @return the empty flag
244      */
245     @Override
246     protected boolean isEmptyInternal() {
247         try {
248             NamingEnumeration<NameClassPair> enumeration = null;
249 
250             try {
251                 enumeration = getBaseContext().list("");
252                 return !enumeration.hasMore();
253             } finally {
254                 // close the enumeration
255                 if (enumeration != null) {
256                     enumeration.close();
257                 }
258             }
259         } catch (final NamingException e) {
260             fireError(ConfigurationErrorEvent.READ, ConfigurationErrorEvent.READ, null, null, e);
261             return true;
262         }
263     }
264 
265     /**
266      * <p>
267      * <strong>This operation is not supported and will throw an UnsupportedOperationException.</strong>
268      * </p>
269      *
270      * @param key the key
271      * @param value the value
272      * @throws UnsupportedOperationException always thrown as this method is not supported
273      */
274     @Override
275     protected void setPropertyInternal(final String key, final Object value) {
276         throw new UnsupportedOperationException("This operation is not supported");
277     }
278 
279     /**
280      * Removes the specified property.
281      *
282      * @param key the key of the property to remove
283      */
284     @Override
285     protected void clearPropertyDirect(final String key) {
286         clearedProperties.add(key);
287     }
288 
289     /**
290      * Checks whether the specified key is contained in this configuration.
291      *
292      * @param key the key to check
293      * @return a flag whether this key is stored in this configuration
294      */
295     @Override
296     protected boolean containsKeyInternal(String key) {
297         if (clearedProperties.contains(key)) {
298             return false;
299         }
300         key = key.replace('.', '/');
301         try {
302             // throws a NamingException if JNDI doesn't contain the key.
303             getBaseContext().lookup(key);
304             return true;
305         } catch (final NameNotFoundException e) {
306             // expected exception, no need to log it
307             return false;
308         } catch (final NamingException e) {
309             fireError(ConfigurationErrorEvent.READ, ConfigurationErrorEvent.READ, key, null, e);
310             return false;
311         }
312     }
313 
314     /**
315      * Gets the prefix.
316      *
317      * @return the prefix
318      */
319     public String getPrefix() {
320         return prefix;
321     }
322 
323     /**
324      * Sets the prefix.
325      *
326      * @param prefix The prefix to set
327      */
328     public void setPrefix(final String prefix) {
329         this.prefix = prefix;
330 
331         // clear the previous baseContext
332         baseContext = null;
333     }
334 
335     /**
336      * Gets the value of the specified property.
337      *
338      * @param key the key of the property
339      * @return the value of this property
340      */
341     @Override
342     protected Object getPropertyInternal(String key) {
343         if (clearedProperties.contains(key)) {
344             return null;
345         }
346 
347         try {
348             key = key.replace('.', '/');
349             return getBaseContext().lookup(key);
350         } catch (final NameNotFoundException | NotContextException nctxex) {
351             // expected exception, no need to log it
352             return null;
353         } catch (final NamingException e) {
354             fireError(ConfigurationErrorEvent.READ, ConfigurationErrorEvent.READ, key, null, e);
355             return null;
356         }
357     }
358 
359     /**
360      * <p>
361      * <strong>This operation is not supported and will throw an UnsupportedOperationException.</strong>
362      * </p>
363      *
364      * @param key the key
365      * @param obj the value
366      * @throws UnsupportedOperationException always thrown as this method is not supported
367      */
368     @Override
369     protected void addPropertyDirect(final String key, final Object obj) {
370         throw new UnsupportedOperationException("This operation is not supported");
371     }
372 
373     /**
374      * Gets the base context with the prefix applied.
375      *
376      * @return the base context
377      * @throws NamingException if an error occurs
378      */
379     public Context getBaseContext() throws NamingException {
380         if (baseContext == null) {
381             baseContext = (Context) getContext().lookup(prefix == null ? "" : prefix);
382         }
383 
384         return baseContext;
385     }
386 
387     /**
388      * Gets the initial context used by this configuration. This context is independent of the prefix specified.
389      *
390      * @return the initial context
391      */
392     public Context getContext() {
393         return context;
394     }
395 
396     /**
397      * Sets the initial context of the configuration.
398      *
399      * @param context the context
400      */
401     public void setContext(final Context context) {
402         // forget the removed properties
403         clearedProperties.clear();
404 
405         // change the context
406         this.context = context;
407     }
408 }