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 log = LoggerFactory.getLogger(JndiTemplate.class);
048
049    private Properties environment;
050
051    /** Create a new JndiTemplate instance. */
052    public JndiTemplate() {
053    }
054
055    /**
056     * Create a new JndiTemplate instance, using the given environment.
057     *
058     * @param environment the Properties to initialize with
059     */
060    public JndiTemplate(Properties environment) {
061        this.environment = environment;
062    }
063
064    /**
065     * Set the environment for the JNDI InitialContext.
066     *
067     * @param environment the Properties to initialize with
068     */
069    public void setEnvironment(Properties environment) {
070        this.environment = environment;
071    }
072
073    /**
074     * Return the environment for the JNDI InitialContext, or <code>null</code> if none should be used.
075     *
076     * @return the environment for the JNDI InitialContext, or <code>null</code> if none should be used.
077     */
078    public Properties getEnvironment() {
079        return this.environment;
080    }
081
082    /**
083     * Execute the given JNDI context callback implementation.
084     *
085     * @param contextCallback JndiCallback implementation
086     * @return a result object returned by the callback, or <code>null</code>
087     * @throws NamingException thrown by the callback implementation
088     * @see #createInitialContext
089     */
090    public Object execute(JndiCallback contextCallback) throws NamingException {
091        Context ctx = createInitialContext();
092        try {
093            return contextCallback.doInContext(ctx);
094        }
095        finally {
096            try {
097                ctx.close();
098            } catch (NamingException ex) {
099                log.debug("Could not close JNDI InitialContext", ex);
100            }
101        }
102    }
103
104    /**
105     * Create a new JNDI initial context. Invoked by {@link #execute}.
106     * <p>The default implementation use this template's environment settings.
107     * Can be subclassed for custom contexts, e.g. for testing.
108     *
109     * @return the initial Context instance
110     * @throws NamingException in case of initialization errors
111     */
112    @SuppressWarnings({"unchecked"})
113    protected Context createInitialContext() throws NamingException {
114        Properties env = getEnvironment();
115        Hashtable icEnv = null;
116        if (env != null) {
117            icEnv = new Hashtable(env.size());
118            for (Enumeration en = env.propertyNames(); en.hasMoreElements();) {
119                String key = (String) en.nextElement();
120                icEnv.put(key, env.getProperty(key));
121            }
122        }
123        return new InitialContext(icEnv);
124    }
125
126    /**
127     * Look up the object with the given name in the current JNDI context.
128     *
129     * @param name the JNDI name of the object
130     * @return object found (cannot be <code>null</code>; if a not so well-behaved
131     *         JNDI implementations returns null, a NamingException gets thrown)
132     * @throws NamingException if there is no object with the given
133     *                         name bound to JNDI
134     */
135    public Object lookup(final String name) throws NamingException {
136        log.debug("Looking up JNDI object with name '{}'", name);
137        return execute(new JndiCallback() {
138            public Object doInContext(Context ctx) throws NamingException {
139                Object located = ctx.lookup(name);
140                if (located == null) {
141                    throw new NameNotFoundException(
142                            "JNDI object with [" + name + "] not found: JNDI implementation returned null");
143                }
144                return located;
145            }
146        });
147    }
148
149    /**
150     * Look up the object with the given name in the current JNDI context.
151     *
152     * @param name         the JNDI name of the object
153     * @param requiredType type the JNDI object must match. Can be an interface or
154     *                     superclass of the actual class, or <code>null</code> for any match. For example,
155     *                     if the value is <code>Object.class</code>, this method will succeed whatever
156     *                     the class of the returned instance.
157     * @return object found (cannot be <code>null</code>; if a not so well-behaved
158     *         JNDI implementations returns null, a NamingException gets thrown)
159     * @throws NamingException if there is no object with the given
160     *                         name bound to JNDI
161     */
162    public Object lookup(String name, Class requiredType) throws NamingException {
163        Object jndiObject = lookup(name);
164        if (requiredType != null && !requiredType.isInstance(jndiObject)) {
165            String msg = "Jndi object acquired under name '" + name + "' is of type [" +
166                    jndiObject.getClass().getName() + "] and not assignable to the required type [" +
167                    requiredType.getName() + "].";
168            throw new NamingException(msg);
169        }
170        return jndiObject;
171    }
172
173    /**
174     * Bind the given object to the current JNDI context, using the given name.
175     *
176     * @param name   the JNDI name of the object
177     * @param object the object to bind
178     * @throws NamingException thrown by JNDI, mostly name already bound
179     */
180    public void bind(final String name, final Object object) throws NamingException {
181        log.debug("Binding JNDI object with name '{}'", name);
182        execute(new JndiCallback() {
183            public Object doInContext(Context ctx) throws NamingException {
184                ctx.bind(name, object);
185                return null;
186            }
187        });
188    }
189
190    /**
191     * Rebind the given object to the current JNDI context, using the given name.
192     * Overwrites any existing binding.
193     *
194     * @param name   the JNDI name of the object
195     * @param object the object to rebind
196     * @throws NamingException thrown by JNDI
197     */
198    public void rebind(final String name, final Object object) throws NamingException {
199        log.debug("Rebinding JNDI object with name '{}'", name);
200        execute(new JndiCallback() {
201            public Object doInContext(Context ctx) throws NamingException {
202                ctx.rebind(name, object);
203                return null;
204            }
205        });
206    }
207
208    /**
209     * Remove the binding for the given name from the current JNDI context.
210     *
211     * @param name the JNDI name of the object
212     * @throws NamingException thrown by JNDI, mostly name not found
213     */
214    public void unbind(final String name) throws NamingException {
215        log.debug("Unbinding JNDI object with name '{}'", name);
216        execute(new JndiCallback() {
217            public Object doInContext(Context ctx) throws NamingException {
218                ctx.unbind(name);
219                return null;
220            }
221        });
222    }
223
224}