Skip to content

Add support for generating IonSexps #242

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
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 @@ -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:
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
Original file line number Diff line number Diff line change
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();
}
}