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