Skip to content

Commit

Permalink
SPR-8986 Add the ability to Scan Packages for JAXB Marshalling
Browse files Browse the repository at this point in the history
Jaxb2Marshaller now has the capability to scan for classes annotated with JAXB2 annotations.
  • Loading branch information
poutsma committed Jan 20, 2012
1 parent bcd8355 commit 79f32c7
Show file tree
Hide file tree
Showing 4 changed files with 195 additions and 8 deletions.
Original file line number Diff line number Diff line change
@@ -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<Class<?>> jaxb2Classes = new ArrayList<Class<?>>();

/** 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;
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -75,8 +75,10 @@

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;
Expand Down Expand Up @@ -117,7 +119,7 @@
*/
public class Jaxb2Marshaller
implements MimeMarshaller, MimeUnmarshaller, GenericMarshaller, GenericUnmarshaller, BeanClassLoaderAware,
InitializingBean {
ResourceLoaderAware, InitializingBean {

private static final String CID = "cid:";

Expand All @@ -130,6 +132,8 @@ public class Jaxb2Marshaller
private String contextPath;

private Class<?>[] classesToBeBound;

private String[] packagesToScan;

private Map<String, ?> jaxbContextProperties;

Expand All @@ -153,6 +157,8 @@ public class Jaxb2Marshaller

private ClassLoader beanClassLoader;

private ResourceLoader resourceLoader;

private JAXBContext jaxbContext;

private Schema schema;
Expand All @@ -175,6 +181,8 @@ public void setContextPaths(String... contextPaths) {

/**
* Set a JAXB context path.
* <p>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");
Expand All @@ -190,7 +198,8 @@ public String getContextPath() {

/**
* Set the list of Java classes to be recognized by a newly created JAXBContext.
* Setting this property or {@link #setContextPath "contextPath"} is required.
* <p>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");
Expand All @@ -204,6 +213,23 @@ public Class<?>[] getClassesToBeBound() {
return this.classesToBeBound;
}

/**
* Set the packages to search using Spring-based scanning for classes with JAXB2 annotations in the classpath.
* <p>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 <code>JAXBContext</code> properties. These implementation-specific
* properties will be set on the underlying <code>JAXBContext</code>.
Expand Down Expand Up @@ -337,13 +363,23 @@ public void setBeanClassLoader(ClassLoader classLoader) {
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();
Expand All @@ -362,6 +398,9 @@ protected synchronized JAXBContext getJaxbContext() {
else if (!ObjectUtils.isEmpty(getClassesToBeBound())) {
this.jaxbContext = createJaxbContextFromClasses();
}
else if (!ObjectUtils.isEmpty(getPackagesToScan())) {
this.jaxbContext = createJaxbContextFromPackages();
}
}
catch (JAXBException ex) {
throw convertJaxbException(ex);
Expand Down Expand Up @@ -405,6 +444,26 @@ private JAXBContext createJaxbContextFromClasses() throws JAXBException {
}
}

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));
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -279,6 +279,13 @@ public void marshalAttachments() throws Exception {
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 {

Expand Down
1 change: 1 addition & 0 deletions org.springframework.oxm/template.mf
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down

0 comments on commit 79f32c7

Please sign in to comment.