Skip to content

Commit

Permalink
Added XML map deserializer to bean mapper, creating "entry" with "key…
Browse files Browse the repository at this point in the history
…" and "value".
  • Loading branch information
essiembre committed Aug 30, 2024
1 parent 9c7dde2 commit e53e4ee
Show file tree
Hide file tree
Showing 11 changed files with 616 additions and 28 deletions.
10 changes: 4 additions & 6 deletions src/main/java/com/norconex/commons/lang/bean/BeanMapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@
import com.fasterxml.jackson.databind.deser.DeserializationProblemHandler;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.jsontype.TypeIdResolver;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;
Expand All @@ -73,14 +72,14 @@
import com.norconex.commons.lang.ClassUtil;
import com.norconex.commons.lang.bean.jackson.EmptyWithClosingTagXmlFactory;
import com.norconex.commons.lang.bean.jackson.JsonXmlCollectionModule;
import com.norconex.commons.lang.bean.jackson.JsonXmlPropertiesDeserializer;
import com.norconex.commons.lang.bean.jackson.JsonXmlMapModule;
import com.norconex.commons.lang.bean.jackson.JsonXmlPropertiesModule;
import com.norconex.commons.lang.bean.spi.PolymorphicTypeLoader;
import com.norconex.commons.lang.bean.spi.PolymorphicTypeProvider;
import com.norconex.commons.lang.config.Configurable;
import com.norconex.commons.lang.convert.GenericJsonModule;
import com.norconex.commons.lang.flow.FlowMapperConfig;
import com.norconex.commons.lang.flow.module.FlowModule;
import com.norconex.commons.lang.map.Properties;

