1
\$\begingroup\$

Introduction

For my purposes, I have written this simple database management system, that can do the following:

  • Support of 7 data types: int, long, float, double, boolean, String, byte[],
  • manipulating columns,
  • saving the entire database into a file and reading it later into main memory.

And these are the most notable constraints:

  • No SQL layer, the user manipulates the database in code,
  • no thread-safety, the user must explicitly synchronize if more than one thread is operating on the database.
  • no ACID.

Code

Database.java

package net.coderodde.javadb;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;

public final class Database {

    private static final int SIZE_BYTES = Integer.BYTES;

    /**
     * The name of this database.
     */
    private String databaseName;

    /**
     * The map mapping the table name to the actual table.
     */
    private final Map<String, Table> tableMap = new LinkedHashMap<>();

    /**
     * The file this database was built from.
     */
    private File file;

    public Database(String databaseName) {
        Objects.requireNonNull(databaseName, "The database name is null.");
        this.databaseName = checkDatabaseName(databaseName);
    }

    public String getDatabaseName() {
        return databaseName;
    }

    public void setDatabaseName(String databaseName) {
        this.databaseName =
                Objects.requireNonNull(
                        databaseName, "The new database name is null.");
    }

    /**
     * Creates a new empty table with given columns.
     * 
     * @param tableName              the table name.
     * @param tableColumnDescriptors the column descriptors.
     */
    public Table createTable(String tableName,
                            TableColumnDescriptor... tableColumnDescriptors) {
        Objects.requireNonNull(tableName, "The table name is null");
        checkTableNameNotEmpty(tableName);
        checkTableNameNotOccupied(tableName);
        checkTableColumnDescriptors(tableColumnDescriptors);
        Table table = new Table(tableName);

        for (TableColumnDescriptor tableColumnDescriptor 
                : tableColumnDescriptors) {
            Objects.requireNonNull(tableColumnDescriptor,
                                   "The table column descriptor is null.");

            table.addTableColumnDescriptor(tableColumnDescriptor);
        }

        tableMap.put(tableName, table);
        return table;
    }

    public Table getTable(String tableName) {
        Objects.requireNonNull(tableName, "The input table name is null.");
        checkTableIsInThisDatabase(tableName);
        return tableMap.get(tableName);
    }

    public void deleteTable(String tableName) {
        Objects.requireNonNull(tableName, "The input table name is null.");
        checkTableIsInThisDatabase(tableName);
        tableMap.remove(tableName);
    }

    public void save() {
        if (file == null) {
            throw new IllegalStateException(
                    "This database was not previously saved.");
        }

        save(file);
    }

    public void save(File file) {
        ByteBuffer bb = serialize();
        byte[] bytes = bb.array();
        // Write the bytes.

        try {
            if (!file.exists()) {
                file.createNewFile();
            }

            FileOutputStream fos = new FileOutputStream(file);
            fos.write(bytes);
            fos.close();
        } catch (Exception ex) {
            throw new RuntimeException("Saving the databse \"" + databaseName +
                    "\" failed.", ex);
        } 
    }

    public void save(String path) {
        save(new File(path));
    }

    public static Database read(File file) {
        try {
            FileInputStream fis = new FileInputStream(file);
            byte[] bytes = new byte[fis.available()];
            ByteBuffer byteBuffer = ByteBuffer.allocate(bytes.length)
                                              .order(ByteOrder.LITTLE_ENDIAN);
            fis.read(bytes);
            byteBuffer.put(bytes);
            byteBuffer.position(0);
            return Database.deserialize(byteBuffer);
        } catch (Exception ex) {
            throw new RuntimeException("Reading the database from file \"" +
                    file.getAbsolutePath() + "\" failed.", ex);
        }
    }

    public static Database read(String path) {
        return read(new File(path));
    }

    public ByteBuffer serialize() {
        ByteBuffer byteBuffer = 
                ByteBuffer.allocate(getSerializationLength())
                          .order(ByteOrder.LITTLE_ENDIAN);

        // Output the database name length:
        byteBuffer.putInt(databaseName.length());

        // Output the database name:
        for (char c : databaseName.toCharArray()) {
            byteBuffer.putChar(c);
        }

        // Output the number of tables:
        byteBuffer.putInt(tableMap.size());

        for (Table table : tableMap.values()) {
            table.serialize(byteBuffer);
        }

        return byteBuffer;
    }

    @Override
    public boolean equals(Object o) {
        if (o == null) {
            return false;
        }

        if (o == this) {
            return true;
        }

        if (!getClass().equals(o.getClass())) {
            return false;
        }

        Database other = (Database) o;

        if (!databaseName.equals(other.databaseName)) {
            return false;
        }

        return tableMap.equals(other.tableMap);
    }

    public static Database deserialize(ByteBuffer byteBuffer) {
        int databaseNameLength = byteBuffer.getInt();
        StringBuilder sb = new StringBuilder(databaseNameLength);

        for (int i = 0; i < databaseNameLength; ++i) {
            sb.append(byteBuffer.getChar());
        }

        String databaseName = sb.toString();
        Database database = new Database(databaseName);

        int numberOfTables = byteBuffer.getInt();

        for (int i = 0; i < numberOfTables; ++i) {
            Table table = Table.deserialize(byteBuffer);
            database.tableMap.put(table.getTableName(), table);
        }

        return database;
    }

