/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License 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 org.apache.avro.io;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.util.BitSet;
import org.apache.avro.AvroTypeException;
import org.apache.avro.Schema;
import org.apache.avro.io.parsing.JsonGrammarGenerator;
import org.apache.avro.io.parsing.Parser;
import org.apache.avro.io.parsing.Symbol;
import org.apache.avro.util.Utf8;
import org.codehaus.jackson.JsonEncoding;
import org.codehaus.jackson.JsonFactory;
import org.codehaus.jackson.JsonGenerator;
import org.codehaus.jackson.util.MinimalPrettyPrinter;
/** An {@link Encoder} for Avro's JSON data encoding.
* </p>
* Construct using {@link EncoderFactory}.
* </p>
* JsonEncoder buffers output, and data may not appear on the output
* until {@link Encoder#flush()} is called.
* </p>
* JsonEncoder is not thread-safe.
* */
public class HackedJsonEncoder extends ParsingEncoder implements Parser.ActionHandler {
final Parser parser;
private JsonGenerator out;
/**
* Has anything been written into the collections?
*/
protected BitSet isEmpty = new BitSet();
public HackedJsonEncoder(Schema sc, OutputStream out) throws IOException {
this(sc, getJsonGenerator(out));
}
HackedJsonEncoder(Schema sc, JsonGenerator out) throws IOException {
configure(out);
this.parser =
new Parser(new JsonGrammarGenerator().generate(sc), this);
}
@Override
public void flush() throws IOException {
parser.processImplicitActions();
if (out != null) {
out.flush();
}
}
// by default, one object per line
private static JsonGenerator getJsonGenerator(OutputStream out)
throws IOException {
if (null == out)
throw new NullPointerException("OutputStream cannot be null");
JsonGenerator g
= new JsonFactory().createJsonGenerator(out, JsonEncoding.UTF8);
MinimalPrettyPrinter pp = new MinimalPrettyPrinter();
pp.setRootValueSeparator("\n");
g.setPrettyPrinter(pp);
return g;
}
/**
* Reconfigures this JsonEncoder to use the output stream provided.
* <p/>
* If the OutputStream provided is null, a NullPointerException is thrown.
* <p/>
* Otherwise, this JsonEncoder will flush its current output and then
* reconfigure its output to use a default UTF8 JsonGenerator that writes
* to the provided OutputStream.
*
* @param out
* The OutputStream to direct output to. Cannot be null.
* @throws IOException
* @return this JsonEncoder
*/
public HackedJsonEncoder configure(OutputStream out) throws IOException {
this.configure(getJsonGenerator(out));
return this;
}
/**
* Reconfigures this JsonEncoder to output to the JsonGenerator provided.
* <p/>
* If the JsonGenerator provided is null, a NullPointerException is thrown.
* <p/>
* Otherwise, this JsonEncoder will flush its current output and then
* reconfigure its output to use the provided JsonGenerator.
*
* @param generator
* The JsonGenerator to direct output to. Cannot be null.
* @throws IOException
* @return this JsonEncoder
*/
public HackedJsonEncoder configure(JsonGenerator generator) throws IOException {
if (null == generator)
throw new NullPointerException("JsonGenerator cannot be null");
if (null != parser) {
flush();
}
this.out = generator;
return this;
}
@Override
public void writeNull() throws IOException {
parser.advance(Symbol.NULL);
out.writeNull();
}
@Override
public void writeBoolean(boolean b) throws IOException {
parser.advance(Symbol.BOOLEAN);
out.writeBoolean(b);
}
@Override
public void writeInt(int n) throws IOException {
parser.advance(Symbol.INT);
out.writeNumber(n);
}
@Override
public void writeLong(long n) throws IOException {
parser.advance(Symbol.LONG);
out.writeNumber(n);
}
@Override
public void writeFloat(float f) throws IOException {
parser.advance(Symbol.FLOAT);
out.writeNumber(f);
}
@Override
public void writeDouble(double d) throws IOException {
parser.advance(Symbol.DOUBLE);
out.writeNumber(d);
}
@Override
public void writeString(Utf8 utf8) throws IOException {
writeString(utf8.toString());
}
@Override
public void writeString(String str) throws IOException {
parser.advance(Symbol.STRING);
if (parser.topSymbol() == Symbol.MAP_KEY_MARKER) {
parser.advance(Symbol.MAP_KEY_MARKER);
out.writeFieldName(str);
} else {
out.writeString(str);
}
}
@Override
public void writeBytes(ByteBuffer bytes) throws IOException {
if (bytes.hasArray()) {
writeBytes(bytes.array(), bytes.position(), bytes.remaining());
} else {
byte[] b = new byte[bytes.remaining()];
for (int i = 0; i < b.length; i++) {
b[i] = bytes.get();
}
writeBytes(b);
}
}
@Override
public void writeBytes(byte[] bytes, int start, int len) throws IOException {
parser.advance(Symbol.BYTES);
writeByteArray(bytes, start, len);
}
private void writeByteArray(byte[] bytes, int start, int len)
throws IOException {
out.writeString(
new String(bytes, start, len, JsonDecoder.CHARSET));
}
@Override
public void writeFixed(byte[] bytes, int start, int len) throws IOException {
parser.advance(Symbol.FIXED);
Symbol.IntCheckAction top = (Symbol.IntCheckAction) parser.popSymbol();
if (len != top.size) {
throw new AvroTypeException(
"Incorrect length for fixed binary: expected " +
top.size + " but received " + len + " bytes.");
}
writeByteArray(bytes, start, len);
}
@Override
public void writeEnum(int e) throws IOException {
parser.advance(Symbol.ENUM);
Symbol.EnumLabelsAction top = (Symbol.EnumLabelsAction) parser.popSymbol();
if (e < 0 || e >= top.size) {
throw new AvroTypeException(
"Enumeration out of range: max is " +
top.size + " but received " + e);
}
out.writeString(top.getLabel(e));
}
@Override
public void writeArrayStart() throws IOException {
parser.advance(Symbol.ARRAY_START);
out.writeStartArray();
push();
isEmpty.set(depth());
}
@Override
public void writeArrayEnd() throws IOException {
if (! isEmpty.get(pos)) {
parser.advance(Symbol.ITEM_END);
}
pop();
parser.advance(Symbol.ARRAY_END);
out.writeEndArray();
}
@Override
public void writeMapStart() throws IOException {
push();
isEmpty.set(depth());
parser.advance(Symbol.MAP_START);
out.writeStartObject();
}
@Override
public void writeMapEnd() throws IOException {
if (! isEmpty.get(pos)) {
parser.advance(Symbol.ITEM_END);
}
pop();
parser.advance(Symbol.MAP_END);
out.writeEndObject();
}
@Override
public void startItem() throws IOException {
if (! isEmpty.get(pos)) {
parser.advance(Symbol.ITEM_END);
}
super.startItem();
isEmpty.clear(depth());
}
@Override
public void writeIndex(int unionIndex) throws IOException {
parser.advance(Symbol.UNION);
Symbol.Alternative top = (Symbol.Alternative) parser.popSymbol();
Symbol symbol = top.getSymbol(unionIndex);
if(symbol != Symbol.NULL && isUnionOfNullWithSomething(top)){
// parser.pushSymbol(Symbol.UNION_END);
parser.pushSymbol(symbol);
return;
}
if (symbol != Symbol.NULL) {
out.writeStartObject();
out.writeFieldName(top.getLabel(unionIndex));
parser.pushSymbol(Symbol.UNION_END);
}
parser.pushSymbol(symbol);
}
private static boolean isUnionOfNullWithSomething(Symbol.Alternative a){
return a.size()==2 && a.findLabel("null")!=-1;
}
@Override
public Symbol doAction(Symbol input, Symbol top) throws IOException {
if (top instanceof Symbol.FieldAdjustAction) {
Symbol.FieldAdjustAction fa = (Symbol.FieldAdjustAction) top;
out.writeFieldName(fa.fname);
} else if (top == Symbol.RECORD_START) {
out.writeStartObject();
} else if (top == Symbol.RECORD_END || top == Symbol.UNION_END) {
out.writeEndObject();
} else if (top != Symbol.FIELD_END) {
throw new AvroTypeException("Unknown action symbol " + top);
}
return null;
}
}