/** * Copyright (C) 2009-2013 FoundationDB, LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.foundationdb.sql.server; import com.foundationdb.server.error.InvalidParameterValueException; import com.foundationdb.server.error.UnsupportedCharsetException; import com.foundationdb.server.error.ZeroDateTimeException; import com.foundationdb.server.types.FormatOptions; import com.foundationdb.server.types.TExecutionContext; import com.foundationdb.server.types.TInstance; import com.foundationdb.server.types.aksql.aktypes.AkGUID; import com.foundationdb.server.types.common.types.TString; import com.foundationdb.server.types.common.types.TypesTranslator; import com.foundationdb.server.types.mcompat.mtypes.MDateAndTime; import com.foundationdb.server.types.value.Value; import com.foundationdb.server.types.value.ValueSource; import com.foundationdb.server.types.value.ValueSources; import com.foundationdb.util.AkibanAppender; import com.foundationdb.util.Strings; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; import java.math.BigDecimal; import java.sql.Types; import java.util.Collections; import java.util.Date; import java.io.*; /** Encode result values for transmission. */ public class ServerValueEncoder { public static enum ZeroDateTimeBehavior { NONE(null), EXCEPTION("exception"), ROUND("round"), CONVERT_TO_NULL("convertToNull"); private String propertyName; ZeroDateTimeBehavior(String propertyName) { this.propertyName = propertyName; } public static ZeroDateTimeBehavior fromProperty(String name) { if (name == null) return NONE; for (ZeroDateTimeBehavior zdtb : values()) { if (name.equals(zdtb.propertyName)) return zdtb; } throw new InvalidParameterValueException(String.format("Invalid name: %s for ZeroDateTimeBehavior", name)); } } public static final ValueSource ROUND_ZERO_DATETIME_SOURCE = new Value(MDateAndTime.DATETIME.instance(false), MDateAndTime.encodeDateTime(1, 1, 1, 0, 0, 0)); public static final ValueSource ROUND_ZERO_DATE_SOURCE = new Value(MDateAndTime.DATE.instance(false), MDateAndTime.encodeDate(1, 1, 1)); private final TypesTranslator typesTranslator; private final String encoding; private ZeroDateTimeBehavior zeroDateTimeBehavior; private FormatOptions options; private final ByteArrayOutputStream byteStream; private final PrintWriter printWriter; private final AkibanAppender appender; private DataOutputStream dataStream; public ServerValueEncoder(TypesTranslator typesTranslator, String encoding, FormatOptions options) { this(typesTranslator, encoding, new ByteArrayOutputStream(), options); } public ServerValueEncoder(TypesTranslator typesTranslator, String encoding, ZeroDateTimeBehavior zeroDateTimeBehavior, FormatOptions options) { this(typesTranslator, encoding, options); this.zeroDateTimeBehavior = zeroDateTimeBehavior; } public ServerValueEncoder(TypesTranslator typesTranslator, String encoding, ByteArrayOutputStream byteStream, FormatOptions options) { this.typesTranslator = typesTranslator; this.encoding = encoding; this.byteStream = byteStream; this.options = options; try { printWriter = new PrintWriter(new OutputStreamWriter(byteStream, encoding)); } catch (UnsupportedEncodingException ex) { throw new UnsupportedCharsetException(encoding); } // If the target encoding is UTF-8, we can support // canAppendBytes() for properly encoded source strings. if ("UTF-8".equals(encoding)) appender = AkibanAppender.of(byteStream, printWriter, "UTF-8"); else appender = AkibanAppender.of(printWriter); } public String getEncoding() { return encoding; } public ByteArrayOutputStream getByteStream() { printWriter.flush(); return byteStream; } public AkibanAppender getAppender() { return appender; } public DataOutputStream getDataStream() { printWriter.flush(); if (dataStream == null) dataStream = new DataOutputStream(byteStream); return dataStream; } /** * Encode the given value into a stream that can then be passed * to * <code>writeByteStream</code>. */ public ByteArrayOutputStream encodeValue(ValueSource value, ServerType type, boolean binary) throws IOException { if (value.isNull()) return null; if ((zeroDateTimeBehavior != ZeroDateTimeBehavior.NONE) && (((type.getType().typeClass() == MDateAndTime.DATE) && (value.getInt32() == 0)) || ((type.getType().typeClass() == MDateAndTime.DATETIME) && (value.getInt64() == 0)))) { switch (zeroDateTimeBehavior) { case EXCEPTION: throw new ZeroDateTimeException(); case ROUND: value = (type.getType().typeClass() == MDateAndTime.DATETIME) ? ROUND_ZERO_DATETIME_SOURCE : ROUND_ZERO_DATE_SOURCE; break; case CONVERT_TO_NULL: return null; } } reset(); appendValue(value, type, binary); return getByteStream(); } /** Encode the given direct value. */ public ByteArrayOutputStream encodePObject(Object value, ServerType type, boolean binary) throws IOException { if (value == null) return null; reset(); appendPObject(value, type, binary); return getByteStream(); } /** Reset the contents of the buffer. */ public void reset() { getByteStream().reset(); } /** Append the given value to the buffer. */ public void appendValue(ValueSource value, ServerType type, boolean binary) throws IOException { if (!binary) { // Handle unusual text encoding of binary types. switch (type.getBinaryEncoding()) { case BINARY_OCTAL_TEXT: processBinaryText(value); break; default: type.getType().format(value, appender); break; } } else { switch (type.getBinaryEncoding()) { case BINARY_OCTAL_TEXT: getByteStream().write(value.getBytes()); break; case INT_8: getDataStream().write((byte)typesTranslator.getIntegerValue(value)); break; case INT_16: getDataStream().writeShort((short)typesTranslator.getIntegerValue(value)); break; case INT_32: getDataStream().writeInt((int)typesTranslator.getIntegerValue(value)); break; case INT_64: getDataStream().writeLong(typesTranslator.getIntegerValue(value)); break; case FLOAT_32: getDataStream().writeFloat(value.getFloat()); break; case FLOAT_64: getDataStream().writeDouble(value.getDouble()); break; case STRING_BYTES: getByteStream().write(value.getString().getBytes(encoding)); break; case BOOLEAN_C: getDataStream().write(value.getBoolean() ? 1 : 0); break; case TIMESTAMP_FLOAT64_SECS_2000_NOTZ: getDataStream().writeDouble(seconds2000NoTZ(typesTranslator.getTimestampMillisValue(value)) + typesTranslator.getTimestampNanosValue(value) / 1.0e9); break; case TIMESTAMP_INT64_MICROS_2000_NOTZ: getDataStream().writeLong(seconds2000NoTZ(typesTranslator.getTimestampMillisValue(value)) * 1000000L + typesTranslator.getTimestampNanosValue(value) / 1000); break; case DAYS_2000: getDataStream().writeInt(days2000(typesTranslator.getTimestampMillisValue(value))); break; case TIME_FLOAT64_SECS_NOTZ: getDataStream().writeDouble(timeSecsNoTZ(typesTranslator.getTimestampMillisValue(value))); break; case TIME_INT64_MICROS_NOTZ: getDataStream().writeLong(timeSecsNoTZ(typesTranslator.getTimestampMillisValue(value)) * 1000000L); break; case DECIMAL_PG_NUMERIC_VAR: for (short d : pgNumericVar(typesTranslator.getDecimalValue(value))) { getDataStream().writeShort(d); } break; case UUID: getDataStream().write(AkGUID.uuidToBytes((java.util.UUID) value.getObject())); break; case NONE: default: throw new UnsupportedOperationException("No binary encoding for " + type); } } } private void processBinaryText(ValueSource value) { FormatOptions.BinaryFormatOption bfo = options.get(FormatOptions.BinaryFormatOption.class); String formattedString = bfo.format(value.getBytes()); printWriter.append(formattedString); } /** Append the given direct object to the buffer. */ public void appendPObject(Object value, ServerType type, boolean binary) throws IOException { if (type.getType().typeClass() instanceof TString && value instanceof String) { // Optimize the common case of directly encoding a string. printWriter.write((String)value); return; } ValueSource source = valuefromObject(value, type); appendValue(source, type, binary); } public ValueSource valuefromObject(Object value, ServerType type) { if (value instanceof Date) { TInstance dateType = javaDateTInstance(value); Value dateValue = new Value(dateType); typesTranslator.setTimestampMillisValue(dateValue, ((Date)value).getTime(), (value instanceof java.sql.Timestamp) ? ((java.sql.Timestamp)value).getNanos() : 0); TInstance targetType = type.getType(); if (dateType.equals(targetType)) return dateValue; TExecutionContext context = new TExecutionContext(Collections.singletonList(dateType), targetType, null); Value result = new Value(targetType); targetType.typeClass().fromObject(context, dateValue, result); return result; } else { // TODO this is inefficient, but I want to get it working. return ValueSources.valuefromObject(value, type.getType()); } } private TInstance javaDateTInstance(Object value) { int jdbcType; if (value instanceof java.sql.Date) { jdbcType = Types.DATE; } else if (value instanceof java.sql.Time) { jdbcType = Types.TIME; } else { jdbcType = Types.TIMESTAMP; } return typesTranslator.typeClassForJDBCType(jdbcType).instance(true); } public void appendString(String string) throws IOException { printWriter.write(string); } public PrintWriter getWriter() { return printWriter; } /** Adjust milliseconds since 1970-01-01 00:00:00-UTC to seconds since * 2000-01-01 00:00:00 timezoneless. A conversion from local time * to UTC involves an offset that varies for Summer time. A * conversion from local time to timezoneless just removes the * zone as though all days were the same length. */ private static long seconds2000NoTZ(long millis) { DateTimeZone dtz = DateTimeZone.getDefault(); millis += dtz.getOffset(millis); return millis / 1000 - 946684800; // 2000-01-01 00:00:00-UTC. } public static int days2000(long millis) { long secs = seconds2000NoTZ(millis); return (int)(secs / 86400); } public static int timeSecsNoTZ(long millis) { DateTime dt = new DateTime(millis); return dt.getSecondOfDay(); } private static final short NUMERIC_POS = 0x0000; private static final short NUMERIC_NEG = 0x4000; private static final short NUMERIC_NAN = (short)0xC000; private static short[] pgNumericVar(BigDecimal n) { short ndigits, weight, sign, dscale; dscale = (short)n.scale(); if (dscale < 0) dscale = 0; String s = n.toPlainString(); int lpos = 0; sign = NUMERIC_POS; if (s.charAt(lpos) == '-') { sign = NUMERIC_NEG; lpos++; } int dposl = s.indexOf('.', lpos), dposr; if (dposl < 0) dposr = dposl = s.length(); else dposr = dposl + 1; int nleft = (dposl - lpos + 3) / 4; weight = (short)(nleft - 1); int nright = (s.length() - dposr + 3) / 4; ndigits = (short)(nleft + nright); while ((ndigits > 0) && (pgNumericDigit(s, ndigits-1, lpos, dposl, dposr, nleft, nright) == 0)) { ndigits--; } short[] digits = new short[ndigits+4]; digits[0] = ndigits; digits[1] = weight; digits[2] = sign; digits[3] = dscale; for (int i = 0; i < ndigits; i++) { digits[i + 4] = pgNumericDigit(s, i, lpos, dposl, dposr, nleft, nright); } return digits; } private static short pgNumericDigit(String s, int index, int lpos, int dposl, int dposr, int nleft, int nright) { short result = 0; if (index < nleft) { int pos = dposl + (index - nleft) * 4; for (int i = 0; i < 4; i++) { result = (short)(result * 10); if (pos >= lpos) result += s.charAt(pos) - '0'; pos++; } } else { int pos = dposr + (index - nleft) * 4; for (int i = 0; i < 4; i++) { result = (short)(result * 10); if (pos < s.length()) result += s.charAt(pos) - '0'; pos++; } } return result; } }