ContextFinder.java revision 600:884d98e00032
1/*
2 * Copyright (c) 2003, 2015, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.  Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26package javax.xml.bind;
27
28import java.io.BufferedReader;
29import java.io.IOException;
30import java.io.InputStream;
31import java.io.InputStreamReader;
32import java.io.UnsupportedEncodingException;
33import java.lang.reflect.InvocationTargetException;
34import java.lang.reflect.Method;
35import java.net.URL;
36import java.security.AccessController;
37import java.util.Map;
38import java.util.Properties;
39import java.util.StringTokenizer;
40import java.util.logging.ConsoleHandler;
41import java.util.logging.Level;
42import java.util.logging.Logger;
43
44
45/**
46 * This class is package private and therefore is not exposed as part of the
47 * JAXB API.
48 *
49 * This code is designed to implement the JAXB 1.0 spec pluggability feature
50 *
51 * @author <ul><li>Ryan Shoemaker, Sun Microsystems, Inc.</li></ul>
52 * @see JAXBContext
53 */
54class ContextFinder {
55
56    /**
57     * When JAXB is in J2SE, rt.jar has to have a JAXB implementation.
58     * However, rt.jar cannot have META-INF/services/javax.xml.bind.JAXBContext
59     * because if it has, it will take precedence over any file that applications have
60     * in their jar files.
61     *
62     * <p>
63     * When the user bundles his own JAXB implementation, we'd like to use it, and we
64     * want the platform default to be used only when there's no other JAXB provider.
65     *
66     * <p>
67     * For this reason, we have to hard-code the class name into the API.
68     */
69    private static final String PLATFORM_DEFAULT_FACTORY_CLASS = "com.sun.xml.internal.bind.v2.ContextFactory";
70
71    // previous value of JAXBContext.JAXB_CONTEXT_FACTORY, using also this to ensure backwards compatibility
72    private static final String JAXB_CONTEXT_FACTORY_DEPRECATED = "javax.xml.bind.context.factory";
73
74    private static final Logger logger;
75
76    static {
77        logger = Logger.getLogger("javax.xml.bind");
78        try {
79            if (AccessController.doPrivileged(new GetPropertyAction("jaxb.debug")) != null) {
80                // disconnect the logger from a bigger framework (if any)
81                // and take the matters into our own hands
82                logger.setUseParentHandlers(false);
83                logger.setLevel(Level.ALL);
84                ConsoleHandler handler = new ConsoleHandler();
85                handler.setLevel(Level.ALL);
86                logger.addHandler(handler);
87            } else {
88                // don't change the setting of this logger
89                // to honor what other frameworks
90                // have done on configurations.
91            }
92        } catch (Throwable t) {
93            // just to be extra safe. in particular System.getProperty may throw
94            // SecurityException.
95        }
96    }
97
98    private static ServiceLoaderUtil.ExceptionHandler<JAXBException> EXCEPTION_HANDLER =
99            new ServiceLoaderUtil.ExceptionHandler<JAXBException>() {
100                @Override
101                public JAXBException createException(Throwable throwable, String message) {
102                    return new JAXBException(message, throwable);
103                }
104            };
105
106    /**
107     * If the {@link InvocationTargetException} wraps an exception that shouldn't be wrapped,
108     * throw the wrapped exception.
109     */
110    private static void handleInvocationTargetException(InvocationTargetException x) throws JAXBException {
111        Throwable t = x.getTargetException();
112        if (t != null) {
113            if (t instanceof JAXBException)
114                // one of our exceptions, just re-throw
115                throw (JAXBException) t;
116            if (t instanceof RuntimeException)
117                // avoid wrapping exceptions unnecessarily
118                throw (RuntimeException) t;
119            if (t instanceof Error)
120                throw (Error) t;
121        }
122    }
123
124
125    /**
126     * Determine if two types (JAXBContext in this case) will generate a ClassCastException.
127     *
128     * For example, (targetType)originalType
129     *
130     * @param originalType
131     *          The Class object of the type being cast
132     * @param targetType
133     *          The Class object of the type that is being cast to
134     * @return JAXBException to be thrown.
135     */
136    private static JAXBException handleClassCastException(Class originalType, Class targetType) {
137        final URL targetTypeURL = which(targetType);
138
139        return new JAXBException(Messages.format(Messages.ILLEGAL_CAST,
140                // we don't care where the impl class is, we want to know where JAXBContext lives in the impl
141                // class' ClassLoader
142                getClassClassLoader(originalType).getResource("javax/xml/bind/JAXBContext.class"),
143                targetTypeURL));
144    }
145
146    /**
147     * Create an instance of a class using the specified ClassLoader
148     */
149    static JAXBContext newInstance(String contextPath,
150                                   String className,
151                                   ClassLoader classLoader,
152                                   Map properties) throws JAXBException {
153
154        try {
155            Class spFactory = ServiceLoaderUtil.safeLoadClass(className, PLATFORM_DEFAULT_FACTORY_CLASS, classLoader);
156            return newInstance(contextPath, spFactory, classLoader, properties);
157        } catch (ClassNotFoundException x) {
158            throw new JAXBException(Messages.format(Messages.PROVIDER_NOT_FOUND, className), x);
159
160        } catch (RuntimeException x) {
161            // avoid wrapping RuntimeException to JAXBException,
162            // because it indicates a bug in this code.
163            throw x;
164        } catch (Exception x) {
165            // can't catch JAXBException because the method is hidden behind
166            // reflection.  Root element collisions detected in the call to
167            // createContext() are reported as JAXBExceptions - just re-throw it
168            // some other type of exception - just wrap it
169            throw new JAXBException(Messages.format(Messages.COULD_NOT_INSTANTIATE, className, x), x);
170        }
171    }
172
173    static JAXBContext newInstance(String contextPath,
174                                   Class spFactory,
175                                   ClassLoader classLoader,
176                                   Map properties) throws JAXBException {
177
178        try {
179            /*
180             * javax.xml.bind.context.factory points to a class which has a
181             * static method called 'createContext' that
182             * returns a javax.xml.JAXBContext.
183             */
184
185            Object context = null;
186
187            // first check the method that takes Map as the third parameter.
188            // this is added in 2.0.
189            try {
190                Method m = spFactory.getMethod("createContext", String.class, ClassLoader.class, Map.class);
191                // any failure in invoking this method would be considered fatal
192                context = m.invoke(null, contextPath, classLoader, properties);
193            } catch (NoSuchMethodException e) {
194                // it's not an error for the provider not to have this method.
195            }
196
197            if (context == null) {
198                // try the old method that doesn't take properties. compatible with 1.0.
199                // it is an error for an implementation not to have both forms of the createContext method.
200                Method m = spFactory.getMethod("createContext", String.class, ClassLoader.class);
201                // any failure in invoking this method would be considered fatal
202                context = m.invoke(null, contextPath, classLoader);
203            }
204
205            if (!(context instanceof JAXBContext)) {
206                // the cast would fail, so generate an exception with a nice message
207                throw handleClassCastException(context.getClass(), JAXBContext.class);
208            }
209            return (JAXBContext) context;
210        } catch (InvocationTargetException x) {
211            handleInvocationTargetException(x);
212            // for other exceptions, wrap the internal target exception
213            // with a JAXBException
214            Throwable e = x;
215            if (x.getTargetException() != null)
216                e = x.getTargetException();
217
218            throw new JAXBException(Messages.format(Messages.COULD_NOT_INSTANTIATE, spFactory, e), e);
219        } catch (RuntimeException x) {
220            // avoid wrapping RuntimeException to JAXBException,
221            // because it indicates a bug in this code.
222            throw x;
223        } catch (Exception x) {
224            // can't catch JAXBException because the method is hidden behind
225            // reflection.  Root element collisions detected in the call to
226            // createContext() are reported as JAXBExceptions - just re-throw it
227            // some other type of exception - just wrap it
228            throw new JAXBException(Messages.format(Messages.COULD_NOT_INSTANTIATE, spFactory, x), x);
229        }
230    }
231
232    /**
233     * Create an instance of a class using the thread context ClassLoader
234     */
235    static JAXBContext newInstance(Class[] classes, Map properties, String className) throws JAXBException {
236
237        Class spi;
238        try {
239            spi = ServiceLoaderUtil.safeLoadClass(className, PLATFORM_DEFAULT_FACTORY_CLASS, getContextClassLoader());
240        } catch (ClassNotFoundException e) {
241            throw new JAXBException(e);
242        }
243
244        if (logger.isLoggable(Level.FINE)) {
245            // extra check to avoid costly which operation if not logged
246            logger.log(Level.FINE, "loaded {0} from {1}", new Object[]{className, which(spi)});
247        }
248
249        return newInstance(classes, properties, spi);
250    }
251
252    static JAXBContext newInstance(Class[] classes,
253                                   Map properties,
254                                   Class spFactory) throws JAXBException {
255        try {
256
257            Method m = spFactory.getMethod("createContext", Class[].class, Map.class);
258            Object context = m.invoke(null, classes, properties);
259            if (!(context instanceof JAXBContext)) {
260                // the cast would fail, so generate an exception with a nice message
261                throw handleClassCastException(context.getClass(), JAXBContext.class);
262            }
263            return (JAXBContext) context;
264
265        } catch (NoSuchMethodException | IllegalAccessException e) {
266            throw new JAXBException(e);
267
268        } catch (InvocationTargetException e) {
269            handleInvocationTargetException(e);
270
271            Throwable x = e;
272            if (e.getTargetException() != null)
273                x = e.getTargetException();
274
275            throw new JAXBException(x);
276        }
277    }
278
279    static JAXBContext find(String factoryId,
280                            String contextPath,
281                            ClassLoader classLoader,
282                            Map properties) throws JAXBException {
283
284        StringTokenizer packages = new StringTokenizer(contextPath, ":");
285        if (!packages.hasMoreTokens()) {
286            // no context is specified
287            throw new JAXBException(Messages.format(Messages.NO_PACKAGE_IN_CONTEXTPATH));
288        }
289
290        // search for jaxb.properties in the class loader of each class first
291        logger.fine("Searching jaxb.properties");
292        while (packages.hasMoreTokens()) {
293            // com.acme.foo - > com/acme/foo/jaxb.properties
294            String factoryClassName =
295                    classNameFromPackageProperties(
296                        classLoader,
297                        packages.nextToken(":").replace('.', '/'),
298                        factoryId,
299                        JAXB_CONTEXT_FACTORY_DEPRECATED);
300
301            if (factoryClassName != null) {
302                return newInstance(contextPath, factoryClassName, classLoader, properties);
303            }
304        }
305
306        String factoryName = classNameFromSystemProperties();
307        if (factoryName != null) return newInstance(contextPath, factoryName, classLoader, properties);
308
309        JAXBContextFactory obj = ServiceLoaderUtil.firstByServiceLoader(
310                JAXBContextFactory.class, logger, EXCEPTION_HANDLER);
311
312        if (obj != null) return obj.createContext(contextPath, classLoader, properties);
313
314        // to ensure backwards compatibility
315        factoryName = firstByServiceLoaderDeprecated(JAXBContext.class, classLoader);
316        if (factoryName != null) return newInstance(contextPath, factoryName, classLoader, properties);
317
318        Class ctxFactory = (Class) ServiceLoaderUtil.lookupUsingOSGiServiceLoader(
319                "javax.xml.bind.JAXBContext", logger);
320
321        if (ctxFactory != null) {
322            return newInstance(contextPath, ctxFactory, classLoader, properties);
323        }
324
325        // else no provider found
326        logger.fine("Trying to create the platform default provider");
327        return newInstance(contextPath, PLATFORM_DEFAULT_FACTORY_CLASS, classLoader, properties);
328    }
329
330    static JAXBContext find(Class<?>[] classes, Map<String, ?> properties) throws JAXBException {
331
332        // search for jaxb.properties in the class loader of each class first
333        logger.fine("Searching jaxb.properties");
334        for (final Class c : classes) {
335            // this classloader is used only to load jaxb.properties, so doing this should be safe.
336            // this is possible for primitives, arrays, and classes that are
337            // loaded by poorly implemented ClassLoaders
338            if (c.getPackage() == null) continue;
339
340            // TODO: do we want to optimize away searching the same package?  org.Foo, org.Bar, com.Baz
341            // classes from the same package might come from different class loades, so it might be a bad idea
342            // TODO: it's easier to look things up from the class
343            // c.getResourceAsStream("jaxb.properties");
344
345            String factoryClassName =
346                    classNameFromPackageProperties(
347                            getClassClassLoader(c),
348                            c.getPackage().getName().replace('.', '/'),
349                            JAXBContext.JAXB_CONTEXT_FACTORY, JAXB_CONTEXT_FACTORY_DEPRECATED);
350
351            if (factoryClassName != null) return newInstance(classes, properties, factoryClassName);
352        }
353
354        String factoryClassName = classNameFromSystemProperties();
355        if (factoryClassName != null) return newInstance(classes, properties, factoryClassName);
356
357        JAXBContextFactory factory =
358                ServiceLoaderUtil.firstByServiceLoader(JAXBContextFactory.class, logger, EXCEPTION_HANDLER);
359
360        if (factory != null) return factory.createContext(classes, properties);
361
362        // to ensure backwards compatibility
363        String className = firstByServiceLoaderDeprecated(JAXBContext.class, getContextClassLoader());
364        if (className != null) return newInstance(classes, properties, className);
365
366        logger.fine("Trying to create the platform default provider");
367        Class ctxFactoryClass =
368                (Class) ServiceLoaderUtil.lookupUsingOSGiServiceLoader("javax.xml.bind.JAXBContext", logger);
369
370        if (ctxFactoryClass != null) {
371            return newInstance(classes, properties, ctxFactoryClass);
372        }
373
374        // else no provider found
375        logger.fine("Trying to create the platform default provider");
376        return newInstance(classes, properties, PLATFORM_DEFAULT_FACTORY_CLASS);
377    }
378
379
380    /**
381     * first factoryId should be the preffered one,
382     * more of those can be provided to support backwards compatibility
383     */
384    private static String classNameFromPackageProperties(ClassLoader classLoader,
385                                                         String packageName,
386                                                         String ... factoryIds) throws JAXBException {
387
388        String resourceName = packageName + "/jaxb.properties";
389        logger.log(Level.FINE, "Trying to locate {0}", resourceName);
390        Properties props = loadJAXBProperties(classLoader, resourceName);
391        if (props != null) {
392            for(String factoryId : factoryIds) {
393                if (props.containsKey(factoryId)) {
394                    return props.getProperty(factoryId);
395                }
396            }
397            throw new JAXBException(Messages.format(Messages.MISSING_PROPERTY, packageName, factoryIds[0]));
398        }
399        return null;
400    }
401
402    private static String classNameFromSystemProperties() throws JAXBException {
403
404        String factoryClassName = getSystemProperty(JAXBContext.JAXB_CONTEXT_FACTORY);
405        if (factoryClassName != null) {
406            return factoryClassName;
407        }
408        // leave this here to assure compatibility
409        factoryClassName = getDeprecatedSystemProperty(JAXB_CONTEXT_FACTORY_DEPRECATED);
410        if (factoryClassName != null) {
411            return factoryClassName;
412        }
413        // leave this here to assure compatibility
414        factoryClassName = getDeprecatedSystemProperty(JAXBContext.class.getName());
415        if (factoryClassName != null) {
416            return factoryClassName;
417        }
418        return null;
419    }
420
421    private static String getDeprecatedSystemProperty(String property) {
422        String value = getSystemProperty(property);
423        if (value != null) {
424            logger.log(Level.WARNING, "Using non-standard property: {0}. Property {1} should be used instead.",
425                    new Object[] {property, JAXBContext.JAXB_CONTEXT_FACTORY});
426        }
427        return value;
428    }
429
430    private static String getSystemProperty(String property) {
431        logger.log(Level.FINE, "Checking system property {0}", property);
432        String value = AccessController.doPrivileged(new GetPropertyAction(property));
433        if (value != null) {
434            logger.log(Level.FINE, "  found {0}", value);
435        } else {
436            logger.log(Level.FINE, "  not found");
437        }
438        return value;
439    }
440
441    private static Properties loadJAXBProperties(ClassLoader classLoader,
442                                                 String propFileName) throws JAXBException {
443
444        Properties props = null;
445        try {
446            URL url;
447            if (classLoader == null)
448                url = ClassLoader.getSystemResource(propFileName);
449            else
450                url = classLoader.getResource(propFileName);
451
452            if (url != null) {
453                logger.log(Level.FINE, "loading props from {0}", url);
454                props = new Properties();
455                InputStream is = url.openStream();
456                props.load(is);
457                is.close();
458            }
459        } catch (IOException ioe) {
460            logger.log(Level.FINE, "Unable to load " + propFileName, ioe);
461            throw new JAXBException(ioe.toString(), ioe);
462        }
463
464        return props;
465    }
466
467
468    /**
469     * Search the given ClassLoader for an instance of the specified class and
470     * return a string representation of the URL that points to the resource.
471     *
472     * @param clazz
473     *          The class to search for
474     * @param loader
475     *          The ClassLoader to search.  If this parameter is null, then the
476     *          system class loader will be searched
477     * @return
478     *          the URL for the class or null if it wasn't found
479     */
480    static URL which(Class clazz, ClassLoader loader) {
481
482        String classnameAsResource = clazz.getName().replace('.', '/') + ".class";
483
484        if (loader == null) {
485            loader = getSystemClassLoader();
486        }
487
488        return loader.getResource(classnameAsResource);
489    }
490
491    /**
492     * Get the URL for the Class from it's ClassLoader.
493     *
494     * Convenience method for {@link #which(Class, ClassLoader)}.
495     *
496     * Equivalent to calling: which(clazz, clazz.getClassLoader())
497     *
498     * @param clazz
499     *          The class to search for
500     * @return
501     *          the URL for the class or null if it wasn't found
502     */
503    static URL which(Class clazz) {
504        return which(clazz, getClassClassLoader(clazz));
505    }
506
507    @SuppressWarnings("unchecked")
508    private static ClassLoader getContextClassLoader() {
509        if (System.getSecurityManager() == null) {
510            return Thread.currentThread().getContextClassLoader();
511        } else {
512            return (ClassLoader) java.security.AccessController.doPrivileged(
513                    new java.security.PrivilegedAction() {
514                        public java.lang.Object run() {
515                            return Thread.currentThread().getContextClassLoader();
516                        }
517                    });
518        }
519    }
520
521    @SuppressWarnings("unchecked")
522    private static ClassLoader getClassClassLoader(final Class c) {
523        if (System.getSecurityManager() == null) {
524            return c.getClassLoader();
525        } else {
526            return (ClassLoader) java.security.AccessController.doPrivileged(
527                    new java.security.PrivilegedAction() {
528                        public java.lang.Object run() {
529                            return c.getClassLoader();
530                        }
531                    });
532        }
533    }
534
535    private static ClassLoader getSystemClassLoader() {
536        if (System.getSecurityManager() == null) {
537            return ClassLoader.getSystemClassLoader();
538        } else {
539            return (ClassLoader) java.security.AccessController.doPrivileged(
540                    new java.security.PrivilegedAction() {
541                        public java.lang.Object run() {
542                            return ClassLoader.getSystemClassLoader();
543                        }
544                    });
545        }
546    }
547
548    // ServiceLoaderUtil.firstByServiceLoaderDeprecated should be used instead.
549    @Deprecated
550    static String firstByServiceLoaderDeprecated(Class spiClass,
551                                                 ClassLoader classLoader) throws JAXBException {
552
553        final String jaxbContextFQCN = spiClass.getName();
554
555        logger.fine("Searching META-INF/services");
556
557        // search META-INF services next
558        BufferedReader r = null;
559        final String resource = "META-INF/services/" + jaxbContextFQCN;
560        try {
561            final InputStream resourceStream =
562                    (classLoader == null) ?
563                            ClassLoader.getSystemResourceAsStream(resource) :
564                            classLoader.getResourceAsStream(resource);
565
566            if (resourceStream != null) {
567                r = new BufferedReader(new InputStreamReader(resourceStream, "UTF-8"));
568                String factoryClassName = r.readLine();
569                if (factoryClassName != null) {
570                    factoryClassName = factoryClassName.trim();
571                }
572                r.close();
573                logger.log(Level.FINE, "Configured factorty class:{0}", factoryClassName);
574                return factoryClassName;
575            } else {
576                logger.log(Level.FINE, "Unable to load:{0}", resource);
577                return null;
578            }
579        } catch (IOException e) {
580            throw new JAXBException(e);
581        } finally {
582            try {
583                if (r != null) {
584                    r.close();
585                }
586            } catch (IOException ex) {
587                logger.log(Level.SEVERE, "Unable to close resource: " + resource, ex);
588            }
589        }
590    }
591
592}
593