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 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    /** The prefix of the context. */
046    private String prefix;
047
048    /** The initial JNDI context. */
049    private Context context;
050
051    /** The base JNDI context. */
052    private Context baseContext;
053
054    /** The Set of keys that have been virtually cleared. */
055    private final Set<String> clearedProperties = new HashSet<>();
056
057    /**
058     * Creates a JNDIConfiguration using the default initial context as the root of the properties.
059     *
060     * @throws NamingException thrown if an error occurs when initializing the default context
061     */
062    public JNDIConfiguration() throws NamingException {
063        this((String) null);
064    }
065
066    /**
067     * Creates a JNDIConfiguration using the default initial context, shifted with the specified prefix, as the root of the
068     * properties.
069     *
070     * @param prefix the prefix
071     *
072     * @throws NamingException thrown if an error occurs when initializing the default context
073     */
074    public JNDIConfiguration(final String prefix) throws NamingException {
075        this(new InitialContext(), prefix);
076    }
077
078    /**
079     * Creates a JNDIConfiguration using the specified initial context as the root of the properties.
080     *
081     * @param context the initial context
082     */
083    public JNDIConfiguration(final Context context) {
084        this(context, null);
085    }
086
087    /**
088     * Creates a JNDIConfiguration using the specified initial context shifted by the specified prefix as the root of the
089     * properties.
090     *
091     * @param context the initial context
092     * @param prefix the prefix
093     */
094    public JNDIConfiguration(final Context context, final String prefix) {
095        this.context = context;
096        this.prefix = prefix;
097        initLogger(new ConfigurationLogger(JNDIConfiguration.class));
098        addErrorLogListener();
099    }
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}