/** * Copyright 2016 Hortonworks. * * Licensed 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 com.hortonworks.registries.schemaregistry.serde; import com.hortonworks.registries.common.Schema; import com.hortonworks.registries.schemaregistry.serde.pull.EndFieldContext; import com.hortonworks.registries.schemaregistry.serde.pull.EndRecordContext; import com.hortonworks.registries.schemaregistry.serde.pull.PullDeserializer; import com.hortonworks.registries.schemaregistry.serde.pull.PullEventContext; import com.hortonworks.registries.schemaregistry.serde.pull.StartFieldContext; import com.hortonworks.registries.schemaregistry.serde.pull.StartRecordContext; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Stack; /** * Sample {@link PullDeserializer} implementation for nested payloads */ public class SampleNestedPullDeserializer implements PullDeserializer<SchemaDetails, Schema.Field> { private final SchemaDetails schemaDetails; private final BufferedReader payloadReader; private State nextState = State.START_RECORD; private String currentLine; private List<PullEventContext.FieldValue> values = new ArrayList<>(); private final Stack<FieldWithIndex> schemaFields = new Stack<>(); private int rootIndex; public SampleNestedPullDeserializer(SchemaDetails schemaDetails, InputStream payloadInputStream) { this.schemaDetails = schemaDetails; this.payloadReader = new BufferedReader(new InputStreamReader(payloadInputStream)); } @Override public boolean hasNext() throws SerDesException { return (nextState != State.END_DESERIALIZE); } @Override public PullEventContext<Schema.Field> next() throws SerDesException { PullEventContext<Schema.Field> context; if (isStartEvent()) { context = handleStartEvent(); } else if (isEndEvent()) { context = new EndRecordContext<>(); currentLine = null; handleEndEvent(); } else { context = processFields(); } return context; } private boolean isEndEvent() { return nextState == State.END_RECORD; } private PullEventContext<Schema.Field> processFields() throws SerDesException { PullEventContext<Schema.Field> context; int startFieldIndex = currentLine.indexOf(':'); int endFieldIndex = currentLine.indexOf(','); if (endFieldIndex == -1) { endFieldIndex = Integer.MAX_VALUE; } if (startFieldIndex == -1) { startFieldIndex = Integer.MAX_VALUE; } if (startFieldIndex == endFieldIndex) { throw new SerDesException("Invalid payload format!!"); } // get current field if (startFieldIndex < endFieldIndex) { //start field String fieldName = currentLine.substring(0, startFieldIndex); //todo go through schema also and validate the current field with the respective field in schema. Schema.Field currentField; if (schemaFields.empty()) { currentField = nextRootField(); schemaFields.push(new FieldWithIndex(currentField)); } else { FieldWithIndex peek = schemaFields.peek(); Schema.Field peekField = peek.field; if (!(peekField instanceof Schema.NestedField)) { throw new SerDesException("Expected nested field"); } peek.index++; currentField = ((Schema.NestedField) peekField).getFields().get(peek.index); schemaFields.push(new FieldWithIndex(currentField)); } if (!fieldName.equals(currentField.getName())) { throw new SerDesException("Expected name [" + fieldName + "], encountered unexpected name [" + currentField.getName() + "]"); } context = new StartFieldContext<>(currentField); updateCurrentLine(startFieldIndex); } else { //end field String value = currentLine.substring(0, endFieldIndex); Schema.Field schemaField = schemaFields.pop().field; MyFieldValue currentFieldValue; if (value.isEmpty()) { // this means all the values stored will be part of this parent field. currentFieldValue = new MyFieldValue(schemaField, values); values = new ArrayList<>(); } else { currentFieldValue = new MyFieldValue(schemaField, value); } values.add(currentFieldValue); context = new EndFieldContext<>(currentFieldValue); updateCurrentLine(endFieldIndex); } if (currentLine.isEmpty()) { nextState = State.END_RECORD; } return context; } @Override public void init(Map<String, ?> config) { } private static class FieldWithIndex { private final Schema.Field field; private int index; public FieldWithIndex(Schema.Field field) { this.field = field; index = -1; } public FieldWithIndex(Schema.Field field, int index) { this.field = field; this.index = index; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; FieldWithIndex that = (FieldWithIndex) o; if (index != that.index) return false; return field != null ? field.equals(that.field) : that.field == null; } @Override public int hashCode() { int result = field != null ? field.hashCode() : 0; result = 31 * result + index; return result; } @Override public String toString() { return "FieldWithIndex{" + "field=" + field + ", index=" + index + '}'; } } private Schema.Field nextRootField() { return schemaDetails.schema.getFields().get(rootIndex++); } private void handleEndEvent() throws SerDesException { try { currentLine = payloadReader.readLine(); } catch (IOException e) { throw new SerDesException(e); } nextState = currentLine != null ? State.START_RECORD : State.END_DESERIALIZE; } private PullEventContext<Schema.Field> handleStartEvent() throws SerDesException { rootIndex = 0; PullEventContext<Schema.Field> context; try { if (currentLine == null) { currentLine = payloadReader.readLine(); } } catch (IOException e) { throw new SerDesException(e); } if (currentLine != null) { nextState = State.PROCESS_FIELDS; context = new StartRecordContext<>(); } else { nextState = State.END_DESERIALIZE; context = new EndRecordContext<>(); } return context; } private void updateCurrentLine(int index) { if (index < currentLine.length()) { currentLine = currentLine.substring(index + 1); } } private boolean isStartEvent() { return nextState == State.START_RECORD; } @Override public SchemaDetails schema() { return schemaDetails; } @Override public void close() throws Exception { payloadReader.close(); } private class MyFieldValue implements PullEventContext.FieldValue<Schema.Field> { private final Schema.Field field; private final Object value; public MyFieldValue(Schema.Field field, Object value) { this.field = field; this.value = value; } @Override public Schema.Field field() { return field; } @Override public Object value() { return value; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; MyFieldValue that = (MyFieldValue) o; if (field != null ? !field.equals(that.field) : that.field != null) return false; return value != null ? value.equals(that.value) : that.value == null; } @Override public int hashCode() { int result = field != null ? field.hashCode() : 0; result = 31 * result + (value != null ? value.hashCode() : 0); return result; } @Override public String toString() { return "MyFieldValue{" + "field=" + field + ", value=" + value + '}'; } } }