JNDIConfiguration.java

  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.  *     http://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. package org.apache.commons.configuration2;

  18. import java.util.ArrayList;
  19. import java.util.Arrays;
  20. import java.util.HashSet;
  21. import java.util.Iterator;
  22. import java.util.List;
  23. import java.util.Set;

  24. import javax.naming.Context;
  25. import javax.naming.InitialContext;
  26. import javax.naming.NameClassPair;
  27. import javax.naming.NameNotFoundException;
  28. import javax.naming.NamingEnumeration;
  29. import javax.naming.NamingException;
  30. import javax.naming.NotContextException;

  31. import org.apache.commons.configuration2.event.ConfigurationErrorEvent;
  32. import org.apache.commons.configuration2.io.ConfigurationLogger;
  33. import org.apache.commons.lang3.StringUtils;

  34. /**
  35.  * This Configuration class allows you to interface with a JNDI datasource. A JNDIConfiguration is read-only, write
  36.  * operations will throw an UnsupportedOperationException. The clear operations are supported but the underlying JNDI
  37.  * data source is not changed.
  38.  */
  39. public class JNDIConfiguration extends AbstractConfiguration {
  40.     /** The prefix of the context. */
  41.     private String prefix;

  42.     /** The initial JNDI context. */
  43.     private Context context;

  44.     /** The base JNDI context. */
  45.     private Context baseContext;

  46.     /** The Set of keys that have been virtually cleared. */
  47.     private final Set<String> clearedProperties = new HashSet<>();

  48.     /**
  49.      * Creates a JNDIConfiguration using the default initial context as the root of the properties.
  50.      *
  51.      * @throws NamingException thrown if an error occurs when initializing the default context
  52.      */
  53.     public JNDIConfiguration() throws NamingException {
  54.         this((String) null);
  55.     }

  56.     /**
  57.      * Creates a JNDIConfiguration using the specified initial context as the root of the properties.
  58.      *
  59.      * @param context the initial context
  60.      */
  61.     public JNDIConfiguration(final Context context) {
  62.         this(context, null);
  63.     }

  64.     /**
  65.      * Creates a JNDIConfiguration using the specified initial context shifted by the specified prefix as the root of the
  66.      * properties.
  67.      *
  68.      * @param context the initial context
  69.      * @param prefix the prefix
  70.      */
  71.     public JNDIConfiguration(final Context context, final String prefix) {
  72.         this.context = context;
  73.         this.prefix = prefix;
  74.         initLogger(new ConfigurationLogger(JNDIConfiguration.class));
  75.         addErrorLogListener();
  76.     }

  77.     /**
  78.      * Creates a JNDIConfiguration using the default initial context, shifted with the specified prefix, as the root of the
  79.      * properties.
  80.      *
  81.      * @param prefix the prefix
  82.      * @throws NamingException thrown if an error occurs when initializing the default context
  83.      */
  84.     public JNDIConfiguration(final String prefix) throws NamingException {
  85.         this(new InitialContext(), prefix);
  86.     }

  87.     /**
  88.      * <p>
  89.      * <strong>This operation is not supported and will throw an UnsupportedOperationException.</strong>
  90.      * </p>
  91.      *
  92.      * @param key the key
  93.      * @param obj the value
  94.      * @throws UnsupportedOperationException always thrown as this method is not supported
  95.      */
  96.     @Override
  97.     protected void addPropertyDirect(final String key, final Object obj) {
  98.         throw new UnsupportedOperationException("This operation is not supported");
  99.     }

  100.     /**
  101.      * Removes the specified property.
  102.      *
  103.      * @param key the key of the property to remove
  104.      */
  105.     @Override
  106.     protected void clearPropertyDirect(final String key) {
  107.         clearedProperties.add(key);
  108.     }

  109.     /**
  110.      * Checks whether the specified key is contained in this configuration.
  111.      *
  112.      * @param key the key to check
  113.      * @return a flag whether this key is stored in this configuration
  114.      */
  115.     @Override
  116.     protected boolean containsKeyInternal(String key) {
  117.         if (clearedProperties.contains(key)) {
  118.             return false;
  119.         }
  120.         key = key.replace('.', '/');
  121.         try {
  122.             // throws a NamingException if JNDI doesn't contain the key.
  123.             getBaseContext().lookup(key);
  124.             return true;
  125.         } catch (final NameNotFoundException e) {
  126.             // expected exception, no need to log it
  127.             return false;
  128.         } catch (final NamingException e) {
  129.             fireError(ConfigurationErrorEvent.READ, ConfigurationErrorEvent.READ, key, null, e);
  130.             return false;
  131.         }
  132.     }

  133.     /**
  134.      * Tests whether this configuration contains one or more matches to this value. This operation stops at first match
  135.      * but may be more expensive than the containsKey method.
  136.      * @since 2.11.0
  137.      */
  138.     @Override
  139.     protected boolean containsValueInternal(final Object value) {
  140.         return contains(getKeys(), value);
  141.     }

  142.     /**
  143.      * Gets the base context with the prefix applied.
  144.      *
  145.      * @return the base context
  146.      * @throws NamingException if an error occurs
  147.      */
  148.     public Context getBaseContext() throws NamingException {
  149.         if (baseContext == null) {
  150.             baseContext = (Context) getContext().lookup(prefix == null ? "" : prefix);
  151.         }

  152.         return baseContext;
  153.     }

  154.     /**
  155.      * Gets the initial context used by this configuration. This context is independent of the prefix specified.
  156.      *
  157.      * @return the initial context
  158.      */
  159.     public Context getContext() {
  160.         return context;
  161.     }

  162.     /**
  163.      * Because JNDI is based on a tree configuration, we need to filter down the tree, till we find the Context specified by
  164.      * the key to start from. Otherwise return null.
  165.      *
  166.      * @param path the path of keys to traverse in order to find the context
  167.      * @param context the context to start from
  168.      * @return The context at that key's location in the JNDI tree, or null if not found
  169.      * @throws NamingException if JNDI has an issue
  170.      */
  171.     private Context getContext(final List<String> path, final Context context) throws NamingException {
  172.         // return the current context if the path is empty
  173.         if (path == null || path.isEmpty()) {
  174.             return context;
  175.         }

  176.         final String key = path.get(0);

  177.         // search a context matching the key in the context's elements
  178.         NamingEnumeration<NameClassPair> elements = null;

  179.         try {
  180.             elements = context.list("");
  181.             while (elements.hasMore()) {
  182.                 final NameClassPair nameClassPair = elements.next();
  183.                 final String name = nameClassPair.getName();
  184.                 final Object object = context.lookup(name);

  185.                 if (object instanceof Context && name.equals(key)) {
  186.                     final Context subcontext = (Context) object;

  187.                     // recursive search in the sub context
  188.                     return getContext(path.subList(1, path.size()), subcontext);
  189.                 }
  190.             }
  191.         } finally {
  192.             if (elements != null) {
  193.                 elements.close();
  194.             }
  195.         }

  196.         return null;
  197.     }

  198.     /**
  199.      * Gets an iterator with all property keys stored in this configuration.
  200.      *
  201.      * @return an iterator with all keys
  202.      */
  203.     @Override
  204.     protected Iterator<String> getKeysInternal() {
  205.         return getKeysInternal("");
  206.     }

  207.     /**
  208.      * Gets an iterator with all property keys starting with the given prefix.
  209.      *
  210.      * @param prefix the prefix
  211.      * @return an iterator with the selected keys
  212.      */
  213.     @Override
  214.     protected Iterator<String> getKeysInternal(final String prefix) {
  215.         // build the path
  216.         final String[] splitPath = StringUtils.split(prefix, DELIMITER);

  217.         final List<String> path = Arrays.asList(splitPath);

  218.         try {
  219.             // find the context matching the specified path
  220.             final Context context = getContext(path, getBaseContext());

  221.             // return all the keys under the context found
  222.             final Set<String> keys = new HashSet<>();
  223.             if (context != null) {
  224.                 recursiveGetKeys(keys, context, prefix, new HashSet<>());
  225.             } else if (containsKey(prefix)) {
  226.                 // add the prefix if it matches exactly a property key
  227.                 keys.add(prefix);
  228.             }

  229.             return keys.iterator();
  230.         } catch (final NameNotFoundException e) {
  231.             // expected exception, no need to log it
  232.             return new ArrayList<String>().iterator();
  233.         } catch (final NamingException e) {
  234.             fireError(ConfigurationErrorEvent.READ, ConfigurationErrorEvent.READ, null, null, e);
  235.             return new ArrayList<String>().iterator();
  236.         }
  237.     }

  238.     /**
  239.      * Gets the prefix.
  240.      *
  241.      * @return the prefix
  242.      */
  243.     public String getPrefix() {
  244.         return prefix;
  245.     }

  246.     /**
  247.      * Gets the value of the specified property.
  248.      *
  249.      * @param key the key of the property
  250.      * @return the value of this property
  251.      */
  252.     @Override
  253.     protected Object getPropertyInternal(String key) {
  254.         if (clearedProperties.contains(key)) {
  255.             return null;
  256.         }

  257.         try {
  258.             key = key.replace('.', '/');
  259.             return getBaseContext().lookup(key);
  260.         } catch (final NameNotFoundException | NotContextException nctxex) {
  261.             // expected exception, no need to log it
  262.             return null;
  263.         } catch (final NamingException e) {
  264.             fireError(ConfigurationErrorEvent.READ, ConfigurationErrorEvent.READ, key, null, e);
  265.             return null;
  266.         }
  267.     }

  268.     /**
  269.      * Returns a flag whether this configuration is empty.
  270.      *
  271.      * @return the empty flag
  272.      */
  273.     @Override
  274.     protected boolean isEmptyInternal() {
  275.         try {
  276.             NamingEnumeration<NameClassPair> enumeration = null;

  277.             try {
  278.                 enumeration = getBaseContext().list("");
  279.                 return !enumeration.hasMore();
  280.             } finally {
  281.                 // close the enumeration
  282.                 if (enumeration != null) {
  283.                     enumeration.close();
  284.                 }
  285.             }
  286.         } catch (final NamingException e) {
  287.             fireError(ConfigurationErrorEvent.READ, ConfigurationErrorEvent.READ, null, null, e);
  288.             return true;
  289.         }
  290.     }

  291.     /**
  292.      * This method recursive traverse the JNDI tree, looking for Context objects. When it finds them, it traverses them as
  293.      * well. Otherwise it just adds the values to the list of keys found.
  294.      *
  295.      * @param keys All the keys that have been found.
  296.      * @param context The parent context
  297.      * @param prefix What prefix we are building on.
  298.      * @param processedCtx a set with the so far processed objects
  299.      * @throws NamingException If JNDI has an issue.
  300.      */
  301.     private void recursiveGetKeys(final Set<String> keys, final Context context, final String prefix, final Set<Context> processedCtx) throws NamingException {
  302.         processedCtx.add(context);
  303.         NamingEnumeration<NameClassPair> elements = null;

  304.         try {
  305.             elements = context.list("");

  306.             // iterates through the context's elements
  307.             while (elements.hasMore()) {
  308.                 final NameClassPair nameClassPair = elements.next();
  309.                 final String name = nameClassPair.getName();
  310.                 final Object object = context.lookup(name);

  311.                 // build the key
  312.                 final StringBuilder keyBuilder = new StringBuilder();
  313.                 keyBuilder.append(prefix);
  314.                 if (keyBuilder.length() > 0) {
  315.                     keyBuilder.append(DELIMITER);
  316.                 }
  317.                 keyBuilder.append(name);

  318.                 if (object instanceof Context) {
  319.                     // add the keys of the sub context
  320.                     final Context subcontext = (Context) object;
  321.                     if (!processedCtx.contains(subcontext)) {
  322.                         recursiveGetKeys(keys, subcontext, keyBuilder.toString(), processedCtx);
  323.                     }
  324.                 } else {
  325.                     // add the key
  326.                     keys.add(keyBuilder.toString());
  327.                 }
  328.             }
  329.         } finally {
  330.             // close the enumeration
  331.             if (elements != null) {
  332.                 elements.close();
  333.             }
  334.         }
  335.     }

  336.     /**
  337.      * Sets the initial context of the configuration.
  338.      *
  339.      * @param context the context
  340.      */
  341.     public void setContext(final Context context) {
  342.         // forget the removed properties
  343.         clearedProperties.clear();

  344.         // change the context
  345.         this.context = context;
  346.     }

  347.     /**
  348.      * Sets the prefix.
  349.      *
  350.      * @param prefix The prefix to set
  351.      */
  352.     public void setPrefix(final String prefix) {
  353.         this.prefix = prefix;

  354.         // clear the previous baseContext
  355.         baseContext = null;
  356.     }

  357.     /**
  358.      * <p>
  359.      * <strong>This operation is not supported and will throw an UnsupportedOperationException.</strong>
  360.      * </p>
  361.      *
  362.      * @param key the key
  363.      * @param value the value
  364.      * @throws UnsupportedOperationException always thrown as this method is not supported
  365.      */
  366.     @Override
  367.     protected void setPropertyInternal(final String key, final Object value) {
  368.         throw new UnsupportedOperationException("This operation is not supported");
  369.     }
  370. }