    void onTableRename(Table table, String oldTableName, String newTableName) {
        tableMap.remove(oldTableName);
        tableMap.put(newTableName, table);
    }

    private int getSerializationLength() {
        int serializationLength = SIZE_BYTES; // Database name length.

        // Database name.
        serializationLength += databaseName.length() * Character.BYTES;
        serializationLength += SIZE_BYTES; // Table count.

        // Tables.
        for (Table table : tableMap.values()) {
            serializationLength += table.getSerializationLength();
        }

        return serializationLength;
    }

    private void checkTableNameNotOccupied(String tableName) {
        if (tableMap.containsKey(tableName)) {
            throw new IllegalStateException(
                    "Table name \"" + tableName + "\" is already occupied.");
        }
    }

    private void checkTableIsInThisDatabase(String tableName) {
        if (!tableMap.containsKey(tableName)) {
            throw new IllegalStateException(
                    "The table with name \"" + tableName + "\" is not in " +
                    "this database.");
        }
    }

    private void checkTableNameNotEmpty(String tableName) {
        if (tableName.isEmpty()) {
            throw new IllegalArgumentException("The table name is empty.");
        }
    }

    private void checkTableColumnDescriptors(
            TableColumnDescriptor[] tableColumnDescriptors) {
        if (tableColumnDescriptors.length == 0) {
            throw new IllegalArgumentException("No column descriptors given.");
        }

        for (int i = 0; i < tableColumnDescriptors.length; ++i) {
            Objects.requireNonNull(tableColumnDescriptors[i],
                    "The " + i + "th table column descriptor is null.");
        }
    }

    public static void main(String[] args) {
        Database db = new Database("my_db");

        TableColumnDescriptor table1Id =
                new TableColumnDescriptor("id", TableCellType.TYPE_INT);

        TableColumnDescriptor table1First =
                new TableColumnDescriptor("first_name", 
                                          TableCellType.TYPE_STRING);

        TableColumnDescriptor table1Last =
                new TableColumnDescriptor("last_name", 
                                          TableCellType.TYPE_STRING);

        Table table1 = db.createTable("person", 
                                      table1Id, 
                                      table1First, 
                                      table1Last);

        TableColumnDescriptor table2Id =
                new TableColumnDescriptor("id", TableCellType.TYPE_INT);

        TableColumnDescriptor table2PersonId =
                new TableColumnDescriptor("person_id", 
                                          TableCellType.TYPE_LONG);

        TableColumnDescriptor table2Msg =
                new TableColumnDescriptor("msg", 
                                          TableCellType.TYPE_STRING);

        Table table2 = db.createTable("msg", 
                                      table2Id, 
                                      table2PersonId, 
                                      table2Msg);

        table1.putTableRow(1, "Rodion", "Efremov");
        table1.putTableRow(2, "Violetta", "Ervasti");

        table2.putTableRow(10, 1L, "Hello!");
        table2.putTableRow(11, 2L, "Bye!");

        db.save("my_db.dat");

        Database db2 = Database.read("my_db.dat");

        Table table1copy = db2.getTable("person");
        Table table2copy = db2.getTable("msg");

        TableView view1 = table1copy.createTableView(table1Id,
                                                     table1First, 
                                                     table1Last);

        TableView view2 = table2copy.createTableView(table2Id,
                                                     table2PersonId,
                                                     table2Msg);

        view1.addTableRow(table1copy.getTableRow(0));
        view1.addTableRow(table1copy.getTableRow(1));

        view2.addTableRow(table2copy.getTableRow(0));
        view2.addTableRow(table2copy.getTableRow(1));

        System.out.println(view1.toString());
        System.out.println(view2);
    }

    private String checkDatabaseName(String databaseName) {
        databaseName = databaseName.trim().toLowerCase();

        if (databaseName.equals("null")) {
            throw new IllegalArgumentException(
                    "\"null\" is not a valid database name.");
        }

        return  databaseName;
    }
}

Table.java

package net.coderodde.javadb;

import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;

/**
 * This class implements a database table. It consists of list of table column 
 * descriptors and the list of table rows.
 * 
 * @author Rodion "rodde" Efremov
 * @version 1.6 (Jul 17, 2017)
 */
public final class Table implements Iterable<TableRow> {

    /**
     * Number of bytes used to decode the table name.
     */
    private static final int SIZE_BYTES = 4;

    /**
     * The name of this table.
     */
    private String tableName;

    /**
     * The database holding this table.
     */
    private Database ownerDatabase;

    /**
     * The list of column descriptors.
     */
    final List<TableColumnDescriptor> tableColumnDescriptorList = 
            new ArrayList<>();

    /**
     * Maps the name of a column to its index. The index in question is the
     * location index of the corresponding table column descriptor.
     */
    final Map<String, Integer> tableColumnNameIndexMap = 
            new HashMap<>();

    /**
     * The actual list of table rows.
     */
    private final List<TableRow> tableRowList = new ArrayList<>();

    /**
     * Constructs a new table for a given owner database.
     * 
     * @param tableName the name of the new table.
     */
    public Table(String tableName) {
        Objects.requireNonNull(tableName, "The table name is null.");
        this.tableName = checkTableName(tableName);
    }

