View Javadoc
1   /* ***************************************************************************
2    * Copyright (c) 2008 Brabenetz Harald, Austria.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * 
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    * 
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   * 
16   *****************************************************************************/
17  package org.settings4j.connector;
18  
19  import java.util.Properties;
20  
21  import javax.naming.Context;
22  import javax.naming.InitialContext;
23  import javax.naming.NameNotFoundException;
24  import javax.naming.NamingException;
25  import javax.naming.NoInitialContextException;
26  
27  import org.apache.commons.lang3.BooleanUtils;
28  import org.apache.commons.lang3.StringUtils;
29  import org.settings4j.Constants;
30  
31  /**
32   * The JNDI Context implementation of an {@link org.settings4j.Connector}.
33   * <p>
34   * <h3>Normal Use</h3>
35   * <p>
36   * This JNDI connector is used in the default settings4j-config:
37   * 
38   * <pre>
39   * &lt;connector name="JNDIConnector"
40   *     class="org.settings4j.connector.JNDIConnector"&gt;
41   *     &lt;contentResolver-ref ref="DefaultContentResolver" /&gt;
42   *     &lt;objectResolver-ref ref="DefaultObjectResolver" /&gt;
43   * &lt;/connector&gt;
44   * </pre>
45   * 
46   * During the first use it will check if JNDI is accessible. If no JNDI context exists, The connector will deactivate
47   * itself. A INFO-Log message will print this information.
48   * <p>
49   * The default contextPathPrefix is "java:comp/env/". This JNDI Connector will first check if a value for
50   * <code>"contextPathPrefix + key"</code> exists and second if a value for the <code>"key"</code> only exists.
51   * <p>
52   * <h3>Custom Use</h3>
53   * <p>
54   * You can also configure the JNDI Connector to connect to another JNDI Context as the default one.
55   * 
56   * <pre>
57   * &lt;connector name="JNDIConnector"
58   *     class="org.settings4j.connector.JNDIConnector"&gt;
59   *     &lt;param name="initialContextFactory" value="org.apache.naming.java.javaURLContextFactory"/&gt;
60   *     &lt;param name="providerUrl" value="localhost:1099"/&gt;
61   *     &lt;param name="urlPkgPrefixes" value="org.apache.naming"/&gt;
62   * &lt;/connector&gt;
63   * </pre>
64   * 
65   * All three parameters must be set "initialContextFactory", "providerUrl", "urlPkgPrefixes" if you want use another
66   * JNDI Context.
67   * <p>
68   * <h3>getString(), getContent(), getObject()</h3>
69   * <p>
70   * <h4>getString()</h4>
71   * <p>
72   * If the getString() JNDI lookup returns an Object which isn't a String, a WARN-Log message will be printed.
73   * <h4>getContent()</h4>
74   * <p>
75   * If the getContent() JNDI lookup returns a String it will try to get a byte-Array Content from the ContentResolvers
76   * (assuming the String is as FileSystemPath or ClassPath). <br />
77   * Else if the getContent() JNDI lookup returns an Object which isn't a byte[], a WARN-Log message will be printed.
78   * <h4>getObject()</h4>
79   * <p>
80   * If the getObject() JNDI lookup returns a String it will try to get an Object from the ObjectResolvers (assuming the
81   * String is as FileSystemPath or ClassPath which can be resolved to an Object).
82   * <p>
83   * 
84   * @author Harald.Brabenetz
85   */
86  public class JNDIConnector extends AbstractConnector {
87  
88      /** General Logger for this Class. */
89      private static final org.slf4j.Logger LOG = org.slf4j.LoggerFactory.getLogger(JNDIConnector.class);
90  
91      private String providerUrl;
92  
93      private String initialContextFactory;
94  
95      private String urlPkgPrefixes;
96  
97      private String contextPathPrefix = "java:comp/env/";
98  
99      private Boolean isJNDIAvailable;
100 
101     /** {@inheritDoc} */
102     public byte[] getContent(final String key) { 
103         final Object obj = lookupInContext(key);
104         if (obj == null) {
105             return null;
106         }
107         
108         // if obj is a String and an Object resolver is available
109         // obj could be a Path.
110         if (obj instanceof String && getContentResolver() != null) {
111             final byte[] content = getContentResolver().getContent((String) obj);
112             if (content != null) {
113                 return content;
114             }
115         }
116 
117         if (obj instanceof byte[]) {
118             return (byte[]) obj;
119         }
120 
121         LOG.warn("Wrong Type: {} for Key: {}", obj.getClass().getName(), key);
122         return null;
123     }
124 
125     /** {@inheritDoc} */
126     public Object getObject(final String key) {
127         final Object obj = lookupInContext(key);
128 
129         // if obj is a String and an Object resolver is available
130         // obj could be a Path to a XML who can be converted to an Object.
131         if (obj instanceof String && getObjectResolver() != null) {
132             final Object convertedObject = getObjectResolver().getObject((String) obj, getContentResolver());
133             if (convertedObject != null) {
134                 return convertedObject;
135             }
136         }
137 
138         return obj;
139     }
140 
141     /** {@inheritDoc} */
142     public String getString(final String key) {
143         final Object obj = lookupInContext(key);
144         try {
145             return (String) obj;
146         } catch (final ClassCastException e) {
147             LOG.warn("Wrong Type: {} for Key: {}", obj.getClass().getName(), key);
148             LOG.debug(e.getMessage(), e);
149             return null;
150         }
151     }
152 
153     /**
154      * Set or replace a new Value for the given key.<br />
155      * If set or replace a value is not possible because of a readonly JNDI-Context, then
156      * {@link Constants#SETTING_NOT_POSSIBLE} must be returned. If set or replace was successful, then
157      * {@link Constants#SETTING_SUCCESS} must be returned.
158      * 
159      * @param key the Key for the configuration-property (will not be normalized:
160      *  add contextPathPrefix, replace '\' with '/').
161      *  e.g.: "com\mycompany\myapp\myParameterKey" => "java:comp/env/com/mycompany/myapp/myParameterKey"
162      * 
163      * @param value the new Object-Value for the given key
164      * @return Returns {@link Constants#SETTING_SUCCESS} or {@link Constants#SETTING_NOT_POSSIBLE}
165      */
166     public int setObject(final String key, final Object value) {
167         return rebindToContext(normalizeKey(key), value);
168     }
169 
170     private InitialContext getJNDIContext() throws NamingException {
171         InitialContext initialContext;
172 
173         if (StringUtils.isEmpty(this.providerUrl) && StringUtils.isEmpty(this.initialContextFactory)
174             && StringUtils.isEmpty(this.urlPkgPrefixes)) {
175 
176             initialContext = new InitialContext();
177         } else {
178             final Properties prop = new Properties();
179             prop.put(Context.PROVIDER_URL, this.providerUrl);
180             prop.put(Context.INITIAL_CONTEXT_FACTORY, this.initialContextFactory);
181             prop.put(Context.URL_PKG_PREFIXES, this.urlPkgPrefixes);
182             initialContext = new InitialContext(prop);
183         }
184 
185         return initialContext;
186     }
187 
188     /**
189      * check if a JNDI context is available and sets the internal Flag setIsJNDIAvailable(Boolean).
190      * <p>
191      * If the internal Flag IsJNDIAvailable is <code>False</code> this Connector is disabled.
192      * 
193      * @return true if a JNDI Context could be initialized.
194      */
195     public boolean isJNDIAvailable() {
196         if (this.isJNDIAvailable == null) {
197             try {
198                 getJNDIContext().lookup(getContextPathPrefix());
199                 LOG.debug("JNDI Context is available.");
200                 this.isJNDIAvailable = Boolean.TRUE;
201             } catch (final NoInitialContextException e) {
202                 LOG.info("No JNDI Context available! JNDIConnector will be disabled: {}", e.getMessage());
203                 this.isJNDIAvailable = Boolean.FALSE;
204             } catch (final NamingException e) {
205                 LOG.info("JNDI Context is available but {}", e.getMessage());
206                 LOG.debug("NamingException in isJNDIAvailable: " + e.getMessage(), e);
207                 this.isJNDIAvailable = Boolean.TRUE;
208             }
209         }
210 
211         return this.isJNDIAvailable.booleanValue();
212     }
213 
214     public void setProviderUrl(final String providerUrl) {
215         this.providerUrl = providerUrl;
216     }
217 
218     public void setInitialContextFactory(final String initialContextFactory) {
219         this.initialContextFactory = initialContextFactory;
220     }
221 
222     public void setUrlPkgPrefixes(final String urlPkgPrefixes) {
223         this.urlPkgPrefixes = urlPkgPrefixes;
224     }
225 
226     private Object lookupInContext(final String key) {
227         return lookupInContext(key, true);
228     }
229 
230     private Object lookupInContext(final String key, final boolean withPrefix) {
231         if (!isJNDIAvailable()) {
232             return null;
233         }
234         final String normalizedKey = normalizeKey(key, withPrefix);
235         InitialContext ctx = null;
236         Object result = null;
237         try {
238             ctx = getJNDIContext();
239             result = ctx.lookup(normalizedKey);
240         } catch (final NoInitialContextException e) {
241             LOG.info("Maybe no JNDI-Context available.");
242             LOG.debug(e.getMessage(), e);
243         } catch (final NamingException e) {
244             LOG.debug("cannot lookup key: " + key + " (" + normalizedKey + ")", e);
245             if (withPrefix) {
246                 result = lookupInContext(key, false);
247             }
248         } finally {
249             if (ctx != null) {
250                 try {
251                     ctx.close();
252                 } catch (final NamingException e) {
253                     LOG.info("cannot close context: " + key + " (" + normalizedKey + ")", e);
254                 }
255             }
256         }
257         return result;
258     }
259 
260     /**
261      * @param key the JNDI-Key (will NOT be normalized).
262      * @param value the JNDI-Value.
263      * @return Constants.SETTING_NOT_POSSIBLE if the JNDI Context ist readonly.
264      */
265     public int rebindToContext(final String key, final Object value) {
266         // don't do a check, but use the result if a check was done.
267         if (BooleanUtils.isFalse(this.isJNDIAvailable)) {
268             // only if isJNDIAvailable() was called an evaluated to false.
269             return Constants.SETTING_NOT_POSSIBLE;
270         }
271         
272         LOG.debug("Try to rebind Key '{}' with value: {}", key, value);
273 
274         InitialContext ctx = null;
275         int result = Constants.SETTING_NOT_POSSIBLE;
276         try {
277             ctx = getJNDIContext();
278             createParentContext(ctx, key);
279             ctx.rebind(key, value);
280             result = Constants.SETTING_SUCCESS;
281         } catch (final NoInitialContextException e) {
282             LOG.info("Maybe no JNDI-Context available.");
283             LOG.debug(e.getMessage(), e);
284         } catch (final NamingException e) {
285             // the JNDI-Context from TOMCAT is readonly
286             // if you try to write it, The following Exception will be thrown:
287             // javax.naming.NamingException: Context is read only
288             LOG.info("cannot bind key: '{}'. {}", key, e.getMessage());
289             if (LOG.isDebugEnabled()) {
290                 LOG.debug("cannot bind key: " + key, e);
291             }
292         } finally {
293             if (ctx != null) {
294                 try {
295                     ctx.close();
296                 } catch (final NamingException e) {
297                     LOG.info("cannot close context: " + key, e);
298                 }
299             }
300         }
301         return result;
302     }
303 
304     private static void createParentContext(final Context ctx, final String key) throws NamingException {
305         // here we need to break by the specified delimiter
306 
307         LOG.debug("createParentContext: {}", key);
308         
309         final String[] path = key.split("/");
310 
311         final int lastIndex = path.length - 1;
312 
313         Context tmpCtx = ctx;
314 
315         for (int i = 0; i < lastIndex; i++) {
316             Object obj = null;
317             try {
318                 obj = tmpCtx.lookup(path[i]);
319             } catch (final NameNotFoundException e) {
320                 LOG.debug("obj is null and subcontext will be generated: {}", path[i]);
321             }
322 
323             if (obj == null) {
324                 tmpCtx = tmpCtx.createSubcontext(path[i]);
325                 LOG.debug("createSubcontext: {}", path[i]);
326             } else if (obj instanceof Context) {
327                 tmpCtx = (Context) obj;
328             } else {
329                 throw new RuntimeException("Illegal node/branch clash. At branch value '" + path[i]
330                     + "' an Object was found: " + obj);
331             }
332         }
333     }
334 
335     private String normalizeKey(final String key) {
336         return normalizeKey(key, true);
337     }
338 
339     private String normalizeKey(final String key, final boolean withPrefix) {
340         if (key == null) {
341             return null;
342         }
343         String normalizeKey = key;
344         if (normalizeKey.startsWith(this.contextPathPrefix)) {
345             return normalizeKey;
346         }
347 
348         normalizeKey = normalizeKey.replace('\\', '/');
349 
350         if (normalizeKey.startsWith("/")) {
351             normalizeKey = normalizeKey.substring(1);
352         }
353         if (withPrefix) {
354             return this.contextPathPrefix + normalizeKey;
355         } else {
356             return normalizeKey;
357         }
358     }
359 
360     public String getContextPathPrefix() {
361         return this.contextPathPrefix;
362     }
363 
364     public void setContextPathPrefix(final String contextPathPrefix) {
365         this.contextPathPrefix = contextPathPrefix;
366     }
367 
368     protected Boolean getIsJNDIAvailable() {
369         return this.isJNDIAvailable;
370     }
371 
372     protected void setIsJNDIAvailable(final Boolean isJNDIAvailable) {
373         this.isJNDIAvailable = isJNDIAvailable;
374     }
375 
376 }