/* Copyright (c) 2011 Danish Maritime Authority.
*
* 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 net.maritimecloud.internal.message.binary.protobuf;
import net.maritimecloud.internal.core.com.google.protobuf.CodedInputStream;
import net.maritimecloud.internal.core.com.google.protobuf.CodedOutputStream;
import net.maritimecloud.internal.message.AbstractMessageReader;
import net.maritimecloud.message.Message;
import net.maritimecloud.message.MessageEnum;
import net.maritimecloud.message.MessageEnumSerializer;
import net.maritimecloud.message.MessageFormatType;
import net.maritimecloud.message.MessageSerializer;
import net.maritimecloud.message.SerializationException;
import net.maritimecloud.message.ValueReader;
import net.maritimecloud.message.ValueSerializer;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Implementation of a message reader that uses the Google Protobuf wire format.
* <p>
* Because of underlying differences in the way the MessageReader and the
* Protobuf CodedMessageReader is used, the message is initially read into
* a {@code fieldValues} map, that maps the field tags to ValueReaders.
*/
public class ProtobufMessageReader extends AbstractMessageReader {
/**
* {@inheritDoc}
*/
@Override
public MessageFormatType getFormatType() {
return MessageFormatType.MACHINE_READABLE;
}
/** Maps the message tags to the corresponding ValueReader instance */
final Map<Integer, ValueReader> fieldValues = new ConcurrentHashMap<>();
/** Keep track of the currently read tag */
final Integer[] tags;
int tagIndex = 0;
/**
* Constructor
* @param in the input stream
*/
public ProtobufMessageReader(InputStream in) throws IOException {
this(CodedInputStream.newInstance(in));
}
/**
* Constructor
* @param cis the coded input stream
*/
public ProtobufMessageReader(CodedInputStream cis) throws IOException {
// Read the message field by field
while (!cis.isAtEnd()) {
int pbTag = cis.readTag();
int tag = ProtobufWireFormat.getTagFieldNumber(pbTag);
int wireType = ProtobufWireFormat.getTagWireType(pbTag);
// We only use two of the supported Protobuf wire types
switch (wireType) {
case ProtobufWireFormat.WIRETYPE_VARINT:
fieldValues.put(tag, new ProtobufValueReader(cis.readSInt64()));
break;
case ProtobufWireFormat.WIRETYPE_LENGTH_DELIMITED:
fieldValues.put(tag, new ProtobufValueReader(CodedInputStream.newInstance(readByteArray(cis))));
break;
default:
throw new SerializationException("Invalid protobuf wire type " + wireType);
}
}
tags = fieldValues.keySet().stream().toArray(Integer[]::new);
}
/**
* Prepends the byte-array at the current tag with the length
* @param cis the coded input stream
* @return the byte array prepended with the length
*/
private byte[] readByteArray(CodedInputStream cis) throws IOException {
ByteArrayOutputStream bout = new ByteArrayOutputStream();
CodedOutputStream cout = CodedOutputStream.newInstance(bout);
cout.writeByteArrayNoTag(cis.readByteArray());
cout.flush();
return bout.toByteArray();
}
/** {@inheritDoc} */
@Override
public boolean isNext(int tag, String name) {
return tagIndex <= tags.length - 1 && tags[tagIndex].equals(tag);
}
/** {@inheritDoc} */
@Override
public <T extends MessageEnum> T readEnum(int tag, String name, MessageEnumSerializer<T> factory)
throws IOException {
int enumValue = readInt(tag, name);
return factory.from(enumValue);
}
/** {@inheritDoc} */
@Override
public <K, V> Map<K, V> readMap(int tag, String name, ValueSerializer<K> keyParser, ValueSerializer<V> valueParser)
throws IOException {
ValueReader r = findOptional(tag, name);
return r == null ? Collections.emptyMap() : r.readMap(keyParser, valueParser);
}
/** {@inheritDoc} */
@Override
public <T extends Message> T readMessage(int tag, String name, MessageSerializer<T> parser) throws IOException {
ValueReader valueReader = findOptional(tag, name);
return valueReader == null ? null : valueReader.readMessage(parser);
}
/** {@inheritDoc} */
@Override
protected ValueReader find(int tag, String name) throws SerializationException {
ValueReader valueReader = findOptional(tag, name);
if (valueReader == null) {
throw new SerializationException("Could not find tag " + tag);
}
return valueReader;
}
/** {@inheritDoc} */
@Override
protected ValueReader findOptional(int tag, String name) {
if (isNext(tag, name)) {
tagIndex++;
return fieldValues.get(tag);
}
return null;
}
/**
* Reads a message from a byte array using the given message serializer
* @param message the message byte array
* @param serializer the message serializer
* @return the message
*/
public static <T extends Message> T read(byte[] message, MessageSerializer<T> serializer) throws IOException {
CodedInputStream bin = CodedInputStream.newInstance(message);
ProtobufMessageReader reader = new ProtobufMessageReader(bin);
return serializer.read(reader);
}
}