    /**
     * Returns the name of this table.
     * 
     * @return the name of this table.
     */
    public String getTableName() {
        return tableName;
    }

    /**
     * Renames this table.
     * 
     * @param newTableName the new table name.
     */
    public void setTableName(String newTableName) {
        String oldTableName = this.tableName;
        this.tableName =
                Objects.requireNonNull(
                        newTableName,
                        "The new table name is null.");

        this.ownerDatabase.onTableRename(this, oldTableName, newTableName);
    }
    /**
     * Inserts a new table row using {@code values} as the cell values.
     * 
     * @param index  the index at which to insert the new table row.
     * @param values the array of values to store.
     */
    public TableRow putTableRowAt(int index, Object... values) {
        checkInsertionIndex(index);
        checkNumberOfValuesNoLargerThanColumnCount(values);

        for (int i = 0; i < values.length; ++i) {
            checkValueType(values[i], tableColumnDescriptorList.get(i));
        }

        TableRow newTableRow = new TableRow(this);
        TableCell newTableCell;

        for (int i = 0; i < values.length; ++i) {
            switch (tableColumnDescriptorList.get(i).getTableCellType()) {
                case TYPE_INT:
                    newTableCell = new TableCell(TableCellType.TYPE_INT);
                    newTableCell.setIntValue((Integer) values[i]);
                    newTableRow.add(newTableCell);
                    break;

                case TYPE_LONG:
                    newTableCell = new TableCell(TableCellType.TYPE_LONG);
                    newTableCell.setLongValue((Long) values[i]);
                    newTableRow.add(newTableCell);
                    break;

                case TYPE_FLOAT:
                    newTableCell = new TableCell(TableCellType.TYPE_FLOAT);
                    newTableCell.setFloatValue((Float) values[i]);
                    newTableRow.add(newTableCell);
                    break;

                case TYPE_DOUBLE:
                    newTableCell = new TableCell(TableCellType.TYPE_DOUBLE);
                    newTableCell.setDoubleValue((Double) values[i]);
                    newTableRow.add(newTableCell);
                    break;

                case TYPE_STRING:
                    newTableCell = new TableCell(TableCellType.TYPE_STRING);
                    newTableCell.setStringValue((String) values[i]);
                    newTableRow.add(newTableCell);
                    break;

                case TYPE_BINARY:
                    newTableCell = new TableCell(TableCellType.TYPE_BINARY);
                    newTableCell.setBinaryData((byte[]) values[i]);
                    newTableRow.add(newTableCell);
                    break;

                case TYPE_BOOLEAN:
                    newTableCell = new TableCell(TableCellType.TYPE_BOOLEAN);
                    newTableCell.setBooleanValue((Boolean) values[i]);
                    newTableRow.add(newTableCell);
                    break;

                default:
                    throw new IllegalStateException("Should not get here.");
            }
        }

        for (int i = values.length; i < tableColumnDescriptorList.size(); ++i) {
            switch (tableColumnDescriptorList.get(i).getTableCellType()) {
                case TYPE_INT:
                    newTableCell = new TableCell(TableCellType.TYPE_INT);
                    newTableRow.add(newTableCell);
                    break;

                case TYPE_LONG:
                    newTableCell = new TableCell(TableCellType.TYPE_LONG);
                    newTableRow.add(newTableCell);
                    break;

                case TYPE_FLOAT:
                    newTableCell = new TableCell(TableCellType.TYPE_FLOAT);
                    newTableRow.add(newTableCell);
                    break;

                case TYPE_DOUBLE:
                    newTableCell = new TableCell(TableCellType.TYPE_DOUBLE);
                    newTableRow.add(newTableCell);
                    break;

                case TYPE_STRING:
                    newTableCell = new TableCell(TableCellType.TYPE_STRING);
                    newTableRow.add(newTableCell);
                    break;

                case TYPE_BINARY:
                    newTableCell = new TableCell(TableCellType.TYPE_BINARY);
                    newTableRow.add(newTableCell);
                    break;

                case TYPE_BOOLEAN:
                    newTableCell = new TableCell(TableCellType.TYPE_BOOLEAN);
                    newTableRow.add(newTableCell);
                    break;

                default:
                    throw new IllegalStateException("Should not get here.");
            }
        }

        newTableRow.ownerTable = this;
        tableRowList.add(index, newTableRow);
        return newTableRow;
    }

    /**
     * Appends a new table row to this table.
     * 
     * @param values the values to use as the row cell values.
     */
    public TableRow putTableRow(Object... values) {
        return putTableRowAt(tableRowList.size(), values);
    }

    /**
     * Returns the {@code index} row of this table.
     * 
     * @param index the row index.
     * @return the table row.
     */
    public TableRow getTableRow(int index) {
        checkAccessIndex(index);
        return tableRowList.get(index);
    }

    /**
     * Removes the {@code index}th row from this table.
     * 
     * @param index the index of the row to remove.
     */
    public void removeRow(int index) {
        checkAccessIndex(index);
        TableRow tableRow = tableRowList.remove(index);
        tableRow.ownerTable = null;
    }

