StackTraceElementDeserTest.java

package tools.jackson.databind.deser.jdk;

import org.junit.jupiter.api.Test;

import com.fasterxml.jackson.annotation.JsonProperty;

import java.io.IOException;

import tools.jackson.core.JsonParser;
import tools.jackson.databind.*;
import tools.jackson.databind.annotation.JsonDeserialize;
import tools.jackson.databind.deser.std.StdDeserializer;
import tools.jackson.databind.json.JsonMapper;
import tools.jackson.databind.module.SimpleModule;
import tools.jackson.databind.testutil.DatabindTestUtil;

import static org.junit.jupiter.api.Assertions.*;

/**
 * Tests for {@link StackTraceElementDeserializer}.
 */
public class StackTraceElementDeserTest extends DatabindTestUtil
{
    static class StackTraceHolder {
        public StackTraceElement[] trace;
    }

    // [databind#429]: mix-in that renames StackTraceElement properties
    abstract static class StackTraceElementMixIn {
        @JsonProperty("class")
        public abstract String getClassName();

        @JsonProperty("method")
        public abstract String getMethodName();

        @JsonProperty("file")
        public abstract String getFileName();

        @JsonProperty("line")
        public abstract int getLineNumber();
    }

    // [databind#429]
    static class StackTraceBean {
        public final static int NUM = 13;

        @JsonProperty("Location")
        @JsonDeserialize(using=MyStackTraceElementDeserializer.class)
        protected StackTraceElement location;
    }

    static class MyStackTraceElementDeserializer extends StdDeserializer<StackTraceElement>
    {
        public MyStackTraceElementDeserializer() { super(StackTraceElement.class); }

        @Override
        public StackTraceElement deserialize(JsonParser p,
                DeserializationContext ctxt) {
            p.skipChildren();
            return new StackTraceElement("a", "b", "b", StackTraceBean.NUM);
        }
    }
    
    private final ObjectMapper MAPPER = jsonMapperBuilder()
            .enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
            .build();

    /*
    /**********************************************************************
    /* Tests for basic deserialization
    /**********************************************************************
     */

    @Test
    public void testBasicStackTraceElement() throws Exception
    {
        String json = a2q("{'className':'com.example.Foo','methodName':'doStuff',"
                + "'fileName':'Foo.java','lineNumber':42}");
        StackTraceElement result = MAPPER.readValue(json, StackTraceElement.class);
        assertNotNull(result);
        assertEquals("com.example.Foo", result.getClassName());
        assertEquals("doStuff", result.getMethodName());
        assertEquals("Foo.java", result.getFileName());
        assertEquals(42, result.getLineNumber());
    }

    @Test
    public void testWithMinimalFields() throws Exception
    {
        // Only className, methodName, fileName and lineNumber
        String json = a2q("{'className':'MyClass','methodName':'myMethod',"
                + "'fileName':'MyClass.java','lineNumber':10}");
        StackTraceElement result = MAPPER.readValue(json, StackTraceElement.class);
        assertNotNull(result);
        assertEquals("MyClass", result.getClassName());
        assertEquals("myMethod", result.getMethodName());
        assertEquals("MyClass.java", result.getFileName());
        assertEquals(10, result.getLineNumber());
    }

    @Test
    public void testWithEmptyObject() throws Exception
    {
        // All fields use defaults
        StackTraceElement result = MAPPER.readValue("{}", StackTraceElement.class);
        assertNotNull(result);
        assertEquals("", result.getClassName());
        assertEquals("", result.getMethodName());
        assertEquals("", result.getFileName());
        assertEquals(-1, result.getLineNumber());
    }

    /*
    /**********************************************************************
    /* Tests for Java 9+ fields (module info)
    /**********************************************************************
     */

    @Test
    public void testWithModuleInfo() throws Exception
    {
        String json = a2q("{'className':'com.example.Bar','methodName':'run',"
                + "'fileName':'Bar.java','lineNumber':100,"
                + "'moduleName':'my.module','moduleVersion':'2.0',"
                + "'classLoaderName':'app'}");
        StackTraceElement result = MAPPER.readValue(json, StackTraceElement.class);
        assertNotNull(result);
        assertEquals("com.example.Bar", result.getClassName());
        assertEquals("run", result.getMethodName());
        assertEquals("Bar.java", result.getFileName());
        assertEquals(100, result.getLineNumber());
        assertEquals("my.module", result.getModuleName());
        assertEquals("2.0", result.getModuleVersion());
        assertEquals("app", result.getClassLoaderName());
    }

