diff --git a/ion/src/main/java/com/fasterxml/jackson/dataformat/ion/IonGenerator.java b/ion/src/main/java/com/fasterxml/jackson/dataformat/ion/IonGenerator.java index e94ccb9b7..4c3b61c1f 100644 --- a/ion/src/main/java/com/fasterxml/jackson/dataformat/ion/IonGenerator.java +++ b/ion/src/main/java/com/fasterxml/jackson/dataformat/ion/IonGenerator.java @@ -135,6 +135,8 @@ public IonGenerator(int jsonFeatures, final int ionFeatures, ObjectCodec codec, IonWriter ion, boolean ionWriterIsManaged, IOContext ctxt, Closeable dst) { super(jsonFeatures, codec); + // Overwrite the writecontext with our own implementation + _writeContext = IonWriteContext.createRootContext(_writeContext.getDupDetector()); _formatFeatures = ionFeatures; _writer = ion; _ionWriterIsManaged = ionWriterIsManaged; @@ -458,12 +460,20 @@ protected void _verifyValueWrite(String msg) throws IOException, JsonGenerationE case JsonWriteContext.STATUS_OK_AFTER_SPACE: _cfgPrettyPrinter.writeRootValueSeparator(this); break; + case IonWriteContext.STATUS_OK_AFTER_SEXP_SEPARATOR: + // Special handling of sexp value separators can be added later. Root value + // separator will be whitespace which is sufficient to separate sexp values + _cfgPrettyPrinter.writeRootValueSeparator(this); + break; case JsonWriteContext.STATUS_OK_AS_IS: // First entry, but of which context? if (_writeContext.inArray()) { _cfgPrettyPrinter.beforeArrayValues(this); } else if (_writeContext.inObject()) { _cfgPrettyPrinter.beforeObjectEntries(this); + } else if(((IonWriteContext) _writeContext).inSexp()) { + // Format sexps like arrays + _cfgPrettyPrinter.beforeArrayValues(this); } break; default: @@ -484,6 +494,11 @@ public void writeEndObject() throws IOException, JsonGenerationException { _writer.stepOut(); } + public void writeEndSexp() throws IOException, JsonGenerationException { + _writeContext = _writeContext.getParent(); + _writer.stepOut(); + } + @Override public void writeFieldName(String value) throws IOException, JsonGenerationException { //This call to _writeContext is copied from Jackson's UTF8JsonGenerator.writeFieldName(String) @@ -515,6 +530,12 @@ public void writeStartObject() throws IOException, JsonGenerationException { _writer.stepIn(IonType.STRUCT); } + public void writeStartSexp() throws IOException, JsonGenerationException { + _verifyValueWrite("start a sexp"); // <-- copied from UTF8JsonGenerator + _writeContext = ((IonWriteContext) _writeContext).createChildSexpContext(); + _writer.stepIn(IonType.SEXP); + } + /* /***************************************************************** /* Support for type ids diff --git a/ion/src/main/java/com/fasterxml/jackson/dataformat/ion/IonWriteContext.java b/ion/src/main/java/com/fasterxml/jackson/dataformat/ion/IonWriteContext.java new file mode 100644 index 000000000..450a3e30a --- /dev/null +++ b/ion/src/main/java/com/fasterxml/jackson/dataformat/ion/IonWriteContext.java @@ -0,0 +1,103 @@ +/* + * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at: + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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.fasterxml.jackson.dataformat.ion; + +import com.fasterxml.jackson.core.json.DupDetector; +import com.fasterxml.jackson.core.json.JsonWriteContext; + +/** + * Extension of JsonWriteContexts that recognizes sexps + *

+ * The JsonWriteContext is used by the pretty printer for handling of the whitespace between tokens, + * and by the generator for verifying whether it's valid to write a given token. The writeStartSexp + * method in the IonGenerator will enter a "sexp context", so we need a new state in the write + * context to track that. Sexp handling is modeled after arrays. + */ +public class IonWriteContext extends JsonWriteContext { + // Both contstants are in the tens instead of the ones to avoid conflict with the native + // Jackson ones + + // Ion-specific contexts + protected final static int TYPE_SEXP = 30; + + // Ion-specific statuses + public final static int STATUS_OK_AFTER_SEXP_SEPARATOR = 60; + + protected IonWriteContext(int type, IonWriteContext parent, DupDetector dups) { + super(type, parent, dups); + } + + public static IonWriteContext createRootContext(DupDetector dd) { + return new IonWriteContext(TYPE_ROOT, null, dd); + } + + public IonWriteContext createChildSexpContext() { + IonWriteContext ctxt = (IonWriteContext) _child; + + if(ctxt == null) { + // same assignment as in createChildObjectContext, createChildArrayContext + _child = ctxt = new IonWriteContext(TYPE_SEXP, this, (_dups == null) ? null : _dups.child()); + } + + // reset returns this, OK to cast + return (IonWriteContext) ctxt.reset(TYPE_SEXP); + } + + public final boolean inSexp() { + return _type == TYPE_SEXP; + } + + // // Overrides + + // We have to override the two createChild*Context methods to return a IonWriteContext + // instead of a JsonWriteContext so sexps can be arbitrarily embedded in ion. Otherwise we + // would only be able to create them as top level values. + // Two methods below are copied from JsonWriteContext + + @Override + public IonWriteContext createChildArrayContext() { + IonWriteContext ctxt = (IonWriteContext) _child; + + if (ctxt == null) { + _child = ctxt = new IonWriteContext(TYPE_ARRAY, this, (_dups == null) ? null : _dups.child()); + return ctxt; + } + + return (IonWriteContext) ctxt.reset(TYPE_ARRAY); + } + + @Override + public IonWriteContext createChildObjectContext() { + IonWriteContext ctxt = (IonWriteContext) _child; + + if (ctxt == null) { + _child = ctxt = new IonWriteContext(TYPE_OBJECT, this, (_dups == null) ? null : _dups.child()); + return ctxt; + } + return (IonWriteContext) ctxt.reset(TYPE_OBJECT); + } + + @Override + public int writeValue() { + // Add special handling for sexp separator + if(_type == TYPE_SEXP) { + int ix = _index; + ++_index; + return (ix < 0) ? STATUS_OK_AS_IS : STATUS_OK_AFTER_SEXP_SEPARATOR; + } + + return super.writeValue(); + } +} diff --git a/ion/src/test/java/com/fasterxml/jackson/dataformat/ion/GenerateSexpTest.java b/ion/src/test/java/com/fasterxml/jackson/dataformat/ion/GenerateSexpTest.java new file mode 100644 index 000000000..f6fb0b98e --- /dev/null +++ b/ion/src/test/java/com/fasterxml/jackson/dataformat/ion/GenerateSexpTest.java @@ -0,0 +1,151 @@ +/* + * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at: + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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.fasterxml.jackson.dataformat.ion; + +import com.amazon.ion.IonSexp; +import com.amazon.ion.IonSystem; +import com.amazon.ion.IonWriter; +import com.amazon.ion.system.IonSystemBuilder; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +/** + * End to end test verifying we can serialize sexps + */ +public class GenerateSexpTest { + + private IonSystem ionSystem; + private IonObjectMapper mapper; + + @Before + public void setup() { + this.ionSystem = IonSystemBuilder.standard().build(); + this.mapper = new IonObjectMapper(new IonFactory(null, ionSystem)); + } + + @Test + public void topLevel() throws IOException { + Assert.assertEquals( + ionSystem.singleValue("(foo \"bar\")"), + mapper.writeValueAsIonValue(new SexpObject("foo", "bar"))); + } + + @Test + public void inList() throws IOException { + Assert.assertEquals( + ionSystem.singleValue("[(foo \"bar\"), (baz \"qux\")]"), + mapper.writeValueAsIonValue( + Arrays.asList(new SexpObject("foo", "bar"), new SexpObject("baz", "qux")))); + } + + @Test + public void inObject() throws IOException { + Assert.assertEquals( + ionSystem.singleValue("{sexpField:(foo \"bar\")}"), + mapper.writeValueAsIonValue(new SexpObjectContainer(new SexpObject("foo", "bar")))); + } + + @Test + public void inOtherSexp() throws IOException { + Assert.assertEquals( + ionSystem.singleValue("(foo (bar \"baz\"))"), + mapper.writeValueAsIonValue(new SexpObject("foo", new SexpObject("bar", "baz")))); + } + + @Test + public void generatorUsedInStreamingWriteText() throws IOException { + Assert.assertArrayEquals("(foo 0)".getBytes(), toBytes(new SexpObject("foo", 0), mapper)); + } + + @Test + public void generatorUsedInStreamingWriteBinary() throws IOException { + byte[] expectedBytes = null; + + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); + IonWriter writer = ionSystem.newBinaryWriter(baos)) { + ionSystem.singleValue("(foo 0)").writeTo(writer); + writer.finish(); + expectedBytes = baos.toByteArray(); + } + + mapper.setCreateBinaryWriters(true); + Assert.assertArrayEquals(expectedBytes, toBytes(new SexpObject("foo", 0), mapper)); + } + + private byte[] toBytes(Object object, IonObjectMapper mapper) throws IOException { + byte[] bytes = null; + + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + mapper.writeValue(baos, object); + bytes = baos.toByteArray(); + } + + return bytes; + } + + private static class SexpObjectContainer { + private SexpObject sexpField; + + SexpObjectContainer(SexpObject sexpField) { + this.sexpField = sexpField; + } + + public SexpObject getSexpField() { + return sexpField; + } + } + + // Create some pojo that defines a custom serializer that creates an IonSexp + @JsonSerialize(using=SexpObjectSerializer.class) + private static class SexpObject { + private String symbolField; + private Object objectField; + + SexpObject(String symbolField, Object objectField) { + this.symbolField = symbolField; + this.objectField = objectField; + } + + public String getSymbolField() { + return symbolField; + } + + public Object getObjectField() { + return objectField; + } + } + + private static class SexpObjectSerializer extends JsonSerializer { + @Override + public void serialize(SexpObject value, JsonGenerator jsonGenerator, + SerializerProvider provider) throws IOException { + final IonGenerator ionGenerator = (IonGenerator) jsonGenerator; + + ionGenerator.writeStartSexp(); + ionGenerator.writeSymbol(value.getSymbolField()); + ionGenerator.writeObject(value.getObjectField()); + ionGenerator.writeEndSexp(); + } + } +} diff --git a/ion/src/test/java/com/fasterxml/jackson/dataformat/ion/IonGeneratorTest.java b/ion/src/test/java/com/fasterxml/jackson/dataformat/ion/IonGeneratorTest.java index 0fd2915fc..fe698dc51 100644 --- a/ion/src/test/java/com/fasterxml/jackson/dataformat/ion/IonGeneratorTest.java +++ b/ion/src/test/java/com/fasterxml/jackson/dataformat/ion/IonGeneratorTest.java @@ -14,8 +14,11 @@ package com.fasterxml.jackson.dataformat.ion; +import org.junit.Rule; import org.junit.Test; import org.junit.Before; +import org.junit.rules.ExpectedException; +import com.fasterxml.jackson.core.JsonGenerationException; import com.fasterxml.jackson.databind.JsonNode; import com.amazon.ion.IonDatagram; @@ -56,9 +59,12 @@ public class IonGeneratorTest { private IonValue testObjectIon; private JsonNode testObjectTree; + @Rule + public ExpectedException thrown = ExpectedException.none(); + @Before public void setUp() throws Exception { - final IonFactory factory = new IonFactory(); + final IonFactory factory = new IonFactory(); this.joiObjectMapper = IonObjectMapper.builder(factory).build(); this.ionSystem = IonSystemBuilder.standard().build(); @@ -113,4 +119,20 @@ public void testTreeWriteVerifiesOnce() throws Exception { final IonStruct struct = (IonStruct) output.get(0); assertThat(struct.get(FIELD), is(testObjectIon)); } + + @Test + public void testWriteFieldNameFailsInSexp() throws Exception { + joiGenerator.writeStartSexp(); + thrown.expect(JsonGenerationException.class); + thrown.expectMessage("Can not write a field name, expecting a value"); + joiGenerator.writeFieldName("foo"); + } + + @Test + public void testWriteStartSexpFailsWithoutWriteFieldName() throws Exception { + joiGenerator.writeStartObject(); + thrown.expect(JsonGenerationException.class); + thrown.expectMessage("Can not start a sexp, expecting field name"); + joiGenerator.writeStartSexp(); + } }