Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Add new line at the end of Jersey generated JSON

I have a Jersey (1.x) based REST service. It uses Jackson 2.4.4 to generate JSON responses. I need to add a newline character at the end of response (cURL users complain that there's no new line in responses). I am using Jersey pretty-print feature (SerializationFeature.INDENT_OUTPUT).

current: {\n "prop" : "value"\n}

wanted: {\n "prop" : "value"\n}\n

  1. I tried using a custom serializer. I need to add \n only at the end of the root object. Serializer is defined per data type, which means, if an instance of such class is nested in a response, I will get \n in the middle of my JSON.

  2. I thought of subclassing com.fasterxml.jackson.core.JsonGenerator.java, overriding close() where i'd add writeRaw('\n'), but that feels very hacky.

  3. Another idea would be to add Servlet filter which would re-write the response from Jersey Filter, adding the \n and incrementing the contentLenght by 1. Seems not only hacky, but also inefficient.

  4. I could also give up Jersey taking care of serializing the content and do ObjectMapper.writeValue() + "\n", but this is quite intrusive to my code (need to change many places).

What is the clean solution for that problem?

I have found these threads for the same problem, but none of them provides solution:

  • http://markmail.org/message/nj4aqheqobmt4o5c
  • http://jackson-users.ning.com/forum/topics/add-newline-after-object-serialization-in-jersey

Update

Finally I went for @arachnid's solution with NewlineAddingPrettyPrinter (also bumper Jackson version to 2.6.2). Sadly, it does not work out of the box with Jaskson as JAX-RS Json provider. Changed PrettyPrinter in ObjectMapper does not get propagated to JsonGenerator (see here why). To make it work, I had to add ResponseFilter which adds ObjectWriterModifier (now I can easily toggle between pretty-print and minimal, based on input param ):

@Provider
public class PrettyPrintFilter extends BaseResponseFilter {

    public ContainerResponse filter(ContainerRequest request, ContainerResponse response) {
        ObjectWriterInjector.set(new PrettyPrintToggler(true));
        return response;
    }

    final class PrettyPrintToggler extends ObjectWriterModifier {

        private static final PrettyPrinter NO_PRETTY_PRINT = new MinimalPrettyPrinter();

        private final boolean usePrettyPrint;

        public PrettyPrintToggler(boolean usePrettyPrint) {
            this.usePrettyPrint = usePrettyPrint;
        }

        @Override
        public ObjectWriter modify(EndpointConfigBase<?> endpoint, MultivaluedMap<String, Object> responseHeaders,
                                   Object valueToWrite, ObjectWriter w, JsonGenerator g) throws IOException {
            if (usePrettyPrint) g.setPrettyPrinter(new NewlineAddingPrettyPrinter());
            else g.setPrettyPrinter(NO_PRETTY_PRINT);
            return w;
        }

    }
}
like image 575
botchniaque Avatar asked Oct 09 '15 14:10

botchniaque


1 Answers

Actually, wrapping up (not subclassing) JsonGenerator isn't too bad:

public static final class NewlineAddingJsonFactory extends JsonFactory {
    @Override
    protected JsonGenerator _createGenerator(Writer out, IOContext ctxt) throws IOException {
        return new NewlineAddingJsonGenerator(super._createGenerator(out, ctxt));
    }

    @Override
    protected JsonGenerator _createUTF8Generator(OutputStream out, IOContext ctxt) throws IOException {
        return new NewlineAddingJsonGenerator(super._createUTF8Generator(out, ctxt));
    }
}

public static final class NewlineAddingJsonGenerator extends JsonGenerator {
    private final JsonGenerator underlying;
    private int depth = 0;

    public NewlineAddingJsonGenerator(JsonGenerator underlying) {
        this.underlying = underlying;
    }

    @Override
    public void writeStartObject() throws IOException {
        underlying.writeStartObject();
        ++depth;
    }

    @Override
    public void writeEndObject() throws IOException {
        underlying.writeEndObject();
        if (--depth == 0) {
            underlying.writeRaw('\n');
        }
    }

    // ... and delegate all the other methods of JsonGenerator (CGLIB can hide this if you put in some time)
}


@Test
public void append_newline_after_end_of_json() throws Exception {
    ObjectWriter writer = new ObjectMapper(new NewlineAddingJsonFactory()).writer();
    assertThat(writer.writeValueAsString(ImmutableMap.of()), equalTo("{}\n"));
    assertThat(writer.writeValueAsString(ImmutableMap.of("foo", "bar")), equalTo("{\"foo\":\"bar\"}\n"));
}

A servlet filter isn't necessarily too bad either, although recently the ServletOutputStream interface has been more involved to intercept properly.

I found doing this via PrettyPrinter problematic on earlier Jackson versions (such as your 2.4.4), in part because of the need to go through an ObjectWriter to configure it properly: only fixed in Jackson 2.6. For completeness, this is a working 2.5 solution:

@Test
public void append_newline_after_end_of_json() throws Exception {
    // Jackson 2.6:
//      ObjectMapper mapper = new ObjectMapper()
//              .setDefaultPrettyPrinter(new NewlineAddingPrettyPrinter())
//              .enable(SerializationFeature.INDENT_OUTPUT);
//      ObjectWriter writer = mapper.writer();

    ObjectMapper mapper = new ObjectMapper();
    ObjectWriter writer = mapper.writer().with(new NewlineAddingPrettyPrinter());
    assertThat(writer.writeValueAsString(ImmutableMap.of()), equalTo("{}\n"));
    assertThat(writer.writeValueAsString(ImmutableMap.of("foo", "bar")),
            equalTo("{\"foo\":\"bar\"}\n"));
}

public static final class NewlineAddingPrettyPrinter
                    extends MinimalPrettyPrinter
                    implements Instantiatable<PrettyPrinter> {
    private int depth = 0;

    @Override
    public void writeStartObject(JsonGenerator jg) throws IOException, JsonGenerationException {
        super.writeStartObject(jg);
        ++depth;
    }

    @Override
    public void writeEndObject(JsonGenerator jg, int nrOfEntries) throws IOException, JsonGenerationException {
        super.writeEndObject(jg, nrOfEntries);
        if (--depth == 0) {
            jg.writeRaw('\n');
        }
    }

    @Override
    public PrettyPrinter createInstance() {
        return new NewlineAddingPrettyPrinter();
    }
}
like image 55
araqnid Avatar answered Sep 28 '22 07:09

araqnid