All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.vertx.pgclient.impl.codec.PgEncoder Maven / Gradle / Ivy

There is a newer version: 5.0.0.CR1
Show newest version
/*
 * Copyright (C) 2018 Julien Viet
 *
 * 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 io.vertx.pgclient.impl.codec;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelOutboundHandlerAdapter;
import io.netty.channel.ChannelPromise;
import io.netty.channel.socket.SocketChannel;
import io.vertx.sqlclient.Tuple;
import io.vertx.pgclient.impl.util.Util;
import io.vertx.sqlclient.impl.HexSequence;
import io.vertx.sqlclient.impl.ParamDesc;
import io.vertx.sqlclient.impl.RowDesc;
import io.vertx.sqlclient.impl.command.CloseConnectionCommand;
import io.vertx.sqlclient.impl.command.CloseCursorCommand;
import io.vertx.sqlclient.impl.command.CloseStatementCommand;
import io.vertx.sqlclient.impl.command.CommandBase;
import io.vertx.sqlclient.impl.command.ExtendedQueryCommand;
import io.vertx.sqlclient.impl.command.InitCommand;
import io.vertx.sqlclient.impl.command.PrepareStatementCommand;
import io.vertx.sqlclient.impl.command.SimpleQueryCommand;

import java.util.ArrayDeque;
import java.util.Map;

import static io.vertx.pgclient.impl.util.Util.writeCString;
import static java.nio.charset.StandardCharsets.UTF_8;

/**
 * @author Emad Alblueshi
 * @author Julien Viet
 */
final class PgEncoder extends ChannelOutboundHandlerAdapter {

  // Frontend message types for {@link io.reactiverse.pgclient.impl.codec.encoder.MessageEncoder}

  private static final byte PASSWORD_MESSAGE = 'p';
  private static final byte QUERY = 'Q';
  private static final byte TERMINATE = 'X';
  private static final byte PARSE = 'P';
  private static final byte BIND = 'B';
  private static final byte DESCRIBE = 'D';
  private static final byte EXECUTE = 'E';
  private static final byte CLOSE = 'C';
  private static final byte SYNC = 'S';

  private final ArrayDeque> inflight;
  private ChannelHandlerContext ctx;
  private ByteBuf out;
  private final HexSequence psSeq = new HexSequence(); // used for generating named prepared statement name
  boolean closeSent;

  PgEncoder(ArrayDeque> inflight) {
    this.inflight = inflight;
  }


