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