1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements. See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership. The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License. You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied. See the License for the
16 * specific language governing permissions and limitations
17 * under the License.
18 */
19 package org.apache.openjpa.lib.conf;
20
21 import java.io.File;
22 import java.security.AccessController;
23 import java.security.PrivilegedActionException;
24 import java.util.ArrayList;
25 import java.util.Arrays;
26 import java.util.Collection;
27 import java.util.Iterator;
28 import java.util.List;
29 import java.util.Map;
30 import java.util.MissingResourceException;
31 import java.util.Properties;
32 import java.util.TreeSet;
33 import javax.naming.Context;
34 import javax.naming.InitialContext;
35 import javax.naming.NamingException;
36
37 import org.apache.commons.lang.StringUtils;
38 import org.apache.commons.lang.exception.NestableRuntimeException;
39 import org.apache.openjpa.lib.log.Log;
40 import org.apache.openjpa.lib.util.J2DoPrivHelper;
41 import org.apache.openjpa.lib.util.Localizer;
42 import org.apache.openjpa.lib.util.Options;
43 import org.apache.openjpa.lib.util.ParseException;
44 import org.apache.openjpa.lib.util.StringDistance;
45 import java.util.concurrent.ConcurrentHashMap;
46 import org.apache.openjpa.lib.util.concurrent.ConcurrentReferenceHashMap;
47 import serp.util.Strings;
48
49 /**
50 * Utility methods dealing with configuration.
51 *
52 * @author Abe White
53 * @nojavadoc
54 */
55 public class Configurations {
56
57 private static final Localizer _loc = Localizer.forPackage
58 (Configurations.class);
59
60 private static final ConcurrentReferenceHashMap _loaders = new
61 ConcurrentReferenceHashMap(ConcurrentReferenceHashMap.WEAK,
62 ConcurrentReferenceHashMap.HARD);
63
64 private static final Object NULL_LOADER = "null-loader";
65
66 /**
67 * Return the class name from the given plugin string, or null if none.
68 */
69 public static String getClassName(String plugin) {
70 return getPluginComponent(plugin, true);
71 }
72
73 /**
74 * Return the properties part of the given plugin string, or null if none.
75 */
76 public static String getProperties(String plugin) {
77 return getPluginComponent(plugin, false);
78 }
79
80 /**
81 * Return either the class name or properties string from a plugin string.
82 */
83 private static String getPluginComponent(String plugin, boolean clsName) {
84 if (plugin != null)
85 plugin = plugin.trim();
86 if (StringUtils.isEmpty(plugin))
87 return null;
88
89 int openParen = -1;
90 if (plugin.charAt(plugin.length() - 1) == ')')
91 openParen = plugin.indexOf('(');
92 if (openParen == -1) {
93 int eq = plugin.indexOf('=');
94 if (eq == -1)
95 return (clsName) ? plugin : null;
96 return (clsName) ? null : plugin;
97 }
98
99 // clsName(props) form
100 if (clsName)
101 return plugin.substring(0, openParen).trim();
102 String prop = plugin.substring(openParen + 1,
103 plugin.length() - 1).trim();
104 return (prop.length() == 0) ? null : prop;
105 }
106
107 /**
108 * Combine the given class name and properties into a plugin string.
109 */
110 public static String getPlugin(String clsName, String props) {
111 if (StringUtils.isEmpty(clsName))
112 return props;
113 if (StringUtils.isEmpty(props))
114 return clsName;
115 return clsName + "(" + props + ")";
116 }
117
118 /**
119 * Return a plugin string that combines the properties of the given plugin
120 * strings, where properties of <code>override</code> will override the
121 * same properties of <code>orig</code>.
122 */
123 public static String combinePlugins(String orig, String override) {
124 if (StringUtils.isEmpty(orig))
125 return override;
126 if (StringUtils.isEmpty(override))
127 return orig;
128
129 String origCls = getClassName(orig);
130 String overrideCls = getClassName(override);
131 String cls;
132 if (StringUtils.isEmpty(origCls))
133 cls = overrideCls;
134 else if (StringUtils.isEmpty(overrideCls))
135 cls = origCls;
136 else if (!origCls.equals(overrideCls))
137 return override; // completely different plugin
138 else
139 cls = origCls;
140
141 String origProps = getProperties(orig);
142 String overrideProps = getProperties(override);
143 if (StringUtils.isEmpty(origProps))
144 return getPlugin(cls, overrideProps);
145 if (StringUtils.isEmpty(overrideProps))
146 return getPlugin(cls, origProps);
147
148 Properties props = parseProperties(origProps);
149 props.putAll(parseProperties(overrideProps));
150 return getPlugin(cls, serializeProperties(props));
151 }
152
153 /**
154 * Create the instance with the given class name, using the given
155 * class loader. No configuration of the instance is performed by
156 * this method.
157 */
158 public static Object newInstance(String clsName, ClassLoader loader) {
159 return newInstance(clsName, null, null, loader, true);
160 }
161
162 /**
163 * Create and configure an instance with the given class name and
164 * properties.
165 */
166 public static Object newInstance(String clsName, Configuration conf,
167 String props, ClassLoader loader) {
168 Object obj = newInstance(clsName, null, conf, loader, true);
169 configureInstance(obj, conf, props);
170 return obj;
171 }
172
173 /**
174 * Helper method used by members of this package to instantiate plugin
175 * values.
176 */
177 static Object newInstance(String clsName, Value val, Configuration conf,
178 ClassLoader loader, boolean fatal) {
179 if (StringUtils.isEmpty(clsName))
180 return null;
181
182 Class cls = null;
183
184 while (cls == null) {
185 // can't have a null reference in the map, so use symbolic
186 // constant as key
187 Object key = loader == null ? NULL_LOADER : loader;
188 Map loaderCache = (Map) _loaders.get(key);
189 if (loaderCache == null) { // We don't have a cache for this loader.
190 loaderCache = new ConcurrentHashMap();
191 _loaders.put(key, loaderCache);
192 } else { // We have a cache for this loader.
193 cls = (Class) loaderCache.get(clsName);
194 }
195
196 if (cls == null) {
197 try {
198 cls = Strings.toClass(clsName, findDerivedLoader(conf,
199 loader));
200 loaderCache.put(clsName, cls);
201 } catch (RuntimeException re) {
202 if (loader != null) // Try one more time with loader=null
203 loader = null;
204 else {
205 if (val != null)
206 re = getCreateException(clsName, val, re);
207 if (fatal)
208 throw re;
209 Log log = (conf == null) ? null : conf
210 .getConfigurationLog();
211 if (log != null && log.isErrorEnabled())
212 log.error(_loc
213 .get("plugin-creation-exception", val), re);
214 return null;
215 }
216 }
217 }
218 }
219
220 try {
221 return AccessController.doPrivileged(
222 J2DoPrivHelper.newInstanceAction(cls));
223 } catch (Exception e) {
224 if (e instanceof PrivilegedActionException) {
225 e = ((PrivilegedActionException) e).getException();
226 }
227 RuntimeException re = new NestableRuntimeException(_loc.get
228 ("obj-create", cls).getMessage(), e);
229 if (fatal)
230 throw re;
231 Log log = (conf == null) ? null : conf.getConfigurationLog();
232 if (log != null && log.isErrorEnabled())
233 log.error(_loc.get("plugin-creation-exception", val), re);
234 return null;
235 }
236 }
237
238 /**
239 * Attempt to find a derived loader that delegates to our target loader.
240 * This allows application loaders that delegate appropriately for known
241 * classes first crack at class names.
242 */
243 private static ClassLoader findDerivedLoader(Configuration conf,
244 ClassLoader loader) {
245 // we always prefer the thread loader, because it's the only thing we
246 // can access that isn't bound to the OpenJPA classloader, unless
247 // the conf object is of a custom class
248 ClassLoader ctxLoader = (ClassLoader) AccessController.doPrivileged(
249 J2DoPrivHelper.getContextClassLoaderAction());
250 if (loader == null) {
251 if (ctxLoader != null)
252 return ctxLoader;
253 if (conf != null)
254 return (ClassLoader) AccessController.doPrivileged(
255 J2DoPrivHelper.getClassLoaderAction(conf.getClass()));
256 return Configurations.class.getClassLoader();
257 }
258
259 for (ClassLoader parent = ctxLoader; parent != null;
260 parent = (ClassLoader) AccessController.doPrivileged(
261 J2DoPrivHelper.getParentAction(parent))) {
262 if (parent == loader)
263 return ctxLoader;
264 }
265 if (conf != null) {
266 for (ClassLoader parent = (ClassLoader)
267 AccessController.doPrivileged(
268 J2DoPrivHelper.getClassLoaderAction(conf.getClass()));
269 parent != null;
270 parent = (ClassLoader) AccessController.doPrivileged(
271 J2DoPrivHelper.getParentAction(parent))) {
272 if (parent == loader)
273 return (ClassLoader) AccessController.doPrivileged(
274 J2DoPrivHelper.getClassLoaderAction(conf.getClass()));
275 }
276 }
277 return loader;
278 }
279
280 /**
281 * Return a List<String> of all the fully-qualified anchors specified in the
282 * properties location listed in <code>opts</code>. If no properties
283 * location is listed in <code>opts</code>, this returns whatever the
284 * product derivations can find in their default configurations.
285 * If the properties location specified in <code>opts</code> already
286 * contains an anchor spec, this returns that anchor. Note that in this
287 * fully-qualified-input case, the logic involving product derivations
288 * and resource parsing is short-circuited, so this method
289 * should not be used as a means to test that a particular anchor is
290 * defined in a given location by invoking with a fully-qualified anchor.
291 *
292 * This does not mutate <code>opts</code>.
293 *
294 * @since 1.1.0
295 */
296 public static List getFullyQualifiedAnchorsInPropertiesLocation(
297 Options opts) {
298 String props = opts.getProperty("properties", "p", null);
299 if (props != null) {
300 int anchorPosition = props.indexOf("#");
301 if (anchorPosition > -1)
302 return Arrays.asList(new String[] { props });
303 }
304
305 return ProductDerivations.getFullyQualifiedAnchorsInPropertiesLocation(
306 props);
307 }
308
309 /**
310 * Set the given {@link Configuration} instance from the command line
311 * options provided. All property names of the given configuration are
312 * recognized; additionally, if a <code>properties</code> or
313 * <code>p</code> argument exists, the resource it
314 * points to will be loaded and set into the given configuration instance.
315 * It can point to either a file or a resource name.
316 */
317 public static void populateConfiguration(Configuration conf, Options opts) {
318 String props = opts.removeProperty("properties", "p", null);
319 ConfigurationProvider provider;
320 if (!StringUtils.isEmpty(props)) {
321 String path = props;
322 String anchor = null;
323 int idx = path.lastIndexOf('#');
324 if (idx != -1) {
325 if (idx < path.length() - 1)
326 anchor = path.substring(idx + 1);
327 path = path.substring(0, idx);
328 if (path.length() == 0)
329 throw new MissingResourceException(_loc.get("anchor-only",
330 props).getMessage(), Configurations.class.getName(),
331 props);
332 }
333
334 File file = new File(path);
335 if (((Boolean) AccessController.doPrivileged(J2DoPrivHelper
336 .isFileAction(file))).booleanValue())
337 provider = ProductDerivations.load(file, anchor, null);
338 else {
339 file = new File("META-INF" + File.separatorChar + path);
340 if (((Boolean) AccessController.doPrivileged(J2DoPrivHelper
341 .isFileAction(file))).booleanValue())
342 provider = ProductDerivations.load(file, anchor, null);
343 else
344 provider = ProductDerivations.load(path, anchor, null);
345 }
346 if (provider != null)
347 provider.setInto(conf);
348 else
349 throw new MissingResourceException(_loc.get("no-provider",
350 props).getMessage(), Configurations.class.getName(),
351 props);
352 } else {
353 provider = ProductDerivations.loadDefaults(null);
354 if (provider != null)
355 provider.setInto(conf);
356 }
357 opts.setInto(conf);
358 }
359
360 /**
361 * Helper method to throw an informative description on instantiation error.
362 */
363 private static RuntimeException getCreateException(String clsName,
364 Value val, Exception e) {
365 // re-throw the exception with some better information
366 final String msg;
367 final Object[] params;
368
369 String alias = val.alias(clsName);
370 String[] aliases = val.getAliases();
371 String[] keys;
372 if (aliases.length == 0)
373 keys = aliases;
374 else {
375 keys = new String[aliases.length / 2];
376 for (int i = 0; i < aliases.length; i += 2)
377 keys[i / 2] = aliases[i];
378 }
379
380 String closest;
381 if (keys.length == 0) {
382 msg = "invalid-plugin";
383 params = new Object[]{ val.getProperty(), alias, e.toString(), };
384 } else if ((closest = StringDistance.getClosestLevenshteinDistance
385 (alias, keys, 0.5f)) == null) {
386 msg = "invalid-plugin-aliases";
387 params = new Object[]{
388 val.getProperty(), alias, e.toString(),
389 new TreeSet(Arrays.asList(keys)), };
390 } else {
391 msg = "invalid-plugin-aliases-hint";
392 params = new Object[]{
393 val.getProperty(), alias, e.toString(),
394 new TreeSet(Arrays.asList(keys)), closest, };
395 }
396 return new ParseException(_loc.get(msg, params), e);
397 }
398
399 /**
400 * Configures the given object with the given properties by
401 * matching the properties string to the object's setter
402 * methods. The properties string should be in the form
403 * "prop1=val1, prop2=val2 ...". Does not validate that setter
404 * methods exist for the properties.
405 *
406 * @throws RuntimeException on configuration error
407 */
408 public static void configureInstance(Object obj, Configuration conf,
409 String properties) {
410 configureInstance(obj, conf, properties, null);
411 }
412
413 /**
414 * Configures the given object with the given properties by
415 * matching the properties string to the object's setter
416 * methods. The properties string should be in the form
417 * "prop1=val1, prop2=val2 ...". Validates that setter methods
418 * exist for the properties.
419 *
420 * @throws RuntimeException on configuration error
421 */
422 public static void configureInstance(Object obj, Configuration conf,
423 String properties, String configurationName) {
424 if (obj == null)
425 return;
426
427 Properties props = null;
428 if (!StringUtils.isEmpty(properties))
429 props = parseProperties(properties);
430 configureInstance(obj, conf, props, configurationName);
431 }
432
433 /**
434 * Configures the given object with the given properties by
435 * matching the properties string to the object's setter
436 * methods. Does not validate that setter methods exist for the properties.
437 *
438 * @throws RuntimeException on configuration error
439 */
440 public static void configureInstance(Object obj, Configuration conf,
441 Properties properties) {
442 configureInstance(obj, conf, properties, null);
443 }
444
445 /**
446 * Configures the given object with the given properties by
447 * matching the properties string to the object's setter
448 * methods. If <code>configurationName</code> is
449 * non-<code>null</code>, validates that setter methods exist for
450 * the properties.
451 *
452 * @throws RuntimeException on configuration error
453 */
454 public static void configureInstance(Object obj, Configuration conf,
455 Properties properties, String configurationName) {
456 if (obj == null)
457 return;
458
459 Options opts;
460 if (properties instanceof Options)
461 opts = (Options) properties;
462 else {
463 opts = new Options();
464 if (properties != null)
465 opts.putAll(properties);
466 }
467
468 Configurable configurable = null;
469 if (conf != null && obj instanceof Configurable)
470 configurable = (Configurable) obj;
471
472 if (configurable != null) {
473 configurable.setConfiguration(conf);
474 configurable.startConfiguration();
475 }
476 Options invalidEntries = opts.setInto(obj);
477 if (obj instanceof GenericConfigurable)
478 ((GenericConfigurable) obj).setInto(invalidEntries);
479
480 if (!invalidEntries.isEmpty() && configurationName != null) {
481 Localizer.Message msg = null;
482 String first = (String) invalidEntries.keySet().iterator().next();
483 if (invalidEntries.keySet().size() == 1 &&
484 first.indexOf('.') == -1) {
485 // if there's just one misspelling and this is not a
486 // path traversal, check for near misses.
487 Collection options = findOptionsFor(obj.getClass());
488 String close = StringDistance.getClosestLevenshteinDistance
489 (first, options, 0.75f);
490 if (close != null)
491 msg = _loc.get("invalid-config-param-hint", new Object[]{
492 configurationName, obj.getClass(), first, close,
493 options, });
494 }
495
496 if (msg == null) {
497 msg = _loc.get("invalid-config-params", new String[]{
498 configurationName, obj.getClass().getName(),
499 invalidEntries.keySet().toString(),
500 findOptionsFor(obj.getClass()).toString(), });
501 }
502 throw new ParseException(msg);
503 }
504 if (configurable != null)
505 configurable.endConfiguration();
506 }
507
508 private static Collection findOptionsFor(Class cls) {
509 Collection c = Options.findOptionsFor(cls);
510
511 // remove Configurable.setConfiguration() and
512 // GenericConfigurable.setInto() from the set, if applicable.
513 if (Configurable.class.isAssignableFrom(cls))
514 c.remove("Configuration");
515 if (GenericConfigurable.class.isAssignableFrom(cls))
516 c.remove("Into");
517
518 return c;
519 }
520
521 /**
522 * Turn a set of properties into a comma-separated string.
523 */
524 public static String serializeProperties(Map map) {
525 if (map == null || map.isEmpty())
526 return null;
527
528 StringBuffer buf = new StringBuffer();
529 Map.Entry entry;
530 String val;
531 for (Iterator itr = map.entrySet().iterator(); itr.hasNext();) {
532 entry = (Map.Entry) itr.next();
533 if (buf.length() > 0)
534 buf.append(", ");
535 buf.append(entry.getKey()).append('=');
536 val = String.valueOf(entry.getValue());
537 if (val.indexOf(',') != -1)
538 buf.append('"').append(val).append('"');
539 else
540 buf.append(val);
541 }
542 return buf.toString();
543 }
544
545 /**
546 * Parse a set of properties from a comma-separated string.
547 */
548 public static Options parseProperties(String properties) {
549 Options opts = new Options();
550 properties = StringUtils.trimToNull(properties);
551 if (properties == null)
552 return opts;
553
554 try {
555 String[] props = Strings.split(properties, ",", 0);
556 int idx;
557 char quote;
558 String prop;
559 String val;
560 for (int i = 0; i < props.length; i++) {
561 idx = props[i].indexOf('=');
562 if (idx == -1) {
563 // if the key is not assigned to any value, set the
564 // value to the same thing as the key, and continue.
565 // This permits GenericConfigurable instances to
566 // behave meaningfully. We might consider setting the
567 // value to some well-known "value was not set, but
568 // key is present" string so that instances getting
569 // values injected can differentiate between a mentioned
570 // property and one set to a particular value.
571 prop = props[i];
572 val = prop;
573 } else {
574 prop = props[i].substring(0, idx).trim();
575 val = props[i].substring(idx + 1).trim();
576 }
577
578 // if the value is quoted, read until the end quote
579 if (((val.startsWith("\"") && val.endsWith("\""))
580 || (val.startsWith("'") && val.endsWith("'")))
581 && val.length() > 1)
582 val = val.substring(1, val.length() - 1);
583 else if (val.startsWith("\"") || val.startsWith("'")) {
584 quote = val.charAt(0);
585 StringBuffer buf = new StringBuffer(val.substring(1));
586 int quotIdx;
587 while (++i < props.length) {
588 buf.append(",");
589
590 quotIdx = props[i].indexOf(quote);
591 if (quotIdx != -1) {
592 buf.append(props[i].substring(0, quotIdx));
593 if (quotIdx + 1 < props[i].length())
594 buf.append(props[i].substring(quotIdx + 1));
595 break;
596 } else
597 buf.append(props[i]);
598 }
599 val = buf.toString();
600 }
601 opts.put(prop, val);
602 }
603 return opts;
604 } catch (RuntimeException re) {
605 throw new ParseException(_loc.get("prop-parse", properties), re);
606 }
607 }
608
609 /**
610 * Looks up the given name in JNDI. If the name is null, null is returned.
611 */
612 public static Object lookup(String name) {
613 if (StringUtils.isEmpty(name))
614 return null;
615
616 Context ctx = null;
617 try {
618 ctx = new InitialContext();
619 return ctx.lookup(name);
620 } catch (NamingException ne) {
621 throw new NestableRuntimeException(
622 _loc.get("naming-err", name).getMessage(), ne);
623 } finally {
624 if (ctx != null)
625 try { ctx.close(); } catch (Exception e) {}
626 }
627 }
628
629 /**
630 * Test whether the map contains the given partial key, prefixed with any
631 * possible configuration prefix.
632 */
633 public static boolean containsProperty(String partialKey, Map props) {
634 if (partialKey == null || props == null || props.isEmpty())
635 return false;
636 else
637 return props.containsKey(
638 ProductDerivations.getConfigurationKey(partialKey, props));
639 }
640
641 /**
642 * Get the property under the given partial key, prefixed with any possible
643 * configuration prefix.
644 */
645 public static Object getProperty(String partialKey, Map m) {
646 if (partialKey == null || m == null || m.isEmpty())
647 return null;
648 else
649 return m.get(ProductDerivations.getConfigurationKey(partialKey, m));
650 }
651
652 /**
653 * Remove the property under the given partial key, prefixed with any
654 * possible configuration prefix.
655 */
656 public static Object removeProperty(String partialKey, Map props) {
657 if (partialKey == null || props == null || props.isEmpty())
658 return null;
659 if (containsProperty(partialKey, props))
660 return props.remove(ProductDerivations.getConfigurationKey(
661 partialKey, props));
662 else
663 return null;
664 }
665
666 /**
667 * Runs <code>runnable</code> against all the anchors in the configuration
668 * pointed to by <code>opts</code>. Each invocation gets a fresh clone of
669 * <code>opts</code> with the <code>properties</code> option set
670 * appropriately.
671 *
672 * @since 1.1.0
673 */
674 public static boolean runAgainstAllAnchors(Options opts,
675 Configurations.Runnable runnable) {
676 if (opts.containsKey("help") || opts.containsKey("-help")) {
677 return false;
678 }
679 List anchors =
680 Configurations.getFullyQualifiedAnchorsInPropertiesLocation(opts);
681
682 // We use 'properties' below; get rid of 'p' to avoid conflicts. This
683 // relies on knowing what getFullyQualifiedAnchorsInPropertiesLocation
684 // looks for.
685 if (opts.containsKey("p"))
686 opts.remove("p");
687
688 boolean ret = true;
689 if (anchors.size() == 0) {
690 ret = launchRunnable(opts, runnable);
691 } else {
692 for (Iterator iter = anchors.iterator(); iter.hasNext(); ) {
693 Options clonedOptions = (Options) opts.clone();
694 clonedOptions.setProperty("properties", iter.next().toString());
695 ret &= launchRunnable(clonedOptions, runnable);
696 }
697 }
698 return ret;
699 }
700
701 private static boolean launchRunnable(Options opts,
702 Configurations.Runnable runnable) {
703 boolean ret = true;
704 try {
705 ret = runnable.run(opts);
706 } catch (Exception e) {
707 if (!(e instanceof RuntimeException))
708 throw new RuntimeException(e);
709 else
710 throw (RuntimeException) e;
711 }
712 return ret;
713 }
714
715 public interface Runnable {
716 public boolean run(Options opts) throws Exception;
717 }
718 }