/*
* JBoss, Home of Professional Open Source.
* See the COPYRIGHT.txt file distributed with this work for information
* regarding copyright ownership. Some portions may be licensed
* to Red Hat, Inc. under one or more contributor license agreements.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301 USA.
*/
package org.teiid.transport;
import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.StreamCorruptedException;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.Calendar;
import java.util.List;
import java.util.Properties;
import org.teiid.logging.LogConstants;
import org.teiid.logging.LogManager;
import org.teiid.logging.MessageLevel;
import org.teiid.net.socket.ServiceInvocationStruct;
import org.teiid.odbc.ODBCServerRemote;
import org.teiid.runtime.RuntimePlugin;
/**
* Represents the messages going from PG ODBC Client --> back end Server
* Some parts of this code is taken from H2's implementation of ODBC
*/
@SuppressWarnings("nls")
public class PgFrontendProtocol extends ByteToMessageDecoder {
private static final int LO_CREAT = 957;
private static final int LO_OPEN = 952;
private static final int LO_CLOSE = 953;
private static final int LO_READ = 954;
private static final int LO_WRITE = 955;
private static final int LO_LSEEK = 956;
private static final int LO_TELL = 958;
private static final int LO_UNLINK = 964;
private int maxObjectSize;
private Byte messageType;
private Integer dataLength;
private boolean initialized = false;
private ODBCServerRemote odbcProxy;
private PGRequest message;
private String user;
private String databaseName;
private PgBackendProtocol pgBackendProtocol;
public PgFrontendProtocol(PgBackendProtocol pgBackendProtocol, int maxObjectSize) {
if (maxObjectSize <= 0) {
throw new IllegalArgumentException("maxObjectSize: " + maxObjectSize); //$NON-NLS-1$
}
this.maxObjectSize = maxObjectSize;
this.pgBackendProtocol = pgBackendProtocol;
// the proxy is used for generating the object based message based on ServiceInvocationStruct class.
this.odbcProxy = (ODBCServerRemote)Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[] {ODBCServerRemote.class}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (LogManager.isMessageToBeRecorded(LogConstants.CTX_ODBC, MessageLevel.TRACE)) {
LogManager.logTrace(LogConstants.CTX_ODBC, "invoking server method:", method.getName(), Arrays.deepToString(args)); //$NON-NLS-1$
}
message = new PGRequest();
message.struct = new ServiceInvocationStruct(args, method.getName(),ODBCServerRemote.class);
return null;
}
});
}
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf buffer, List<Object> out) throws Exception {
if (this.initialized && this.messageType == null) {
if (buffer.readableBytes() < 1 ) {
return ;
}
this.messageType = buffer.readByte();
if (this.messageType < 0 ) {
this.odbcProxy.terminate();
out.add(this.message);
}
}
if (!this.initialized) {
this.messageType = 'I';
}
if (this.dataLength == null) {
if (buffer.readableBytes() < 4) {
return ;
}
this.dataLength = buffer.readInt();
if (this.dataLength <= 0) {
throw new StreamCorruptedException("invalid data length: " + this.dataLength); //$NON-NLS-1$
}
if (this.dataLength > this.maxObjectSize) {
throw new StreamCorruptedException("data length too big: " + this.dataLength + " (max: " + this.maxObjectSize + ')'); //$NON-NLS-1$ //$NON-NLS-2$
}
}
if (buffer.readableBytes() < this.dataLength - 4) {
return;
}
byte[] data = createByteArray(this.dataLength - 4);
buffer.readBytes(data);
createRequestMessage(
this.messageType,
new NullTerminatedStringDataInputStream(data,
new DataInputStream(new ByteArrayInputStream(data, 0,this.dataLength - 4)),
this.pgBackendProtocol.getEncoding()), ctx.channel());
this.dataLength = null;
this.messageType = null;
out.add(this.message);
}
private Object createRequestMessage(byte messageType, NullTerminatedStringDataInputStream data, Channel channel) throws IOException{
switch(messageType) {
case 'I':
this.initialized = true;
return buildInitialize(data, channel);
case 'p':
return buildLogin(data, channel);
case 'P':
return buildParse(data);
case 'B':
return buildBind(data);
case 'E':
return buildExecute(data);
case 'Q':
return buildExecuteQuery(data);
case 'D':
return buildDescribe(data);
case 'X':
return buildTeminate();
case 'S':
return buildSync();
case 'C':
return buildClose(data);
case 'H':
return buildFlush();
case 'F':
return buildFunctionCall(data);
default:
return buildError();
}
}
private Object buildError() {
this.odbcProxy.unsupportedOperation("option not suported");
return message;
}
private Object buildFlush() {
this.odbcProxy.flush();
return message;
}
private Object buildTeminate() {
this.odbcProxy.terminate();
return message;
}
private Object buildInitialize(NullTerminatedStringDataInputStream data, Channel channel) throws IOException{
Properties props = new Properties();
int version = data.readInt();
props.setProperty("version", Integer.toString(version));
// SSL Request
if (version == 80877103) {
this.initialized = false;
this.odbcProxy.sslRequest();
return message;
}
if (this.pgBackendProtocol.secureData() && channel.pipeline().get(org.teiid.transport.PgBackendProtocol.SSL_HANDLER_KEY) == null) {
this.odbcProxy.unsupportedOperation(RuntimePlugin.Util.gs(RuntimePlugin.Event.TEIID40123));
return message;
}
trace("StartupMessage version", version, "(", (version >> 16), ".", (version & 0xff), ")");
while (true) {
String param = data.readString();
if (param.length() == 0) {
break;
}
String value = data.readString();
props.setProperty(param, value);
}
this.user = props.getProperty("user");
this.databaseName = props.getProperty("database");
String clientEncoding = props.getProperty("client_encoding", PgBackendProtocol.DEFAULT_ENCODING);
props.setProperty("client_encoding", clientEncoding);
props.setProperty("default_transaction_isolation", "read committed");
props.setProperty("integer_datetimes", "on");
props.setProperty("DateStyle", "ISO");
props.setProperty("TimeZone", Calendar.getInstance().getTimeZone().getDisplayName());
this.odbcProxy.initialize(props);
return message;
}
private Object buildLogin(NullTerminatedStringDataInputStream data, Channel channel) {
this.odbcProxy.logon(this.databaseName, this.user, data, channel.remoteAddress());
return message;
}
private Object buildParse(NullTerminatedStringDataInputStream data) throws IOException {
String name = data.readString();
String sql = data.readString();
//The number of parameter data types specified (can be zero). Note that this is not
//an indication of the number of parameters that might appear in the query string, only
//the number that the frontend wants to prespecify types for.
int count = data.readShort();
int[] paramType = new int[count];
for (int i = 0; i < count; i++) {
int type = data.readInt();
paramType[i] = type;
}
this.odbcProxy.prepare(name, sql, paramType);
return message;
}
private Object buildBind(NullTerminatedStringDataInputStream data) throws IOException {
String bindName = data.readString();
String prepName = data.readString();
int formatCodeCount = data.readShort();
int[] formatCodes = new int[formatCodeCount];
for (int i = 0; i < formatCodeCount; i++) {
formatCodes[i] = data.readShort();
}
int paramCount = data.readShort();
Object[] params = new Object[paramCount];
for (int i = 0; i < paramCount; i++) {
int paramLen = data.readInt();
byte[] paramdata = createByteArray(paramLen);
data.readFully(paramdata);
// the params can be either text or binary
if (formatCodeCount == 0 || (formatCodeCount == 1 && formatCodes[0] == 0) || formatCodes[i] == 0) {
params[i] = new String(paramdata, this.pgBackendProtocol.getEncoding());
}
else {
params[i] = paramdata;
}
}
int resultCodeCount = data.readShort();
short[] resultColumnFormat = null;
if (resultCodeCount != 0) {
resultColumnFormat = new short[resultCodeCount];
for (int i = 0; i < resultCodeCount; i++) {
resultColumnFormat[i] = data.readShort();
}
}
this.odbcProxy.bindParameters(bindName, prepName, params, resultCodeCount, resultColumnFormat, this.pgBackendProtocol.getEncoding());
return message;
}
private Object buildExecute(NullTerminatedStringDataInputStream data) throws IOException {
String portalName = data.readString();
int maxRows = data.readInt();
this.odbcProxy.execute(portalName, maxRows);
return message;
}
private Object buildDescribe(NullTerminatedStringDataInputStream data) throws IOException{
char type = (char) data.readByte();
String name = data.readString();
if (type == 'S') {
this.odbcProxy.getParameterDescription(name);
return message;
} else if (type == 'P') {
this.odbcProxy.getResultSetMetaDataDescription(name);
return message;
} else {
trace("expected S or P, got ", type);
this.odbcProxy.unsupportedOperation("expected S or P");
return message;
}
}
private Object buildSync() {
this.odbcProxy.sync();
return message;
}
private Object buildExecuteQuery(NullTerminatedStringDataInputStream data) throws IOException {
String query = data.readString();
this.odbcProxy.executeQuery(query);
return message;
}
static byte[] createByteArray(int length) throws StreamCorruptedException{
try {
return new byte[length];
} catch(OutOfMemoryError e) {
throw new StreamCorruptedException("data too big: " + e.getMessage()); //$NON-NLS-1$
}
}
private Object buildClose(NullTerminatedStringDataInputStream data) throws IOException {
char type = (char)data.read();
String name = data.readString();
if (type == 'S') {
this.odbcProxy.closePreparedStatement(name);
}
else if (type == 'P') {
this.odbcProxy.closeBoundStatement(name);
}
else {
this.odbcProxy.unsupportedOperation("unknown close type specified");
}
return message;
}
/**
* LO functions are always binary, so I am ignoring the formats, return types. The below is not used
* leaving for future if ever LO is revisited
*/
@SuppressWarnings("unused")
private Object buildFunctionCall(NullTerminatedStringDataInputStream data) throws IOException {
int funcID = data.readInt();
// read data types of arguments
int formatCount = data.readShort();
int[] formatTypes = new int[formatCount];
for (int i = 0; i< formatCount; i++) {
formatTypes[i] = data.readShort();
}
// arguments
data.readShort(); // ignore the param count; we know them by functions supported.
int oid = readInt(data);
switch(funcID) {
case LO_CREAT:
break;
case LO_OPEN:
int mode = readInt(data);
break;
case LO_CLOSE:
break;
case LO_READ:
int length = readInt(data);
break;
case LO_WRITE:
byte[] contents = readByteArray(data);
break;
case LO_LSEEK:
int offset = readInt(data);
int where = readInt(data);
break;
case LO_TELL:
break;
case LO_UNLINK:
break;
}
this.odbcProxy.functionCall(oid);
return message;
}
private int readInt(NullTerminatedStringDataInputStream data) throws IOException {
data.readInt(); // ignore this this is length always 4
return data.readInt();
}
private byte[] readByteArray(NullTerminatedStringDataInputStream data) throws IOException {
int length = data.readInt();
if (length == -1 || length == 0) {
return null;
}
byte[] content = createByteArray(length);
data.read(content, 0, length);
return content;
}
public static class PGRequest {
ServiceInvocationStruct struct;
}
public static class NullTerminatedStringDataInputStream extends DataInputStream{
private Charset encoding;
private byte[] rawData;
public NullTerminatedStringDataInputStream(byte[] rawData, DataInputStream in, Charset encoding) {
super(in);
this.encoding = encoding;
this.rawData = rawData;
}
public String readString() throws IOException {
ByteArrayOutputStream buff = new ByteArrayOutputStream();
while (true) {
int x = read();
if (x <= 0) {
break;
}
buff.write(x);
}
return new String(buff.toByteArray(), this.encoding);
}
public byte[] readServiceToken() {
return this.rawData;
}
}
private static void trace(Object... msg) {
LogManager.logTrace(LogConstants.CTX_ODBC, msg);
}
}