Skip to content

Commit

Permalink
Add support for generating IonSexps (#242)
Browse files Browse the repository at this point in the history
Add writeStartSexp and writeEndSexp methods to the IonGenerator. This requires extending the
JsonWriteContext into a new class that recognizes the sexp context.
  • Loading branch information
jhhladky committed Feb 18, 2021
1 parent 7b6af88 commit 0231777
Show file tree
Hide file tree
Showing 4 changed files with 298 additions and 1 deletion.
Expand Up @@ -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;
Expand Down Expand Up @@ -456,12 +458,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:
Expand All @@ -482,6 +492,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)
Expand Down Expand Up @@ -513,6 +528,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
Expand Down
@@ -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
* <p>
* 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();
}
}
@@ -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<SexpObject> {
@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();
}
}
}
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
}
}

0 comments on commit 0231777

Please sign in to comment.