/**
* 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.server.store.format.protobuf;
import com.foundationdb.ais.model.Column;
import com.foundationdb.ais.model.Group;
import com.foundationdb.ais.model.Join;
import com.foundationdb.ais.model.Table;
import com.foundationdb.qp.row.Row;
import com.foundationdb.qp.row.ValuesHolderRow;
import com.foundationdb.qp.rowtype.RowType;
import com.foundationdb.qp.util.SchemaCache;
import com.google.protobuf.DynamicMessage;
import com.google.protobuf.Descriptors.Descriptor;
import com.google.protobuf.Descriptors.FileDescriptor;
import com.google.protobuf.Descriptors.FieldDescriptor;
import com.google.protobuf.TextFormat;
import com.foundationdb.server.store.format.protobuf.CustomOptions.TableOptions;
import com.foundationdb.server.store.format.protobuf.CustomOptions.ColumnOptions;
import java.util.List;
import java.util.HashMap;
import java.util.Map;
import java.io.IOException;
public abstract class ProtobufRowConverter
{
protected final int tableId;
protected final Descriptor messageType;
protected ProtobufRowConverter(int tableId, Descriptor messageType) {
this.tableId = tableId;
this.messageType = messageType;
}
/** Get the id of the group's table, should caller wish to store it. */
public int getTableId() {
return tableId;
}
/** Get the message type against which the top-level message should be decoded. */
public Descriptor getMessageType() {
return messageType;
}
/** Convert the given row into a message. */
public abstract DynamicMessage encode(Row row);
/** Copy the given message into the given row data. */
public abstract Row decode(DynamicMessage msg);
public String shortFormat(DynamicMessage msg) {
return TextFormat.shortDebugString(msg);
}
public void format(DynamicMessage msg, Appendable output) throws IOException {
TextFormat.print(msg, output);
}
public static ProtobufRowConverter forGroup(Group group,
FileDescriptor fileDescriptor) {
// Find the group message.
Descriptor groupMessage = null;
List<Descriptor> messages = fileDescriptor.getMessageTypes();
for (int i = messages.size() - 1; i >= 0; i--) {
Descriptor message = messages.get(i);
if (message.getOptions().getExtension(TableOptions.fdbsql).getIsGroup()) {
groupMessage = message;
break;
}
}
if (groupMessage != null) {
return new GroupConverter(group, groupMessage);
}
else {
assert (messages.size() == 1 && group.getRoot().getChildJoins().isEmpty());
return new TableConverter(group.getRoot(), messages.get(0));
}
}
static class GroupConverter extends ProtobufRowConverter {
private final Map<Integer,ProtobufRowConverter> tableConvertersByTableId;
private final Map<Integer,FieldDescriptor> groupFieldsByTabelId;
private final Map<FieldDescriptor,ProtobufRowConverter> tableConvertersByField;
public GroupConverter(Group group, Descriptor groupMessage) {
super(group.getRoot().getTableId(), groupMessage);
Map<String,Table> tablesByUuid = new HashMap<>();
getUuids(group.getRoot(), tablesByUuid);
tableConvertersByTableId = new HashMap<>(tablesByUuid.size());
groupFieldsByTabelId = new HashMap<>(tablesByUuid.size());
tableConvertersByField = new HashMap<>(tablesByUuid.size());
for (FieldDescriptor field : groupMessage.getFields()) {
String uuid = field.getOptions().getExtension(ColumnOptions.fdbsql).getUuid();
Table table = tablesByUuid.get(uuid);
if (table != null) {
ProtobufRowConverter converter = new TableConverter(table, field.getMessageType());
tableConvertersByTableId.put(table.getTableId(), converter);
groupFieldsByTabelId.put(table.getTableId(), field);
tableConvertersByField.put(field, converter);
}
}
}
private void getUuids(Table table, Map<String,Table> tablesByUuid) {
tablesByUuid.put(table.getUuid().toString(), table);
for (Join join : table.getChildJoins()) {
getUuids(join.getChild(), tablesByUuid);
}
}
@Override
public DynamicMessage encode(Row row) {
Integer tableId = row.rowType().table().getTableId();
DynamicMessage inside =
tableConvertersByTableId.get(tableId).encode(row);
DynamicMessage.Builder builder = DynamicMessage.newBuilder(messageType);
builder.setField(groupFieldsByTabelId.get(tableId), inside);
return builder.build();
}
@Override
public Row decode(DynamicMessage msg) {
boolean first = true;
Row row = null;
for (FieldDescriptor field : msg.getAllFields().keySet()) {
ProtobufRowConverter tableConverter =
tableConvertersByField.get(field);
if (tableConverter != null) {
assert first;
first = false;
row = tableConverter.decode((DynamicMessage)msg.getField(field));
}
}
assert !first;
return row;
}
}
static class TableConverter extends ProtobufRowConverter {
private final int nfields;
private final ProtobufRowConversion[] conversions;
private final FieldDescriptor[] fields;
private final FieldDescriptor[] nullFields;
private final Map<FieldDescriptor,Integer> columnIndexesByField;
private final Map<FieldDescriptor,Integer> nullableIndexesByField;
private final RowType rowType;
public TableConverter(Table table, Descriptor tableMessage) {
super(table.getTableId(), tableMessage);
nfields = table.getColumnsIncludingInternal().size();
conversions = new ProtobufRowConversion[nfields];
fields = new FieldDescriptor[nfields];
columnIndexesByField = new HashMap<>(nfields);
Map<String,Integer> columnIndexedByUuid = new HashMap<>(nfields);
for (int i = 0; i < nfields; i++) {
Column column = table.getColumnsIncludingInternal().get(i);
conversions[i] = ProtobufRowConversion.forTInstance(column.getType());
columnIndexedByUuid.put(column.getUuid().toString(), i);
}
FieldDescriptor[] nullFields = null;
Map<FieldDescriptor,Integer> nullableIndexesByField = null;
for (FieldDescriptor field : tableMessage.getFields()) {
ColumnOptions options = field.getOptions().getExtension(ColumnOptions.fdbsql);
if (options.hasUuid()) {
Integer columnIndex = columnIndexedByUuid.get(options.getUuid());
if (columnIndex != null) {
fields[columnIndex] = field;
columnIndexesByField.put(field, columnIndex);
}
}
else if (options.hasNullForField()) {
if (nullFields == null) {
nullFields = new FieldDescriptor[nfields];
nullableIndexesByField = new HashMap<>(nfields);
}
FieldDescriptor forField = tableMessage.findFieldByNumber(options.getNullForField());
Integer columnIndex = columnIndexesByField.get(forField);
nullFields[columnIndex] = field;
nullableIndexesByField.put(field, columnIndex);
}
}
this.nullFields = nullFields;
this.nullableIndexesByField = nullableIndexesByField;
this.rowType = SchemaCache.globalSchema(table.getAIS()).tableRowType(table);
}
@Override
public DynamicMessage encode(Row row) {
DynamicMessage.Builder builder = DynamicMessage.newBuilder(messageType);
for (int i = 0; i < fields.length; i++) {
if (row.value(i).isNull()) {
if (nullFields != null) {
FieldDescriptor nullField = nullFields[i];
if (nullField != null) {
builder.setField(nullField, Boolean.TRUE);
}
}
} else {
conversions[i].setValue(builder, fields[i], row.value(i));
}
}
return builder.build();
}
@Override
public Row decode(DynamicMessage msg) {
Object[] objects = new Object[fields.length];
for (FieldDescriptor field : msg.getAllFields().keySet()) {
Integer columnIndex = columnIndexesByField.get(field);
if (columnIndex != null) {
objects[columnIndex] = conversions[columnIndex].getValue(msg, field);
}
else {
Integer nullIndex = nullableIndexesByField.get(field);
if (nullIndex != null) {
// TODO: It's already null, because we aren't
// handling defaults yet.
objects[nullIndex] = null;
}
}
}
ValuesHolderRow row = new ValuesHolderRow (rowType, objects);
return row;
}
}
}