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