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