MessageFieldArgument.java

  1. /*
  2.  * Copyright 2018-2019 Providence Authors
  3.  *
  4.  * Licensed to the Apache Software Foundation (ASF) under one
  5.  * or more contributor license agreements. See the NOTICE file
  6.  * distributed with this work for additional information
  7.  * regarding copyright ownership. The ASF licenses this file
  8.  * to you under the Apache License, Version 2.0 (the
  9.  * "License"); you may not use this file except in compliance
  10.  * with the License. You may obtain a copy of the License at
  11.  *
  12.  *   http://www.apache.org/licenses/LICENSE-2.0
  13.  *
  14.  * Unless required by applicable law or agreed to in writing,
  15.  * software distributed under the License is distributed on an
  16.  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  17.  * KIND, either express or implied. See the License for the
  18.  * specific language governing permissions and limitations
  19.  * under the License.
  20.  */
  21. package net.morimekta.providence.jdbi.v2;

  22. import net.morimekta.providence.PEnumValue;
  23. import net.morimekta.providence.PMessage;
  24. import net.morimekta.providence.PMessageOrBuilder;
  25. import net.morimekta.providence.PType;
  26. import net.morimekta.providence.descriptor.PField;
  27. import net.morimekta.providence.serializer.BinarySerializer;
  28. import net.morimekta.providence.serializer.JsonSerializer;
  29. import net.morimekta.util.Binary;
  30. import org.skife.jdbi.v2.StatementContext;
  31. import org.skife.jdbi.v2.tweak.Argument;

  32. import javax.annotation.Nonnull;
  33. import java.io.ByteArrayInputStream;
  34. import java.io.ByteArrayOutputStream;
  35. import java.io.IOException;
  36. import java.io.PrintWriter;
  37. import java.io.StringReader;
  38. import java.io.StringWriter;
  39. import java.sql.Date;
  40. import java.sql.PreparedStatement;
  41. import java.sql.SQLDataException;
  42. import java.sql.SQLException;
  43. import java.sql.Timestamp;
  44. import java.sql.Types;
  45. import java.util.Calendar;
  46. import java.util.Objects;
  47. import java.util.TimeZone;

  48. /**
  49.  * Smart mapping of message fields to SQL bound argument. It will
  50.  * map the type to whichever type is default or selected (if supported)
  51.  * for most field types.
  52.  *
  53.  * @param <M> The message type.
  54.  */
  55. public class MessageFieldArgument<M extends PMessage<M>> implements Argument {
  56.     private static final BinarySerializer BINARY = new BinarySerializer();
  57.     private static final JsonSerializer   JSON   = new JsonSerializer().named();
  58.     private static final Calendar         UTC    = Calendar.getInstance(TimeZone.getTimeZone("UTC"));

  59.     private final PMessageOrBuilder<M> message;
  60.     private final PField<M>            field;
  61.     private final int                  type;

  62.     /**
  63.      * Create a message field argument.
  64.      *
  65.      * @param message The message to get the field from.
  66.      * @param field The field to select.
  67.      */
  68.     public MessageFieldArgument(@Nonnull PMessageOrBuilder<M> message, @Nonnull PField<M> field) {
  69.         this(message, field, getDefaultColumnType(field));
  70.     }

  71.     /**
  72.      * Create a message field argument.
  73.      *
  74.      * @param message The message to get the field from.
  75.      * @param field The field to select.
  76.      * @param type The SQL type. See {@link Types}.
  77.      */
  78.     public MessageFieldArgument(@Nonnull PMessageOrBuilder<M> message, @Nonnull PField<M> field, int type) {
  79.         this.message = message;
  80.         this.field = field;
  81.         this.type = type;
  82.     }

  83.     @Override
  84.     public String toString() {
  85.         if (message.has(field)) {
  86.             if (field.getType() == PType.STRING) {
  87.                 return "'" + message.get(field) + "'";
  88.             } else if (field.getType() == PType.BINARY) {
  89.                 Binary binary = message.get(field);
  90.                 return binary.toHexString();
  91.             } else if (field.getType() == PType.MESSAGE) {
  92.                 return ((PMessage) message.get(field)).asString();
  93.             }
  94.             return String.valueOf((Object) message.get(field));
  95.         }

  96.         return "null";
  97.     }

  98.     @Override
  99.     public boolean equals(Object o) {
  100.         if (o == this) return true;
  101.         if (!(o instanceof MessageFieldArgument)) return false;
  102.         MessageFieldArgument other = (MessageFieldArgument) o;
  103.         return Objects.equals(message, other.message) &&
  104.                 Objects.equals(field, other.field) &&
  105.                 type == other.type;
  106.     }

  107.     @Override
  108.     public int hashCode() {
  109.         return Objects.hash(getClass(), message, field, type);
  110.     }

  111.     @Override
  112.     @SuppressWarnings("unchecked")
  113.     public void apply(int position, PreparedStatement statement, StatementContext ctx) throws SQLException {
  114.         if (message.has(field)) {
  115.             switch (field.getType()) {
  116.                 case BOOL: {
  117.                     boolean value = message.get(field);
  118.                     if (type == Types.BOOLEAN || type == Types.BIT) {
  119.                         statement.setBoolean(position, value);
  120.                     } else {
  121.                         statement.setInt(position, value ? 1 : 0);
  122.                     }
  123.                     break;
  124.                 }
  125.                 case BYTE: {
  126.                     statement.setByte(position, message.get(field));
  127.                     break;
  128.                 }
  129.                 case I16: {
  130.                     statement.setShort(position, message.get(field));
  131.                     break;
  132.                 }
  133.                 case I32: {
  134.                     if (type == Types.TIMESTAMP || type == Types.TIMESTAMP_WITH_TIMEZONE) {
  135.                         Timestamp timestamp = new Timestamp(1000L * (int) message.get(field));
  136.                         statement.setTimestamp(position, timestamp, UTC);
  137.                     } else if (type == Types.DATE) {
  138.                         Date date = new Date(1000L * (int) message.get(field));
  139.                         statement.setDate(position, date, UTC);
  140.                     } else {
  141.                         statement.setInt(position, message.get(field));
  142.                     }
  143.                     break;
  144.                 }
  145.                 case I64: {
  146.                     if (type == Types.TIMESTAMP || type == Types.TIMESTAMP_WITH_TIMEZONE) {
  147.                         Timestamp timestamp = new Timestamp(message.get(field));
  148.                         statement.setTimestamp(position, timestamp, UTC);
  149.                     } else if (type == Types.DATE) {
  150.                         Date date = new Date(message.get(field));
  151.                         statement.setDate(position, date, UTC);
  152.                     } else {
  153.                         statement.setLong(position, message.get(field));
  154.                     }
  155.                     break;
  156.                 }
  157.                 case DOUBLE: {
  158.                     statement.setDouble(position, message.get(field));
  159.                     break;
  160.                 }
  161.                 case STRING: {
  162.                     switch (type) {
  163.                         case Types.CLOB:
  164.                         case Types.NCLOB: {
  165.                             StringReader reader = new StringReader(message.get(field));
  166.                             statement.setClob(position, reader);
  167.                             break;
  168.                         }
  169.                         default: {
  170.                             statement.setString(position, message.get(field));
  171.                             break;
  172.                         }
  173.                     }
  174.                     break;
  175.                 }
  176.                 case BINARY: {
  177.                     Binary binary = message.get(field);
  178.                     switch (type) {
  179.                         case Types.BINARY:
  180.                         case Types.VARBINARY: {
  181.                             statement.setBytes(position, binary.get());
  182.                             break;
  183.                         }
  184.                         case Types.LONGVARBINARY: {
  185.                             statement.setBinaryStream(position, binary.getInputStream());
  186.                             break;
  187.                         }
  188.                         case Types.BLOB: {
  189.                             statement.setBlob(position, binary.getInputStream());
  190.                             break;
  191.                         }
  192.                         case Types.CHAR:
  193.                         case Types.VARCHAR:
  194.                         case Types.LONGVARCHAR:
  195.                         case Types.NCHAR:
  196.                         case Types.NVARCHAR: {
  197.                             statement.setString(position, binary.toBase64());
  198.                             break;
  199.                         }
  200.                         default:
  201.                             throw new SQLDataException("Unknown binary field type: " + type + " for " + field);
  202.                     }
  203.                     break;
  204.                 }
  205.                 case ENUM: {
  206.                     PEnumValue value = message.get(field);
  207.                     statement.setInt(position, value.asInteger());
  208.                     break;
  209.                 }
  210.                 case MESSAGE: {
  211.                     PMessage value = message.get(field);
  212.                     switch (type) {
  213.                         case Types.BINARY:
  214.                         case Types.LONGVARBINARY:
  215.                         case Types.VARBINARY: {
  216.                             ByteArrayOutputStream out = new ByteArrayOutputStream();
  217.                             try {
  218.                                 BINARY.serialize(out, value);
  219.                                 statement.setBytes(position, out.toByteArray());
  220.                             } catch (IOException e) {
  221.                                 throw new SQLDataException(e.getMessage(), e);
  222.                             }
  223.                             break;
  224.                         }
  225.                         case Types.BLOB: {
  226.                             ByteArrayOutputStream out = new ByteArrayOutputStream();
  227.                             try {
  228.                                 BINARY.serialize(out, value);
  229.                                 statement.setBlob(position, new ByteArrayInputStream(out.toByteArray()));
  230.                             } catch (IOException e) {
  231.                                 throw new SQLDataException(e.getMessage(), e);
  232.                             }
  233.                             break;
  234.                         }
  235.                         case Types.CHAR:
  236.                         case Types.VARCHAR:
  237.                         case Types.LONGVARCHAR:
  238.                         case Types.NCHAR:
  239.                         case Types.NVARCHAR: {
  240.                             StringWriter writer = new StringWriter();
  241.                             try {
  242.                                 JSON.serialize(new PrintWriter(writer), value);
  243.                                 statement.setString(position, writer.getBuffer().toString());
  244.                             } catch (IOException e) {
  245.                                 throw new SQLDataException(e.getMessage(), e);
  246.                             }
  247.                             break;
  248.                         }
  249.                         case Types.NCLOB:
  250.                         case Types.CLOB: {
  251.                             StringWriter writer = new StringWriter();
  252.                             try {
  253.                                 JSON.serialize(new PrintWriter(writer), value);
  254.                                 statement.setClob(position, new StringReader(writer.getBuffer().toString()));
  255.                             } catch (IOException e) {
  256.                                 throw new SQLDataException(e.getMessage(), e);
  257.                             }
  258.                             break;
  259.                         }
  260.                         default:
  261.                             throw new SQLDataException("Unknown message field type: " + type + " for " + field);
  262.                     }
  263.                     break;
  264.                 }
  265.                 default:
  266.                     throw new SQLDataException("Unhandled field type in SQL: " + field);
  267.             }
  268.         } else {
  269.             statement.setNull(position, type);
  270.         }
  271.     }

  272.     static int getDefaultColumnType(PField field) {
  273.         switch (field.getType()) {
  274.             case BOOL: return Types.BIT;
  275.             case BYTE: return Types.TINYINT;
  276.             case I16: return Types.SMALLINT;
  277.             case I32: return Types.INTEGER;
  278.             case I64: return Types.BIGINT;
  279.             case DOUBLE: return Types.DOUBLE;
  280.             case STRING: return Types.VARCHAR;
  281.             case BINARY: return Types.VARBINARY;
  282.             case ENUM: return Types.INTEGER;
  283.             case MESSAGE: return Types.VARCHAR;  // JSON string.
  284.             default: {
  285.                 throw new IllegalArgumentException("No default column type for " + field.toString());
  286.             }
  287.         }
  288.     }
  289. }