Search code examples
javahibernatemappingdozer

org.dozer.MappingProcessor - Field mapping error ByteArray


I have following entity, DTO class and dozer mapping file. I'm trying to copy the hibernate entity which contains a 2 dim byte array to the new DTO using dozer mapping. Getting the java.lang.IllegalArgumentException: array element type mismatch.

Any idea?

mapping file:

<mapping map-id="i" wildcard="false">
        <class-a>com.csinfra.jdbmon.web.client.dto.Config.HostGroups.HostGroup.CheckGroup.Check.Type.MultiResult</class-a>
        <class-b>com.csinfra.jdbmon.web.client.dto.MultiResultDTO</class-b>
        <field>
            <a>id</a>
            <b>id</b>
        </field>
        <field>
            <a>columns</a>
            <b>columns</b>
        </field>                        
    </mapping>

Entity class:

    @Entity(name="multiResult")
@Table(name="multiResult")
public static class MultiResult implements Serializable {

    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    @javax.persistence.Column(name = "id", unique = true, nullable = false)
    private Long id;

    @Lob
    @javax.persistence.Column(name = "columns",length = 10000)
    private byte[][] columns;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public byte[][] getColumns() {
        return columns;
    }

    public void setColumns(byte[][] columns) {
        this.columns = columns;
    }
}    

DTO class:

public class MultiResultDTO implements IsSerializable {

    private Long id;
    private byte[][] columns;

    public MultiResultDTO(){}

    public byte[][] getColumns() {
        return columns;
    }

    public void setColumns(byte[][] columns) {
        this.columns = columns;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }   
}

Exception:

19165 ERROR org.dozer.MappingProcessor - Field mapping error -->
  MapId: null
  Type: null
  Source parent class: com.csinfra.jdbmon.web.client.dto.Config$HostGroups$HostGroup$CheckGroup$Check$Type$MultiResult
  Source field name: columns
  Source field type: class [[B
  Source field value: [[B@127a7396
  Dest parent class: com.csinfra.jdbmon.web.client.dto.MultiResultDTO
  Dest field name: columns
  Dest field type: [[B
java.lang.IllegalArgumentException: array element type mismatch
    at java.lang.reflect.Array.set(Native Method)
    at org.dozer.MappingProcessor.addToPrimitiveArray(MappingProcessor.java:712)
    at org.dozer.MappingProcessor.mapArrayToArray(MappingProcessor.java:629)
    . . .
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:799)
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66)
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:861)
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1455)
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.lang.Thread.run(Thread.java:745)

Solution

  • It's either a design decision or a bug. I reproduced it in a simpler setting and filed. It seems that it's possible to patch the code to at least support multidimensional arrays (multidimensional collections still don't work this way), but I only did that as a proof of concept for the "happy path" of constructing a totally new object, omitting the part where the destination object might already have non-null fields. The amount of work needed to be done to support that in all cases is quite big which hints that it is a design decision to only support 1-d collections and arrays. But that's just my take and I see that code for the first time, maybe we will get more info from experienced Dozer developers at some point.


    With that said, you can easily overcome that in your particular case by writing a custom converter for your "columns" parameter (or actually for any 1-d or 2-d byte array).

    A longer but somewhat cleaner way IMO (doesn't rely on Dozer taking any part in that, as that seems somewhat unreliable): define a converter class

    public class ByteArray2dConverter extends DozerConverter<byte[][], byte[][]> {
    
        public ByteArray2dConverter() {
            super(byte[][].class, byte[][].class);
        }
    
        public byte[][] convertTo(byte[][] source, byte[][] destination) {
            if (source == null) {
                return null;
            }
            byte[][] result = new byte[source.length][];
            for (int i = 0; i < source.length; i++) {
                byte[] element = source[i];
                if (element != null) {
                    result[i] = Arrays.copyOf(element, element.length);
                }
            }
            return result;
        }
    
        public byte[][] convertFrom(byte[][] source, byte[][] destination) {
            return convertTo(source, destination);
        }
    }
    

    ...and add a custom-converter attribute to the "columns" field in your XML mapping file:

    <mapping map-id="i" wildcard="false">
        <class-a>com.csinfra.jdbmon.web.client.dto.Config.HostGroups.HostGroup.CheckGroup.Check.Type.MultiResult</class-a>
        <class-b>com.csinfra.jdbmon.web.client.dto.MultiResultDTO</class-b>
        ...
        <field custom-converter="com.csinfra...ByteArray2dConverter">
            <a>columns</a>
            <b>columns</b>
        </field>
    </mapping>
    

    Alternatively, you can save some typing if you allow Dozer to map the "top-level" array and only use a custom converter for the second level: define a converter

    public class ByteArray1dConverter extends DozerConverter<byte[], byte[]> {
    
        public ByteArray1dConverter() {
            super(byte[].class, byte[].class);
        }
    
        public byte[] convertTo(byte[] source, byte[] destination) {
            return source == null ? null : Arrays.copyOf(source, source.length);
        }
    
        public byte[] convertFrom(byte[] source, byte[] destination) {
            return convertTo(source, destination);
        }
    }
    

    ...and then in the mappings XML add a section (on the same level as the "mapping" section):

    ...
    <configuration>
        <custom-converters>
            <converter
                    type="com.csinfra...ByteArray1dConverter">
                <class-a>[B</class-a>
                <class-b>[B</class-b>
            </converter>
        </custom-converters>
    </configuration>
    <mapping map-id="i" wildcard="false">
        ...
    

    This way you tell Dozer to use your converter in all conversions between two byte arrays (you can do the same in the previous case instead of defining a custom-converter on the field level in the mapping XML).


    Yet another option is to use a 1-d array of objects each having a 1-d byte array, Dozer is fine with that. Something like Column[] columns, where the Column class has a field byte[] columnBytes.