diff --git a/org.springframework.oxm/src/main/java/org/springframework/oxm/jaxb/ClassPathJaxb2TypeScanner.java b/org.springframework.oxm/src/main/java/org/springframework/oxm/jaxb/ClassPathJaxb2TypeScanner.java new file mode 100644 index 00000000000..c6f97d2493c --- /dev/null +++ b/org.springframework.oxm/src/main/java/org/springframework/oxm/jaxb/ClassPathJaxb2TypeScanner.java @@ -0,0 +1,120 @@ +/* + * Copyright 2002-2012 the original author or authors. + * + * Licensed 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.springframework.oxm.jaxb; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import javax.xml.bind.annotation.XmlEnum; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlSeeAlso; +import javax.xml.bind.annotation.XmlType; + +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.core.io.support.ResourcePatternResolver; +import org.springframework.core.io.support.ResourcePatternUtils; +import org.springframework.core.type.classreading.CachingMetadataReaderFactory; +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.core.type.filter.AnnotationTypeFilter; +import org.springframework.core.type.filter.TypeFilter; +import org.springframework.oxm.UncategorizedMappingException; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * Helper class for {@link Jaxb2Marshaller} that scans given packages for classes marked with JAXB2 annotations. + * + * @author Arjen Poutsma + * @author David Harrigan + * @see #scanPackages() + */ +class ClassPathJaxb2TypeScanner { + + private static final String RESOURCE_PATTERN = "/**/*.class"; + + private final TypeFilter[] jaxb2TypeFilters = + new TypeFilter[]{new AnnotationTypeFilter(XmlRootElement.class, false), + new AnnotationTypeFilter(XmlType.class, false), new AnnotationTypeFilter(XmlSeeAlso.class, false), + new AnnotationTypeFilter(XmlEnum.class, false)}; + + private final String[] packagesToScan; + + private ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver(); + + private List> jaxb2Classes = new ArrayList>(); + + /** Constructs a new {@code ClassPathJaxb2TypeScanner} for the given packages. */ + ClassPathJaxb2TypeScanner(String[] packagesToScan) { + Assert.notEmpty(packagesToScan, "'packagesToScan' must not be empty"); + this.packagesToScan = packagesToScan; + } + + void setResourceLoader(ResourceLoader resourceLoader) { + if (resourceLoader != null) { + this.resourcePatternResolver = ResourcePatternUtils.getResourcePatternResolver(resourceLoader); + } + } + + /** Returns the JAXB2 classes found in the specified packages. */ + Class[] getJaxb2Classes() { + return jaxb2Classes.toArray(new Class[jaxb2Classes.size()]); + } + + /** + * Scans the packages for classes marked with JAXB2 annotations. + * + * @throws UncategorizedMappingException in case of errors + */ + void scanPackages() throws UncategorizedMappingException { + try { + for (String packageToScan : packagesToScan) { + String pattern = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + + ClassUtils.convertClassNameToResourcePath(packageToScan) + RESOURCE_PATTERN; + Resource[] resources = resourcePatternResolver.getResources(pattern); + MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(resourcePatternResolver); + for (Resource resource : resources) { + MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(resource); + if (isJaxb2Class(metadataReader, metadataReaderFactory)) { + String className = metadataReader.getClassMetadata().getClassName(); + Class jaxb2AnnotatedClass = resourcePatternResolver.getClassLoader().loadClass(className); + jaxb2Classes.add(jaxb2AnnotatedClass); + } + } + } + } + catch (IOException ex) { + throw new UncategorizedMappingException("Failed to scan classpath for unlisted classes", ex); + } + catch (ClassNotFoundException ex) { + throw new UncategorizedMappingException("Failed to load annotated classes from classpath", ex); + } + } + + private boolean isJaxb2Class(MetadataReader reader, MetadataReaderFactory factory) throws IOException { + for (TypeFilter filter : jaxb2TypeFilters) { + if (filter.match(reader, factory)) { + return true; + } + } + return false; + } + + +} diff --git a/org.springframework.oxm/src/main/java/org/springframework/oxm/jaxb/Jaxb2Marshaller.java b/org.springframework.oxm/src/main/java/org/springframework/oxm/jaxb/Jaxb2Marshaller.java index d97b393a035..e16ba7e6e95 100644 --- a/org.springframework.oxm/src/main/java/org/springframework/oxm/jaxb/Jaxb2Marshaller.java +++ b/org.springframework.oxm/src/main/java/org/springframework/oxm/jaxb/Jaxb2Marshaller.java @@ -16,7 +16,7 @@ package org.springframework.oxm.jaxb; -import java.awt.Image; +import java.awt.*; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; @@ -75,8 +75,10 @@ import org.xml.sax.helpers.XMLReaderFactory; import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ResourceLoaderAware; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; import org.springframework.oxm.GenericMarshaller; import org.springframework.oxm.GenericUnmarshaller; import org.springframework.oxm.MarshallingFailureException; @@ -117,7 +119,7 @@ import org.springframework.util.xml.StaxUtils; */ public class Jaxb2Marshaller implements MimeMarshaller, MimeUnmarshaller, GenericMarshaller, GenericUnmarshaller, BeanClassLoaderAware, - InitializingBean { + ResourceLoaderAware, InitializingBean { private static final String CID = "cid:"; @@ -130,6 +132,8 @@ public class Jaxb2Marshaller private String contextPath; private Class[] classesToBeBound; + + private String[] packagesToScan; private Map jaxbContextProperties; @@ -153,6 +157,8 @@ public class Jaxb2Marshaller private ClassLoader beanClassLoader; + private ResourceLoader resourceLoader; + private JAXBContext jaxbContext; private Schema schema; @@ -175,6 +181,8 @@ public class Jaxb2Marshaller /** * Set a JAXB context path. + *

Setting this property, {@link #setClassesToBeBound "classesToBeBound"}, or + * {@link #setPackagesToScan "packagesToScan"} is required. */ public void setContextPath(String contextPath) { Assert.hasText(contextPath, "'contextPath' must not be null"); @@ -190,7 +198,8 @@ public class Jaxb2Marshaller /** * Set the list of Java classes to be recognized by a newly created JAXBContext. - * Setting this property or {@link #setContextPath "contextPath"} is required. + *

Setting this property, {@link #setContextPath "contextPath"}, or + * {@link #setPackagesToScan "packagesToScan"} is required. */ public void setClassesToBeBound(Class... classesToBeBound) { Assert.notEmpty(classesToBeBound, "'classesToBeBound' must not be empty"); @@ -204,6 +213,23 @@ public class Jaxb2Marshaller return this.classesToBeBound; } + /** + * Set the packages to search using Spring-based scanning for classes with JAXB2 annotations in the classpath. + *

Setting this property, {@link #setContextPath "contextPath"}, or + * {@link #setClassesToBeBound "classesToBeBound"} is required. This is analogous to Spring's component-scan feature + * ({@link org.springframework.context.annotation.ClassPathBeanDefinitionScanner}). + */ + public void setPackagesToScan(String[] packagesToScan) { + this.packagesToScan = packagesToScan; + } + + /** + * Returns the packages to search for JAXB2 annotations. + */ + public String[] getPackagesToScan() { + return packagesToScan; + } + /** * Set the JAXBContext properties. These implementation-specific * properties will be set on the underlying JAXBContext. @@ -337,13 +363,23 @@ public class Jaxb2Marshaller this.beanClassLoader = classLoader; } + public void setResourceLoader(ResourceLoader resourceLoader) { + this.resourceLoader = resourceLoader; + } public final void afterPropertiesSet() throws Exception { - if (StringUtils.hasLength(getContextPath()) && !ObjectUtils.isEmpty(getClassesToBeBound())) { - throw new IllegalArgumentException("Specify either 'contextPath' or 'classesToBeBound property'; not both"); + boolean hasContextPath = StringUtils.hasLength(getContextPath()); + boolean hasClassesToBeBound = !ObjectUtils.isEmpty(getClassesToBeBound()); + boolean hasPackagesToScan = !ObjectUtils.isEmpty(getPackagesToScan()); + + if (hasContextPath && (hasClassesToBeBound || hasPackagesToScan) || + (hasClassesToBeBound && hasPackagesToScan)) { + throw new IllegalArgumentException("Specify either 'contextPath', 'classesToBeBound', " + + "or 'packagesToScan'"); } - else if (!StringUtils.hasLength(getContextPath()) && ObjectUtils.isEmpty(getClassesToBeBound())) { - throw new IllegalArgumentException("Setting either 'contextPath' or 'classesToBeBound' is required"); + if (!hasContextPath && !hasClassesToBeBound && !hasPackagesToScan) { + throw new IllegalArgumentException( + "Setting either 'contextPath', 'classesToBeBound', " + "or 'packagesToScan' is required"); } if (!this.lazyInit) { getJaxbContext(); @@ -362,6 +398,9 @@ public class Jaxb2Marshaller else if (!ObjectUtils.isEmpty(getClassesToBeBound())) { this.jaxbContext = createJaxbContextFromClasses(); } + else if (!ObjectUtils.isEmpty(getPackagesToScan())) { + this.jaxbContext = createJaxbContextFromPackages(); + } } catch (JAXBException ex) { throw convertJaxbException(ex); @@ -405,6 +444,26 @@ public class Jaxb2Marshaller } } + private JAXBContext createJaxbContextFromPackages() throws JAXBException { + if (logger.isInfoEnabled()) { + logger.info("Creating JAXBContext by scanning packages [" + + StringUtils.arrayToCommaDelimitedString(getPackagesToScan()) + "]"); + } + ClassPathJaxb2TypeScanner scanner = new ClassPathJaxb2TypeScanner(getPackagesToScan()); + scanner.setResourceLoader(this.resourceLoader); + scanner.scanPackages(); + Class[] jaxb2Classes = scanner.getJaxb2Classes(); + if (logger.isDebugEnabled()) { + logger.debug("Found JAXB2 classes: [" + StringUtils.arrayToCommaDelimitedString(jaxb2Classes) + "]"); + } + if (this.jaxbContextProperties != null) { + return JAXBContext.newInstance(jaxb2Classes, this.jaxbContextProperties); + } + else { + return JAXBContext.newInstance(jaxb2Classes); + } + } + private Schema loadSchema(Resource[] resources, String schemaLanguage) throws IOException, SAXException { if (logger.isDebugEnabled()) { logger.debug("Setting validation schema to " + StringUtils.arrayToCommaDelimitedString(this.schemaResources)); diff --git a/org.springframework.oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2MarshallerTests.java b/org.springframework.oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2MarshallerTests.java index 8e743037072..63af36289fa 100644 --- a/org.springframework.oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2MarshallerTests.java +++ b/org.springframework.oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2MarshallerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2011 the original author or authors. + * Copyright 2002-2012 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -279,6 +279,13 @@ public class Jaxb2MarshallerTests extends AbstractMarshallerTests { assertTrue("No XML written", writer.toString().length() > 0); } + @Test + public void supportsPackagesToScan() throws Exception { + marshaller = new Jaxb2Marshaller(); + marshaller.setPackagesToScan(new String[] {CONTEXT_PATH}); + marshaller.afterPropertiesSet(); + } + @XmlRootElement public static class DummyRootElement { diff --git a/org.springframework.oxm/template.mf b/org.springframework.oxm/template.mf index 5cf0002e593..221f362d580 100644 --- a/org.springframework.oxm/template.mf +++ b/org.springframework.oxm/template.mf @@ -12,6 +12,7 @@ Import-Template: org.exolab.castor.*;version="[1.2.0, 2.0.0)";resolution:=optional, org.jibx.runtime.*;version="[1.1.5, 2.0.0)";resolution:=optional, org.springframework.beans.*;version=${spring.osgi.range}, + org.springframework.context.*;version=${spring.osgi.range}, org.springframework.core.*;version=${spring.osgi.range}, org.springframework.util.*;version=${spring.osgi.range}, org.w3c.dom.*;version="0",