import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
Expand Down Expand Up @@ -163,9 +162,6 @@ public enum Format {
.setProperty(
XMLOutputFactory.IS_REPAIRING_NAMESPACES,
true);
m.registerModule(new SimpleModule().addDeserializer(
Properties.class,
new JsonXmlPropertiesDeserializer()));
return m;
}),
JSON(
Expand Down Expand Up @@ -479,6 +475,8 @@ public ObjectMapper toObjectMapper(Format format) {
// misc:
if (format == Format.XML) {
mapper.registerModule(new JsonXmlCollectionModule());
mapper.registerModule(new JsonXmlMapModule());
mapper.registerModule(new JsonXmlPropertiesModule());
}

mapper.addMixIn(Object.class, NonDefaultInclusionMixIn.class);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/* Copyright 2023 Norconex Inc.
*
* 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 com.norconex.commons.lang.bean.jackson;

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.util.HashMap;

import com.norconex.commons.lang.bean.BeanMapper;

/**
* <p>
* This annotation must be used with the {@link JsonXmlMapModule}.
* The module is registered automatically for XML source if you are using
* {@link BeanMapper}.
* Use this annotation only when you wish to overwrite default settings
* via its attributes. It is otherwise of no use.
* </p>
* @since 3.0.0
*/
@Retention(RUNTIME)
@Target({ TYPE, FIELD })
public @interface JsonXmlMap {

/**
* Field name to use for each map entries when serializing as XML.
* This name does not affect reading. Default is "entry".
* @return entry field name, when writing XML
*/
public String entryName() default "entry";

/**
* Field name to use for each map entry keys when serializing as XML.
* Default is "key".
* @return entry key field name, when writing XML
*/
public String keyName() default "key";

/**
* Field name to use for each map entry values when serializing as XML.
* Default is "value".
* @return entry value field name, when writing XML
*/
public String valueName() default "value";

/**
* Concrete Map type to use when deserializing. Has to be assignable
* to the type of your Map property. Default will try to detect and
* use {@link HashMap} as fallback.
* @return map concrete type
*/
public Class<?> mapType() default Void.class;

/**
* Concrete key type to use when deserializing. Has to be assignable
* to the type of your Map entry key property.
* Default will try to detect.
* @return map entry key concrete type
*/
public Class<?> keyType() default Void.class;

/**
* Concrete value type to use when deserializing. Has to be assignable
* to the type of your Map entry value property.
* Default will try to detect.
* @return map entry value concrete type
*/
public Class<?> valueType() default Void.class;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/* Copyright 2024 Norconex Inc.
*
* 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 com.norconex.commons.lang.bean.jackson;

import static org.apache.commons.lang3.StringUtils.defaultIfBlank;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.norconex.commons.lang.ClassUtil;

/**
* Deserializes a list of "entry" elements each having a "key" and "value"
* child elements into a Map.
* @param <T> Map concrete type
*/
public class JsonXmlMapDeserializer<T extends Map<?, ?>>
extends StdDeserializer<T>
implements ContextualDeserializer {

private static final long serialVersionUID = 1L;

private transient BeanProperty currentProperty;
private JavaType mapType;

public JsonXmlMapDeserializer() {
this((Class<T>) null);
}

public JsonXmlMapDeserializer(Class<T> vc) {
super(vc);
}

private JsonXmlMapDeserializer(
BeanProperty currentProperty, JavaType mapType) {
super(Map.class);
this.currentProperty = currentProperty;
this.mapType = mapType;
}

@Override
public StdDeserializer<?> createContextual(
DeserializationContext ctxt, BeanProperty property) {
return new JsonXmlMapDeserializer<>(
property,
property != null
? property.getType()
: ctxt.constructType(Map.class));
}

@SuppressWarnings("unchecked")
@Override
public T deserialize(JsonParser p, DeserializationContext ctxt)
throws IOException {
var map = createMap();
var keyName = "key";
var valueName = "value";
Class<?> keyType = null;
Class<?> valueType = null;
if (currentProperty != null) {
var annot = currentProperty.getAnnotation(JsonXmlMap.class);
if (annot != null) {
keyName = defaultIfBlank(annot.keyName(), keyName);
valueName = defaultIfBlank(annot.valueName(), valueName);
if (!Void.class.equals(annot.keyType())) {
keyType = annot.keyType();
}
if (!Void.class.equals(annot.valueType())) {
valueType = annot.valueType();
}
}
}

while (p.nextToken() != JsonToken.END_OBJECT) {

if (p.getCurrentToken() == JsonToken.START_OBJECT) {
Object key = null;
Object value = null;

while (p.nextToken() != JsonToken.END_OBJECT) {
var fieldName = p.currentName();
p.nextToken(); // Move to the value of the field

if (keyName.equals(fieldName)) {
key = ctxt.readValue(p,
fieldType(keyType, mapType.containedType(0)));
} else if (valueName.equals(fieldName)) {
value = ctxt.readValue(p,
fieldType(valueType, mapType.containedType(1)));
}
}

if (key != null) {
map.put(key, value);
}
}
}
return (T) map;
}

private Class<?> fieldType(Class<?> annotType, JavaType containedType) {
if (annotType != null) {
return annotType;
}
if (containedType != null) {
return containedType.getRawClass();
}
return null;
}

@SuppressWarnings("unchecked")
private Map<Object, Object> createMap() {
// Map type established in this priority order:
// - specified on annotation
// - actual type detected and instantiable
// - Fall-back to HashMap

// from annotation
if (currentProperty != null) {
var annot = currentProperty.getAnnotation(JsonXmlMap.class);
if (annot != null && !Void.class.equals(annot.mapType())) {
return (Map<Object, Object>) ClassUtil.newInstance(
annot.mapType());
}
}

// from actual type
try {
var map = ClassUtil.newInstance(mapType.getRawClass());
if (map != null) {
return (Map<Object, Object>) map;
}
} catch (Exception e) {
// swallow
}

return ClassUtil.newInstance(HashMap.class);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/* Copyright 2023-2024 Norconex Inc.
*
* 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 com.norconex.commons.lang.bean.jackson;

import java.util.Map;

import com.fasterxml.jackson.databind.module.SimpleModule;
import com.norconex.commons.lang.bean.BeanMapper;

/**
* Jackson module providing (de)serializers for Map, so they can be
* written and read as XML without special hacks.
* Already registered in {@link BeanMapper}.
* @since 3.0.0
*/
public class JsonXmlMapModule extends SimpleModule {

private static final long serialVersionUID = 1L;

@SuppressWarnings("unchecked")
public JsonXmlMapModule() {
this.addSerializer((Class<Map<?, ?>>) (Class<?>) Map.class,
new JsonXmlMapSerializer<>());
this.addDeserializer(Map.class, new JsonXmlMapDeserializer<>());
}
}
Loading

0 comments on commit e53e4ee

Please sign in to comment.