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