    @Test
    public void testWithNullModuleInfo() throws Exception
    {
        String json = a2q("{'className':'Test','methodName':'test',"
                + "'fileName':'Test.java','lineNumber':1,"
                + "'moduleName':null,'moduleVersion':null,'classLoaderName':null}");
        StackTraceElement result = MAPPER.readValue(json, StackTraceElement.class);
        assertNotNull(result);
        assertNull(result.getModuleName());
        assertNull(result.getModuleVersion());
        assertNull(result.getClassLoaderName());
    }

    /*
    /**********************************************************************
    /* Tests for round-trip
    /**********************************************************************
     */

    @Test
    public void testRoundTrip() throws Exception
    {
        StackTraceElement orig = new StackTraceElement(
                "appLoader", "java.base", "17.0.1",
                "java.lang.String", "valueOf", "String.java", 3456);
        String json = MAPPER.writeValueAsString(orig);
        StackTraceElement result = MAPPER.readValue(json, StackTraceElement.class);
        assertEquals(orig, result);
    }

    @Test
    public void testRoundTripFromActualException() throws Exception
    {
        Exception ex;
        try {
            throw new RuntimeException("test");
        } catch (RuntimeException e) {
            ex = e;
        }
        StackTraceElement[] origTrace = ex.getStackTrace();
        assertNotNull(origTrace);
        assertTrue(origTrace.length > 0);

        // Serialize first element
        StackTraceElement first = origTrace[0];
        String json = MAPPER.writeValueAsString(first);
        StackTraceElement result = MAPPER.readValue(json, StackTraceElement.class);

        assertEquals(first.getClassName(), result.getClassName());
        assertEquals(first.getMethodName(), result.getMethodName());
        assertEquals(first.getFileName(), result.getFileName());
        assertEquals(first.getLineNumber(), result.getLineNumber());
    }

    /*
    /**********************************************************************
    /* Tests for array of StackTraceElements
    /**********************************************************************
     */

    @Test
    public void testStackTraceArray() throws Exception
    {
        String json = a2q("[{'className':'A','methodName':'a','fileName':'A.java','lineNumber':1},"
                + "{'className':'B','methodName':'b','fileName':'B.java','lineNumber':2}]");
        StackTraceElement[] result = MAPPER.readValue(json, StackTraceElement[].class);
        assertNotNull(result);
        assertEquals(2, result.length);
        assertEquals("A", result[0].getClassName());
        assertEquals("B", result[1].getClassName());
    }

    @Test
    public void testEmptyStackTraceArray() throws Exception
    {
        StackTraceElement[] result = MAPPER.readValue("[]", StackTraceElement[].class);
        assertNotNull(result);
        assertEquals(0, result.length);
    }

    @Test
    public void testStackTraceInHolder() throws Exception
    {
        String json = a2q("{'trace':["
                + "{'className':'X','methodName':'doX','fileName':'X.java','lineNumber':99}"
                + "]}");
        StackTraceHolder result = MAPPER.readValue(json, StackTraceHolder.class);
        assertNotNull(result);
        assertNotNull(result.trace);
        assertEquals(1, result.trace.length);
        assertEquals("X", result.trace[0].getClassName());
        assertEquals(99, result.trace[0].getLineNumber());
    }

    /*
    /**********************************************************************
    /* Tests for edge cases
    /**********************************************************************
     */

    @Test
    public void testUnknownPropertiesIgnored() throws Exception
    {
        // Adapter class has fields like 'declaringClass', 'nativeMethod', 'format'
        // that don't map to StackTraceElement constructor args directly
        String json = a2q("{'className':'Test','methodName':'m','fileName':'T.java',"
                + "'lineNumber':5,'nativeMethod':true,'declaringClass':'Test',"
                + "'format':'some format'}");
        StackTraceElement result = MAPPER.readValue(json, StackTraceElement.class);
        assertNotNull(result);
        assertEquals("Test", result.getClassName());
    }

    @Test
    public void testLineNumberAsString() throws Exception
    {
        // Some formats may provide lineNumber as a String
        String json = a2q("{'className':'Cls','methodName':'m','fileName':'Cls.java','lineNumber':'77'}");
        StackTraceElement result = MAPPER.readValue(json, StackTraceElement.class);
        assertNotNull(result);
        assertEquals(77, result.getLineNumber());
    }

