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 specified initial context as the root of the properties.
068     *
069     * @param context the initial context
070     */
071    public JNDIConfiguration(final Context context) {
072        this(context, null);
073    }
074
075    /**
076     * Creates a JNDIConfiguration using the specified initial context shifted by the specified prefix as the root of the
077     * properties.
078     *
079     * @param context the initial context
080     * @param prefix the prefix
081     */
082    public JNDIConfiguration(final Context context, final String prefix) {
083        this.context = context;
084        this.prefix = prefix;
085        initLogger(new ConfigurationLogger(JNDIConfiguration.class));
086        addErrorLogListener();
087    }
088
089    /**
090     * Creates a JNDIConfiguration using the default initial context, shifted with the specified prefix, as the root of the
091     * properties.
092     *
093     * @param prefix the prefix
094     *
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     * @since 2.11.0
154     */
155    @Override
156    protected boolean containsValueInternal(final Object value) {
157        return contains(getKeys(), value);
158    }
159
160    /**
161     * Gets the base context with the prefix applied.
162     *
163     * @return the base context
164     * @throws NamingException if an error occurs
165     */
166    public Context getBaseContext() throws NamingException {
167        if (baseContext == null) {
168            baseContext = (Context) getContext().lookup(prefix == null ? "" : prefix);
169        }
170
171        return baseContext;
172    }
173
174    /**
175     * Gets the initial context used by this configuration. This context is independent of the prefix specified.
176     *
177     * @return the initial context
178     */
179    public Context getContext() {
180        return context;
181    }
182
183    /**
184     * Because JNDI is based on a tree configuration, we need to filter down the tree, till we find the Context specified by
185     * the key to start from. Otherwise return null.
186     *
187     * @param path the path of keys to traverse in order to find the context
188     * @param context the context to start from
189     * @return The context at that key's location in the JNDI tree, or null if not found
190     * @throws NamingException if JNDI has an issue
191     */
192    private Context getContext(final List<String> path, final Context context) throws NamingException {
193        // return the current context if the path is empty
194        if (path == null || path.isEmpty()) {
195            return context;
196        }
197
198        final String key = path.get(0);
199
200        // search a context matching the key in the context's elements
201        NamingEnumeration<NameClassPair> elements = null;
202
203        try {
204            elements = context.list("");
205            while (elements.hasMore()) {
206                final NameClassPair nameClassPair = elements.next();
207                final String name = nameClassPair.getName();
208                final Object object = context.lookup(name);
209
210                if (object instanceof Context && name.equals(key)) {
211                    final Context subcontext = (Context) object;
212
213                    // recursive search in the sub context
214                    return getContext(path.subList(1, path.size()), subcontext);
215                }
216            }
217        } finally {
218            if (elements != null) {
219                elements.close();
220            }
221        }
222
223        return null;
224    }
225
226    /**
227     * Gets an iterator with all property keys stored in this configuration.
228     *
229     * @return an iterator with all keys
230     */
231    @Override
232    protected Iterator<String> getKeysInternal() {
233        return getKeysInternal("");
234    }
235
236    /**
237     * Gets an iterator with all property keys starting with the given prefix.
238     *
239     * @param prefix the prefix
240     * @return an iterator with the selected keys
241     */
242    @Override
243    protected Iterator<String> getKeysInternal(final String prefix) {
244        // build the path
245        final String[] splitPath = StringUtils.split(prefix, ".");
246
247        final List<String> path = Arrays.asList(splitPath);
248
249        try {
250            // find the context matching the specified path
251            final Context context = getContext(path, getBaseContext());
252
253            // return all the keys under the context found
254            final Set<String> keys = new HashSet<>();
255            if (context != null) {
256                recursiveGetKeys(keys, context, prefix, new HashSet<>());
257            } else if (containsKey(prefix)) {
258                // add the prefix if it matches exactly a property key
259                keys.add(prefix);
260            }
261
262            return keys.iterator();
263        } catch (final NameNotFoundException e) {
264            // expected exception, no need to log it
265            return new ArrayList<String>().iterator();
266        } catch (final NamingException e) {
267            fireError(ConfigurationErrorEvent.READ, ConfigurationErrorEvent.READ, null, null, e);
268            return new ArrayList<String>().iterator();
269        }
270    }
271
272    /**
273     * Gets the prefix.
274     *
275     * @return the prefix
276     */
277    public String getPrefix() {
278        return prefix;
279    }
280
281    /**
282     * Gets the value of the specified property.
283     *
284     * @param key the key of the property
285     * @return the value of this property
286     */
287    @Override
288    protected Object getPropertyInternal(String key) {
289        if (clearedProperties.contains(key)) {
290            return null;
291        }
292
293        try {
294            key = key.replace('.', '/');
295            return getBaseContext().lookup(key);
296        } catch (final NameNotFoundException | NotContextException nctxex) {
297            // expected exception, no need to log it
298            return null;
299        } catch (final NamingException e) {
300            fireError(ConfigurationErrorEvent.READ, ConfigurationErrorEvent.READ, key, null, e);
301            return null;
302        }
303    }
304
305    /**
306     * Returns a flag whether this configuration is empty.
307     *
308     * @return the empty flag
309     */
310    @Override
311    protected boolean isEmptyInternal() {
312        try {
313            NamingEnumeration<NameClassPair> enumeration = null;
314
315            try {
316                enumeration = getBaseContext().list("");
317                return !enumeration.hasMore();
318            } finally {
319                // close the enumeration
320                if (enumeration != null) {
321                    enumeration.close();
322                }
323            }
324        } catch (final NamingException e) {
325            fireError(ConfigurationErrorEvent.READ, ConfigurationErrorEvent.READ, null, null, e);
326            return true;
327        }
328    }
329
330    /**
331     * This method recursive traverse the JNDI tree, looking for Context objects. When it finds them, it traverses them as
332     * well. Otherwise it just adds the values to the list of keys found.
333     *
334     * @param keys All the keys that have been found.
335     * @param context The parent context
336     * @param prefix What prefix we are building on.
337     * @param processedCtx a set with the so far processed objects
338     * @throws NamingException If JNDI has an issue.
339     */
340    private void recursiveGetKeys(final Set<String> keys, final Context context, final String prefix, final Set<Context> processedCtx) throws NamingException {
341        processedCtx.add(context);
342        NamingEnumeration<NameClassPair> elements = null;
343
344        try {
345            elements = context.list("");
346
347            // iterates through the context's elements
348            while (elements.hasMore()) {
349                final NameClassPair nameClassPair = elements.next();
350                final String name = nameClassPair.getName();
351                final Object object = context.lookup(name);
352
353                // build the key
354                final StringBuilder keyBuilder = new StringBuilder();
355                keyBuilder.append(prefix);
356                if (keyBuilder.length() > 0) {
357                    keyBuilder.append(".");
358                }
359                keyBuilder.append(name);
360
361                if (object instanceof Context) {
362                    // add the keys of the sub context
363                    final Context subcontext = (Context) object;
364                    if (!processedCtx.contains(subcontext)) {
365                        recursiveGetKeys(keys, subcontext, keyBuilder.toString(), processedCtx);
366                    }
367                } else {
368                    // add the key
369                    keys.add(keyBuilder.toString());
370                }
371            }
372        } finally {
373            // close the enumeration
374            if (elements != null) {
375                elements.close();
376            }
377        }
378    }
379
380    /**
381     * Sets the initial context of the configuration.
382     *
383     * @param context the context
384     */
385    public void setContext(final Context context) {
386        // forget the removed properties
387        clearedProperties.clear();
388
389        // change the context
390        this.context = context;
391    }
392
393    /**
394     * Sets the prefix.
395     *
396     * @param prefix The prefix to set
397     */
398    public void setPrefix(final String prefix) {
399        this.prefix = prefix;
400
401        // clear the previous baseContext
402        baseContext = null;
403    }
404
405    /**
406     * <p>
407     * <strong>This operation is not supported and will throw an UnsupportedOperationException.</strong>
408     * </p>
409     *
410     * @param key the key
411     * @param value the value
412     * @throws UnsupportedOperationException always thrown as this method is not supported
413     */
414    @Override
415    protected void setPropertyInternal(final String key, final Object value) {
416        throw new UnsupportedOperationException("This operation is not supported");
417    }
418}