/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.cxf.common.jaxb; import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; import java.lang.ref.WeakReference; import java.lang.reflect.Method; import java.nio.charset.StandardCharsets; import java.security.AccessController; import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.regex.Pattern; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.annotation.XmlElementDecl; import javax.xml.transform.dom.DOMSource; import org.apache.cxf.common.classloader.ClassLoaderUtils; import org.apache.cxf.common.util.CacheMap; import org.apache.cxf.common.util.CachedClass; import org.apache.cxf.common.util.StringUtils; /** * */ public final class JAXBContextCache { /** * Return holder of the context, classes, etc... * Do NOT hold onto these strongly as that can lock the JAXBContext and Set<Class> objects * into memory. It preferred to grab the context and classes (if needed) from this object * immediately after the call to getCachedContextAndSchemas and then discard it. The * main purpose of this class is to hold onto the context/set strongly until the caller * has a chance to copy those into a place where they can hold onto it strongly as * needed. */ public static final class CachedContextAndSchemas { private final JAXBContext context; private final Set<Class<?>> classes; private final WeakReference<CachedContextAndSchemasInternal> ccas; private CachedContextAndSchemas(JAXBContext context, Set<Class<?>> classes, CachedContextAndSchemasInternal i) { this.context = context; this.classes = classes; ccas = new WeakReference<CachedContextAndSchemasInternal>(i); } public JAXBContext getContext() { return context; } public Set<Class<?>> getClasses() { return classes; } public Collection<DOMSource> getSchemas() { CachedContextAndSchemasInternal i = ccas.get(); if (i != null) { return i.getSchemas(); } return null; } public void setSchemas(Collection<DOMSource> schemas) { CachedContextAndSchemasInternal i = ccas.get(); if (i != null) { i.setSchemas(schemas); } } } private static final class CachedContextAndSchemasInternal { private final WeakReference<JAXBContext> context; private final WeakReference<Set<Class<?>>> classes; private Collection<DOMSource> schemas; CachedContextAndSchemasInternal(JAXBContext context, Set<Class<?>> classes) { this.context = new WeakReference<JAXBContext>(context); this.classes = new WeakReference<Set<Class<?>>>(classes); } public JAXBContext getContext() { return context.get(); } public Set<Class<?>> getClasses() { return classes.get(); } public Collection<DOMSource> getSchemas() { return schemas; } public void setSchemas(Collection<DOMSource> schemas) { this.schemas = schemas; } } private static final Map<Set<Class<?>>, Map<String, CachedContextAndSchemasInternal>> JAXBCONTEXT_CACHE = new CacheMap<Set<Class<?>>, Map<String, CachedContextAndSchemasInternal>>(); private static final Map<Package, CachedClass> OBJECT_FACTORY_CACHE = new CacheMap<Package, CachedClass>(); private static final boolean HAS_MOXY; static { boolean b = false; try { JAXBContext ctx = JAXBContext.newInstance(String.class); b = ctx.getClass().getName().contains(".eclipse"); } catch (Throwable t) { //ignore } HAS_MOXY = b; } private JAXBContextCache() { //utility class } /** * Clear any caches to make sure new contexts are created */ public static void clearCaches() { synchronized (JAXBCONTEXT_CACHE) { JAXBCONTEXT_CACHE.clear(); } synchronized (OBJECT_FACTORY_CACHE) { OBJECT_FACTORY_CACHE.clear(); } } public static void scanPackages(Set<Class<?>> classes) { JAXBUtils.scanPackages(classes, OBJECT_FACTORY_CACHE); } public static CachedContextAndSchemas getCachedContextAndSchemas(Class<?> ... cls) throws JAXBException { Set<Class<?>> classes = new HashSet<Class<?>>(); for (Class<?> c : cls) { classes.add(c); } scanPackages(classes); return JAXBContextCache.getCachedContextAndSchemas(classes, null, null, null, false); } public static CachedContextAndSchemas getCachedContextAndSchemas(String pkg, Map<String, Object> props, ClassLoader loader) throws JAXBException { Set<Class<?>> classes = new HashSet<Class<?>>(); addPackage(classes, pkg, loader); return getCachedContextAndSchemas(classes, null, props, null, true); } public static CachedContextAndSchemas getCachedContextAndSchemas(final Set<Class<?>> classes, String defaultNs, Map<String, Object> props, Collection<Object> typeRefs, boolean exact) throws JAXBException { for (Class<?> clz : classes) { if (clz.getName().endsWith("ObjectFactory") && checkObjectFactoryNamespaces(clz)) { // kind of a hack, but ObjectFactories may be created with empty // namespaces defaultNs = null; } } Map<String, Object> map = new HashMap<>(); if (defaultNs != null) { if (HAS_MOXY) { map.put("eclipselink.default-target-namespace", defaultNs); } map.put("com.sun.xml.bind.defaultNamespaceRemap", defaultNs); } if (props != null) { map.putAll(props); } CachedContextAndSchemasInternal cachedContextAndSchemasInternal = null; JAXBContext context = null; Map<String, CachedContextAndSchemasInternal> cachedContextAndSchemasInternalMap = null; if (typeRefs == null || typeRefs.isEmpty()) { synchronized (JAXBCONTEXT_CACHE) { if (exact) { cachedContextAndSchemasInternalMap = JAXBCONTEXT_CACHE.get(classes); if (cachedContextAndSchemasInternalMap != null && defaultNs != null) { cachedContextAndSchemasInternal = cachedContextAndSchemasInternalMap.get(defaultNs); } } else { for (Entry<Set<Class<?>>, Map<String, CachedContextAndSchemasInternal>> k : JAXBCONTEXT_CACHE.entrySet()) { Set<Class<?>> key = k.getKey(); if (key != null && key.containsAll(classes)) { cachedContextAndSchemasInternalMap = k.getValue(); if (defaultNs != null) { cachedContextAndSchemasInternal = cachedContextAndSchemasInternalMap.get(defaultNs); } else { cachedContextAndSchemasInternal = cachedContextAndSchemasInternalMap.get(""); } break; } } } if (cachedContextAndSchemasInternal != null) { context = cachedContextAndSchemasInternal.getContext(); if (context == null) { final Set<Class<?>> cls = cachedContextAndSchemasInternal.getClasses(); if (cls != null) { JAXBCONTEXT_CACHE.remove(cls); } cachedContextAndSchemasInternal = null; } else { return new CachedContextAndSchemas(context, cachedContextAndSchemasInternal.getClasses(), cachedContextAndSchemasInternal); } } } } try { context = createContext(classes, map, typeRefs); } catch (JAXBException ex) { // load jaxb needed class and try to create jaxb context boolean added = addJaxbObjectFactory(ex, classes); if (added) { try { context = AccessController.doPrivileged(new PrivilegedExceptionAction<JAXBContext>() { public JAXBContext run() throws Exception { return JAXBContext.newInstance(classes .toArray(new Class[classes.size()]), null); } }); } catch (PrivilegedActionException e) { throw ex; } } if (context == null) { throw ex; } } cachedContextAndSchemasInternal = new CachedContextAndSchemasInternal(context, classes); synchronized (JAXBCONTEXT_CACHE) { if (typeRefs == null || typeRefs.isEmpty()) { if (cachedContextAndSchemasInternalMap == null) { cachedContextAndSchemasInternalMap = new CacheMap<String, CachedContextAndSchemasInternal>(); } cachedContextAndSchemasInternalMap.put((defaultNs != null) ? defaultNs : "", cachedContextAndSchemasInternal); JAXBCONTEXT_CACHE.put(classes, cachedContextAndSchemasInternalMap); } } return new CachedContextAndSchemas(context, classes, cachedContextAndSchemasInternal); } private static boolean checkObjectFactoryNamespaces(Class<?> clz) { for (Method meth : clz.getMethods()) { XmlElementDecl decl = meth.getAnnotation(XmlElementDecl.class); if (decl != null && XmlElementDecl.GLOBAL.class.equals(decl.scope()) && StringUtils.isEmpty(decl.namespace())) { return true; } } return false; } private static JAXBContext createContext(final Set<Class<?>> classes, final Map<String, Object> map, Collection<Object> typeRefs) throws JAXBException { JAXBContext ctx; if (typeRefs != null && !typeRefs.isEmpty()) { Class<?> fact = null; String pfx = "com.sun.xml.bind."; try { fact = ClassLoaderUtils.loadClass("com.sun.xml.bind.v2.ContextFactory", JAXBContextCache.class); } catch (Throwable t) { try { fact = ClassLoaderUtils.loadClass("com.sun.xml.internal.bind.v2.ContextFactory", JAXBContextCache.class); pfx = "com.sun.xml.internal.bind."; } catch (Throwable t2) { //ignore } } if (fact != null) { for (Method m : fact.getMethods()) { if ("createContext".equals(m.getName()) && m.getParameterTypes().length == 9) { try { return (JAXBContext)m.invoke(null, classes.toArray(new Class[classes.size()]), typeRefs, map.get(pfx + "subclassReplacements"), map.get(pfx + "defaultNamespaceRemap"), map.get(pfx + "c14n") == null ? Boolean.FALSE : map.get(pfx + "c14n"), map.get(pfx + "v2.model.annotation.RuntimeAnnotationReader"), map.get(pfx + "XmlAccessorFactory") == null ? Boolean.FALSE : map.get(pfx + "XmlAccessorFactory"), map.get(pfx + "treatEverythingNillable") == null ? Boolean.FALSE : map.get(pfx + "treatEverythingNillable"), map.get("retainReferenceToInfo") == null ? Boolean.FALSE : map.get("retainReferenceToInfo")); } catch (Throwable e) { //ignore } } } } } try { ctx = AccessController.doPrivileged(new PrivilegedExceptionAction<JAXBContext>() { public JAXBContext run() throws Exception { return JAXBContext.newInstance(classes.toArray(new Class[classes.size()]), map); } }); } catch (PrivilegedActionException e2) { if (e2.getException() instanceof JAXBException) { JAXBException ex = (JAXBException)e2.getException(); if (map.containsKey("com.sun.xml.bind.defaultNamespaceRemap") && ex.getMessage() != null && ex.getMessage().contains("com.sun.xml.bind.defaultNamespaceRemap")) { map.put("com.sun.xml.internal.bind.defaultNamespaceRemap", map.remove("com.sun.xml.bind.defaultNamespaceRemap")); ctx = JAXBContext.newInstance(classes.toArray(new Class[classes.size()]), map); } else { throw ex; } } else { throw new RuntimeException(e2.getException()); } } return ctx; } // Now we can not add all the classes that Jaxb needed into JaxbContext, // especially when // an ObjectFactory is pointed to by an jaxb @XmlElementDecl annotation // added this workaround method to load the jaxb needed ObjectFactory class private static boolean addJaxbObjectFactory(JAXBException e1, Set<Class<?>> classes) { boolean added = false; java.io.ByteArrayOutputStream bout = new java.io.ByteArrayOutputStream(); java.io.PrintStream pout = new java.io.PrintStream(bout); e1.printStackTrace(pout); String str = new String(bout.toByteArray()); Pattern pattern = Pattern.compile("(?<=There's\\sno\\sObjectFactory\\swith\\san\\s" + "@XmlElementDecl\\sfor\\sthe\\selement\\s\\{)\\S*(?=\\})"); java.util.regex.Matcher matcher = pattern.matcher(str); while (matcher.find()) { String pkgName = JAXBUtils.namespaceURIToPackage(matcher.group()); try { Class<?> clz = JAXBContextCache.class.getClassLoader() .loadClass(pkgName + "." + "ObjectFactory"); if (!classes.contains(clz)) { classes.add(clz); added = true; } } catch (ClassNotFoundException e) { // do nothing } } return added; } public static void addPackage(Set<Class<?>> classes, String pkg, ClassLoader loader) { try { classes.add(Class.forName(pkg + ".ObjectFactory", false, loader)); } catch (Exception ex) { //ignore } try (InputStream ins = loader.getResourceAsStream("/" + pkg.replace('.', '/') + "/jaxb.index"); BufferedReader reader = new BufferedReader(new InputStreamReader(ins, StandardCharsets.UTF_8))) { if (!StringUtils.isEmpty(pkg)) { pkg += "."; } String line = reader.readLine(); while (line != null) { line = line.trim(); if (line.indexOf("#") != -1) { line = line.substring(0, line.indexOf("#")); } if (!StringUtils.isEmpty(line)) { try { Class<?> ncls = Class.forName(pkg + line, false, loader); classes.add(ncls); } catch (Exception e) { // ignore } } line = reader.readLine(); } } catch (Exception ex) { //ignore } } }