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     * @throws NamingException thrown if an error occurs when initializing the default context
095     */
096    public JNDIConfiguration(final String prefix) throws NamingException {
097        this(new InitialContext(), prefix);
098    }
099
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}