/* * 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.tuscany.sca.databinding.jaxb; import java.awt.Image; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.lang.ref.SoftReference; import java.net.URI; import java.security.AccessController; import java.security.PrivilegedAction; import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import javax.activation.DataHandler; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; import javax.xml.bind.Unmarshaller; import javax.xml.bind.annotation.XmlEnum; import javax.xml.bind.annotation.XmlRootElement; import javax.xml.bind.annotation.XmlSeeAlso; import javax.xml.bind.annotation.XmlTransient; import javax.xml.bind.annotation.XmlType; import javax.xml.datatype.DatatypeFactory; import javax.xml.transform.Source; import org.apache.tuscany.sca.common.java.collection.LRUCache; import org.apache.tuscany.sca.core.ExtensionPointRegistry; import org.apache.tuscany.sca.extensibility.ClassLoaderContext; import org.oasisopen.sca.ServiceRuntimeException; /** * @version $Rev$ $Date$ */ public class JAXBContextCache { private static final int CACHE_SIZE = 128; private static HashMap> loadClassMap = new HashMap>(); static { loadClassMap.put("byte", byte.class); loadClassMap.put("int", int.class); loadClassMap.put("short", short.class); loadClassMap.put("long", long.class); loadClassMap.put("float", float.class); loadClassMap.put("double", double.class); loadClassMap.put("boolean", boolean.class); loadClassMap.put("char", char.class); loadClassMap.put("void", void.class); } protected static Class[] JAXB_BUILTIN_CLASSES = {byte[].class, boolean.class, byte.class, char.class, double.class, float.class, int.class, long.class, short.class, void.class, java.awt.Image.class, java.io.File.class, java.lang.Boolean.class, java.lang.Byte.class, java.lang.Character.class, java.lang.Class.class, java.lang.Double.class, java.lang.Float.class, java.lang.Integer.class, java.lang.Long.class, java.lang.Object.class, java.lang.Short.class, java.lang.String.class, java.lang.Void.class, java.math.BigDecimal.class, java.math.BigInteger.class, java.net.URI.class, java.net.URL.class, java.util.Calendar.class, java.util.Date.class, java.util.GregorianCalendar.class, java.util.UUID.class, javax.activation.DataHandler.class, javax.xml.bind.JAXBElement.class, javax.xml.datatype.Duration.class, javax.xml.datatype.XMLGregorianCalendar.class, javax.xml.namespace.QName.class, javax.xml.transform.Source.class}; protected static final Set> BUILTIN_CLASSES_SET = new HashSet>(Arrays.asList(JAXB_BUILTIN_CLASSES)); /* protected static Class[] COMMON_ARRAY_CLASSES = new Class[] {char[].class, short[].class, int[].class, long[].class, float[].class, double[].class, String[].class }; protected static final Set> COMMON_CLASSES_SET = new HashSet>(Arrays.asList(COMMON_ARRAY_CLASSES)); */ protected LRUCache cache; protected Pool mpool; protected Pool upool; // protected JAXBContext commonContext; protected JAXBContext defaultContext; private ExtensionPointRegistry registry; public JAXBContextCache(ExtensionPointRegistry registry) { this(CACHE_SIZE, CACHE_SIZE, CACHE_SIZE, registry); } public JAXBContextCache(int contextSize, int marshallerSize, int unmarshallerSize, ExtensionPointRegistry registry) { this.registry = registry; cache = new LRUCache(contextSize); mpool = new Pool(); upool = new Pool(); defaultContext = getDefaultJAXBContext(); } private JAXBContext newJAXBContext(final Class... classesToBeBound) throws JAXBException { try { return AccessController.doPrivileged(new PrivilegedExceptionAction() { public JAXBContext run() throws JAXBException { // Try to set up TCCL so that JAXBContext service discovery works in OSGi ClassLoader tccl = ClassLoaderContext.setContextClassLoader(JAXBContextCache.class.getClassLoader(), registry.getServiceDiscovery(), // The service provider of JAXBContext doesn't extend JAXBContext // We should use the service name instead of the class JAXBContext.class.getName(), DatatypeFactory.class.getName()); try { JAXBContext context = JAXBContext.newInstance(classesToBeBound); return context; } finally { if (tccl != null) { Thread.currentThread().setContextClassLoader(tccl); } } } }); } catch (PrivilegedActionException e) { throw (JAXBException)e.getException(); } } public JAXBContext getDefaultJAXBContext() { try { return newJAXBContext(); } catch (JAXBException e) { throw new IllegalArgumentException(e); } } /** * @param name of primitive type * @return primitive Class or null */ public static Class getPrimitiveClass(String text) { return loadClassMap.get(text); } /** * Return the class for this name * * @return Class */ private static Class forName(final String className, final boolean initialize, final ClassLoader classloader) throws ClassNotFoundException { // NOTE: This method must remain private because it uses AccessController Class cl = null; try { cl = AccessController.doPrivileged(new PrivilegedExceptionAction>() { public Class run() throws ClassNotFoundException { // Class.forName does not support primitives Class cls = getPrimitiveClass(className); if (cls == null) { cls = Class.forName(className, initialize, classloader); } return cls; } }); } catch (PrivilegedActionException e) { throw (ClassNotFoundException)e.getException(); } return cl; } public Marshaller getMarshaller(JAXBContext context) throws JAXBException { Marshaller marshaller = mpool.get(context); if (marshaller == null) { marshaller = context.createMarshaller(); } marshaller.setProperty(Marshaller.JAXB_ENCODING, "UTF-8"); marshaller.setProperty(Marshaller.JAXB_FRAGMENT, Boolean.TRUE); return marshaller; } public void releaseJAXBMarshaller(JAXBContext context, Marshaller marshaller) { if (marshaller != null) { marshaller.setAttachmentMarshaller(null); mpool.put(context, marshaller); // No point unsetting marshaller's JAXB_FRAGMENT property, since we'll just reset it when // doing the next get. } } public Unmarshaller getUnmarshaller(JAXBContext context) throws JAXBException { Unmarshaller unmarshaller = upool.get(context); if (unmarshaller == null) { unmarshaller = context.createUnmarshaller(); } return unmarshaller; } public void releaseJAXBUnmarshaller(JAXBContext context, Unmarshaller unmarshaller) { if (unmarshaller != null) { unmarshaller.setAttachmentUnmarshaller(null); upool.put(context, unmarshaller); } } public LRUCache getCache() { return cache; } public JAXBContext getJAXBContext(Class cls) throws JAXBException { if (BUILTIN_CLASSES_SET.contains(cls)) { return defaultContext; } return getJAXBContext(new Class[] {cls}); } public JAXBContext getJAXBContext(Class[] classes) throws JAXBException { Set> classSet = new HashSet>(Arrays.asList(classes)); return getJAXBContext(classSet); } public JAXBContext getJAXBContext(Set> classes) throws JAXBException { // Remove the JAXB built-in types to maximize the cache hit Set> classSet = new HashSet>(classes); classSet.removeAll(BUILTIN_CLASSES_SET); // FIXME: [rfeng] Remove java classes that are mapped to the same XSD type to avoid // conflicts if (classSet.contains(Date[].class)) { classSet.remove(Calendar[].class); } if (classSet.contains(URI[].class)) { classSet.remove(UUID[].class); } if (classSet.contains(Source[].class)) { classSet.remove(Image[].class); classSet.remove(DataHandler[].class); } classSet = getJAXBClasses(classSet); if(classSet.isEmpty()) { return defaultContext; } synchronized (cache) { JAXBContext context = cache.get(classSet); if (context != null) { return context; } context = newJAXBContext(classSet.toArray(new Class[classSet.size()])); cache.put(classSet, context); return context; } } public void clear() { synchronized (cache) { cache.clear(); } /* synchronized (upool) { upool.clear(); } synchronized (upool) { upool.clear(); } */ } // // This inner class is copied in its entirety from the Axis2 utility class, // org.apache.axis2.jaxws.message.databinding.JAXBUtils. We could look into extending but it's such a basic data structure // without other dependencies so we might be better off copying it and avoiding a new // Axis2 dependency here. // /** * Pool a list of items for a specific key * * @param Key * @param Pooled object */ private static class Pool { private SoftReference>> softMap = new SoftReference>>( new ConcurrentHashMap>()); // The maps are freed up when a LOAD FACTOR is hit private static final int MAX_LIST_FACTOR = 50; private static final int MAX_LOAD_FACTOR = 32; // Maximum number of JAXBContext to store /** * @param key * @return removed item from pool or null. */ public V get(K key) { List values = getValues(key); synchronized (values) { if (values.size()>0) { V v = values.remove(values.size()-1); return v; } } return null; } /** * Add item back to pool * @param key * @param value */ public void put(K key, V value) { adjustSize(); List values = getValues(key); synchronized (values) { if (values.size() < MAX_LIST_FACTOR) { values.add(value); } } } /** * Get or create a list of the values for the key * @param key * @return list of values. */ private List getValues(K key) { Map> map = softMap.get(); List values = null; if (map != null) { values = map.get(key); if(values !=null) { return values; } } synchronized (this) { if (map != null) { values = map.get(key); } if (values == null) { if (map == null) { map = new ConcurrentHashMap>(); softMap = new SoftReference>>(map); } values = new ArrayList(); map.put(key, values); } return values; } } /** * AdjustSize * When the number of keys exceeds the maximum load, half * of the entries are deleted. * * The assumption is that the JAXBContexts, UnMarshallers, Marshallers, etc. require * a large footprint. */ private void adjustSize() { Map> map = softMap.get(); if (map != null && map.size() > MAX_LOAD_FACTOR) { // Remove every other Entry in the map. Iterator it = map.entrySet().iterator(); boolean removeIt = false; while (it.hasNext()) { it.next(); if (removeIt) { it.remove(); } removeIt = !removeIt; } } } public void removeCtx(K key){ Map> map = softMap.get(); if (map !=null && key !=null){ map.remove(key); } } } /** * Find the JAXB classes (looking into packages) to be bound * @param classes A collection of classes * @return A set of classes that include the ObjectFactory and indexed JAXB classes * @throws JAXBException */ private static Set> getJAXBClasses(Collection> classes) throws JAXBException { Set> classSet = new HashSet>(); // Index the packages Map pkgs = getPackages(classes); Set nonJAXBPackages = new HashSet(); for (Map.Entry p : pkgs.entrySet()) { Package pkg = p.getKey(); if (pkg == null) { continue; } Set> set = getJAXBClasses(pkg.getName(), p.getValue()); if (set.isEmpty()) { // No JAXB package nonJAXBPackages.add(pkg); } else { // Add JAXB ObjectFactory and indexed classes classSet.addAll(set); } } // Adding classes that are not part of JAXB packages for (Class cls : classes) { Package pkg = getPackage(cls); if (pkg == null || nonJAXBPackages.contains(pkg)) { classSet.add(cls); } else { // TUSCANY-3162: Test if a class is generated by JAXB // There might be the case that non-JAXB classes are in the same package as the JAXB classes if (!cls.isAnnotationPresent(XmlType.class) && !cls.isAnnotationPresent(XmlEnum.class) && !cls.isAnnotationPresent(XmlSeeAlso.class) && !cls.isAnnotationPresent(XmlRootElement.class) && !cls.isAnnotationPresent(XmlTransient.class)) { classSet.add(cls); } } } return classSet; } /** * Get the package for a class, taking array into account * @param cls * @return */ private static Package getPackage(Class cls) { Class type = cls; while (type.isArray()) { type = type.getComponentType(); } return type.getPackage(); } /** * Get a map of packages * @param classes * @return */ private static Map getPackages(Collection> classes) { Map pkgs = new HashMap(); for (Class cls : classes) { Package pkg = getPackage(cls); if (pkg != null) { final Class fcls = cls; ClassLoader cl = AccessController.doPrivileged(new PrivilegedAction() { public ClassLoader run() { ClassLoader cl = fcls.getClassLoader(); return cl; } }); pkgs.put(pkg, cl); } } return pkgs; } /** * Find ObjectFactory and indexed JAXB classes for the package * @param pkg * @param classLoader * @return * @throws JAXBException */ private static Set> getJAXBClasses(String pkg, ClassLoader classLoader) throws JAXBException { Set> classes = new HashSet>(); List> indexedClasses; // look for ObjectFactory and load it final Class o; try { o = forName(pkg + ".ObjectFactory", false, classLoader); classes.add(o); } catch (ClassNotFoundException e) { // not necessarily an error } // look for jaxb.index and load the list of classes try { indexedClasses = loadIndexedClasses(pkg, classLoader); } catch (IOException e) { throw new JAXBException(e); } if (indexedClasses != null) { classes.addAll(indexedClasses); } return classes; } /** * Look for jaxb.index file in the specified package and load it's contents * * @param pkg package name to search in * @param classLoader ClassLoader to search in * @return a List of Class objects to load, null if there weren't any * @throws IOException if there is an error reading the index file * @throws JAXBException if there are any errors in the index file */ private static List> loadIndexedClasses(String pkg, ClassLoader classLoader) throws IOException, JAXBException { if (classLoader == null) { return null; } final String resource = pkg.replace('.', '/') + "/jaxb.index"; final InputStream resourceAsStream = classLoader.getResourceAsStream(resource); if (resourceAsStream == null) { return null; } BufferedReader in = new BufferedReader(new InputStreamReader(resourceAsStream, "UTF-8")); try { List> classes = new ArrayList>(); String className = in.readLine(); while (className != null) { className = className.trim(); if (className.startsWith("#") || (className.length() == 0)) { className = in.readLine(); continue; } try { classes.add(forName(pkg + '.' + className, false, classLoader)); } catch (ClassNotFoundException e) { throw new JAXBException(e); } className = in.readLine(); } return classes; } finally { in.close(); } } public void removeJAXBContextFromPools(JAXBContext ctx){ if (mpool != null && ctx != null){ mpool.removeCtx(ctx); } if (upool != null && ctx !=null){ upool.removeCtx(ctx); } } /** * Removes all the cached information relating to a contribution. The * contribution is identified by the contribution classloader passed in * as a parameter. This is used when a contribution is removed from * the runtime. * * @param contributionClassloader */ public void removeJAXBContextForContribution(ClassLoader contributionClassloader){ if (cache != null){ try { synchronized(cache) { Set objSet = cache.keySet(); List toRemove = new ArrayList(); Iterator i = objSet.iterator(); while(i.hasNext()) { Object obj = i.next(); if (obj instanceof Set){ Set innerSet = (Set)obj; Iterator j = innerSet.iterator(); loop: while(j.hasNext()) { Class cls = j.next(); ClassLoader cl = cls.getClassLoader(); while (cl != null){ if (cl == contributionClassloader){ toRemove.add(obj); break loop; } // take account of generated classes cl = cl.getParent(); } } } } for (Object obj : toRemove){ JAXBContext ctx = cache.get(obj); removeJAXBContextFromPools(ctx); cache.remove(obj); } } } catch(Exception e) { throw new ServiceRuntimeException(e); } } } }