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 *     https://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 java.util.ArrayList;
021import java.util.Arrays;
022import java.util.HashSet;
023import java.util.Iterator;
024import java.util.List;
025import java.util.Set;
026
027import javax.naming.Context;
028import javax.naming.InitialContext;
029import javax.naming.NameClassPair;
030import javax.naming.NameNotFoundException;
031import javax.naming.NamingEnumeration;
032import javax.naming.NamingException;
033import javax.naming.NotContextException;
034
035import org.apache.commons.configuration2.event.ConfigurationErrorEvent;
036import org.apache.commons.configuration2.io.ConfigurationLogger;
037import org.apache.commons.lang3.StringUtils;
038
039/**
040 * This Configuration class allows you to interface with a JNDI datasource. A JNDIConfiguration is read-only, write
041 * operations will throw an UnsupportedOperationException. The clear operations are supported but the underlying JNDI
042 * data source is not changed.
043 */
044public class JNDIConfiguration extends AbstractConfiguration {
045
046    /** The prefix of the context. */
047    private String prefix;
048
049    /** The initial JNDI context. */
050    private Context context;
051
052    /** The base JNDI context. */
053    private Context baseContext;
054
055    /** The Set of keys that have been virtually cleared. */
056    private final Set<String> clearedProperties = new HashSet<>();
057
058    /**
059     * Creates a JNDIConfiguration using the default initial context as the root of the properties.
060     *
061     * @throws NamingException thrown if an error occurs when initializing the default context
062     */
063    public JNDIConfiguration() throws NamingException {
064        this((String) null);
065    }
066
067    /**
068     * Creates a JNDIConfiguration using the specified initial context as the root of the properties.
069     *
070     * @param context the initial context
071     */
072    public JNDIConfiguration(final Context context) {
073        this(context, null);
074    }
075
076    /**
077     * Creates a JNDIConfiguration using the specified initial context shifted by the specified prefix as the root of the
078     * properties.
079     *
080     * @param context the initial context
081     * @param prefix the prefix
082     */
083    public JNDIConfiguration(final Context context, final String prefix) {
084        this.context = context;
085        this.prefix = prefix;
086        initLogger(new ConfigurationLogger(JNDIConfiguration.class));
087        addErrorLogListener();
088    }
089
090    /**
091     * Creates a JNDIConfiguration using the default initial context, shifted with the specified prefix, as the root of the
092     * properties.
093     *
094     * @param prefix the prefix
095     * @throws NamingException thrown if an error occurs when initializing the default context
096     */
097    public JNDIConfiguration(final String prefix) throws NamingException {
098        this(new InitialContext(), prefix);
099    }
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}