Skip to content

Commit

Permalink
Merge pull request #3051 from brunomendola/feature/2734/jackson-xml
Browse files Browse the repository at this point in the history
jackson-dataformat-xml plugin

fixes #2734
  • Loading branch information
dilipkrish committed Aug 3, 2019
2 parents 4d4577b + c761cfe commit 41adbd3
Show file tree
Hide file tree
Showing 8 changed files with 450 additions and 22 deletions.
1 change: 1 addition & 0 deletions springfox-schema/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ ext {
dependencies {
compile project(':springfox-core')
compile project(':springfox-spi')
provided "com.fasterxml.jackson.dataformat:jackson-dataformat-xml:${jackson}"
testCompile libs.test
testCompile project(':springfox-core').sourceSets.test.output
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
*
* Copyright 2017-2019 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 springfox.documentation.schema;

import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.util.ClassUtils;

import static org.springframework.util.ClassUtils.forName;

public abstract class ClassPresentInClassPathCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return isPresent(getClassName(), context.getClassLoader());
}

protected abstract String getClassName();

private static boolean isPresent(String className, ClassLoader classLoader) {
if (classLoader == null) {
classLoader = ClassUtils.getDefaultClassLoader();
}
try {
forName(className, classLoader);
return true;
} catch (Throwable ex) {
return false;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package springfox.documentation.schema;

public class JacksonXmlPresentInClassPathCondition extends ClassPresentInClassPathCondition {
@Override
protected String getClassName() {
return "com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,28 +18,9 @@
*/
package springfox.documentation.schema;

import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.util.ClassUtils;

import static org.springframework.util.ClassUtils.*;

public class JaxbPresentInClassPathCondition implements Condition {
public class JaxbPresentInClassPathCondition extends ClassPresentInClassPathCondition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return isPresent("javax.xml.bind.annotation.XmlElement", context.getClassLoader());
}

private static boolean isPresent(String className, ClassLoader classLoader) {
if (classLoader == null) {
classLoader = ClassUtils.getDefaultClassLoader();
}
try {
forName(className, classLoader);
return true;
} catch (Throwable ex) {
return false;
}
protected String getClassName() {
return "javax.xml.bind.annotation.XmlElement";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
*
* Copyright 2017-2019 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 springfox.documentation.schema.plugins;

import com.fasterxml.classmate.TypeResolver;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Conditional;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;
import springfox.documentation.schema.JacksonXmlPresentInClassPathCondition;
import springfox.documentation.schema.Xml;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.schema.ModelBuilderPlugin;
import springfox.documentation.spi.schema.contexts.ModelContext;

@Component
@Conditional(JacksonXmlPresentInClassPathCondition.class)
public class JacksonXmlModelPlugin implements ModelBuilderPlugin {
private final TypeResolver typeResolver;

@Autowired
public JacksonXmlModelPlugin(TypeResolver typeResolver) {
this.typeResolver = typeResolver;
}

@Override
public void apply(ModelContext context) {
JacksonXmlRootElement root = AnnotationUtils.findAnnotation(forClass(context), JacksonXmlRootElement.class);
if (root != null) {
context.getBuilder().xml(buildXml(root));
}
}

private Xml buildXml(JacksonXmlRootElement annotation) {
return new Xml()
.name(defaultToNull(annotation.localName()))
.attribute(false)
.namespace(defaultToNull(annotation.namespace()))
.wrapped(false);
}

private String defaultToNull(String value) {
return "##default".equalsIgnoreCase(value) ? null : value;
}

private Class<?> forClass(ModelContext context) {
return typeResolver.resolve(context.getType()).getErasedType();
}

@Override
public boolean supports(DocumentationType delimiter) {
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
*
* Copyright 2017-2019 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 springfox.documentation.schema.property;

import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import org.springframework.context.annotation.Conditional;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;
import springfox.documentation.schema.JacksonXmlPresentInClassPathCondition;
import springfox.documentation.schema.Xml;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.schema.ModelPropertyBuilderPlugin;
import springfox.documentation.spi.schema.contexts.ModelPropertyContext;

import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.util.Optional;
import java.util.function.Predicate;

import static java.util.Optional.empty;
import static java.util.Optional.ofNullable;
import static springfox.documentation.schema.Annotations.findPropertyAnnotation;

@Component
@Conditional(JacksonXmlPresentInClassPathCondition.class)
public class JacksonXmlPropertyPlugin implements ModelPropertyBuilderPlugin {

@Override
public void apply(ModelPropertyContext context) {
Optional<JacksonXmlProperty> propertyAnnotation = findAnnotation(context, JacksonXmlProperty.class);

if (propertyAnnotation.isPresent()) {
if (propertyAnnotation.get().isAttribute()) {
context.getBuilder()
.xml(new Xml()
.attribute(true)
.namespace(defaultToNull(propertyAnnotation.get().namespace()))
.name(propertyName(propertyAnnotation))
.wrapped(false));
} else {
Optional<JacksonXmlElementWrapper> wrapper = findAnnotation(context, JacksonXmlElementWrapper.class);
context.getBuilder()
.xml(new Xml()
.attribute(false)
.namespace(defaultToNull(propertyAnnotation.get().namespace()))
.name(wrapperName(wrapper, propertyAnnotation))
.wrapped(wrapper.isPresent()));
}
}
}

private static <T extends Annotation> Optional<T> findAnnotation(
ModelPropertyContext context,
Class<T> annotationClass) {
Optional<T> annotation = empty();
if (context.getAnnotatedElement().isPresent()) {
annotation = annotation.map(Optional::of).orElse(findAnnotation(
context.getAnnotatedElement().get(),
annotationClass));
}
if (context.getBeanPropertyDefinition().isPresent()) {
annotation = annotation.map(Optional::of).orElse(findPropertyAnnotation(
context.getBeanPropertyDefinition().get(),
annotationClass));
}
return annotation;
}

public static <T extends Annotation> Optional<T> findAnnotation(
AnnotatedElement annotated,
Class<T> annotation) {
return ofNullable(AnnotationUtils.getAnnotation(annotated, annotation));
}

private String wrapperName(Optional<JacksonXmlElementWrapper> wrapper, Optional<JacksonXmlProperty> property) {
if (wrapper.isPresent() && wrapper.get().useWrapping()) {
return ofNullable(defaultToNull(ofNullable(wrapper.get().localName())
.filter(((Predicate<String>) String::isEmpty).negate()).orElse(null)))
.orElse(ofNullable(propertyName(property))
.orElse(null));
}
return propertyName(property);
}

private String propertyName(Optional<JacksonXmlProperty> property) {
if (property.isPresent()) {
return defaultToNull(ofNullable(property.get().localName())
.filter(((Predicate<String>) String::isEmpty).negate())
.orElse(null));
}
return null;
}

private String defaultToNull(String value) {
return "##default".equalsIgnoreCase(value) ? null : value;
}

@Override
public boolean supports(DocumentationType delimiter) {
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package springfox.documentation.schema.plugins

import com.fasterxml.classmate.TypeResolver
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement
import spock.lang.Specification
import spock.lang.Unroll
import springfox.documentation.schema.DefaultGenericTypeNamingStrategy
import springfox.documentation.spi.DocumentationType
import springfox.documentation.spi.schema.AlternateTypeProvider
import springfox.documentation.spi.schema.contexts.ModelContext

import static java.util.Collections.emptySet

class JacksonXmlModelPluginSpec extends Specification {
def "Should support all swagger documentation types"() {
given:
def sut = new JacksonXmlModelPlugin()

expect:
sut.supports(DocumentationType.SPRING_WEB)
sut.supports(DocumentationType.SWAGGER_12)
sut.supports(DocumentationType.SWAGGER_2)
}

@Unroll
def "Xml model plugin parses #type.localName annotation as expected"() {
given:
def resolver = new TypeResolver()
JacksonXmlModelPlugin sut = new JacksonXmlModelPlugin(resolver)
ModelContext context = ModelContext.inputParam(
"0_0",
"group",
resolver.resolve(type),
Optional.empty(),
new HashSet<>(),
DocumentationType.SWAGGER_12,
new AlternateTypeProvider([]),
new DefaultGenericTypeNamingStrategy(),
emptySet())
when:
sut.apply(context)

then:
context.builder.build()?.xml?.name == expected

where:
type | expected
XmlNotAnnotated | null
XmlRootElementAnnotated | "root"
}

class XmlNotAnnotated {
}

@JacksonXmlRootElement(localName = "root")
class XmlRootElementAnnotated {
}
}

0 comments on commit 41adbd3

Please sign in to comment.