    @Test
    public void testSingleValueArrayUnwrap() throws Exception
    {
        ObjectReader r = MAPPER.readerFor(StackTraceElement.class)
                .with(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS);
        String json = a2q("[{'className':'W','methodName':'wrap','fileName':'W.java','lineNumber':1}]");
        StackTraceElement result = r.readValue(json);
        assertNotNull(result);
        assertEquals("W", result.getClassName());
    }

    /*
    /**********************************************************************
    /* Tests for [databind#429]: mix-in @JsonProperty support
    /**********************************************************************
     */

    // [databind#429]
    @Test
    public void testDeserWithMixInPropertyNames() throws Exception
    {
        ObjectMapper mapper = JsonMapper.builder()
                .addMixIn(StackTraceElement.class, StackTraceElementMixIn.class)
                .enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
                .build();

        String json = a2q("{'class':'com.example.Foo','method':'doStuff',"
                + "'file':'Foo.java','line':42}");
        StackTraceElement result = mapper.readValue(json, StackTraceElement.class);
        assertNotNull(result);
        assertEquals("com.example.Foo", result.getClassName());
        assertEquals("doStuff", result.getMethodName());
        assertEquals("Foo.java", result.getFileName());
        assertEquals(42, result.getLineNumber());
    }

    // [databind#429]
    @Test
    public void testRoundTripWithMixIn() throws Exception
    {
        ObjectMapper mapper = JsonMapper.builder()
                .addMixIn(StackTraceElement.class, StackTraceElementMixIn.class)
                .build();

        StackTraceElement orig = new StackTraceElement(
                "appLoader", "java.base", "17.0.1",
                "java.lang.String", "valueOf", "String.java", 3456);

        String json = mapper.writeValueAsString(orig);
        // Verify serialization uses mix-in names
        assertTrue(json.contains("\"class\""));
        assertTrue(json.contains("\"method\""));
        assertTrue(json.contains("\"file\""));
        assertTrue(json.contains("\"line\""));

        // Verify deserialization with mix-in names
        StackTraceElement result = mapper.readValue(json, StackTraceElement.class);
        assertEquals(orig.getClassName(), result.getClassName());
        assertEquals(orig.getMethodName(), result.getMethodName());
        assertEquals(orig.getFileName(), result.getFileName());
        assertEquals(orig.getLineNumber(), result.getLineNumber());
    }

    // [databind#429]: ensure standard (no mix-in) deserialization still works
    @Test
    public void testStandardDeserUnaffectedByMixInFeature() throws Exception
    {
        // Use the default mapper without any mix-ins
        String json = a2q("{'className':'com.example.Bar','methodName':'run',"
                + "'fileName':'Bar.java','lineNumber':100}");
        StackTraceElement result = MAPPER.readValue(json, StackTraceElement.class);
        assertNotNull(result);
        assertEquals("com.example.Bar", result.getClassName());
        assertEquals("run", result.getMethodName());
        assertEquals("Bar.java", result.getFileName());
        assertEquals(100, result.getLineNumber());
    }

    // [databind#429]
    @Test
    public void testStackTraceElementWithCustom() throws Exception
    {
        // first, via bean that contains StackTraceElement
        StackTraceBean bean = MAPPER.readValue(a2q("{'Location':'foobar'}"),
                StackTraceBean.class);
        assertNotNull(bean.location);
        assertEquals(StackTraceBean.NUM, bean.location.getLineNumber());

        // and then directly, iff registered
        ObjectMapper mapper = jsonMapperBuilder()
                .addModule(new SimpleModule()
                        .addDeserializer(StackTraceElement.class, new MyStackTraceElementDeserializer()))
                .build();
        StackTraceElement elem = mapper.readValue("123", StackTraceElement.class);
        assertNotNull(elem);
        assertEquals(StackTraceBean.NUM, elem.getLineNumber());

        // and finally, even as part of real exception

        IOException ioe = mapper.readValue(a2q("{'stackTrace':[ 123, 456 ]}"),
                IOException.class);
        assertNotNull(ioe);
        StackTraceElement[] traces = ioe.getStackTrace();
        assertNotNull(traces);
        assertEquals(2, traces.length);
        assertEquals(StackTraceBean.NUM, traces[0].getLineNumber());
        assertEquals(StackTraceBean.NUM, traces[1].getLineNumber());
    }

}