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 * https://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
46 /** The prefix of the context. */
47 private String prefix;
48
49 /** The initial JNDI context. */
50 private Context context;
51
52 /** The base JNDI context. */
53 private Context baseContext;
54
55 /** The Set of keys that have been virtually cleared. */
56 private final Set<String> clearedProperties = new HashSet<>();
57
58 /**
59 * Creates a JNDIConfiguration using the default initial context as the root of the properties.
60 *
61 * @throws NamingException thrown if an error occurs when initializing the default context
62 */
63 public JNDIConfiguration() throws NamingException {
64 this((String) null);
65 }
66
67 /**
68 * Creates a JNDIConfiguration using the specified initial context as the root of the properties.
69 *
70 * @param context the initial context
71 */
72 public JNDIConfiguration(final Context context) {
73 this(context, null);
74 }
75
76 /**
77 * Creates a JNDIConfiguration using the specified initial context shifted by the specified prefix as the root of the
78 * properties.
79 *
80 * @param context the initial context
81 * @param prefix the prefix
82 */
83 public JNDIConfiguration(final Context context, final String prefix) {
84 this.context = context;
85 this.prefix = prefix;
86 initLogger(new ConfigurationLogger(JNDIConfiguration.class));
87 addErrorLogListener();
88 }
89
90 /**
91 * Creates a JNDIConfiguration using the default initial context, shifted with the specified prefix, as the root of the
92 * properties.
93 *
94 * @param prefix the prefix
95 * @throws NamingException thrown if an error occurs when initializing the default context
96 */
97 public JNDIConfiguration(final String prefix) throws NamingException {
98 this(new InitialContext(), prefix);
99 }
100
101 /**
102 * <p>
103 * <strong>This operation is not supported and will throw an UnsupportedOperationException.</strong>
104 * </p>
105 *
106 * @param key the key
107 * @param obj the value
108 * @throws UnsupportedOperationException always thrown as this method is not supported
109 */
110 @Override
111 protected void addPropertyDirect(final String key, final Object obj) {
112 throw new UnsupportedOperationException("This operation is not supported");
113 }
114
115 /**
116 * Removes the specified property.
117 *
118 * @param key the key of the property to remove
119 */
120 @Override
121 protected void clearPropertyDirect(final String key) {
122 clearedProperties.add(key);
123 }
124
125 /**
126 * Checks whether the specified key is contained in this configuration.
127 *
128 * @param key the key to check
129 * @return a flag whether this key is stored in this configuration
130 */
131 @Override
132 protected boolean containsKeyInternal(String key) {
133 if (clearedProperties.contains(key)) {
134 return false;
135 }
136 key = key.replace('.', '/');
137 try {
138 // throws a NamingException if JNDI doesn't contain the key.
139 getBaseContext().lookup(key);
140 return true;
141 } catch (final NameNotFoundException e) {
142 // expected exception, no need to log it
143 return false;
144 } catch (final NamingException e) {
145 fireError(ConfigurationErrorEvent.READ, ConfigurationErrorEvent.READ, key, null, e);
146 return false;
147 }
148 }
149
150 /**
151 * Tests whether this configuration contains one or more matches to this value. This operation stops at first match
152 * but may be more expensive than the containsKey method.
153 *
154 * @since 2.11.0
155 */
156 @Override
157 protected boolean containsValueInternal(final Object value) {
158 return contains(getKeys(), value);
159 }
160
161 /**
162 * Gets the base context with the prefix applied.
163 *
164 * @return the base context
165 * @throws NamingException if an error occurs
166 */
167 public Context getBaseContext() throws NamingException {
168 if (baseContext == null) {
169 baseContext = (Context) getContext().lookup(prefix == null ? "" : prefix);
170 }
171
172 return baseContext;
173 }
174
175 /**
176 * Gets the initial context used by this configuration. This context is independent of the prefix specified.
177 *
178 * @return the initial context
179 */
180 public Context getContext() {
181 return context;
182 }
183
184 /**
185 * Because JNDI is based on a tree configuration, we need to filter down the tree, till we find the Context specified by
186 * the key to start from. Otherwise return null.
187 *
188 * @param path the path of keys to traverse in order to find the context
189 * @param context the context to start from
190 * @return The context at that key's location in the JNDI tree, or null if not found
191 * @throws NamingException if JNDI has an issue
192 */
193 private Context getContext(final List<String> path, final Context context) throws NamingException {
194 // return the current context if the path is empty
195 if (path == null || path.isEmpty()) {
196 return context;
197 }
198
199 final String key = path.get(0);
200
201 // search a context matching the key in the context's elements
202 NamingEnumeration<NameClassPair> elements = null;
203
204 try {
205 elements = context.list("");
206 while (elements.hasMore()) {
207 final NameClassPair nameClassPair = elements.next();
208 final String name = nameClassPair.getName();
209 final Object object = context.lookup(name);
210
211 if (object instanceof Context && name.equals(key)) {
212 final Context subcontext = (Context) object;
213
214 // recursive search in the sub context
215 return getContext(path.subList(1, path.size()), subcontext);
216 }
217 }
218 } finally {
219 if (elements != null) {
220 elements.close();
221 }
222 }
223
224 return null;
225 }
226
227 /**
228 * Gets an iterator with all property keys stored in this configuration.
229 *
230 * @return an iterator with all keys
231 */
232 @Override
233 protected Iterator<String> getKeysInternal() {
234 return getKeysInternal("");
235 }
236
237 /**
238 * Gets an iterator with all property keys starting with the given prefix.
239 *
240 * @param prefix the prefix
241 * @return an iterator with the selected keys
242 */
243 @Override
244 protected Iterator<String> getKeysInternal(final String prefix) {
245 // build the path
246 final String[] splitPath = StringUtils.split(prefix, DELIMITER);
247
248 final List<String> path = Arrays.asList(splitPath);
249
250 try {
251 // find the context matching the specified path
252 final Context context = getContext(path, getBaseContext());
253
254 // return all the keys under the context found
255 final Set<String> keys = new HashSet<>();
256 if (context != null) {
257 recursiveGetKeys(keys, context, prefix, new HashSet<>());
258 } else if (containsKey(prefix)) {
259 // add the prefix if it matches exactly a property key
260 keys.add(prefix);
261 }
262
263 return keys.iterator();
264 } catch (final NameNotFoundException e) {
265 // expected exception, no need to log it
266 return new ArrayList<String>().iterator();
267 } catch (final NamingException e) {
268 fireError(ConfigurationErrorEvent.READ, ConfigurationErrorEvent.READ, null, null, e);
269 return new ArrayList<String>().iterator();
270 }
271 }
272
273 /**
274 * Gets the prefix.
275 *
276 * @return the prefix
277 */
278 public String getPrefix() {
279 return prefix;
280 }
281
282 /**
283 * Gets the value of the specified property.
284 *
285 * @param key the key of the property
286 * @return the value of this property
287 */
288 @Override
289 protected Object getPropertyInternal(String key) {
290 if (clearedProperties.contains(key)) {
291 return null;
292 }
293
294 try {
295 key = key.replace('.', '/');
296 return getBaseContext().lookup(key);
297 } catch (final NameNotFoundException | NotContextException nctxex) {
298 // expected exception, no need to log it
299 return null;
300 } catch (final NamingException e) {
301 fireError(ConfigurationErrorEvent.READ, ConfigurationErrorEvent.READ, key, null, e);
302 return null;
303 }
304 }
305
306 /**
307 * Returns a flag whether this configuration is empty.
308 *
309 * @return the empty flag
310 */
311 @Override
312 protected boolean isEmptyInternal() {
313 try {
314 NamingEnumeration<NameClassPair> enumeration = null;
315
316 try {
317 enumeration = getBaseContext().list("");
318 return !enumeration.hasMore();
319 } finally {
320 // close the enumeration
321 if (enumeration != null) {
322 enumeration.close();
323 }
324 }
325 } catch (final NamingException e) {
326 fireError(ConfigurationErrorEvent.READ, ConfigurationErrorEvent.READ, null, null, e);
327 return true;
328 }
329 }
330
331 /**
332 * This method recursive traverse the JNDI tree, looking for Context objects. When it finds them, it traverses them as
333 * well. Otherwise it just adds the values to the list of keys found.
334 *
335 * @param keys All the keys that have been found.
336 * @param context The parent context
337 * @param prefix What prefix we are building on.
338 * @param processedCtx a set with the so far processed objects
339 * @throws NamingException If JNDI has an issue.
340 */
341 private void recursiveGetKeys(final Set<String> keys, final Context context, final String prefix, final Set<Context> processedCtx) throws NamingException {
342 processedCtx.add(context);
343 NamingEnumeration<NameClassPair> elements = null;
344
345 try {
346 elements = context.list("");
347
348 // iterates through the context's elements
349 while (elements.hasMore()) {
350 final NameClassPair nameClassPair = elements.next();
351 final String name = nameClassPair.getName();
352 final Object object = context.lookup(name);
353
354 // build the key
355 final StringBuilder keyBuilder = new StringBuilder();
356 keyBuilder.append(prefix);
357 if (keyBuilder.length() > 0) {
358 keyBuilder.append(DELIMITER);
359 }
360 keyBuilder.append(name);
361
362 if (object instanceof Context) {
363 // add the keys of the sub context
364 final Context subcontext = (Context) object;
365 if (!processedCtx.contains(subcontext)) {
366 recursiveGetKeys(keys, subcontext, keyBuilder.toString(), processedCtx);
367 }
368 } else {
369 // add the key
370 keys.add(keyBuilder.toString());
371 }
372 }
373 } finally {
374 // close the enumeration
375 if (elements != null) {
376 elements.close();
377 }
378 }
379 }
380
381 /**
382 * Sets the initial context of the configuration.
383 *
384 * @param context the context
385 */
386 public void setContext(final Context context) {
387 // forget the removed properties
388 clearedProperties.clear();
389
390 // change the context
391 this.context = context;
392 }
393
394 /**
395 * Sets the prefix.
396 *
397 * @param prefix The prefix to set
398 */
399 public void setPrefix(final String prefix) {
400 this.prefix = prefix;
401
402 // clear the previous baseContext
403 baseContext = null;
404 }
405
406 /**
407 * <p>
408 * <strong>This operation is not supported and will throw an UnsupportedOperationException.</strong>
409 * </p>
410 *
411 * @param key the key
412 * @param value the value
413 * @throws UnsupportedOperationException always thrown as this method is not supported
414 */
415 @Override
416 protected void setPropertyInternal(final String key, final Object value) {
417 throw new UnsupportedOperationException("This operation is not supported");
418 }
419 }