-
Notifications
You must be signed in to change notification settings - Fork 34
/
StdJndiLoader.java
346 lines (288 loc) · 12.8 KB
/
StdJndiLoader.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
package org.yarnandtail.andhow.load.std;
import org.yarnandtail.andhow.internal.PropertyConfigurationInternal;
import java.util.ArrayList;
import java.util.List;
import javax.naming.*;
import org.yarnandtail.andhow.GroupInfo;
import org.yarnandtail.andhow.api.*;
import org.yarnandtail.andhow.internal.LoaderProblem.JndiContextMissing;
import org.yarnandtail.andhow.load.BaseLoader;
import org.yarnandtail.andhow.property.QuotedSpacePreservingTrimmer;
import org.yarnandtail.andhow.property.StrProp;
import org.yarnandtail.andhow.sample.JndiLoaderSamplePrinter;
import org.yarnandtail.andhow.util.AndHowLog;
import org.yarnandtail.andhow.util.TextUtil;
/**
* Attempts to look up the name of each known {@code Property} in the JNDI
* environment and loads the value for any that are found.
* <h3>Position in Standard Loading Order, first to last</h3>
* <ul>
* <li>StdFixedValueLoader
* <li>StdMainStringArgsLoader
* <li>StdSysPropLoader
* <li>StdEnvVarLoader
* <li><b>StdJndiLoader <-- This loader</b>
* <li>StdPropFileOnFilesystemLoader
* <li>StdPropFileOnClasspathLoader
* </ul>
* <h3>Typical Use Case</h3>
* A web or service application runs in an application container such as Tomcat.
* Application containers provide a JNDI environment which can be used to
* configure applications running in their environment.
* <h3>Basic Behaviors</h3>
* <ul>
* <li><b>Pre-trims String values: No</b> (Individual Properties may still trim values)
* <li><b>Complains about unrecognized properties: No</b>
* <li><b>Complains about missing JNDI environment: No</b> (by default)
* <li><b>Default behavior: Always attempts to look up each Property in the JNDI environment</b>
* <li><b>Is case sensitive: Yes</b> (This is one of the only loaders that is case sensitive)
* </ul>
* <h3>Loader Details and Configuration</h3>
* While most other loaders are case insensitive, the JNDI loader is case
* sensitive because the JNDI API is case sensitive.
* Also, while most other loaders consume a configuration resource
* (e.g. a properties file) and read all the names and values, the JNDI loader
* works the other way: It goes through the list of known Properties looks up
* each property name in the JNDI context. This because its not possible to
* somehow read the entire JNDI environment.
* <br>
* JNDI implementations vary in how they name properties, so the loader will
* try several common name forms, for example, the JNDI loader will attempt to
* look up the following JNDI names for a property named {@code org.foo.My_Prop}:
* <ul>
* <li>{@code java:comp/env/org/foo/My_Prop}
* <li>{@code java:comp/env/org.foo.My_Prop}
* <li>{@code java:org/foo/My_Prop}
* <li>{@code java:org.foo.My_Prop}
* <li>{@code org/foo/My_Prop}
* <li>{@code org.foo.My_Prop}
* </ul>
* This list has two different styles of property names: dot separated
* 'AndHow style' and slash separated JNDI style. Additionally, AndHow looks
* for three different roots (the part that comes before the variable name):
* {@code java:comp/env/} is used by Tomcat and several other application servers,
* {@code java:} is used by some non-container environments and at least one
* application server (Glassfish) uses no root at all. In all, AndHow will
* search for each property under six different names (2 X 3).
* AndHow will throw an error at startup if it finds multiple names in the
* JNDI context that refer to the same property.
* <br>
* Specifying JNDI environment variables varies by environment, but here is an
* example of specifying some properties in a Tomcat context.xml file:
* <pre>{@code
* <Context>
* . . .
* <Environment name="org/simple/GettingStarted/COUNT_DOWN_START" value="3" type="java.lang.Integer" override="false"/>
* <Environment name="org/simple/GettingStarted/LAUNCH_CMD" value="GoGoGo!" type="java.lang.String" override="false"/>
* . . .
* </Context>
* }</pre>
* <br>
* In the example above, Tomcat will automatically prepend {@code java:comp/env/}
* to the name it associates with each value. As the example shows, JNDI values
* can be typed. If AndHow finds the value to already be the type it expects
* (e.g. an {@code Integer}), great! If AndHow finds a String and needs a
* different type, AndHow will do the conversion. Any other type of conversion
* (e.g. from a {@code Short} to an {@code Integer}) will result in an exception.
* <br>
* If your JNDI environment uses a non-default different root, it can be added
* using one of the built-in Properties for the JNDI loader. Those property
* values would need to be loaded prior to the JNDI loader,
* so using system properties, for example, would work. Here is an example of
* adding the custom JNDI root {@code java:xyz/} as a system property on command line:
* <pre>
* java -Dorg.yarnandtail.andhow.load.std.StdJndiLoader.CONFIG.ADDED_JNDI_ROOTS=java:xyz/ -jar MyJarName.jar
* </pre>
* <h3>This is a Standard Loader</h3>
* Like all {@code StandardLoader}'s, this loader is intended to be auto-created
* by AndHow. The set of standard loaders and their order can bet set
* via the {@code AndHowConfiguration.setStandardLoaders()} methods.
* Other loaders which don't implement the {@code StandardLoader} interface can
* be inserted into the load order via the
* {@code AndHowConfiguration.insertLoaderBefore/After()}.
*/
public class StdJndiLoader extends BaseLoader implements LookupLoader, StandardLoader {
protected static AndHowLog log = AndHowLog.getLogger(StdJndiLoader.class);
private boolean failedEnvironmentAProblem = false;
/**
* There is no reason to use the constructor in production application code
* because AndHow creates a single instance on demand at runtime.
*/
public StdJndiLoader() {
}
@Override
public LoaderValues load(PropertyConfigurationInternal appConfigDef,
LoaderEnvironment environment, ValidatedValuesWithContext existingValues) {
Context ctx = environment.getJndiContext().getContext();
if (ctx != null) { // Happy path
return doLoad(appConfigDef, ctx, existingValues);
} else if (isFailedEnvironmentAProblem()) { // No JNDI... and this is a problem
Problem p = new JndiContextMissing(this);
log.error(p.getProblemDescription(), environment.getJndiContext().getException());
return new LoaderValues(this, p);
} else { // No JNDI... but that is OK.
log.debug("No JNDI Environment found, or a naming error encountered. " +
"The StdJndiLoader is configured to ignore this.");
return new LoaderValues(this);
}
}
/**
* The actual loading, called only when there is a non-null Jndi Context.
*
* @param appConfigDef The definition of all known Properties and naming metadata.
* @param ctx The non-null Jndi Context
* @param existingValues The values already set by prior loaders, which may configure
* the behavior of this loader.
* @return The Property values loaded by this loader and/or the problems discovered while
* attempting to load those Property values.
*/
protected LoaderValues doLoad(PropertyConfigurationInternal appConfigDef, Context ctx,
ValidatedValuesWithContext existingValues) {
ProblemList<Problem> problems = new ProblemList();
ArrayList<ValidatedValue> values = new ArrayList();
List<String> jndiRoots = buildJndiRoots(existingValues);
for (Property<?> prop : appConfigDef.getProperties()) {
List<String> propJndiNames = buildJndiNames(appConfigDef, jndiRoots, prop);
for (String propName : propJndiNames) {
try {
Object o = ctx.lookup(propName);
if (o != null) {
attemptToAdd(appConfigDef, values, problems, prop, o);
}
} catch (NamingException ne) {
//Ignore - this is expected if a value is not found
}
}
}
return new LoaderValues(this, values, problems);
}
@Override
public boolean isTrimmingRequiredForStringValues() {
return false;
}
@Override
public boolean isFlaggable() { return false; }
@Override
public String getSpecificLoadDescription() {
return "JNDI properties in the system-wide JNDI context";
}
@Override
public Class<?> getClassConfig() {
return CONFIG.class;
}
@Override
public SamplePrinter getConfigSamplePrinter() {
return new JndiLoaderSamplePrinter();
}
/**
* Combines the values of STANDARD_JNDI_ROOTS and ADDED_JNDI_ROOTS into one list of jndi root contexts to search.
*
* Expected values might look like: java: or java:/comp/env/
*
* @param values The configuration state.
* @return Never null and never non-empty.
*/
protected List<String> buildJndiRoots(ValidatedValues values) {
ArrayList<String> myJndiRoots = new ArrayList();
//Add the added roots to the search list first, since they are pretty
//likely to be the correct ones if someone explicitly added them.
//We still check them all anyway, since a duplicate entry would be ambiguous.
if (values.getValue(CONFIG.ADDED_JNDI_ROOTS) != null) {
List<String> addRoots = split(values.getValue(CONFIG.ADDED_JNDI_ROOTS));
myJndiRoots.addAll(addRoots);
}
List<String> addRoots = split(values.getValue(CONFIG.STANDARD_JNDI_ROOTS));
myJndiRoots.addAll(addRoots);
return myJndiRoots;
}
/**
* Builds a complete list of complete JNDI names to search for a parameter value.
*
* @param appConfigDef
* @param roots
* @param prop
* @return An ordered list of jndi names, with (hopefully) the most likely names first.
*/
protected List<String> buildJndiNames(PropertyConfigurationInternal appConfigDef, List<String> roots, Property prop) {
List<String> propNames = new ArrayList(); // w/o jndi root prefix
List<String> propJndiNames = new ArrayList(); // w/ jndi root prefix - return value
// Add Uri name (org/project/Class/Property) & classpath name (org.project.Class.Property)
propNames.add(appConfigDef.getNamingStrategy().getUriName(appConfigDef.getCanonicalName(prop)));
propNames.add(appConfigDef.getCanonicalName(prop));
// Add all the 'in' aliases
appConfigDef.getAliases(prop).stream().filter(a -> a.isIn()).forEach(a -> {
propNames.add(a.getActualName());
// Add URI version of in-alias if different (my.alias -> my/alias)
if (appConfigDef.getNamingStrategy().isUriNameDistinct(a.getActualName())) {
propNames.add(appConfigDef.getNamingStrategy().getUriName(a.getActualName()));
}
});
for (String root : roots) {
for (String propName : propNames) {
propJndiNames.add(root + propName);
}
}
return propJndiNames;
}
/**
* Spits a comma separate list of JNDI roots into individual root strings.
*
* Use double quotes to indicate and preserve and empty or white space string.
*
* @param rootStr
* @return A list of JNDI root strings
*/
protected List<String> split(String rootStr) {
List<String> cleanRoots = new ArrayList();
if (rootStr != null) {
QuotedSpacePreservingTrimmer trimmer = QuotedSpacePreservingTrimmer.instance();
String[] roots = rootStr.split(",");
for (int i = 0; i < roots.length; i++) {
String s = trimmer.trim(roots[i]);
if (s != null) cleanRoots.add(s);
}
return cleanRoots;
} else {
return TextUtil.EMPTY_STRING_LIST;
}
}
@GroupInfo(
name = "JndiLoader Configuration",
desc = "Since JNDI providers use different base URIs to store "
+ "entries, base URIs must be configurable. "
+ "The most common URI roots are \"java:\", \"java:comp/env/\" or just \"\"."
+ "To preserve whitespace or indicate an empty string, use double quotes around an individual comma separated value."
+ "If your container/provider uses something different, set one of these properties. "
+ "All configured JNDI roots will be searched for each application property."
+ "Typically there are multiple roots to search and multiple forms of "
+ "property names, leading to the possibility of duplicate/conflicting JNDI entries. "
+ "If multiple entries are found in JNDI for a property, a runtime error is thrown at startup.")
public static interface CONFIG {
StrProp STANDARD_JNDI_ROOTS = StrProp.builder()
.defaultValue("java:comp/env/,java:,\"\"").notNull()
.desc("A comma separated list of standard JNDI root locations to be searched for properties. "
+ "Setting this property will replace the standard list, "
+ "use ADDED_JNDI_ROOTS to only add to the list. ")
.helpText("The final JNDI URIs to be searched will look like this '[root][Property Name]'").build();
StrProp ADDED_JNDI_ROOTS = StrProp.builder()
.desc("A comma separated list of JNDI root locations to be prepended to the standard list for searching. "
+ "Setting this property does not affect the STANDARD_JNDI_ROOTS.")
.helpText("The final JNDI URIs to be searched will look like this '[root][Property Name]'").build();
}
@Override
public String getLoaderType() {
return "JNDI";
}
@Override
public String getLoaderDialect() {
return null;
}
@Override
public void setFailedEnvironmentAProblem(boolean isAProblem) {
failedEnvironmentAProblem = isAProblem;
}
@Override
public boolean isFailedEnvironmentAProblem() {
return failedEnvironmentAProblem;
}
}