001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.shiro.jndi;
020
021import java.util.Enumeration;
022import java.util.Hashtable;
023import java.util.Properties;
024import javax.naming.Context;
025import javax.naming.InitialContext;
026import javax.naming.NameNotFoundException;
027import javax.naming.NamingException;
028
029import org.slf4j.Logger;
030import org.slf4j.LoggerFactory;
031
032/**
033 * Helper class that simplifies JNDI operations. It provides methods to lookup and
034 * bind objects, and allows implementations of the {@link JndiCallback} interface
035 * to perform any operation they like with a JNDI naming context provided.
036 * <p/>
037 * <p>Note that this implementation is an almost exact copy of the Spring Framework's identically named class from
038 * their 2.5.4 distribution - we didn't want to re-invent the wheel, but not require a full dependency on the
039 * Spring framework, nor does Spring make available only its JNDI classes in a small jar, or we would have used that.
040 * Since Shiro is also Apache 2.0 licensed, all regular licenses and conditions and authors have remained in tact.
041 *
042 * @see JndiCallback
043 * @see #execute
044 */
045public class JndiTemplate {
046
047    private static final Logger LOGGER = LoggerFactory.getLogger(JndiTemplate.class);
048
049    private Properties environment;
050
051    /**
052     * Create a new JndiTemplate instance.
053     */
054    public JndiTemplate() {
055    }
056
057    /**
058     * Create a new JndiTemplate instance, using the given environment.
059     *
060     * @param environment the Properties to initialize with
061     */
062    public JndiTemplate(Properties environment) {
063        this.environment = environment;
064    }
065
066    /**
067     * Set the environment for the JNDI InitialContext.
068     *
069     * @param environment the Properties to initialize with
070     */
071    public void setEnvironment(Properties environment) {
072        this.environment = environment;
073    }
074
075    /**
076     * Return the environment for the JNDI InitialContext, or <code>null</code> if none should be used.
077     *
078     * @return the environment for the JNDI InitialContext, or <code>null</code> if none should be used.
079     */
080    public Properties getEnvironment() {
081        return this.environment;
082    }
083
084    /**
085     * Execute the given JNDI context callback implementation.
086     *
087     * @param contextCallback JndiCallback implementation
088     * @return a result object returned by the callback, or <code>null</code>
089     * @throws NamingException thrown by the callback implementation
090     * @see #createInitialContext
091     */
092    public Object execute(JndiCallback contextCallback) throws NamingException {
093        Context ctx = createInitialContext();
094        try {
095            return contextCallback.doInContext(ctx);
096        } finally {
097            try {
098                ctx.close();
099            } catch (NamingException ex) {
100                LOGGER.debug("Could not close JNDI InitialContext", ex);
101            }
102        }
103    }
104
105    /**
106     * Create a new JNDI initial context. Invoked by {@link #execute}.
107     * <p>The default implementation use this template's environment settings.
108     * Can be subclassed for custom contexts, e.g. for testing.
109     *
110     * @return the initial Context instance
111     * @throws NamingException in case of initialization errors
112     */
113    @SuppressWarnings({"unchecked"})
114    protected Context createInitialContext() throws NamingException {
115        Properties env = getEnvironment();
116        Hashtable icEnv = null;
117        if (env != null) {
118            icEnv = new Hashtable(env.size());
119            for (Enumeration en = env.propertyNames(); en.hasMoreElements(); ) {
120                String key = (String) en.nextElement();
121                icEnv.put(key, env.getProperty(key));
122            }
123        }
124        return new InitialContext(icEnv);
125    }
126
127    /**
128     * Look up the object with the given name in the current JNDI context.
129     *
130     * @param name the JNDI name of the object
131     * @return object found (cannot be <code>null</code>; if a not so well-behaved
132     * JNDI implementations returns null, a NamingException gets thrown)
133     * @throws NamingException if there is no object with the given
134     *                         name bound to JNDI
135     */
136    public Object lookup(final String name) throws NamingException {
137        LOGGER.debug("Looking up JNDI object with name '{}'", name);
138        return execute(new JndiCallback() {
139            public Object doInContext(Context ctx) throws NamingException {
140                Object located = ctx.lookup(name);
141                if (located == null) {
142                    throw new NameNotFoundException(
143                            "JNDI object with [" + name + "] not found: JNDI implementation returned null");
144                }
145                return located;
146            }
147        });
148    }
149
150    /**
151     * Look up the object with the given name in the current JNDI context.
152     *
153     * @param name         the JNDI name of the object
154     * @param requiredType type the JNDI object must match. Can be an interface or
155     *                     superclass of the actual class, or <code>null</code> for any match. For example,
156     *                     if the value is <code>Object.class</code>, this method will succeed whatever
157     *                     the class of the returned instance.
158     * @return object found (cannot be <code>null</code>; if a not so well-behaved
159     * JNDI implementations returns null, a NamingException gets thrown)
160     * @throws NamingException if there is no object with the given
161     *                         name bound to JNDI
162     */
163    public Object lookup(String name, Class requiredType) throws NamingException {
164        Object jndiObject = lookup(name);
165        if (requiredType != null && !requiredType.isInstance(jndiObject)) {
166            String msg = "Jndi object acquired under name '" + name + "' is of type ["
167                    + jndiObject.getClass().getName() + "] and not assignable to the required type ["
168                    + requiredType.getName() + "].";
169            throw new NamingException(msg);
170        }
171        return jndiObject;
172    }
173
174    /**
175     * Bind the given object to the current JNDI context, using the given name.
176     *
177     * @param name   the JNDI name of the object
178     * @param object the object to bind
179     * @throws NamingException thrown by JNDI, mostly name already bound
180     */
181    public void bind(final String name, final Object object) throws NamingException {
182        LOGGER.debug("Binding JNDI object with name '{}'", name);
183        execute(new JndiCallback() {
184            public Object doInContext(Context ctx) throws NamingException {
185                ctx.bind(name, object);
186                return null;
187            }
188        });
189    }
190
191    /**
192     * Rebind the given object to the current JNDI context, using the given name.
193     * Overwrites any existing binding.
194     *
195     * @param name   the JNDI name of the object
196     * @param object the object to rebind
197     * @throws NamingException thrown by JNDI
198     */
199    public void rebind(final String name, final Object object) throws NamingException {
200        LOGGER.debug("Rebinding JNDI object with name '{}'", name);
201        execute(new JndiCallback() {
202            public Object doInContext(Context ctx) throws NamingException {
203                ctx.rebind(name, object);
204                return null;
205            }
206        });
207    }
208
209    /**
210     * Remove the binding for the given name from the current JNDI context.
211     *
212     * @param name the JNDI name of the object
213     * @throws NamingException thrown by JNDI, mostly name not found
214     */
215    public void unbind(final String name) throws NamingException {
216        LOGGER.debug("Unbinding JNDI object with name '{}'", name);
217        execute(new JndiCallback() {
218            public Object doInContext(Context ctx) throws NamingException {
219                ctx.unbind(name);
220                return null;
221            }
222        });
223    }
224
225}