/*
* 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.addthis.hydra.data.query;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import com.addthis.basis.util.LessBytes;
import com.addthis.bundle.channel.DataChannelError;
import com.addthis.bundle.core.Bundle;
import com.addthis.bundle.core.BundleFactory;
import com.addthis.bundle.core.list.ListBundle;
import com.addthis.bundle.io.BundleReader;
import com.addthis.bundle.io.DataChannelCodec;
import com.addthis.hydra.data.util.BundleUtils;
import com.addthis.meshy.service.stream.StreamSource;
import com.google.common.base.Throwables;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@NotThreadSafe
public class FramedDataChannelReader implements BundleReader {
private static final Logger log = LoggerFactory.getLogger(FramedDataChannelReader.class);
public static final int FRAME_MORE = 0;
public static final int FRAME_EOF = 1;
public static final int FRAME_ERROR = 2;
public static final int FRAME_BUSY = 3;
private final StreamSource streamSource;
private final DataChannelCodec.ClassIndexMap classMap;
private final DataChannelCodec.FieldIndexMap fieldMap;
private final BundleFactory factory;
private final BlockingQueue<byte[]> queue;
private final int pollWaitTime;
private ByteArrayInputStream bis;
private DataChannelError err;
private boolean eof;
public FramedDataChannelReader(StreamSource streamSource, int pollWaitTime) {
this(streamSource, pollWaitTime,
DataChannelCodec.createClassIndexMap(), DataChannelCodec.createFieldIndexMap());
}
public FramedDataChannelReader(StreamSource streamSource, int pollWaitTime,
DataChannelCodec.ClassIndexMap classMap,
DataChannelCodec.FieldIndexMap fieldMap) {
this.streamSource = streamSource;
this.pollWaitTime = pollWaitTime;
this.classMap = classMap;
this.fieldMap = fieldMap;
this.queue = streamSource.getMessageQueue();
this.factory = new ListBundle();
this.eof = false;
}
@Override
@Nullable
public Bundle read() throws IOException {
if (err != null) {
throw err;
}
if (eof) {
return null;
}
if ((bis == null) || (bis.available() == 0)) {
byte[] data;
try {
data = queue.poll(pollWaitTime, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
throw Throwables.propagate(e);
}
if (data == null) {
// poll timeout no data yet
return null;
}
handlePollResult(data);
if (eof) {
return null;
} else {
// more data to read
bis = new ByteArrayInputStream(data);
}
}
int frame = bis.read();
switch (frame) {
case FRAME_BUSY:
err = new DataChannelError("busy frames are not supported");
throw err;
case FRAME_MORE:
return DataChannelCodec.decodeBundle(factory.createBundle(), LessBytes.readBytes(bis), fieldMap, classMap);
case FRAME_EOF:
close();
return null;
case FRAME_ERROR:
try {
String error = LessBytes.readString(bis);
String errorMessage = LessBytes.readString(bis);
Class clazz = Class.forName(error);
err = (DataChannelError) clazz.getConstructor(String.class).newInstance(errorMessage);
} catch (DataChannelError ex) {
err = ex;
throw ex;
} catch (Exception ex) {
throw new DataChannelError(ex);
}
throw err;
default:
err = new DataChannelError("invalid framing: " + frame);
throw err;
}
}
private void handlePollResult(@Nonnull byte[] data) throws IOException {
streamSource.performBufferAccounting(data);
try {
streamSource.throwIfErrorSignal(data);
} catch (Throwable sourceError) {
// kind of silly, but I just feel like being defensive at the moment
err = BundleUtils.promoteHackForThrowables(sourceError);
throw sourceError;
}
if (streamSource.isCloseSignal(data)) {
// maybe this should be an error? leaving it as is for now to mainting existing behavior
log.warn("bundle stream ended without eof frame");
close();
}
}
@Override public void close() throws IOException {
eof = true;
}
public boolean isClosed() {
return eof;
}
@Override public BundleFactory getFactory() {
return factory;
}
}