    /**
     * Returns the iterator that iterates over table rows in their natural
     * order.
     * 
     * @return the row iterator.
     */
    @Override
    public Iterator<TableRow> iterator() {
        return new Iterator(){
            private final Iterator<TableRow> iterator = tableRowList.iterator();

            @Override
            public boolean hasNext() {
                return iterator.hasNext();
            }

            @Override
            public Object next() {
                return iterator.next();
            }
        };
    }

    public TableView createTableView(
            TableColumnDescriptor... tableColumnDescriptors) {
        checkViewTableColumnDescriptors(tableColumnDescriptors);
        TableView tableView = 
                new TableView(this, Arrays.asList(tableColumnDescriptors));

        return tableView;
    }

    /**
     * Appends the table column descriptor to this table. The current rows are 
     * extended behind the scene.
     * 
     * @param tableColumnDescriptor the table column descriptor to append. 
     */
    public void addTableColumnDescriptor(
            TableColumnDescriptor tableColumnDescriptor) {
        String newTableColumnDescriptorName = 
                tableColumnDescriptor.getTableColumnName();

        if (tableColumnNameIndexMap.containsKey(newTableColumnDescriptorName)) {
            throw new IllegalStateException(
                    "The column with name \"" + newTableColumnDescriptorName +
                            " is already in this table.");
        }

        int index = tableColumnDescriptorList.size();

        Objects.requireNonNull(tableColumnDescriptor,
                "The table column descriptor is null.");

        tableColumnDescriptorList.add(tableColumnDescriptor);

        for (TableRow tableRow : tableRowList) {
            tableRow.add(new TableCell(
                    tableColumnDescriptor.getTableCellType()));
        }

        tableColumnNameIndexMap.put(tableColumnDescriptor.getTableColumnName(), 
                                    index);
    }

    /**
     * Returns the {@code columnIndex}th table column descriptor.
     * 
     * @param columnIndex the index of the column descriptor to return.
     * @return the table column descriptor.
     */
    public TableColumnDescriptor getTableColumnDescriptor(int columnIndex) {
        return tableColumnDescriptorList.get(columnIndex);
    }

    /**
     * Returns the table column descriptor with the given name.
     * 
     * @param columnName the name of the column.
     * 
     * @return the table column descriptor with the name {@code columnName}.
     * 
     * @throws IllegalArgumentException if there is no column with the given 
     *                                  name.
     */
    public TableColumnDescriptor getTableColumnDescriptor(String columnName) {
        Integer index = tableColumnNameIndexMap.get(columnName);

        if (index == null) {
            throw new IllegalArgumentException(
                    "\"" + columnName + "\": no such column.");
        }

        return tableColumnDescriptorList.get(index);
    }

    public boolean containsTableColumnDescriptor(String columnName) {
        return tableColumnNameIndexMap.containsKey(columnName);
    }

    /**
     * Removes the {@code columnIndex}th column from this table.
     * 
     * @param columnIndex the index of the column to remove.
     * 
     * @return the table column descriptor.
     */
    public TableColumnDescriptor removeTableColumnDescriptor(int columnIndex) {
        TableColumnDescriptor tableColumnDescriptor = 
                tableColumnDescriptorList.remove(columnIndex);

        for (TableRow tableRow : tableRowList) {
            tableRow.remove(columnIndex);
        }

        return tableColumnDescriptor;
    }

    /**
     * Removes the table column with the given name.
     * 
     * @param columnName the name of the column to remove.
     * 
     * @return the table column descriptor.
     */
    public TableColumnDescriptor
         removeTableColumnDescriptor(String columnName) {
        Integer index = tableColumnNameIndexMap.get(columnName);

        if (index == null) {
            throw new IllegalArgumentException(
                    columnName + ": no such column.");
        }

        for (TableRow tableRow : tableRowList) {
            tableRow.remove(index);
        }

        TableColumnDescriptor tableColumnDescriptor = 
                tableColumnDescriptorList.get(index);

        tableColumnDescriptorList.remove(tableColumnDescriptor);
        tableColumnNameIndexMap.clear();

        // Remap the column index map:
        int columnIndex = 0;

        for (TableColumnDescriptor tcd
                : tableColumnDescriptorList) {
            tableColumnNameIndexMap.put(tcd.getTableColumnName(), 
                                        columnIndex++);
        }

        return tableColumnDescriptor;
    }

    @Override
    public boolean equals(Object o) {
        if (o == null) {
            return false;
        }

        if (o == this) {
            return true;
        }

        if (!getClass().equals(o.getClass())) {
            return false;
        }

        Table other = (Table) o;

        if (!tableColumnDescriptorList.equals(other.tableColumnDescriptorList)) {
            return false;
        }

        return tableRowList.equals(other.tableRowList);
    }

    int getSerializationLength() {
        int serializationLength = SIZE_BYTES + 
                Character.BYTES * tableName.length() + 2 * SIZE_BYTES;

        for (TableColumnDescriptor tableColumnDescriptor : 
                tableColumnDescriptorList) {
            serializationLength += 
                    tableColumnDescriptor.getSerializationLength();
        }

        for (TableRow tableRow : tableRowList) {
            serializationLength += tableRow.getSerializationLength();
        }

        return serializationLength;
    }

    void serialize(ByteBuffer byteBuffer) {
        int tableNameLength = tableName.length();
        // Dump the length of the table length:
        byteBuffer.putInt(tableNameLength);

        // Dump the actual name of the table:
        for (char c : tableName.toCharArray()) {
            byteBuffer.putChar(c);
        }

        // Dump the number of columns:
        byteBuffer.putInt(tableColumnDescriptorList.size());

        for (TableColumnDescriptor tableColumnDescriptor : 
                tableColumnDescriptorList) {
            tableColumnDescriptor.serialize(byteBuffer);
        }

        // Dump the number of rows:
        byteBuffer.putInt(tableRowList.size());

        for (TableRow tableRow : tableRowList) {
            tableRow.serialize(byteBuffer);
        }
    }

    static Table deserialize(ByteBuffer byteBuffer) {
        int tableNameLength = byteBuffer.getInt();
        StringBuilder sb = new StringBuilder(tableNameLength);

        for (int i = 0; i < tableNameLength; ++i) {
            sb.append(byteBuffer.getChar());
        }

        int numberOfColumns = byteBuffer.getInt();
        Table table = new Table(sb.toString());

        for (int i = 0; i < numberOfColumns; ++i) {
            TableColumnDescriptor tableColumnDescriptor =
                    TableColumnDescriptor.deserialize(byteBuffer);

            table.addTableColumnDescriptor(tableColumnDescriptor);
        }

        int numberOfRows = byteBuffer.getInt();

        for (int i = 0; i < numberOfRows; ++i) {
            TableRow tableRow = TableRow.deserialize(byteBuffer,
                                                     numberOfColumns);
            table.addRow(tableRow);
        }

        return table;
    }

    void onTableColumnRename(TableColumnDescriptor tableColumn, 
                             String oldTableColumnName, 
                             String newTableColumnName) {
        int index = tableColumnDescriptorList.indexOf(tableColumn);

        if (index < 0) {
            throw new IllegalStateException(
                    "Trying to update a column that is not in this table.");
        }
    }

    private void addRow(TableRow tableRow) {
        tableRow.ownerTable = this;
        tableRowList.add(tableRow);
    }

    private void checkInsertionIndex(int index) {
        if (index < 0) {
            throw new IndexOutOfBoundsException("The insertion index is " +
                    "negative: " + index);
        }

        if (index > tableRowList.size()) {
            throw new IndexOutOfBoundsException("The insertion index is " +
                    "too large: " + index + ". Must be at most " + 
                    tableRowList.size());
        }
    }

    private void checkAccessIndex(int index) {
        if (index < 0) {
            throw new IndexOutOfBoundsException("The access index is " +
                    "negative: " + index);
        }

        if (index >= tableRowList.size()) {
            throw new IndexOutOfBoundsException("The insertion index is " +
                    "too large: " + index + ". Must be at most " + 
                    (tableRowList.size() - 1));
        }
    }

    private void checkNumberOfValuesNoLargerThanColumnCount(Object[] values) {
        if (values.length > tableColumnDescriptorList.size()) {
            throw new IllegalArgumentException("More values (" +
                    values.length + ") than number of columns (" +
                    tableColumnDescriptorList.size() + ".");
        }
    }

    private void checkValueType(Object value, 
                                TableColumnDescriptor tableColumnDescriptor) {
        if (value == null) {
            return;
        }

        if (value instanceof Integer) {
            if (!tableColumnDescriptor.getTableCellType()
                                      .equals(TableCellType.TYPE_INT)) {
                throw new IllegalArgumentException("Expected type mismatch. " +
                        "Expected " + 
                        tableColumnDescriptor.getTableCellType()
                                             .getTypeName() + ", " + 
                        "integer received.");                        
            }
        }

        if (value instanceof Long) {
            if (!tableColumnDescriptor.getTableCellType()
                                      .equals(TableCellType.TYPE_LONG)) {
                throw new IllegalArgumentException("Expected type mismatch. " +
                        "Expected " + 
                        tableColumnDescriptor.getTableCellType()
                                             .getTypeName() + ", " + 
                        "long received.");                        
            }
        }

        if (value instanceof Float) {
            if (!tableColumnDescriptor.getTableCellType()
                                      .equals(TableCellType.TYPE_FLOAT)) {
                throw new IllegalArgumentException("Expected type mismatch. " +
                        "Expected " + 
                        tableColumnDescriptor.getTableCellType()
                                             .getTypeName() + ", " + 
                        "float received.");                        
            }
        }

        if (value instanceof Double) {
            if (!tableColumnDescriptor.getTableCellType()
                                      .equals(TableCellType.TYPE_DOUBLE)) {
                throw new IllegalArgumentException("Expected type mismatch. " +
                        "Expected " + 
                        tableColumnDescriptor.getTableCellType()
                                             .getTypeName() + ", " + 
                        "double received.");                        
            }
        }

        if (value instanceof String) {
            if (!tableColumnDescriptor.getTableCellType()
                                      .equals(TableCellType.TYPE_STRING)) {
                throw new IllegalArgumentException("Expected type mismatch. " +
                        "Expected " + 
                        tableColumnDescriptor.getTableCellType()
                                             .getTypeName() + ", " + 
                        "string received.");                        
            }
        }

        if (value instanceof byte[]) {
            if (!tableColumnDescriptor.getTableCellType()
                                      .equals(TableCellType.TYPE_BINARY)) {
                throw new IllegalArgumentException("Expected type mismatch. " +
                        "Expected " + 
                        tableColumnDescriptor.getTableCellType()
                                             .getTypeName() + ", " + 
                        "binary received.");                        
            }
        }

        if (value instanceof Boolean) {
            if (!tableColumnDescriptor.getTableCellType()
                                      .equals(TableCellType.TYPE_BOOLEAN)) {
                throw new IllegalArgumentException("Expected type mismatch. " +
                        "Expected " + 
                        tableColumnDescriptor.getTableCellType()
                                             .getTypeName() + ", " + 
                        "boolean received.");                        
            }
        }
    }

    private void checkViewTableColumnDescriptors(
            TableColumnDescriptor[] tableColumnDescriptors) {
        Objects.requireNonNull(tableColumnDescriptors, 
                               "Table view descriptors are null.");

        for (TableColumnDescriptor tableColumnDescriptor 
                : tableColumnDescriptors) {
            Objects.requireNonNull(
                    tableColumnDescriptor, 
                    "The table column descriptor is null.");

            if (!this.tableColumnDescriptorList
                     .contains(tableColumnDescriptor)) {
                throw new IllegalArgumentException(
                        "The table column descriptor is not in this table.");
            }
        }
    }

    private String checkTableName(String tableName) {
        tableName = tableName.trim().toLowerCase();

        if (tableName.equals("null")) {
            throw new IllegalArgumentException(
                    "\"null\" is not a valid table name.");
        }

        return  tableName;
    }
}

TableColumnDescriptor.java

package net.coderodde.javadb;

import java.nio.ByteBuffer;
import java.util.Objects;

/**
 * This class implements a table column descriptor. It holds the name of the 
 * column and its data type.
 * 
 * @author Rodion "rodde" Efremov 
 * @version 1.6 (Jul 16, 2017)
 */
public final class TableColumnDescriptor {

    /**
     * Number of bytes it takes to encode a size count.
     */
    private static final int SIZE_BYTES = 4;;

    /**
     * The name of this table column.
     */
    private String tableColumnName;

    /**
     * The data type of this table column.
     */
    private TableCellType tableCellType;

    /**
     * The table that owns this table column.
     */
    private Table ownerTable;

    /**
     * Constructs a new table column descriptor.
     * 
     * @param tableColumnName the name of the column.
     * @param tableCellType   the data type of the column.
     */
    public TableColumnDescriptor(String tableColumnName,
                                 TableCellType tableCellType) {
        Objects.requireNonNull(tableColumnName, "Table column name is null.");
        Objects.requireNonNull(tableCellType, "Table cell type is null.");
        setTableColumnName(checkTableColumnName(tableColumnName));
        setTableCellType(tableCellType);
    }

    /**
     * Returns the name of this column.
     * 
     * @return the name of this column. 
     */
    public String getTableColumnName() {
        return tableColumnName;
    }

    /**
     * Returns the data type of this column.
     * 
     * @return the data type of this column.
     */
    public TableCellType getTableCellType() {
        return tableCellType;
    }

    /**
     * Renames this table column.
     * 
     * @param newTableColumnName the new table column name.
     */
    public void setTableColumnName(String newTableColumnName) {
        String oldTableColumnName = this.tableColumnName;

        this.tableColumnName =
                Objects.requireNonNull(newTableColumnName, 
                                       "The table column name is null.");
        if (ownerTable != null) {
            ownerTable.onTableColumnRename(this,
                                           oldTableColumnName,
                                           newTableColumnName);
        }
    }

    public void setTableCellType(TableCellType tableCellType) {
        this.tableCellType = Objects.requireNonNull(tableCellType,
                                                    "Table cell type is null.");
    }

    public int getSerializationLength() {
        return SIZE_BYTES + Character.BYTES * tableColumnName.length() + 1;
    }

    @Override
    public int hashCode() {
        return tableColumnName.hashCode() ^ tableCellType.getTypeId();
    }

    @Override
    public boolean equals(Object o) {
        if (o == null) {
            return false;
        }

        if (o == this) {
            return true;
        }

        if (!getClass().equals(o.getClass())) {
            return false;
        }

        TableColumnDescriptor other = (TableColumnDescriptor) o;

        return tableColumnName.equals(other.tableColumnName)
                && tableCellType.equals(other.tableCellType);
    }

    void serialize(ByteBuffer byteBuffer) {
        byteBuffer.putInt(tableColumnName.length());

        for (char c : tableColumnName.toCharArray()) {
            byteBuffer.putChar(c);
        }

        byteBuffer.put(tableCellType.getTypeId());
    }

    public static TableColumnDescriptor deserialize(ByteBuffer byteBuffer) {
        int tableColumnNameLength = byteBuffer.getInt();
        StringBuilder sb = new StringBuilder(tableColumnNameLength);

        for (int i = 0; i < tableColumnNameLength; ++i) {
            sb.append(byteBuffer.getChar());
        }

        byte typeId = byteBuffer.get();

        TableColumnDescriptor tableColumnDescriptor = 
                new TableColumnDescriptor(
                        sb.toString(), 
                        TableCellType.getTableCellType(typeId));

        return tableColumnDescriptor;
    }

    private String checkTableColumnName(String tableColumnName) {
        tableColumnName = tableColumnName.trim().toLowerCase();

        if (tableColumnName.equals("null")) {
            throw new IllegalArgumentException(
                    "\"null\" is not a valid table column name.");
        }

        return  tableColumnName;
    }
}

TableRow.java

package net.coderodde.javadb;

import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/**
 * This class implements a row in a table.
 * 
 * @author Rodion "rodde" Efremov
 * @version 1.6 (Jul 16, 2017)
 */
public class TableRow {

    /**
     * This list stores the actual table cells.
     */
    private final List<TableCell> tableCellList = new ArrayList<>();

    /**
     * The table this row belongs to.
     */
    Table ownerTable;

    TableRow(Table onwerTable) {
        this.ownerTable = ownerTable;
    }

    /**
     * Returns the number of cells in this table row.
     * 
     * @return the number of cells.
     */
    public int getNumberOfCells() {
        return tableCellList.size();
    }

    /**
     * Returns the table cell by its respective column name.
     * 
     * @param columnName the name of the column.
     * 
     * @return the table cell with the given column name.
     */
    public TableCell get(String columnName) {
        if (ownerTable == null) {
            throw new IllegalStateException(
                    "This table row does not belong to a table. Use " +
                            "get(int) instead.");
        }

        Integer columnIndex = 
                ownerTable.tableColumnNameIndexMap.get(columnName);

        if (columnIndex == null) {
            throw new IllegalArgumentException("\"" +
                    columnName + "\": no such column.");
        }

        return tableCellList.get(columnIndex);
    }

    /**
     * Returns the {@code index}th cell.
     * 
     * @param index the index of the cell.
     * 
     * @return the table cell.
     */
    public TableCell get(int index) {
        checkAccessIndex(index);
        return tableCellList.get(index);
    }

    /**
     * Adds the table cell to this table row at a given 
     * 
     * @param index     the position index.
     * @param tableCell the table cell to add.
     */
    public void add(int index, TableCell tableCell) {        
        checkInsertionIndex(index);
        Objects.requireNonNull(tableCell, "The table cell is null.");
        tableCellList.add(index, tableCell);
    }

    public void add(TableCell tableCell) {
        add(tableCellList.size(), tableCell);
    }

    /**
     * Removes the {@code index}th table row cell from this row.
     * 
     * @param index the index of the cell to remove.
     */
    public void remove(int index) {
        checkAccessIndex(index);
        tableCellList.remove(index);
    }

    public int getSerializationLength() {
        int serializationLength = 0;

        for (TableCell tableCell : tableCellList) {
            serializationLength += tableCell.getSerializationLength();
        }

        return serializationLength;
    }

    @Override
    public boolean equals(Object o) {
        if (o == null) {
            return false;
        }

        if (o == this) {
            return true;
        }

        if (!getClass().equals(o.getClass())) {
            return false;
        }

        TableRow other = (TableRow) o;

        if (getNumberOfCells() != other.getNumberOfCells()) {
            return false;
        }

        for (int i = 0; i < getNumberOfCells(); ++i) {
            if (!get(i).equals(other.get(i))) {
                return false;
            }
        }

        return true;
    }

    public void serialize(ByteBuffer byteBuffer) {
        for (TableCell tableCell : tableCellList) {
            tableCell.serialize(byteBuffer);
        }
    }

    public static TableRow deserialize(ByteBuffer byteBuffer,
                                       int numberOfCells) {
        TableRow tableRow = new TableRow(null);

        for (int i = 0; i < numberOfCells; ++i) {
            tableRow.add(TableCell.deserialize(byteBuffer));
        }

        return tableRow;
    }

    private void checkAccessIndex(int index) {
        if (index < 0) {
            throw new IndexOutOfBoundsException(
                    "The column access index is negative: " + index + ".");
        }

        if (index >= tableCellList.size()) {
            throw new IndexOutOfBoundsException(
                    "The column access index is too large: " + index + ". " +
                    "The length of the row is " + tableCellList.size() + " " +
                    "cells.");
        }
    }

    private void checkInsertionIndex(int index) {
        if (index < 0) {
            throw new IndexOutOfBoundsException(
                    "The column insertion index is negative: " + index + ".");
        }

        if (index > tableCellList.size()) {
            throw new IndexOutOfBoundsException(
                    "The column insertion index is too large: " + index + ". " +
                    "The length of the row is " + tableCellList.size() + " " +
                    "cells.");
        }
    }
}

TableView.java

package net.coderodde.javadb;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

public final class TableView {

    private final List<TableRow> tableRowList = new ArrayList<>();
    private final List<TableColumnDescriptor> tableColumnDescriptorList;
    private final Map<TableColumnDescriptor, Integer> cellMap = new HashMap<>();

    TableView(Table ownerTable, 
              List<TableColumnDescriptor> tableColumnDescriptorList) {
        this.tableColumnDescriptorList = tableColumnDescriptorList;

        for (int i = 0; i < ownerTable.tableColumnDescriptorList.size(); ++i) {
            if (tableColumnDescriptorList
                    .contains(ownerTable.tableColumnDescriptorList.get(i))) {
                cellMap.put(ownerTable.tableColumnDescriptorList.get(i), i);
            }
        }
    }

    public void addTableRow(TableRow tableRow) {
        tableRowList.add(Objects.requireNonNull(tableRow, "Table row is null."));
    }

    public TableRow removeTableRow(int index) {
        return tableRowList.remove(index);
    }

    public TableRow getTableRow(int index) {
        return tableRowList.get(index);
    }

    @Override
    public String toString() {
        Map<TableColumnDescriptor, Integer> columnWidthMap =
                getColumnWidthMap();

        int viewWidth = getViewWidth(columnWidthMap);
        int viewHeight = tableRowList.size() + 4;
        StringBuilder sb = new StringBuilder(viewHeight * (viewWidth + 1) - 1);
        StringBuilder separatorBar = getSeparatorBar(columnWidthMap, viewWidth);
        sb.append(separatorBar).append('\n');
        sb.append(header(viewWidth, columnWidthMap)).append('\n');
        sb.append(separatorBar);

        for (TableRow tableRow : tableRowList) {
            sb.append('\n');
            sb.append(tableRowToString(tableRow, viewWidth, columnWidthMap));
        }

        return sb.append('\n').append(separatorBar).toString();
    }

    private StringBuilder tableRowToString(
            TableRow tableRow,
            int width,
            Map<TableColumnDescriptor, Integer> columnWidthMap) {
        StringBuilder sb = new StringBuilder(width);
        sb.append('|');

        for (TableColumnDescriptor tableColumnDescriptor 
                : tableColumnDescriptorList) {
            Object value = tableRow.get(cellMap.get(tableColumnDescriptor))
                                   .getValue();
            String text;

            if (value == null) {
                text = "NULL";
            } else {
                text = value.toString();
            }

            sb.append(getCellText(columnWidthMap.get(tableColumnDescriptor),
                                  text));
        }

        return sb;
    }

    private StringBuilder getCellText(int width, String text) {
        StringBuilder sb = new StringBuilder(width);
        int len = width - 1;
        sb.append(String.format(" %-" + len + "s|", text));
        return sb;
    }

    private StringBuilder 
        header(int width, Map<TableColumnDescriptor, Integer> columnWidthMap) {
        StringBuilder sb = new StringBuilder(width);
        sb.append('|');

        for (TableColumnDescriptor tableColumnDescriptor 
                : tableColumnDescriptorList) {
            String columnName = tableColumnDescriptor.getTableColumnName();
            int len = columnWidthMap.get(tableColumnDescriptor) - 1;
            sb.append(String.format(" %-" + len + "s|", columnName));
        }

        return sb;
    }

    private StringBuilder 
        getSeparatorBar(Map<TableColumnDescriptor, Integer> columnWidthMap,
                        int viewWidth) {
        StringBuilder sb = new StringBuilder(viewWidth).append('+');

        for (TableColumnDescriptor tableColumnDescriptor :
                tableColumnDescriptorList) {
            sb.append(getBar(columnWidthMap.get(tableColumnDescriptor)))
              .append('+');
        }

        return sb;
    }

    private String getBar(int width) {
        StringBuilder sb = new StringBuilder(width);

        for (int i = 0; i < width; ++i) {
            sb.append('-');
        }

        return sb.toString();
    }

    private int getViewWidth(
            Map<TableColumnDescriptor, Integer> columnWidthMap) {
        int width = columnWidthMap.size() + 1;

        for (Map.Entry<TableColumnDescriptor, Integer> e 
                : columnWidthMap.entrySet()) {
            width += e.getValue();
        }

        return width;
    }

    private Map<TableColumnDescriptor, Integer> getColumnWidthMap() {
        Map<TableColumnDescriptor, Integer> columnLengthMap = new HashMap<>();

        for (TableColumnDescriptor tableColumnDescriptor : 
                tableColumnDescriptorList) {
            columnLengthMap.put(tableColumnDescriptor, 
                                tableColumnDescriptor.getTableColumnName()
                                                     .length());
        }

        for (TableColumnDescriptor tableColumnDescriptor :
                    tableColumnDescriptorList) {
            int index = cellMap.get(tableColumnDescriptor);
            int maxColumnWidth = columnLengthMap.get(tableColumnDescriptor);

            for (TableRow tableRow : tableRowList) {
                String cellContent = tableRow.get(index).getValue().toString();
                int cellContentWidth = cellContent.length();
                maxColumnWidth = Math.max(maxColumnWidth, cellContentWidth);
            }

            columnLengthMap.put(tableColumnDescriptor, maxColumnWidth);
        }

        for (Map.Entry<TableColumnDescriptor, Integer> e 
                : columnLengthMap.entrySet()) {
            // Add the left and right margin spaces.
            e.setValue(e.getValue() + 2);
        }

        return columnLengthMap;
    }
}

The entire project lives here.

The demo output is:

+----+------------+-----------+
| id | first_name | last_name |
+----+------------+-----------+
| 1  | Rodion     | Efremov   |
| 2  | Violetta   | Ervasti   |
+----+------------+-----------+
+----+-----------+--------+
| id | person_id | msg    |
+----+-----------+--------+
| 10 | 1         | Hello! |
| 11 | 2         | Bye!   |
+----+-----------+--------+

Please tell me anything that comes to mind.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.