Skip to content

Commit 0231777

Browse files
authored
Add support for generating IonSexps (#242)
Add writeStartSexp and writeEndSexp methods to the IonGenerator. This requires extending the JsonWriteContext into a new class that recognizes the sexp context.
1 parent 7b6af88 commit 0231777

File tree

4 files changed

+298
-1
lines changed

4 files changed

+298
-1
lines changed

ion/src/main/java/com/fasterxml/jackson/dataformat/ion/IonGenerator.java

+21
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,8 @@ public IonGenerator(int jsonFeatures, final int ionFeatures, ObjectCodec codec,
135135
IonWriter ion, boolean ionWriterIsManaged, IOContext ctxt, Closeable dst)
136136
{
137137
super(jsonFeatures, codec);
138+
// Overwrite the writecontext with our own implementation
139+
_writeContext = IonWriteContext.createRootContext(_writeContext.getDupDetector());
138140
_formatFeatures = ionFeatures;
139141
_writer = ion;
140142
_ionWriterIsManaged = ionWriterIsManaged;
@@ -456,12 +458,20 @@ protected void _verifyValueWrite(String msg) throws IOException, JsonGenerationE
456458
case JsonWriteContext.STATUS_OK_AFTER_SPACE:
457459
_cfgPrettyPrinter.writeRootValueSeparator(this);
458460
break;
461+
case IonWriteContext.STATUS_OK_AFTER_SEXP_SEPARATOR:
462+
// Special handling of sexp value separators can be added later. Root value
463+
// separator will be whitespace which is sufficient to separate sexp values
464+
_cfgPrettyPrinter.writeRootValueSeparator(this);
465+
break;
459466
case JsonWriteContext.STATUS_OK_AS_IS:
460467
// First entry, but of which context?
461468
if (_writeContext.inArray()) {
462469
_cfgPrettyPrinter.beforeArrayValues(this);
463470
} else if (_writeContext.inObject()) {
464471
_cfgPrettyPrinter.beforeObjectEntries(this);
472+
} else if(((IonWriteContext) _writeContext).inSexp()) {
473+
// Format sexps like arrays
474+
_cfgPrettyPrinter.beforeArrayValues(this);
465475
}
466476
break;
467477
default:
@@ -482,6 +492,11 @@ public void writeEndObject() throws IOException, JsonGenerationException {
482492
_writer.stepOut();
483493
}
484494

495+
public void writeEndSexp() throws IOException, JsonGenerationException {
496+
_writeContext = _writeContext.getParent();
497+
_writer.stepOut();
498+
}
499+
485500
@Override
486501
public void writeFieldName(String value) throws IOException, JsonGenerationException {
487502
//This call to _writeContext is copied from Jackson's UTF8JsonGenerator.writeFieldName(String)
@@ -513,6 +528,12 @@ public void writeStartObject() throws IOException, JsonGenerationException {
513528
_writer.stepIn(IonType.STRUCT);
514529
}
515530

531+
public void writeStartSexp() throws IOException, JsonGenerationException {
532+
_verifyValueWrite("start a sexp"); // <-- copied from UTF8JsonGenerator
533+
_writeContext = ((IonWriteContext) _writeContext).createChildSexpContext();
534+
_writer.stepIn(IonType.SEXP);
535+
}
536+
516537
/*
517538
/*****************************************************************
518539
/* Support for type ids
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/*
2+
* Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at:
7+
*
8+
* http://aws.amazon.com/apache2.0/
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific
12+
* language governing permissions and limitations under the License.
13+
*/
14+
15+
package com.fasterxml.jackson.dataformat.ion;
16+
17+
import com.fasterxml.jackson.core.json.DupDetector;
18+
import com.fasterxml.jackson.core.json.JsonWriteContext;
19+
20+
/**
21+
* Extension of JsonWriteContexts that recognizes sexps
22+
* <p>
23+
* The JsonWriteContext is used by the pretty printer for handling of the whitespace between tokens,
24+
* and by the generator for verifying whether it's valid to write a given token. The writeStartSexp
25+
* method in the IonGenerator will enter a "sexp context", so we need a new state in the write
26+
* context to track that. Sexp handling is modeled after arrays.
27+
*/
28+
public class IonWriteContext extends JsonWriteContext {
29+
// Both contstants are in the tens instead of the ones to avoid conflict with the native
30+
// Jackson ones
31+
32+
// Ion-specific contexts
33+
protected final static int TYPE_SEXP = 30;
34+
35+
// Ion-specific statuses
36+
public final static int STATUS_OK_AFTER_SEXP_SEPARATOR = 60;
37+
38+
protected IonWriteContext(int type, IonWriteContext parent, DupDetector dups) {
39+
super(type, parent, dups);
40+
}
41+
42+
public static IonWriteContext createRootContext(DupDetector dd) {
43+
return new IonWriteContext(TYPE_ROOT, null, dd);
44+
}
45+
46+
public IonWriteContext createChildSexpContext() {
47+
IonWriteContext ctxt = (IonWriteContext) _child;
48+
49+
if(ctxt == null) {
50+
// same assignment as in createChildObjectContext, createChildArrayContext
51+
_child = ctxt = new IonWriteContext(TYPE_SEXP, this, (_dups == null) ? null : _dups.child());
52+
}
53+
54+
// reset returns this, OK to cast
55+
return (IonWriteContext) ctxt.reset(TYPE_SEXP);
56+
}
57+
58+
public final boolean inSexp() {
59+
return _type == TYPE_SEXP;
60+
}
61+
62+
// // Overrides
63+
64+
// We have to override the two createChild*Context methods to return a IonWriteContext
65+
// instead of a JsonWriteContext so sexps can be arbitrarily embedded in ion. Otherwise we
66+
// would only be able to create them as top level values.
67+
// Two methods below are copied from JsonWriteContext
68+
69+
@Override
70+
public IonWriteContext createChildArrayContext() {
71+
IonWriteContext ctxt = (IonWriteContext) _child;
72+
73+
if (ctxt == null) {
74+
_child = ctxt = new IonWriteContext(TYPE_ARRAY, this, (_dups == null) ? null : _dups.child());
75+
return ctxt;
76+
}
77+
78+
return (IonWriteContext) ctxt.reset(TYPE_ARRAY);
79+
}
80+
81+
@Override
82+
public IonWriteContext createChildObjectContext() {
83+
IonWriteContext ctxt = (IonWriteContext) _child;
84+
85+
if (ctxt == null) {
86+
_child = ctxt = new IonWriteContext(TYPE_OBJECT, this, (_dups == null) ? null : _dups.child());
87+
return ctxt;
88+
}
89+
return (IonWriteContext) ctxt.reset(TYPE_OBJECT);
90+
}
91+
92+
@Override
93+
public int writeValue() {
94+
// Add special handling for sexp separator
95+
if(_type == TYPE_SEXP) {
96+
int ix = _index;
97+
++_index;
98+
return (ix < 0) ? STATUS_OK_AS_IS : STATUS_OK_AFTER_SEXP_SEPARATOR;
99+
}
100+
101+
return super.writeValue();
102+
}
103+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
/*
2+
* Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at:
7+
*
8+
* http://aws.amazon.com/apache2.0/
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific
12+
* language governing permissions and limitations under the License.
13+
*/
14+
15+
package com.fasterxml.jackson.dataformat.ion;
16+
17+
import com.amazon.ion.IonSexp;
18+
import com.amazon.ion.IonSystem;
19+
import com.amazon.ion.IonWriter;
20+
import com.amazon.ion.system.IonSystemBuilder;
21+
import com.fasterxml.jackson.core.JsonGenerator;
22+
import com.fasterxml.jackson.databind.JsonSerializer;
23+
import com.fasterxml.jackson.databind.SerializerProvider;
24+
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
25+
import java.io.ByteArrayOutputStream;
26+
import java.io.IOException;
27+
import java.util.Arrays;
28+
import java.util.List;
29+
import org.junit.Assert;
30+
import org.junit.Before;
31+
import org.junit.Test;
32+
33+
/**
34+
* End to end test verifying we can serialize sexps
35+
*/
36+
public class GenerateSexpTest {
37+
38+
private IonSystem ionSystem;
39+
private IonObjectMapper mapper;
40+
41+
@Before
42+
public void setup() {
43+
this.ionSystem = IonSystemBuilder.standard().build();
44+
this.mapper = new IonObjectMapper(new IonFactory(null, ionSystem));
45+
}
46+
47+
@Test
48+
public void topLevel() throws IOException {
49+
Assert.assertEquals(
50+
ionSystem.singleValue("(foo \"bar\")"),
51+
mapper.writeValueAsIonValue(new SexpObject("foo", "bar")));
52+
}
53+
54+
@Test
55+
public void inList() throws IOException {
56+
Assert.assertEquals(
57+
ionSystem.singleValue("[(foo \"bar\"), (baz \"qux\")]"),
58+
mapper.writeValueAsIonValue(
59+
Arrays.asList(new SexpObject("foo", "bar"), new SexpObject("baz", "qux"))));
60+
}
61+
62+
@Test
63+
public void inObject() throws IOException {
64+
Assert.assertEquals(
65+
ionSystem.singleValue("{sexpField:(foo \"bar\")}"),
66+
mapper.writeValueAsIonValue(new SexpObjectContainer(new SexpObject("foo", "bar"))));
67+
}
68+
69+
@Test
70+
public void inOtherSexp() throws IOException {
71+
Assert.assertEquals(
72+
ionSystem.singleValue("(foo (bar \"baz\"))"),
73+
mapper.writeValueAsIonValue(new SexpObject("foo", new SexpObject("bar", "baz"))));
74+
}
75+
76+
@Test
77+
public void generatorUsedInStreamingWriteText() throws IOException {
78+
Assert.assertArrayEquals("(foo 0)".getBytes(), toBytes(new SexpObject("foo", 0), mapper));
79+
}
80+
81+
@Test
82+
public void generatorUsedInStreamingWriteBinary() throws IOException {
83+
byte[] expectedBytes = null;
84+
85+
try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
86+
IonWriter writer = ionSystem.newBinaryWriter(baos)) {
87+
ionSystem.singleValue("(foo 0)").writeTo(writer);
88+
writer.finish();
89+
expectedBytes = baos.toByteArray();
90+
}
91+
92+
mapper.setCreateBinaryWriters(true);
93+
Assert.assertArrayEquals(expectedBytes, toBytes(new SexpObject("foo", 0), mapper));
94+
}
95+
96+
private byte[] toBytes(Object object, IonObjectMapper mapper) throws IOException {
97+
byte[] bytes = null;
98+
99+
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
100+
mapper.writeValue(baos, object);
101+
bytes = baos.toByteArray();
102+
}
103+
104+
return bytes;
105+
}
106+
107+
private static class SexpObjectContainer {
108+
private SexpObject sexpField;
109+
110+
SexpObjectContainer(SexpObject sexpField) {
111+
this.sexpField = sexpField;
112+
}
113+
114+
public SexpObject getSexpField() {
115+
return sexpField;
116+
}
117+
}
118+
119+
// Create some pojo that defines a custom serializer that creates an IonSexp
120+
@JsonSerialize(using=SexpObjectSerializer.class)
121+
private static class SexpObject {
122+
private String symbolField;
123+
private Object objectField;
124+
125+
SexpObject(String symbolField, Object objectField) {
126+
this.symbolField = symbolField;
127+
this.objectField = objectField;
128+
}
129+
130+
public String getSymbolField() {
131+
return symbolField;
132+
}
133+
134+
public Object getObjectField() {
135+
return objectField;
136+
}
137+
}
138+
139+
private static class SexpObjectSerializer extends JsonSerializer<SexpObject> {
140+
@Override
141+
public void serialize(SexpObject value, JsonGenerator jsonGenerator,
142+
SerializerProvider provider) throws IOException {
143+
final IonGenerator ionGenerator = (IonGenerator) jsonGenerator;
144+
145+
ionGenerator.writeStartSexp();
146+
ionGenerator.writeSymbol(value.getSymbolField());
147+
ionGenerator.writeObject(value.getObjectField());
148+
ionGenerator.writeEndSexp();
149+
}
150+
}
151+
}

ion/src/test/java/com/fasterxml/jackson/dataformat/ion/IonGeneratorTest.java

+23-1
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,11 @@
1414

1515
package com.fasterxml.jackson.dataformat.ion;
1616

17+
import org.junit.Rule;
1718
import org.junit.Test;
1819
import org.junit.Before;
20+
import org.junit.rules.ExpectedException;
21+
import com.fasterxml.jackson.core.JsonGenerationException;
1922
import com.fasterxml.jackson.databind.JsonNode;
2023

2124
import com.amazon.ion.IonDatagram;
@@ -56,9 +59,12 @@ public class IonGeneratorTest {
5659
private IonValue testObjectIon;
5760
private JsonNode testObjectTree;
5861

62+
@Rule
63+
public ExpectedException thrown = ExpectedException.none();
64+
5965
@Before
6066
public void setUp() throws Exception {
61-
final IonFactory factory = new IonFactory();
67+
final IonFactory factory = new IonFactory();
6268

6369
this.joiObjectMapper = IonObjectMapper.builder(factory).build();
6470
this.ionSystem = IonSystemBuilder.standard().build();
@@ -113,4 +119,20 @@ public void testTreeWriteVerifiesOnce() throws Exception {
113119
final IonStruct struct = (IonStruct) output.get(0);
114120
assertThat(struct.get(FIELD), is(testObjectIon));
115121
}
122+
123+
@Test
124+
public void testWriteFieldNameFailsInSexp() throws Exception {
125+
joiGenerator.writeStartSexp();
126+
thrown.expect(JsonGenerationException.class);
127+
thrown.expectMessage("Can not write a field name, expecting a value");
128+
joiGenerator.writeFieldName("foo");
129+
}
130+
131+
@Test
132+
public void testWriteStartSexpFailsWithoutWriteFieldName() throws Exception {
133+
joiGenerator.writeStartObject();
134+
thrown.expect(JsonGenerationException.class);
135+
thrown.expectMessage("Can not start a sexp, expecting field name");
136+
joiGenerator.writeStartSexp();
137+
}
116138
}

0 commit comments

Comments
 (0)