  @Override
  public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
    if (!closeSent) {
      CloseConnectionCommand cmd = CloseConnectionCommand.INSTANCE;
      PgCommandCodec codec = wrap(cmd);
      codec.encode(this);
    }
  }

  @Override
  public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
    if (out != null) {
      ByteBuf buff = this.out;
      this.out = null;
      buff.release();
    }
  }

  void write(CommandBase cmd) {
    PgCommandCodec codec = wrap(cmd);
    codec.completionHandler = resp -> {
      PgCommandCodec c = inflight.poll();
      resp.cmd = (CommandBase) c.cmd;
      ctx.fireChannelRead(resp);
    };
    codec.noticeHandler = ctx::fireChannelRead;
    inflight.add(codec);
    codec.encode(this);
  }

  private PgCommandCodec wrap(CommandBase cmd) {
    if (cmd instanceof InitCommand) {
      return new InitCommandCodec((InitCommand) cmd);
    } else if (cmd instanceof SimpleQueryCommand) {
      return new SimpleQueryCodec<>((SimpleQueryCommand) cmd);
    } else if (cmd instanceof ExtendedQueryCommand) {
      return new ExtendedQueryCommandCodec<>((ExtendedQueryCommand) cmd);
    } else if (cmd instanceof PrepareStatementCommand) {
      return new PrepareStatementCommandCodec((PrepareStatementCommand) cmd);
    } else if (cmd instanceof CloseConnectionCommand) {
      return CloseConnectionCommandCodec.INSTANCE;
    } else if (cmd instanceof CloseCursorCommand) {
      return new ClosePortalCommandCodec((CloseCursorCommand) cmd);
    } else if (cmd instanceof CloseStatementCommand) {
      return new CloseStatementCommandCodec((CloseStatementCommand) cmd);
    }
    throw new AssertionError();
  }

  @Override
  public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
    this.ctx = ctx;
  }

  @Override
  public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
    if (msg instanceof CommandBase) {
      CommandBase cmd = (CommandBase) msg;
      write(cmd);
    } else {
      super.write(ctx, msg, promise);
    }
  }

  @Override
  public void flush(ChannelHandlerContext ctx) {
    flush();
  }

  void close() {
    ByteBuf buff;
    if (out != null) {
      buff = out;
      out = null;
    } else {
      buff = Unpooled.EMPTY_BUFFER;
    }
    ctx.writeAndFlush(buff).addListener(v -> {
      Channel ch = channelHandlerContext().channel();
      if (ch instanceof SocketChannel) {
        SocketChannel channel = (SocketChannel) ch;
        channel.shutdownOutput();
      }
    });
  }

  void flush() {
    if (out != null) {
      ByteBuf buff = out;
      out = null;
      ctx.writeAndFlush(buff, ctx.voidPromise());
    } else {
      ctx.flush();
    }
  }

  /**
   * This message immediately closes the connection. On receipt of this message,
   * the backend closes the connection and terminates.
   */
  void writeTerminate() {
    ensureBuffer();
    out.writeByte(TERMINATE);
    out.writeInt(4);
  }

  /**
   * 

* The purpose of this message is to provide a resynchronization point for error recovery. * When an error is detected while processing any extended-query message, the backend issues {@link ErrorResponse}, * then reads and discards messages until this message is reached, then issues {@link ReadyForQuery} and returns to normal * message processing. *

* Note that no skipping occurs if an error is detected while processing this message which ensures that there is one * and only one {@link ReadyForQuery} sent for each of this message. *

* Note this message does not cause a transaction block opened with BEGIN to be closed. It is possible to detect this * situation in {@link ReadyForQuery#txStatus()} that includes {@link TxStatus} information. */ void writeSync() { ensureBuffer(); out.writeByte(SYNC); out.writeInt(4); } /** *

* The message closes an existing prepared statement or portal and releases resources. * Note that closing a prepared statement implicitly closes any open portals that were constructed from that statement. *

* The response is either {@link CloseComplete} or {@link ErrorResponse} * * @param portal */ void writeClosePortal(String portal) { ensureBuffer(); int pos = out.writerIndex(); out.writeByte(CLOSE); out.writeInt(0); out.writeByte('P'); // 'S' to close a prepared statement or 'P' to close a portal Util.writeCStringUTF8(out, portal); out.setInt(pos + 1, out.writerIndex() - pos - 1); } void writeClosePreparedStatement(byte[] statementName) { ensureBuffer(); int pos = out.writerIndex(); out.writeByte(CLOSE); out.writeInt(0); out.writeByte('S'); // 'S' to close a prepared statement or 'P' to close a portal out.writeBytes(statementName); out.setInt(pos + 1, out.writerIndex() - pos - 1); } void writeStartupMessage(StartupMessage msg) { ensureBuffer(); int pos = out.writerIndex(); out.writeInt(0); // protocol version out.writeShort(3); out.writeShort(0); writeCString(out, StartupMessage.BUFF_USER); Util.writeCStringUTF8(out, msg.username); writeCString(out, StartupMessage.BUFF_DATABASE); Util.writeCStringUTF8(out, msg.database); for (Map.Entry property : msg.properties.entrySet()) { writeCString(out, property.getKey(), UTF_8); writeCString(out, property.getValue(), UTF_8); } out.writeByte(0); out.setInt(pos, out.writerIndex() - pos); } void writePasswordMessage(PasswordMessage msg) { ensureBuffer(); int pos = out.writerIndex(); out.writeByte(PASSWORD_MESSAGE); out.writeInt(0); Util.writeCStringUTF8(out, msg.hash); out.setInt(pos + 1, out.writerIndex() - pos- 1); } void writeScramClientInitialMessage(ScramClientInitialMessage msg) { ensureBuffer(); out.writeByte(PASSWORD_MESSAGE); int totalLengthPosition = out.writerIndex(); out.writeInt(0); // message length -> will be set later Util.writeCStringUTF8(out, msg.mechanism); int msgPosition = out.writerIndex(); out.writeInt(0); out.writeCharSequence(msg.message, UTF_8); // rewind to set the message and total length out.setInt(msgPosition, out.writerIndex() - msgPosition - Integer.BYTES); out.setInt(totalLengthPosition, out.writerIndex() - totalLengthPosition); } void writeScramClientFinalMessage(ScramClientFinalMessage msg) { ensureBuffer(); out.writeByte(PASSWORD_MESSAGE); int totalLengthPosition = out.writerIndex(); out.writeInt(0); // message length -> will be set later out.writeCharSequence(msg.message, UTF_8); // rewind to set the message length out.setInt(totalLengthPosition, out.writerIndex() - totalLengthPosition); } /** *

* This message includes an SQL command (or commands) expressed as a text string. *

* The possible response messages from the backend are * {@link CommandComplete}, {@link RowDesc}, {@link DataRow}, {@link EmptyQueryResponse}, {@link ErrorResponse}, * {@link ReadyForQuery} and {@link NoticeResponse} */ void writeQuery(Query query) { ensureBuffer(); int pos = out.writerIndex(); out.writeByte(QUERY); out.writeInt(0); Util.writeCStringUTF8(out, query.sql); out.setInt(pos + 1, out.writerIndex() - pos - 1); } /** *

* The message that using "statement" variant specifies the name of an existing prepared statement. *

* The response is a {@link ParamDesc} message describing the parameters needed by the statement, * followed by a {@link RowDesc} message describing the rows that will be returned when the statement is eventually * executed or a {@link NoData} message if the statement will not return rows. * {@link ErrorResponse} is issued if there is no such prepared statement. *

* Note that since {@link Bind} has not yet been issued, the formats to be used for returned columns are not yet known to * the backend; the format code fields in the {@link RowDesc} message will be zeroes in this case. *

* The message that using "portal" variant specifies the name of an existing portal. *

* The response is a {@link RowDesc} message describing the rows that will be returned by executing the portal; * or a {@link NoData} message if the portal does not contain a query that will return rows; or {@link ErrorResponse} * if there is no such portal. */ void writeDescribe(Describe describe) { ensureBuffer(); int pos = out.writerIndex(); out.writeByte(DESCRIBE); out.writeInt(0); if (describe.statement.length > 1) { out.writeByte('S'); out.writeBytes(describe.statement); } else if (describe.portal != null) { out.writeByte('P'); Util.writeCStringUTF8(out, describe.portal); } else { out.writeByte('S'); Util.writeCStringUTF8(out, ""); } out.setInt(pos + 1, out.writerIndex() - pos- 1); } void writeParse(String sql, byte[] statement, DataType[] parameterTypes) { ensureBuffer(); int pos = out.writerIndex(); out.writeByte(PARSE); out.writeInt(0); out.writeBytes(statement); Util.writeCStringUTF8(out, sql); if (parameterTypes == null) { // Let pg figure out out.writeShort(0); } else { out.writeShort(parameterTypes.length); for (DataType parameterType : parameterTypes) { out.writeInt(parameterType.id); } } out.setInt(pos + 1, out.writerIndex() - pos - 1); } /** * The message specifies the portal and a maximum row count (zero meaning "fetch all rows") of the result. *

* The row count of the result is only meaningful for portals containing commands that return row sets; * in other cases the command is always executed to completion, and the row count of the result is ignored. *

* The possible responses to this message are the same as {@link Query} message, except that * it doesn't cause {@link ReadyForQuery} or {@link RowDesc} to be issued. *

* If Execute terminates before completing the execution of a portal, it will send a {@link PortalSuspended} message; * the appearance of this message tells the frontend that another Execute should be issued against the same portal to * complete the operation. The {@link CommandComplete} message indicating completion of the source SQL command * is not sent until the portal's execution is completed. Therefore, This message is always terminated by * the appearance of exactly one of these messages: {@link CommandComplete}, * {@link EmptyQueryResponse}, {@link ErrorResponse} or {@link PortalSuspended}. * * @author Emad Alblueshi */ void writeExecute(String portal, int rowCount) { ensureBuffer(); int pos = out.writerIndex(); out.writeByte(EXECUTE); out.writeInt(0); if (portal != null) { out.writeCharSequence(portal, UTF_8); } out.writeByte(0); out.writeInt(rowCount); // Zero denotes "no limit" maybe for ReadStream out.setInt(pos + 1, out.writerIndex() - pos - 1); } /** *

* The message gives the name of the prepared statement, the name of portal, * and the values to use for any parameter values present in the prepared statement. * The supplied parameter set must match those needed by the prepared statement. *

* The response is either {@link BindComplete} or {@link ErrorResponse}. */ void writeBind(Bind bind, String portal, Tuple paramValues) { ensureBuffer(); int pos = out.writerIndex(); out.writeByte(BIND); out.writeInt(0); if (portal != null) { out.writeCharSequence(portal, UTF_8); } out.writeByte(0); out.writeBytes(bind.statement); int paramLen = paramValues.size(); out.writeShort(paramLen); // Parameter formats for (int c = 0;c < paramLen;c++) { // for now each format is Binary out.writeShort(bind.paramTypes[c].supportsBinary ? 1 : 0); } out.writeShort(paramLen); for (int c = 0;c < paramLen;c++) { Object param = paramValues.getValue(c); if (param == null) { // NULL value out.writeInt(-1); } else { DataType dataType = bind.paramTypes[c]; if (dataType.supportsBinary) { int idx = out.writerIndex(); out.writeInt(0); DataTypeCodec.encodeBinary(dataType, param, out); out.setInt(idx, out.writerIndex() - idx - 4); } else { DataTypeCodec.encodeText(dataType, param, out); } } } // MAKE resultColumsn non null to avoid null check // Result columns are all in Binary format if (bind.resultColumns.length > 0) { out.writeShort(bind.resultColumns.length); for (PgColumnDesc resultColumn : bind.resultColumns) { out.writeShort(resultColumn.dataType.supportsBinary ? 1 : 0); } } else { out.writeShort(1); out.writeShort(1); } out.setInt(pos + 1, out.writerIndex() - pos - 1); } private void ensureBuffer() { if (out == null) { out = ctx.alloc().ioBuffer(); } } byte[] nextStatementName() { return psSeq.next(); } public ChannelHandlerContext channelHandlerContext() { return ctx; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy