Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Choice fix #5

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ test {

dependencies {
compile group: "com.fasterxml.jackson.module", name: "jackson-module-jsonSchema", version: "2.9.6"
compile "com.fasterxml.jackson.module:jackson-module-jaxb-annotations:2.9.6"
compile "com.fasterxml.jackson.module:jackson-module-jaxb-annotations:2.10.1"
compile group: "com.fasterxml.jackson.datatype", name: "jackson-datatype-jsr353", version: "2.9.6"
compile "com.sun.xml.bind:jaxb-xjc:2.1.6"
compile "com.sun.xml.ws:jaxws-tools:2.2.1"
Expand Down
1 change: 0 additions & 1 deletion src/main/java/io/elastic/soap/handlers/RequestHandler.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package io.elastic.soap.handlers;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.module.jaxb.JaxbAnnotationModule;
import io.elastic.soap.compilers.model.SoapBodyDescriptor;
import io.elastic.soap.exceptions.ComponentException;
import io.elastic.soap.utils.Utils;
Expand Down
140 changes: 140 additions & 0 deletions src/main/java/io/elastic/soap/jackson/AbstractChoiceDeserializer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package io.elastic.soap.jackson;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.elastic.soap.utils.Utils;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import javax.xml.bind.JAXBElement;

public abstract class AbstractChoiceDeserializer extends JsonDeserializer {

protected final Class rawType;
protected final JavaType javaType;
protected final ObjectMapper mapper;

protected AbstractChoiceDeserializer(JavaType javaType) {
this.javaType = javaType;
this.rawType = javaType.getRawClass();
this.mapper = Utils.getConfiguredObjectMapper();
}

/**
* How this works: axios converts wsdl choice element to one of the following structure:
* 1. field with annotation XmlElements that contains array of XmlElement. XmlElement has property name and property type(java class of choice)
* 2. field with annotation XmlElementsRefs that contains array of XmlElementReg. XmlElementRef has property name and property type(java class of choice)
* Field created by axios usually looks like: List<Object> or List<Serializble>. Note in runtime we will have: List
* This method do the following:
* 1. Check that json value is possible to convert one of type provided by XmlElement or XmlElementRef annotations.
* 2. Converts value to type of field created by axios.
* @param p jackson parser.
* @param ctxt jackson context.
* @return deserialize value of choice element.
*/
@Override
public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
final JsonNode targetNode = p.getCodec().readTree(p);
nodeOneOfPossibleTypes(targetNode, this.getPossibleTypes());
return this.mapper.convertValue(targetNode, this.rawType);
}

/**
* @return List of possible types of choice element.
*/
public abstract List<Class> getPossibleTypes();

/**
* Checks that provided JsonNode from choice element has structure of one of choice element.
* In case of JAXBElement type was generated it impossible to check the structure, in this case true will be returned.
* @param node node to be checked
* @param possibleTypes possible types of node
* @throws IllegalArgumentException if node structure is not one of provided possible types.
*/
public void nodeOneOfPossibleTypes(final JsonNode node, final List<Class> possibleTypes) throws IllegalArgumentException {
if (possibleTypes.contains(JAXBElement.class)) {
return;
}
boolean result = false;
for (Class type : possibleTypes) {
result = nodeCanBeConvertedToType(node, type);
if (result) {
break;
}
}
if (!result && isNodeAndRawTypeArray(node)) {
result = handleArrayNode(node, possibleTypes);
}
if (!result) {
throw new IllegalArgumentException(constructExceptionString(node, possibleTypes));
}
}

/**
* @param node json node
* @return return true if node and raw type is array
*/
public boolean isNodeAndRawTypeArray(final JsonNode node) {
return node.isArray() && (this.javaType.isArrayType() || this.javaType.isCollectionLikeType());
}
/**
* Checks each item of node over provided possible types also each item in array must have same type.
* @param arrayNode ArrayNode
* @param possibleTypes possible types of node
* @return true if each item of array node can be converted to one of possible type.
*/
public boolean handleArrayNode(JsonNode arrayNode, List<Class> possibleTypes) {
Class targetType = null;
for (JsonNode node : arrayNode) {
targetType = Optional.ofNullable(targetType).orElse(findNodeType(node, possibleTypes));
boolean canBeConverted = nodeCanBeConvertedToType(node, targetType);
if (!canBeConverted) {
return false;
}
}
return true;
}

/**
* @param node json node.
* @param possibleTypes possible types of node.
* @return type of node
* @throws IllegalArgumentException if node can be converted to any of provided possibleTypes
*/
private Class findNodeType(JsonNode node, List<Class> possibleTypes) {
for (Class type : possibleTypes) {
if (nodeCanBeConvertedToType(node, type)) {
return type;
}
}
throw new IllegalArgumentException(constructExceptionString(node, possibleTypes));
}


/**
*
* @param node json node.
* @param type type to be checked.
* @return true if node can be converted to provided type, false otherwise.
*/
public boolean nodeCanBeConvertedToType(final JsonNode node, Class type) {
try {
this.mapper.convertValue(node, type);
return true;
} catch (IllegalArgumentException ex) {
return false;
}
}

public String constructExceptionString(final JsonNode value, final List<Class> possibleTypes) {
StringBuilder bd = new StringBuilder("Failed to convert choice value: ");
bd.append(value.toPrettyString()).append("to one of: ");
bd.append(possibleTypes.stream().map(Class::getSimpleName).collect(Collectors.joining(","))).append(".");
return bd.toString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package io.elastic.soap.jackson;

import com.fasterxml.jackson.databind.JavaType;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import javax.xml.bind.annotation.XmlElementRef;
import javax.xml.bind.annotation.XmlElementRefs;

public class XMLElementRefsChoiceDeserializer extends AbstractChoiceDeserializer {

private final XmlElementRefs annotation;

public XMLElementRefsChoiceDeserializer(final JavaType type, final XmlElementRefs annotation) {
super(type);
this.annotation = annotation;
}

@Override
public List<Class> getPossibleTypes() {
return Arrays.stream(this.annotation.value())
.map(XmlElementRef::type)
.filter(c -> !c.equals(XmlElementRef.DEFAULT.class))
.collect(Collectors.toList());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package io.elastic.soap.jackson;

import com.fasterxml.jackson.databind.JavaType;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElements;

public class XMLElementsChoiceDeserializer extends AbstractChoiceDeserializer {


private final XmlElements annotation;

public XMLElementsChoiceDeserializer(final JavaType type, final XmlElements annotation) {
super(type);
this.annotation = annotation;
}

@Override
public List<Class> getPossibleTypes() {
return Arrays.stream(this.annotation.value())
.map(XmlElement::type)
.filter(c -> !c.equals(XmlElement.DEFAULT.class))
.collect(Collectors.toList());
}
}
61 changes: 61 additions & 0 deletions src/main/java/io/elastic/soap/jackson/XMLElementsIntrospector.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package io.elastic.soap.jackson;

import com.fasterxml.jackson.databind.PropertyName;
import com.fasterxml.jackson.databind.introspect.Annotated;
import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementRefs;
import javax.xml.bind.annotation.XmlElements;

public class XMLElementsIntrospector extends JacksonAnnotationIntrospector {

/**
* Finds alias names of field in annotations XmlElements and XmlElementRefs.
* @param a annotated field.
* @return alias names of field.
*/
@Override
public List<PropertyName> findPropertyAliases(Annotated a) {
if (a.hasAnnotation(XmlElements.class) || a.hasAnnotation(XmlElementRefs.class)) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's possible to wrap this two condition blocks into one if (a.hasAnnotation(XmlElement.class)) {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No

final List<PropertyName> result = Optional.ofNullable(super.findPropertyAliases(a)).orElse(new ArrayList<>());
final List<PropertyName> names = getXMLElementsNames(a);
result.addAll(names);
return result;
}
if (a.hasAnnotation(XmlElement.class)) {
final List<PropertyName> result = Optional.ofNullable(super.findPropertyAliases(a)).orElse(new ArrayList<>());
XmlElement element = a.getAnnotation(XmlElement.class);
result.add(new PropertyName(element.name()));
return result;
}
return super.findPropertyAliases(a);
}

/**
* Return custom deserializer in case field annotated with XmlElements or XmlElementRefs annotations.
* @param a annotated field.
* @return deserializer for field.
*/
@Override
public Object findDeserializer(Annotated a) {
if (a.hasAnnotation(XmlElementRefs.class)) {
return new XMLElementRefsChoiceDeserializer(a.getType(), a.getAnnotation(XmlElementRefs.class));
}
if (a.hasAnnotation(XmlElements.class)) {
return new XMLElementsChoiceDeserializer(a.getType(), a.getAnnotation(XmlElements.class));
}
return super.findDeserializer(a);
}

public List<PropertyName> getXMLElementsNames(final Annotated a) {
if (a.hasAnnotation(XmlElements.class)) {
return Arrays.stream(a.getAnnotation(XmlElements.class).value()).map(e -> new PropertyName(e.name())).collect(Collectors.toList());
}
return Arrays.stream(a.getAnnotation(XmlElementRefs.class).value()).map(e -> new PropertyName(e.name())).collect(Collectors.toList());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ public JsonObject getMetaModel(final JsonObject configuration) {
final String portTypeName = wsdl.getBinding(bindingName).getPortType().getName();
final Operation operation = wsdl.getOperation(operationName, portTypeName);
final JsonObject in = generateSchema(operation.getInput().getMessage());
final JsonObject out = generateSchema(operation.getInput().getMessage());
final JsonObject out = generateSchema(operation.getOutput().getMessage());
final JsonObject result = Json.createObjectBuilder()
.add("in", in)
.add("out", out)
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/io/elastic/soap/utils/Utils.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.elastic.soap.utils;

import ch.qos.logback.classic.Level;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr353.JSR353Module;
Expand All @@ -13,6 +14,7 @@
import io.elastic.soap.compilers.JaxbCompiler;
import io.elastic.soap.compilers.model.SoapBodyDescriptor;
import io.elastic.soap.exceptions.ComponentException;
import io.elastic.soap.jackson.XMLElementsIntrospector;
import io.elastic.soap.services.SoapCallService;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
Expand Down Expand Up @@ -64,7 +66,10 @@ private Utils() {
public static ObjectMapper getConfiguredObjectMapper() {
final JaxbAnnotationModule module = new JaxbAnnotationModule();
final ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS, true);
objectMapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);
objectMapper.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true);
objectMapper.setAnnotationIntrospector(new XMLElementsIntrospector());
objectMapper.registerModule(new JSR353Module());
objectMapper.registerModule(module);
return objectMapper;
Expand Down
55 changes: 55 additions & 0 deletions src/test/java/io/elastic/soap/jackson/ChoiceMetadataTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package io.elastic.soap.jackson;

import io.elastic.soap.handlers.RequestHandler;
import java.io.InputStream;
import java.io.InputStreamReader;
import javax.json.Json;
import javax.json.JsonArray;
import javax.json.JsonObject;
import javax.json.JsonReader;
import javax.json.JsonValue;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

public class ChoiceMetadataTest {

@Test
public void serializeClassWithXmlElementsAnnotation() throws ClassNotFoundException {
final RequestHandler handler = new RequestHandler();
final String weatherDescription = "XmlElementsChoice";
final Class clazz = XmlElementsChoice.class;
readResourceFileAsJsonArray("choicesElements.json").stream().map(JsonValue::asJsonObject).forEach(o -> {
System.out.println(o);
Object result = this.wrapAndTest(handler, o, weatherDescription, clazz);
Assertions.assertNotNull(result);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assert not null for so complicated task?

});
}

@Test
public void serializeClassWithXmlElementRefssAnnotation() throws ClassNotFoundException {
final RequestHandler handler = new RequestHandler();
final String weatherDescription = "XmlElementRefsChoice";
final Class clazz = XmlElementRefsChoice.class;
readResourceFileAsJsonArray("choicesRefs.json").stream().map(JsonValue::asJsonObject).forEach(o -> {
System.out.println(o);
Object result = this.wrapAndTest(handler, o, weatherDescription, clazz);
Assertions.assertNotNull(result);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assert not null for so complicated task?

});
}

public Object wrapAndTest(RequestHandler handler, JsonObject request, String elementName, Class clazz) {
try {
return handler.getObjectFromJson(request, elementName, clazz);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public JsonArray readResourceFileAsJsonArray(final String path) {
InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(path);
JsonReader jsonReader = Json.createReader(new InputStreamReader(inputStream));
JsonArray choices = jsonReader.readArray();
jsonReader.close();
return choices;
}

}
Loading