diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/AutoCloseableProvider.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/AutoCloseableProvider.java new file mode 100644 index 000000000..aa712a32b --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/AutoCloseableProvider.java @@ -0,0 +1,17 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db; + +import ru.fizteh.fivt.storage.structured.TableProvider; + +import java.io.IOException; +import java.util.List; + +public interface AutoCloseableProvider extends TableProvider, AutoCloseable { + @Override + void close(); + + @Override + AutoCloseableTable getTable(String name); + + @Override + AutoCloseableTable createTable(String name, List> columnTypes) throws IOException; +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/AutoCloseableTable.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/AutoCloseableTable.java new file mode 100644 index 000000000..8e4c67aff --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/AutoCloseableTable.java @@ -0,0 +1,8 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db; + +import ru.fizteh.fivt.storage.structured.Table; + +public interface AutoCloseableTable extends Table, AutoCloseable { + @Override + void close(); +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/AutoCloseableTableProviderFactory.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/AutoCloseableTableProviderFactory.java new file mode 100644 index 000000000..a6df2037c --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/AutoCloseableTableProviderFactory.java @@ -0,0 +1,11 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db; + +import ru.fizteh.fivt.storage.structured.TableProviderFactory; + +import java.io.Closeable; +import java.io.IOException; + +public interface AutoCloseableTableProviderFactory extends TableProviderFactory, Closeable { + @Override + AutoCloseableProvider create(String path) throws IOException; +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/DBTableProvider.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/DBTableProvider.java index edfda2a76..ce8b41343 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/DBTableProvider.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/DBTableProvider.java @@ -3,12 +3,18 @@ import ru.fizteh.fivt.storage.structured.ColumnFormatException; import ru.fizteh.fivt.storage.structured.Storeable; import ru.fizteh.fivt.storage.structured.Table; -import ru.fizteh.fivt.storage.structured.TableProvider; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.DatabaseIOException; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.TableCorruptIOException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.json.JSONMaker; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.json.JSONParsedObject; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.json.JSONParser; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.ConvenientCollection; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.ConvenientMap; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Log; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Utility; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.ValidityController; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.ValidityController.KillLock; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.ValidityController.UseLock; import java.io.IOException; import java.nio.file.DirectoryStream; @@ -21,26 +27,24 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.function.Function; +import java.util.function.Predicate; -public class DBTableProvider implements TableProvider { - - public static final char LIST_SEPARATOR_CHARACTER = ','; - - public static final char QUOTE_CHARACTER = '\"'; - public static final char ESCAPE_CHARACTER = '/'; - public static final String QUOTED_STRING_REGEX = - Utility.getQuotedStringRegex(QUOTE_CHARACTER + "", ESCAPE_CHARACTER + ""); +final class DBTableProvider implements AutoCloseableProvider { + private static final char QUOTE_CHARACTER = '\"'; private static final Collection> SUPPORTED_TYPES = new ConvenientCollection<>( - new HashSet>()).addNext(Integer.class).addNext(Long.class).addNext(Byte.class) - .addNext(Double.class).addNext(Float.class).addNext(Boolean.class) - .addNext(String.class); + new HashSet>()).chainAdd(Integer.class).chainAdd(Long.class).chainAdd(Byte.class) + .chainAdd(Double.class).chainAdd(Float.class).chainAdd(Boolean.class) + .chainAdd(String.class); private static final Map, Function> PARSERS = new ConvenientMap<>(new HashMap, Function>()) - .putNext(Integer.class, Integer::parseInt).putNext(Long.class, Long::parseLong) - .putNext(Byte.class, Byte::parseByte).putNext( + .chainPut(Integer.class, Integer::parseInt).chainPut(Long.class, Long::parseLong) + .chainPut(Byte.class, Byte::parseByte).chainPut( Boolean.class, str -> { Utility.checkNotNull(str, "String to parse"); if (str.matches("(?i)true|false")) { @@ -48,297 +52,268 @@ public class DBTableProvider implements TableProvider { } else { throw new ColumnFormatException("Expected 'true' or 'false' as boolean"); } - }).putNext(Double.class, Double::parseDouble).putNext(Float.class, Float::parseFloat) - .putNext( - String.class, - s -> Utility.unquoteString(s, QUOTE_CHARACTER + "", ESCAPE_CHARACTER + "")); + }).chainPut(Double.class, Double::parseDouble).chainPut(Float.class, Float::parseFloat) + .chainPut( + String.class, str -> { + if (!str.startsWith(QUOTE_CHARACTER + "") || !str + .endsWith(QUOTE_CHARACTER + "")) { + throw new ColumnFormatException("(String expected to be in quotes"); + } + return str.substring(1, str.length() - 1); + }); private final Path databaseRoot; - /** * Mapping between table names and tables. Corrupt tables are null. */ - private final Map tables; - + private final Map tables; /** * Mapping (table name, last corruption reason). To keep user informed. */ private final Map corruptTables; + /** + * Lock for getting/creating/removing tables access management. + */ + private final ReadWriteLock persistenceLock = new ReentrantReadWriteLock(true); + private final ValidityController validityController = new ValidityController(); + private final DBTableProviderFactory factory; + /** + * Special flag that prevents from reacting on event raised by this object. + */ + private boolean tableClosedByMe = false; /** * Constructs a database table provider. - * @throws ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception - * .DatabaseIOException + * @throws ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.DatabaseIOException * If failed to scan database directory. */ - DBTableProvider(Path databaseRoot) throws DatabaseIOException { + DBTableProvider(Path databaseRoot, DBTableProviderFactory factory) throws DatabaseIOException { this.databaseRoot = databaseRoot; + this.factory = factory; this.tables = new HashMap<>(); this.corruptTables = new HashMap<>(); - reloadTables(); + reloadAllTables(); } - @Override - public StoreableTableImpl getTable(String name) throws IllegalArgumentException { - Utility.checkTableNameIsCorrect(name); - if (tables.containsKey(name)) { - StoreableTableImpl table = tables.get(name); - if (table == null) { - DatabaseIOException corruptionReason = corruptTables.get(name); - throw new IllegalArgumentException( - corruptionReason.getMessage(), corruptionReason); - } - return table; - } else { - return null; - } - } - - @Override - public StoreableTableImpl createTable(String name, List> columnTypes) - throws IllegalArgumentException, DatabaseIOException { - Utility.checkTableNameIsCorrect(name); - - if (columnTypes == null) { - throw new IllegalArgumentException("Column types list must not be null"); + public Path getDatabaseRoot() { + try (UseLock useLock = validityController.use()) { + return databaseRoot; } - if (columnTypes.isEmpty()) { - throw new IllegalArgumentException("Column types list must not be empty"); - } - Utility.checkAllTypesAreSupported(columnTypes, SUPPORTED_TYPES); - - Path tablePath = databaseRoot.resolve(name); - - if (tables.containsKey(name) && tables.get(name) != null) { - return null; - } - - StoreableTableImpl newTable = StoreableTableImpl.createTable(this, tablePath, columnTypes); - tables.put(name, newTable); - return newTable; } @Override public void removeTable(String name) throws IllegalArgumentException, IllegalStateException, DatabaseIOException { - Utility.checkTableNameIsCorrect(name); - Path tablePath = databaseRoot.resolve(name); - - if (!tables.containsKey(name)) { - throw new IllegalStateException(name + " not exists"); - } - - StoreableTableImpl removed = tables.remove(name); - if (removed != null) { - removed.invalidate(); - } + try (UseLock useLock = validityController.use()) { + Utility.checkTableNameIsCorrect(name); + Path tablePath = databaseRoot.resolve(name); + + persistenceLock.writeLock().lock(); + try { + if (!tables.containsKey(name)) { + throw new IllegalStateException(name + " not exists"); + } - corruptTables.remove(name); + AutoCloseableTable removed = tables.remove(name); + if (removed != null) { + // After invalidation all attempts to commit from other threads fail with + // IllegalStateException. Now we can delete the table without fear that it will be written + // to the file system again. + removed.close(); + } - if (!Files.exists(tablePath)) { - return; - } + corruptTables.remove(name); - try { - Utility.rm(tablePath); - } catch (IOException exc) { - // Mark as corrupt. - tables.put(name, null); + if (!Files.exists(tablePath)) { + return; + } - TableCorruptIOException corruptionReason = new TableCorruptIOException( - name, "Failed to drop table: " + exc.toString(), exc); - corruptTables.put(name, corruptionReason); - throw corruptionReason; + try { + Utility.rm(tablePath); + } catch (IOException exc) { + // Mark as corrupt. + tables.put(name, null); + + TableCorruptIOException corruptionReason = new TableCorruptIOException( + name, "Failed to drop table: " + exc.toString(), exc); + corruptTables.put(name, corruptionReason); + throw corruptionReason; + } + } finally { + persistenceLock.writeLock().unlock(); + } } } @Override public Storeable deserialize(Table table, String value) throws ParseException { - Utility.checkNotNull(table, "Table"); - Utility.checkNotNull(value, "Value"); - - String partRegex = "null|true|false|-?[0-9]+(\\.[0-9]+)?"; - partRegex += "|" + QUOTED_STRING_REGEX; - partRegex = "\\s*(" + partRegex + ")\\s*"; - String regex = "^\\s*\\[" + partRegex + "(," + partRegex + ")*" + "\\]\\s*$"; - - if (!value.matches(regex)) { - throw new ParseException( - "wrong type (Does not match JSON simple list regular expression)", -1); - } + try (UseLock useLock = validityController.use()) { + Utility.checkNotNull(table, "Table"); + Utility.checkNotNull(value, "Value"); - int leftBound = value.indexOf('['); - int rightBound = value.lastIndexOf(']'); + int leftBound = value.indexOf('['); + int rightBound = value.lastIndexOf(']'); - Storeable storeable = createFor(table); - - int columnsCount = table.getColumnsCount(); - int currentColumn = 0; - - int index = leftBound + 1; - - for (; index < rightBound; ) { - char currentChar = value.charAt(index); + if (leftBound < 0 || rightBound < 0) { + throw new ParseException("wrong type (Arguments must be inside square brackets)", -1); + } - if (Character.isSpaceChar(currentChar)) { - // Space that does not mean anything. + Storeable storeable = createFor(table); - index++; - } else if (LIST_SEPARATOR_CHARACTER == currentChar) { - // Next list element. + JSONParsedObject parsedObject; + try { + parsedObject = JSONParser.parseJSON(value.substring(leftBound, rightBound + 1)); + } catch (ParseException exc) { + throw new ParseException("wrong type (" + exc.getMessage() + ")", exc.getErrorOffset()); + } + if (!parsedObject.isStandardArray()) { + throw new ParseException("wrong type (Arguments must be given as array)", -1); + } - currentColumn++; - if (currentColumn >= columnsCount) { - throw new ParseException( - "wrong type (Too many elements in the list; expected: " + columnsCount + ")", - index); - } - index++; - } else { - // Boolean, Number, Null, String. + Object[] args = parsedObject.asArray(); - // End of element (exclusive). - int elementEnd; + if (args.length != table.getColumnsCount()) { + throw new ParseException("wrong type (Irregular number of arguments given)", -1); + } - if (QUOTE_CHARACTER == currentChar) { - // As soon as the given value matches JSON format, closing quotes - // are guaranteed to - // have been found and no exception can be thrown here -> so format - // 'wrong type(.. - // .)' support is not necessary here. + try { + for (int i = 0; i < args.length; i++) { + Object elementObj; + + if (args[i] == null) { + elementObj = null; + } else if (args[i] instanceof JSONParsedObject) { + throw new ParseException("wrong type (Complex types are not supported)", i); + } else { + String str = args[i].toString(); + if (args[i] instanceof String) { + str = QUOTE_CHARACTER + str + QUOTE_CHARACTER; + } - elementEnd = Utility.findClosingQuotes( - value, index + 1, rightBound, QUOTE_CHARACTER, ESCAPE_CHARACTER) + 1; - } else { - elementEnd = value.indexOf(LIST_SEPARATOR_CHARACTER, index + 1); - if (elementEnd == -1) { - elementEnd = rightBound; + elementObj = PARSERS.get(table.getColumnType(i)).apply(str); } - } - // Parsing the value. - Object elementObj; - - String elementStr = value.substring(index, elementEnd).trim(); - if ("null".equals(elementStr)) { - elementObj = null; - } else { - Class elementClass = table.getColumnType(currentColumn); - try { - elementObj = PARSERS.get(elementClass).apply(elementStr); - } catch (RuntimeException exc) { - throw new ParseException( - "wrong type (" + exc.getMessage() + ")", index); - } + storeable.setColumnAt(i, elementObj); } - - storeable.setColumnAt(currentColumn, elementObj); - index = elementEnd; + } catch (RuntimeException exc) { + throw new ParseException("wrong type (" + exc.getMessage() + ")", -1); } - } - if (currentColumn + 1 != columnsCount) { - throw new ParseException( - "wrong type (Too few elements in the list; expected: " + columnsCount + ")", -1); + return storeable; } - - return storeable; } @Override public String serialize(Table table, Storeable value) throws ColumnFormatException { - Utility.checkNotNull(table, "Table"); - Utility.checkNotNull(value, "Value"); - - StoreableTableImpl.checkStoreableAppropriate(table, value); - - StringBuilder sb = new StringBuilder(); - sb.append("["); - boolean comma = false; - - for (int col = 0, colsCount = table.getColumnsCount(); col < colsCount; col++) { + try (UseLock useLock = validityController.use()) { + Utility.checkNotNull(table, "Table"); + Utility.checkNotNull(value, "Value"); - String colValueStr; // If null, write null. + StoreableTableImpl.checkStoreableAppropriate(table, value); - if (String.class.equals(table.getColumnType(col))) { - colValueStr = Utility.quoteString( - value.getStringAt(col), QUOTE_CHARACTER + "", ESCAPE_CHARACTER + ""); + if (value instanceof StoreableImpl) { + // Optimization: we do not create new array. + return JSONMaker.makeJSON(value); } else { - Object colValue = value.getColumnAt(col); - if (colValue == null) { - colValueStr = null; - } else { - colValueStr = colValue.toString(); + Object[] values = new Object[table.getColumnsCount()]; + for (int i = 0; i < values.length; i++) { + values[i] = value.getColumnAt(i); } + return JSONMaker.makeJSON(values); } - - if (comma) { - sb.append(","); - } - comma = true; - sb.append(colValueStr == null ? "null" : colValueStr); } - - sb.append("]"); - return sb.toString(); } @Override public Storeable createFor(Table table) { - Utility.checkNotNull(table, "Table"); - return new StoreableImpl(table); + try (UseLock useLock = validityController.use()) { + Utility.checkNotNull(table, "Table"); + return new StoreableImpl(table); + } } @Override public Storeable createFor(Table table, List values) throws ColumnFormatException, IndexOutOfBoundsException { - Utility.checkNotNull(table, "Table"); - Utility.checkNotNull(values, "Values list"); + try (UseLock useLock = validityController.use()) { + Utility.checkNotNull(table, "Table"); + Utility.checkNotNull(values, "Values list"); + + if (table.getColumnsCount() != values.size()) { + throw new IndexOutOfBoundsException( + "Wrong number of values given; expected: " + + table.getColumnsCount() + + ", actual: " + + values.size()); + } - if (table.getColumnsCount() != values.size()) { - throw new IndexOutOfBoundsException( - "Wrong number of values given; expected: " + table.getColumnsCount() + ", actual: " - + values.size()); - } + Storeable storeable = new StoreableImpl(table); - Storeable storeable = new StoreableImpl(table); + int column = 0; + for (Object value : values) { + storeable.setColumnAt(column++, value); + } - int column = 0; - for (Object value : values) { - storeable.setColumnAt(column++, value); + return storeable; } - - return storeable; } @Override public List getTableNames() { - return new LinkedList<>(tables.keySet()); + try (UseLock useLock = validityController.use()) { + persistenceLock.readLock().lock(); + try { + return new LinkedList<>(tables.keySet()); + } finally { + persistenceLock.readLock().unlock(); + } + } } /** - * Scans database directory and reads all tables from it. - * @throws ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.DatabaseIOException + * Loads table from the given path. Not thread-safe. */ + private void loadTable(Path tablePath) { + String tableName = tablePath.getFileName().toString(); + + try { + AutoCloseableTable table = StoreableTableImpl.getTable(this, this::onTableClosed, tablePath); + tables.put(tableName, table); + } catch (DatabaseIOException exc) { + // Mark as corrupt. + tables.put(tableName, null); + corruptTables.put( + tableName, + (exc instanceof TableCorruptIOException + ? (TableCorruptIOException) exc + : new TableCorruptIOException(tableName, exc.getMessage(), exc))); + } + } - private void reloadTables() throws DatabaseIOException { + /** + * Loads tables from file system that are not registered.
+ * Not thread-safe. + * @throws DatabaseIOException + */ + private void loadMissingTables() throws DatabaseIOException { + loadTables((name) -> !tables.containsKey(name)); + } + + private void reloadAllTables() throws DatabaseIOException { tables.clear(); + corruptTables.clear(); + loadTables((tableName) -> true); + } + /** + * Scans database directory and reads all tables from it. Not thread-safe. + * @throws ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.DatabaseIOException + */ + private void loadTables(Predicate loadFilter) throws DatabaseIOException { try (DirectoryStream dirStream = Files.newDirectoryStream(databaseRoot)) { for (Path tablePath : dirStream) { - String tableName = tablePath.getFileName().toString(); - - try { - StoreableTableImpl table = StoreableTableImpl.getTable(this, tablePath); - tables.put(tableName, table); - } catch (DatabaseIOException exc) { - // mark as corrupt - tables.put(tableName, null); - corruptTables.put( - tableName, - (exc instanceof TableCorruptIOException - ? (TableCorruptIOException) exc - : new TableCorruptIOException(tableName, exc.getMessage(), exc))); + if (loadFilter.test(tablePath.getFileName().toString())) { + loadTable(tablePath); } } } catch (IOException exc) { @@ -346,4 +321,140 @@ private void reloadTables() throws DatabaseIOException { } } + /** + * The given table is dismissed. + * @param table + * link to the closed table (no matter if it is proxy or pure). + */ + void onTableClosed(Table table) { + try (UseLock useLock = validityController.use()) { + if (tableClosedByMe) { + return; + } + persistenceLock.writeLock().lock(); + try { + tables.remove(table.getName()); + } finally { + persistenceLock.writeLock().unlock(); + } + } + } + + @Override + public String toString() { + try (UseLock lock = validityController.use()) { + return DBTableProvider.class.getSimpleName() + "[" + databaseRoot + "]"; + } + } + + @Override + protected void finalize() throws Throwable { + super.finalize(); + close(); + } + + @Override + public void close() { + try (KillLock lock = validityController.useAndKill()) { + tableClosedByMe = true; + persistenceLock.writeLock().lock(); + try { + tables.values().stream().filter(table -> table != null).forEach(AutoCloseableTable::close); + tables.clear(); + corruptTables.clear(); + + // Deleting empty files and empty folders. + try (DirectoryStream dirStream = Files.newDirectoryStream(databaseRoot)) { + for (Path tableDirectory : dirStream) { + Utility.removeEmptyFilesAndFolders(tableDirectory); + } + + Log.log(DBTableProvider.class, "Cleaned up successfully"); + } catch (IOException exc) { + Log.log(DBTableProvider.class, exc, "Failed to clean up"); + } + } finally { + persistenceLock.writeLock().unlock(); + } + + factory.onProviderClosed(this); + } finally { + tableClosedByMe = false; + } + } + + @Override + public AutoCloseableTable getTable(String name) throws IllegalArgumentException { + try (UseLock useLock = validityController.use()) { + Utility.checkTableNameIsCorrect(name); + + Lock lock = persistenceLock.readLock(); + lock.lock(); + try { + if (!tables.containsKey(name)) { + // Read table from FS. Must get a better lock. + lock.unlock(); + lock = persistenceLock.writeLock(); + lock.lock(); + if (!tables.containsKey(name)) { + try { + loadMissingTables(); + } catch (DatabaseIOException exc) { + throw new IllegalArgumentException(exc.getMessage(), exc); + } + } + } + + if (tables.containsKey(name)) { + AutoCloseableTable table = tables.get(name); + if (table == null) { + // Table is corrupt. + DatabaseIOException corruptionReason = corruptTables.get(name); + throw new IllegalArgumentException( + corruptionReason.getMessage(), corruptionReason); + } + + // Table is normal. + return table; + } else { + // Table not exists. + return null; + } + } finally { + lock.unlock(); + } + } + } + + @Override + public AutoCloseableTable createTable(String name, List> columnTypes) + throws IllegalArgumentException, DatabaseIOException { + try (UseLock useLock = validityController.use()) { + Utility.checkTableNameIsCorrect(name); + + if (columnTypes == null) { + throw new IllegalArgumentException("Column types list must not be null"); + } + if (columnTypes.isEmpty()) { + throw new IllegalArgumentException("Column types list must not be empty"); + } + Utility.checkAllTypesAreSupported(columnTypes, SUPPORTED_TYPES); + + Path tablePath = databaseRoot.resolve(name); + + persistenceLock.writeLock().lock(); + try { + if (tables.containsKey(name) && tables.get(name) != null) { + return null; + } + + AutoCloseableTable newTable = + StoreableTableImpl.createTable(this, this::onTableClosed, tablePath, columnTypes); + tables.put(name, newTable); + return newTable; + } finally { + persistenceLock.writeLock().unlock(); + } + } + } } diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/DBTableProviderFactory.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/DBTableProviderFactory.java index 881f303cc..e83f4815b 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/DBTableProviderFactory.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/DBTableProviderFactory.java @@ -1,16 +1,78 @@ package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db; -import ru.fizteh.fivt.storage.structured.TableProviderFactory; +import ru.fizteh.fivt.proxy.LoggingProxyFactory; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.DatabaseIOException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Log; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.LoggingProxyFactoryXML; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Utility; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.ValidityController; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.ValidityController.KillLock; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.ValidityController.UseLock; +import java.io.FileOutputStream; import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.Writer; import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.IdentityHashMap; -public class DBTableProviderFactory implements TableProviderFactory { +public final class DBTableProviderFactory implements AutoCloseableTableProviderFactory { + private static final LoggingProxyFactory LOGGING_PROXY_FACTORY = new LoggingProxyFactoryXML(); + private static final Writer LOG_WRITER; + + static { + Writer tempWriter; + + try { + tempWriter = new OutputStreamWriter(new FileOutputStream("Proxy.log")); + } catch (IOException exc) { + Log.log(DBTableProviderFactory.class, exc, "Failed to create log"); + tempWriter = null; + } + + LOG_WRITER = tempWriter; + } + + private final ValidityController validityController = new ValidityController(); + private final IdentityHashMap generatedProviders = + new IdentityHashMap<>(); + + private boolean providerClosedByMe = false; + + static T wrapImplementation(T implementation, Class interfaceClass) { + if (LOG_WRITER != null) { + return (T) LOGGING_PROXY_FACTORY.wrap(LOG_WRITER, implementation, interfaceClass); + } else { + return implementation; + } + } + + @Override + public synchronized void close() { + try (KillLock lock = validityController.useAndKill()) { + providerClosedByMe = true; + generatedProviders.keySet().forEach(AutoCloseableProvider::close); + generatedProviders.clear(); + } finally { + providerClosedByMe = false; + } + } + + /** + * Unregisters the given provider. + * @param provider + * Pure (not proxied) link to the closed provider. + */ + synchronized void onProviderClosed(AutoCloseableProvider provider) { + try (UseLock useLock = validityController.use()) { + if (!providerClosedByMe) { + generatedProviders.remove(provider); + } + } + } private void checkDatabaseDirectory(final Path databaseRoot) throws DatabaseIOException { if (!Files.isDirectory(databaseRoot)) { @@ -31,25 +93,37 @@ private void checkDatabaseDirectory(final Path databaseRoot) throws DatabaseIOEx } @Override - public DBTableProvider create(String dir) throws IllegalArgumentException, DatabaseIOException { - Utility.checkNotNull(dir, "Directory"); - - Path databaseRoot = Paths.get(dir).normalize(); - if (!Files.exists(databaseRoot)) { - if (databaseRoot.getParent() == null || !Files.isDirectory(databaseRoot.getParent())) { - throw new DatabaseIOException( - "Database directory parent path does not exist or is not a directory"); - } + protected void finalize() throws Throwable { + super.finalize(); + close(); + } + + @Override + public synchronized AutoCloseableProvider create(String dir) + throws IllegalArgumentException, DatabaseIOException { + try (UseLock useLock = validityController.use()) { + Utility.checkNotNull(dir, "Directory"); - try { - Files.createDirectory(databaseRoot); - } catch (IOException exc) { - throw new DatabaseIOException("Failed to establish database on path " + dir, exc); + Path databaseRoot = Paths.get(dir).normalize(); + if (!Files.exists(databaseRoot)) { + if (databaseRoot.getParent() == null || !Files.isDirectory(databaseRoot.getParent())) { + throw new DatabaseIOException( + "Database directory parent path does not exist or is not a directory"); + } + + try { + Files.createDirectory(databaseRoot); + } catch (IOException exc) { + throw new DatabaseIOException("Failed to establish database on path " + dir, exc); + } + } else { + checkDatabaseDirectory(databaseRoot); } - } else { - checkDatabaseDirectory(databaseRoot); - } - return new DBTableProvider(databaseRoot); + AutoCloseableProvider provider = new DBTableProvider(databaseRoot, this); + AutoCloseableProvider wrappedProvider = wrapImplementation(provider, AutoCloseableProvider.class); + generatedProviders.put(provider, Boolean.TRUE); + return wrappedProvider; + } } } diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/Database.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/Database.java index 939995800..7ad52bd22 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/Database.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/Database.java @@ -3,10 +3,11 @@ import ru.fizteh.fivt.storage.structured.Table; import ru.fizteh.fivt.storage.structured.TableProvider; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.DatabaseIOException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.InvalidatedObjectException; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.NoActiveTableException; import java.io.IOException; -import java.nio.file.Path; +import java.io.PrintStream; import java.util.List; /** @@ -14,11 +15,12 @@ * @author phoenix */ public class Database { - protected final TableProvider provider; + private final TableProvider provider; /** * Root directory of all database files */ - private final Path dbDirectory; + private final String dbDirectory; + private final PrintStream outputStream; /** * Table in use.
All operations (like {@code put}, {@code get}, etc.) are performed with * this table. @@ -28,12 +30,11 @@ public class Database { /** * Establishes a database instance on given folder.
If the folder exists, the old database * is used.
If the folder does not exist, a new database is created within the folder. - * @throws ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.DatabaseIOException */ - public Database(Path dbDirectory) throws DatabaseIOException { + public Database(TableProvider provider, String dbDirectory, PrintStream outputStream) { + this.outputStream = outputStream; this.dbDirectory = dbDirectory; - DBTableProviderFactory factory = new DBTableProviderFactory(); - this.provider = factory.create(dbDirectory.toString()); + this.provider = provider; } public TableProvider getProvider() { @@ -60,7 +61,14 @@ public boolean createTable(String tableName, List> columnTypes) * Name of table to drop. */ public void dropTable(String tableName) throws IllegalArgumentException, IOException { - String activeTableName = activeTable == null ? null : activeTable.getName(); + String activeTableName = null; + if (activeTable != null) { + try { + activeTableName = activeTable.getName(); + } catch (InvalidatedObjectException exc) { + // Ignore it. + } + } provider.removeTable(tableName); @@ -74,7 +82,7 @@ public Table getActiveTable() throws NoActiveTableException { return activeTable; } - public Path getDbDirectory() { + public String getDbDirectory() { return dbDirectory; } @@ -99,9 +107,10 @@ public int rollback() { } public void showTables() { - System.out.println("table_name row_count"); + outputStream.println("table_name row_count"); List tableNames = provider.getTableNames(); + StringBuilder sb = new StringBuilder(); for (String tableName : tableNames) { boolean valid; int changesCount = 0; @@ -114,8 +123,10 @@ public void showTables() { valid = false; } - System.out.println(String.format("%s %s", tableName, valid ? changesCount : "corrupt")); + sb.append(String.format("%s %s", tableName, valid ? changesCount : "corrupt")); + sb.append(System.lineSeparator()); } + outputStream.print(sb.toString()); } /** @@ -124,27 +135,32 @@ public void showTables() { * Name of table to use. */ public void useTable(String tableName) throws IOException, IllegalArgumentException { - if (activeTable != null) { - if (tableName.equals(activeTable.getName())) { - return; - } - - int uncommitted = activeTable.getNumberOfUncommittedChanges(); - if (uncommitted != 0) { - throw new DatabaseIOException(String.format("%d unsaved changes", uncommitted)); + try { + if (activeTable != null) { + if (tableName.equals(activeTable.getName())) { + return; + } + + int uncommitted = activeTable.getNumberOfUncommittedChanges(); + if (uncommitted != 0) { + throw new DatabaseIOException(String.format("%d unsaved changes", uncommitted)); + } } - } - Table oldActiveTable = activeTable; + Table oldActiveTable = activeTable; - try { - activeTable = provider.getTable(tableName); - if (activeTable == null) { - throw new IllegalArgumentException(tableName + " not exists"); + try { + activeTable = provider.getTable(tableName); + if (activeTable == null) { + throw new IllegalArgumentException(tableName + " not exists"); + } + } catch (Exception exc) { + activeTable = oldActiveTable; + throw exc; } - } catch (Exception exc) { - activeTable = oldActiveTable; - throw exc; + } catch (InvalidatedObjectException exc) { + activeTable = null; + useTable(tableName); } } } diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/ProviderWrap.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/ProviderWrap.java new file mode 100644 index 000000000..93b3ea3f6 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/ProviderWrap.java @@ -0,0 +1,249 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db; + +import ru.fizteh.fivt.storage.structured.ColumnFormatException; +import ru.fizteh.fivt.storage.structured.Storeable; +import ru.fizteh.fivt.storage.structured.Table; +import ru.fizteh.fivt.storage.structured.TableProvider; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.InvalidatedObjectException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.ValidityController; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.ValidityController.KillLock; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.ValidityController.UseLock; + +import java.io.IOException; +import java.text.ParseException; +import java.util.List; + +/** + * Wraps the given table provider.
+ * {@link ProviderWrap#close()} method just invalidates the wrap. Original provider is not affected.
+ * Also returned tables are wraps that can be closed without affecting original tables. + */ +public class ProviderWrap implements AutoCloseableProvider { + private final TableProvider provider; + + private final ValidityController providerVC = new ValidityController(); + + public ProviderWrap(TableProvider provider) { + this.provider = provider; + } + + @Override + public void removeTable(String name) throws IOException { + try (UseLock lock = providerVC.use()) { + provider.removeTable(name); + } + } + + @Override + public Storeable deserialize(Table table, String value) throws ParseException { + try (UseLock lock = providerVC.use()) { + return provider.deserialize(table, value); + } + } + + @Override + public String serialize(Table table, Storeable value) throws ColumnFormatException { + try (UseLock lock = providerVC.use()) { + return provider.serialize(table, value); + } + } + + @Override + public Storeable createFor(Table table) { + try (UseLock lock = providerVC.use()) { + return provider.createFor(table); + } + } + + @Override + public Storeable createFor(Table table, List values) + throws ColumnFormatException, IndexOutOfBoundsException { + try (UseLock lock = providerVC.use()) { + return provider.createFor(table, values); + } + } + + @Override + public List getTableNames() { + try (UseLock lock = providerVC.use()) { + return provider.getTableNames(); + } + } + + @Override + public void close() { + try (KillLock lock = providerVC.useAndKill()) { + // Killing. + } + } + + @Override + public AutoCloseableTable getTable(String name) { + try (UseLock lock = providerVC.use()) { + Table table = provider.getTable(name); + return table == null ? null : new TableWrap(table); + } + } + + @Override + public AutoCloseableTable createTable(String name, List> columnTypes) throws IOException { + try (UseLock lock = providerVC.use()) { + Table table = provider.createTable(name, columnTypes); + return table == null ? null : new TableWrap(table); + } + } + + class TableWrap implements AutoCloseableTable { + private final Table table; + private final ValidityController tableVC = new ValidityController(); + + public TableWrap(Table table) { + this.table = table; + } + + private void forceClose(UseLock tableLock) { + try (KillLock lock = tableLock.obtainKillLockInstead()) { + close(); + } + } + + @Override + public void close() { + try (KillLock tableLock = tableVC.useAndKill()) { + // Killing. + } + } + + @Override + public Storeable put(String key, Storeable value) throws ColumnFormatException { + try (UseLock tableLock = tableVC.use()) { + try (UseLock providerLock = providerVC.use()) { + return table.put(key, value); + } catch (InvalidatedObjectException exc) { + forceClose(tableLock); + throw exc; + } + } + } + + @Override + public Storeable remove(String key) { + try (UseLock tableLock = tableVC.use()) { + try (UseLock providerLock = providerVC.use()) { + return table.remove(key); + } catch (InvalidatedObjectException exc) { + forceClose(tableLock); + throw exc; + } + } + } + + @Override + public int size() { + try (UseLock tableLock = tableVC.use()) { + try (UseLock providerLock = providerVC.use()) { + return table.size(); + } catch (InvalidatedObjectException exc) { + forceClose(tableLock); + throw exc; + } + } + } + + @Override + public List list() { + try (UseLock tableLock = tableVC.use()) { + try (UseLock providerLock = providerVC.use()) { + return table.list(); + } catch (InvalidatedObjectException exc) { + forceClose(tableLock); + throw exc; + } + } + } + + @Override + public int commit() throws IOException { + try (UseLock tableLock = tableVC.use()) { + try (UseLock providerLock = providerVC.use()) { + return table.commit(); + } catch (InvalidatedObjectException exc) { + forceClose(tableLock); + throw exc; + } + } + } + + @Override + public int rollback() { + try (UseLock tableLock = tableVC.use()) { + try (UseLock providerLock = providerVC.use()) { + return table.rollback(); + } catch (InvalidatedObjectException exc) { + forceClose(tableLock); + throw exc; + } + } + } + + @Override + public int getNumberOfUncommittedChanges() { + try (UseLock tableLock = tableVC.use()) { + try (UseLock providerLock = providerVC.use()) { + return table.getNumberOfUncommittedChanges(); + } catch (InvalidatedObjectException exc) { + forceClose(tableLock); + throw exc; + } + } + } + + @Override + public int getColumnsCount() { + try (UseLock tableLock = tableVC.use()) { + try (UseLock providerLock = providerVC.use()) { + return table.getColumnsCount(); + } catch (InvalidatedObjectException exc) { + forceClose(tableLock); + throw exc; + } + } + } + + @Override + public Class getColumnType(int columnIndex) throws IndexOutOfBoundsException { + try (UseLock tableLock = tableVC.use()) { + try (UseLock providerLock = providerVC.use()) { + return table.getColumnType(columnIndex); + } catch (InvalidatedObjectException exc) { + forceClose(tableLock); + throw exc; + } + } + } + + @Override + public String getName() { + try (UseLock tableLock = tableVC.use()) { + try (UseLock providerLock = providerVC.use()) { + return table.getName(); + } catch (InvalidatedObjectException exc) { + forceClose(tableLock); + throw exc; + } + } + } + + @Override + public Storeable get(String key) { + try (UseLock tableLock = tableVC.use()) { + try (UseLock providerLock = providerVC.use()) { + return table.get(key); + } catch (InvalidatedObjectException exc) { + forceClose(tableLock); + throw exc; + } + } + } + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/StoreableImpl.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/StoreableImpl.java index 8a407bf51..628184e29 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/StoreableImpl.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/StoreableImpl.java @@ -3,13 +3,25 @@ import ru.fizteh.fivt.storage.structured.ColumnFormatException; import ru.fizteh.fivt.storage.structured.Storeable; import ru.fizteh.fivt.storage.structured.Table; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.json.JSONComplexObject; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.json.JSONField; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; -public class StoreableImpl implements Storeable { +/** + * Implementation of Storeable that can be put to the table it is assigned to as a value.
+ * Not thread-safe.
+ * Not bound to any table. + */ +@JSONComplexObject(wrapper = true) +public final class StoreableImpl implements Storeable, Serializable { + @JSONField private final Object[] values; - private Table host; + private final List> types; /** * Creates a new instance of Storeable with null values as default. @@ -17,21 +29,28 @@ public class StoreableImpl implements Storeable { * Host table. */ StoreableImpl(Table host) { - this.host = host; + if (host instanceof StoreableTableImpl) { + // Memory optimization. + types = ((StoreableTableImpl) host).getColumnTypes(); + } else { + types = new ArrayList<>(host.getColumnsCount()); + for (int i = 0; i < host.getColumnsCount(); i++) { + types.add(host.getColumnType(i)); + } + } this.values = new Object[host.getColumnsCount()]; } - Table getHost() { - return host; + List> getTypes() { + return types; } private void ensureMatchColumnType(int columnIndex, Class clazz) throws ColumnFormatException { - Class columnType = host.getColumnType(columnIndex); + Class columnType = types.get(columnIndex); if (!columnType.equals(clazz)) { throw new ColumnFormatException( String.format( - "wrong type (Table '%s', col %d: Expected instance of %s, but got %s)", - host.getName(), + "wrong type (col %d: Expected instance of %s, but got %s)", columnIndex, columnType.getSimpleName(), clazz.getSimpleName())); @@ -106,7 +125,7 @@ private T getTypedValue(int columnIndex, Class clazz) @Override public int hashCode() { - return host.hashCode(); + return values.length; } @Override @@ -116,21 +135,11 @@ public boolean equals(Object obj) { } StoreableImpl storeable = (StoreableImpl) obj; - if (host != storeable.host) { - return false; - } - - if (host.getColumnsCount() != storeable.host.getColumnsCount()) { + if (values.length != storeable.values.length) { return false; } - for (int col = 0; col < host.getColumnsCount(); col++) { - if (!host.getColumnType(col).equals(storeable.host.getColumnType(col))) { - return false; - } - } - - for (int col = 0; col < host.getColumnsCount(); col++) { + for (int col = 0; col < storeable.values.length; col++) { if (!Objects.equals(values[col], storeable.values[col])) { return false; } @@ -141,9 +150,20 @@ public boolean equals(Object obj) { @Override public String toString() { - if (host instanceof StoreableTableImpl) { - return ((StoreableTableImpl) host).getProvider().serialize(host, this); + StringBuilder sb = new StringBuilder(StoreableImpl.class.getSimpleName()).append('['); + + boolean comma = false; + for (Object obj : values) { + if (comma) { + sb.append(','); + } + comma = true; + if (obj != null) { + sb.append(obj.toString()); + } } - return super.toString(); + + sb.append(']'); + return sb.toString(); } } diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/StoreableTableImpl.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/StoreableTableImpl.java index 3ebc0de7a..58d3a7d11 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/StoreableTableImpl.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/StoreableTableImpl.java @@ -11,6 +11,9 @@ import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.ConvenientMap; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Log; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Utility; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.ValidityController; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.ValidityController.KillLock; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.ValidityController.UseLock; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test.TestBase; import java.io.IOException; @@ -25,16 +28,17 @@ import java.util.List; import java.util.Map; import java.util.Scanner; +import java.util.function.Consumer; -public class StoreableTableImpl implements Table { +public final class StoreableTableImpl implements AutoCloseableTable { private static final Map, String> CLASSES_TO_NAMES_MAP = - new ConvenientMap<>(new HashMap, String>()).putNext(Integer.class, "int") - .putNext(Long.class, "long") - .putNext(Byte.class, "byte") - .putNext(Double.class, "double") - .putNext(Float.class, "float") - .putNext(Boolean.class, "boolean") - .putNext(String.class, "String"); + new ConvenientMap<>(new HashMap, String>()).chainPut(Integer.class, "int") + .chainPut(Long.class, "long") + .chainPut(Byte.class, "byte") + .chainPut(Double.class, "double") + .chainPut(Float.class, "float") + .chainPut(Boolean.class, "boolean") + .chainPut(String.class, "String"); private static final Map> NAMES_TO_CLASSES_MAP = Utility.inverseMap(CLASSES_TO_NAMES_MAP); @@ -47,17 +51,24 @@ public class StoreableTableImpl implements Table { private final List> columnTypes; - private boolean invalidated; + private final ValidityController validityController = new ValidityController(); - private StoreableTableImpl(TableProvider provider, StringTableImpl store, List> columnTypes) { + private final Consumer onTableClosedListener; + + private StoreableTableImpl(TableProvider provider, + Consumer
onTableClosedListener, + StringTableImpl store, + List> columnTypes) { this.provider = provider; - this.invalidated = false; + this.onTableClosedListener = onTableClosedListener; this.store = store; - this.columnTypes = Collections.unmodifiableList(new ArrayList>(columnTypes)); + this.columnTypes = Collections.unmodifiableList(new ArrayList<>(columnTypes)); } - static StoreableTableImpl createTable(TableProvider provider, Path tablePath, List> columnTypes) - throws DatabaseIOException { + static AutoCloseableTable createTable(TableProvider provider, + Consumer
onTableClosedListener, + Path tablePath, + List> columnTypes) throws DatabaseIOException { // Creating storage. StringTableImpl store = StringTableImpl.createTable(tablePath); @@ -76,7 +87,9 @@ static StoreableTableImpl createTable(TableProvider provider, Path tablePath, Li "Failed to create table types description file: " + exc.toString(), exc); } - return new StoreableTableImpl(provider, store, columnTypes); + AutoCloseableTable table = + new StoreableTableImpl(provider, onTableClosedListener, store, columnTypes); + return DBTableProviderFactory.wrapImplementation(table, AutoCloseableTable.class); } /** @@ -104,7 +117,9 @@ public static List> parseColumnTypes(String typesString) throws Illegal return columnTypes; } - static StoreableTableImpl getTable(TableProvider provider, Path tablePath) throws DatabaseIOException { + static AutoCloseableTable getTable(TableProvider provider, + Consumer
onTableClosedListener, + Path tablePath) throws DatabaseIOException { StringTableImpl store = StringTableImpl .getTable(tablePath, path -> path != null && path.toString().equals(COLUMNS_FORMAT_FILENAME)); List> columnTypes; @@ -128,15 +143,19 @@ static StoreableTableImpl getTable(TableProvider provider, Path tablePath) throw String msg = exc.getMessage(); String descriptionPart = msg.substring(msg.indexOf('('), msg.lastIndexOf(')')); throw new DatabaseIOException( - "wrong type (Invalid type description file for table " + store.getName() + ": " - + descriptionPart + ")"); + "wrong type (Invalid type description file for table " + + store.getName() + + ": " + + descriptionPart + + ")"); } else { throw new TableCorruptIOException( store.getName(), "Invalid type description file: " + exc.getMessage()); } } - StoreableTableImpl table = new StoreableTableImpl(provider, store, columnTypes); + StoreableTableImpl table = + new StoreableTableImpl(provider, onTableClosedListener, store, columnTypes); // Checking that all stored values are of proper type. List keys = table.list(); @@ -150,19 +169,20 @@ static StoreableTableImpl getTable(TableProvider provider, Path tablePath) throw } } - return table; + return DBTableProviderFactory.wrapImplementation(table, AutoCloseableTable.class); } /** * Checks whether the given storeable can be stored in the given table as a value. - * @throws ColumnFormatException + * @throws ru.fizteh.fivt.storage.structured.ColumnFormatException * If columns count differs or some column has wrong type. Note that if some column has null * value, its type cannot be determined. - * @throws java.lang.IllegalStateException + * @throws IllegalStateException * If the given storeable is already assigned to another table. This check can be performed only - * for instances of {@link StoreableTableImpl}. + * for instances of {@link ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db + * .StoreableTableImpl}. */ - public static void checkStoreableAppropriate(Table table, Storeable storeable) + static void checkStoreableAppropriate(Table table, Storeable storeable) throws ColumnFormatException, IllegalStateException { Utility.checkNotNull(table, "Table"); Utility.checkNotNull(storeable, "Value"); @@ -183,97 +203,88 @@ public static void checkStoreableAppropriate(Table table, Storeable storeable) // It should be caught and ignored here for normal work. } - // Checking column types where possible. - for (int column = 0, columnsCount = table.getColumnsCount(); column < columnsCount; column++) { - Class expectedType = table.getColumnType(column); - Object element = storeable.getColumnAt(column); - - if (element != null) { - Class actualType = element.getClass(); - if (!expectedType.equals(actualType)) { - throw new ColumnFormatException( - String.format( - "wrong type (Column #%d expected to have type %s, but actual" - + " type is %s)", - column, - expectedType.getSimpleName(), - actualType.getSimpleName())); + // Optimization for checking column types. + if (!(storeable instanceof StoreableImpl + && table instanceof StoreableTableImpl + && ((StoreableImpl) storeable).getTypes() == ((StoreableTableImpl) table).getColumnTypes())) { + // Checking column types where possible. + for (int column = 0, columnsCount = table.getColumnsCount(); column < columnsCount; column++) { + Class expectedType = table.getColumnType(column); + Object element = storeable.getColumnAt(column); + + if (element != null) { + Class actualType = element.getClass(); + if (!expectedType.equals(actualType)) { + throw new ColumnFormatException( + String.format( + "wrong type (Column #%d expected to have type %s, but actual" + + " type is %s)", + column, + expectedType.getSimpleName(), + actualType.getSimpleName())); + } } } } - - if (storeable instanceof StoreableImpl) { - if (((StoreableImpl) storeable).getHost() != table) { - throw new IllegalStateException( - "Cannot put storeable assigned to one table to another table"); - } - } - } - - TableProvider getProvider() { - return provider; - } - - /** - * Mark this table as invalidated (all further operations throw {@link java.lang.IllegalStateException}). - */ - void invalidate() { - invalidated = true; } - private void checkValidity() throws IllegalStateException { - if (invalidated) { - throw new IllegalStateException(store.getName() + " is invalidated"); - } + List> getColumnTypes() { + return columnTypes; } @Override public Storeable put(String key, Storeable value) throws ColumnFormatException { - checkValidity(); - Utility.checkNotNull(key, "Key"); - Utility.checkNotNull(value, "Value"); - checkStoreableAppropriate(this, value); - - Storeable previousValue = getWithoutChecks(key); - String serialized = provider.serialize(this, value); - store.put(key, serialized); - return previousValue; + try (UseLock lock = validityController.use()) { + Utility.checkNotNull(key, "Key"); + Utility.checkNotNull(value, "Value"); + checkStoreableAppropriate(this, value); + + Storeable previousValue = getWithoutChecks(key); + String serialized = provider.serialize(this, value); + store.put(key, serialized); + return previousValue; + } } @Override public Storeable remove(String key) { - checkValidity(); - Utility.checkNotNull(key, "Key"); + try (UseLock lock = validityController.use()) { + Utility.checkNotNull(key, "Key"); - Storeable previousValue = getWithoutChecks(key); - store.remove(key); - return previousValue; + Storeable previousValue = getWithoutChecks(key); + store.remove(key); + return previousValue; + } } @Override public int size() { - checkValidity(); - return store.size(); + try (UseLock lock = validityController.use()) { + return store.size(); + } } /** * Collects all keys from all table parts assigned to this table. */ public List list() { - checkValidity(); - return store.list(); + try (UseLock lock = validityController.use()) { + return store.list(); + } } @Override public int commit() throws DatabaseIOException { - checkValidity(); - return store.commit(); + try (UseLock lock = validityController.use()) { + return store.commit(); + } } @Override public int rollback() { - checkValidity(); - return store.rollback(); + try (UseLock lock = validityController.use()) { + return store.rollback(); + } } /** @@ -281,37 +292,42 @@ public int rollback() { */ @Override public int getNumberOfUncommittedChanges() { - checkValidity(); - return store.getNumberOfUncommittedChanges(); + try (UseLock lock = validityController.use()) { + return store.getNumberOfUncommittedChanges(); + } } @Override public int getColumnsCount() { - checkValidity(); - return columnTypes.size(); + try (UseLock lock = validityController.use()) { + return columnTypes.size(); + } } @Override public Class getColumnType(int columnIndex) throws IndexOutOfBoundsException { - checkValidity(); - if (columnIndex < 0 || columnIndex >= getColumnsCount()) { - throw new IndexOutOfBoundsException( - "columnIndex must be between zero (inclusive) and columnsCount (exclusive)"); + try (UseLock lock = validityController.use()) { + if (columnIndex < 0 || columnIndex >= getColumnsCount()) { + throw new IndexOutOfBoundsException( + "columnIndex must be between zero (inclusive) and columnsCount (exclusive)"); + } + return columnTypes.get(columnIndex); } - return columnTypes.get(columnIndex); } @Override public String getName() { - checkValidity(); - return store.getName(); + try (UseLock lock = validityController.use()) { + return store.getName(); + } } @Override public Storeable get(String key) { - checkValidity(); - Utility.checkNotNull(key, "Key"); - return getWithoutChecks(key); + try (UseLock lock = validityController.use()) { + Utility.checkNotNull(key, "Key"); + return getWithoutChecks(key); + } } /** @@ -330,4 +346,25 @@ private Storeable getWithoutChecks(String key) throws ImproperStoreableException throw new ImproperStoreableException(exc.getMessage(), exc); } } + + @Override + public String toString() { + try (UseLock lock = validityController.use()) { + return StoreableTableImpl.class.getSimpleName() + "[" + store.getTableRoot() + "]"; + } + } + + @Override + protected void finalize() throws Throwable { + super.finalize(); + close(); + } + + @Override + public void close() { + try (KillLock lock = validityController.useAndKill()) { + rollback(); + onTableClosedListener.accept(this); + } + } } diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/StringTableImpl.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/StringTableImpl.java index 3388c3384..e6db148e7 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/StringTableImpl.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/StringTableImpl.java @@ -14,8 +14,10 @@ import java.util.HashMap; import java.util.LinkedList; import java.util.List; -import java.util.Map.Entry; +import java.util.Map; import java.util.Set; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.function.Predicate; /** @@ -33,11 +35,15 @@ public final class StringTableImpl { private final Path tableRoot; private final String tableName; + /** + * Lock for exploit of table and writing to the file system. + */ + private final ReadWriteLock persistenceLock = new ReentrantReadWriteLock(true); /** * Mapping between table parts and local hashes of keys that can be stored inside them. * @see #getHash(String) */ - private HashMap tableParts; + private Map tableParts; /** * Constructor for cloning and safe table creation/obtaining. @@ -213,51 +219,43 @@ private void checkFileSystem(Predicate filter) throws DatabaseIOException } } - public void readFromFileSystem() throws DBFileCorruptIOException, TableCorruptIOException { - StringTableImpl thisClone = clone(); - tableParts.clear(); - + void readFromFileSystem() throws DBFileCorruptIOException, TableCorruptIOException { + persistenceLock.writeLock().lock(); try { - for (int dir = 0; dir < DIRECTORIES_COUNT; dir++) { - for (int file = 0; file < FILES_COUNT; file++) { - int partHash = buildHash(dir, file); - TablePart fmap = new TablePart(makeTablePartFilePath(partHash)); - if (Files.exists(fmap.getTablePartFilePath())) { - fmap.readFromFile(); - } + Map oldTableParts = tableParts; + tableParts = new HashMap<>(); - // checking keys' hashes - Set keySet = fmap.keySet(); - for (String key : keySet) { - int keyHash = getHash(key); - if (keyHash != partHash) { - throw new TableCorruptIOException( - tableName, "Some keys are stored in improper places"); + try { + for (int dir = 0; dir < DIRECTORIES_COUNT; dir++) { + for (int file = 0; file < FILES_COUNT; file++) { + int partHash = buildHash(dir, file); + + TablePart tablePart = new TablePart(makeTablePartFilePath(partHash)); + if (Files.exists(tablePart.getTablePartFilePath())) { + tablePart.readFromFile(); } - } - tableParts.put(partHash, fmap); + // checking keys' hashes + Set keySet = tablePart.keySet(); + for (String key : keySet) { + int keyHash = getHash(key); + if (keyHash != partHash) { + throw new TableCorruptIOException( + tableName, "Some keys are stored in improper places"); + } + } + + tableParts.put(partHash, tablePart); + } } + } catch (Exception exc) { + this.tableParts = oldTableParts; + throw exc; } - } catch (Exception exc) { - this.tableParts = thisClone.tableParts; - throw exc; - } - } - - /** - * Clones the whole table - */ - @Override - protected StringTableImpl clone() { - StringTableImpl cloneTable = new StringTableImpl(tableRoot); - - for (Entry entry : tableParts.entrySet()) { - cloneTable.tableParts.put(entry.getKey(), entry.getValue().clone()); + } finally { + persistenceLock.writeLock().unlock(); } - - return cloneTable; } public String getName() { @@ -265,16 +263,31 @@ public String getName() { } public String get(String key) { - return obtainTablePart(key).get(key); + persistenceLock.readLock().lock(); + try { + return obtainTablePart(key).get(key); + } finally { + persistenceLock.readLock().unlock(); + } } public String put(String key, String value) { Utility.checkNotNull(value, "Value"); - return obtainTablePart(key).put(key, value); + persistenceLock.readLock().lock(); + try { + return obtainTablePart(key).put(key, value); + } finally { + persistenceLock.readLock().unlock(); + } } public String remove(String key) { - return obtainTablePart(key).remove(key); + persistenceLock.readLock().lock(); + try { + return obtainTablePart(key).remove(key); + } finally { + persistenceLock.readLock().unlock(); + } } /** @@ -283,8 +296,13 @@ public String remove(String key) { public int size() { int rowsNumber = 0; - for (TablePart part : tableParts.values()) { - rowsNumber += part.size(); + persistenceLock.readLock().lock(); + try { + for (TablePart part : tableParts.values()) { + rowsNumber += part.size(); + } + } finally { + persistenceLock.readLock().unlock(); } return rowsNumber; @@ -292,17 +310,25 @@ public int size() { public int commit() throws DatabaseIOException { int diffsCount = 0; - for (TablePart part : tableParts.values()) { - diffsCount += part.commit(); + + persistenceLock.writeLock().lock(); + try { + for (TablePart part : tableParts.values()) { + diffsCount += part.commit(); + } + } finally { + persistenceLock.writeLock().unlock(); } return diffsCount; } public int rollback() { int diffsCount = 0; + for (TablePart part : tableParts.values()) { diffsCount += part.rollback(); } + return diffsCount; } @@ -312,8 +338,13 @@ public int rollback() { public List list() { List keySet = new LinkedList<>(); - for (TablePart part : tableParts.values()) { - keySet.addAll(part.keySet()); + persistenceLock.readLock().lock(); + try { + for (TablePart part : tableParts.values()) { + keySet.addAll(part.keySet()); + } + } finally { + persistenceLock.readLock().unlock(); } return keySet; @@ -330,7 +361,8 @@ private Path makeTablePartFilePath(int hash) { } /** - * Gets {@link TablePart} instance assigned to this {@code hash} from memory + * Gets {@link TablePart} instance assigned to + * this {@code hash} from memory. Not thread-safe. * @param key * key that is hold by desired table. */ @@ -339,6 +371,9 @@ private TablePart obtainTablePart(String key) { return tableParts.get(getHash(key)); } + /** + * Counts number of uncommitted changes for this thread. + */ public int getNumberOfUncommittedChanges() { int diffsCount = 0; for (TablePart part : tableParts.values()) { diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/TablePart.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/TablePart.java index ae272f0eb..46b30f953 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/TablePart.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/TablePart.java @@ -15,25 +15,31 @@ import java.nio.file.Path; import java.util.HashMap; import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; import java.util.Set; import java.util.TreeMap; /** * This class represents a table part implemented as usual {@link java.util.HashMap} and stored in a separate - * file. + * file.
+ * This class is not thread-safe. * @author phoenix */ public class TablePart { - public static final int READ_BUFFER_SIZE = 16 * 1024; - + private static final int READ_BUFFER_SIZE = 16 * 1024; + /** + * A pair (key, value) describes put. A pair (key, null) describes removal. + */ + private final ThreadLocal> diffMap = ThreadLocal.withInitial(HashMap::new); private Path tablePartFilePath; - - private HashMap tablePartMap; - - private HashMap lastCommittedMap; + /** + * Map with last changes that are written to the file system.
+ */ + private Map lastCommittedMap; /** - * Private constructor for cloning + * Private constructor for cloning. */ private TablePart() { @@ -51,25 +57,15 @@ public TablePart(Path tablePartFilePath) { this.tablePartFilePath = tablePartFilePath; - tablePartMap = new HashMap<>(); lastCommittedMap = new HashMap<>(); } - /** - * Silently clones the object - no changes in file system are made. - */ - @SuppressWarnings("unchecked") - @Override - public TablePart clone() { - TablePart fmap = new TablePart(); - fmap.tablePartMap = (HashMap) this.tablePartMap.clone(); - fmap.lastCommittedMap = (HashMap) this.lastCommittedMap.clone(); - fmap.tablePartFilePath = this.tablePartFilePath; - return fmap; - } - public String get(String key) { - return tablePartMap.get(key); + if (diffMap.get().containsKey(key)) { + return diffMap.get().get(key); + } else { + return lastCommittedMap.get(key); + } } public Path getTablePartFilePath() { @@ -77,35 +73,33 @@ public Path getTablePartFilePath() { } public Set keySet() { - return tablePartMap.keySet(); + return makeNewActualVersion().keySet(); } public String put(String key, String value) { - return tablePartMap.put(key, value); + String oldValue = get(key); + diffMap.get().put(key, value); + return oldValue; } /** - * Reads database from file system (all previous data is purged).
If an error occurs the - * state before this operation is recovered. + * Reads database from file system (all previous data is purged).
+ * If an error occurs the state before this operation is recovered.
+ * Thread-local uncommitted diffs are not effected.
* @throws ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.DBFileCorruptIOException */ @SuppressWarnings("unchecked") public void readFromFile() throws DBFileCorruptIOException { - /* - * if an exception occurs and database is cloned, recover if cloned - * object is null - no recover is performed. - */ - HashMap cloneDBMap = (HashMap) tablePartMap.clone(); - tablePartMap.clear(); + // For recover purposes. + Map oldLastCommittedMap = lastCommittedMap; + lastCommittedMap = new HashMap<>(); try (DataInputStream stream = new DataInputStream( new FileInputStream( tablePartFilePath.toString()))) { - /* - * structure: (no spaces or newlines) 00<4 - * bytes:offset> 00<4 bytes:offset> ... - * ... - */ + // Structure: (no spaces or newlines) 00<4 + // bytes:offset> 00<4 bytes:offset> ... + // ... byte[] buffer = new byte[1024]; int bufferSize = 0; @@ -170,23 +164,25 @@ public void readFromFile() throws DBFileCorruptIOException { } } - // empty map + // Empty map. if (offsets.isEmpty()) { return; } - // reading values - String currentKey = offsets.get(nextValue); // value matching this - // key is now being - // built - offsets.remove(nextValue); // next value start boundary + // Reading values. + + // Value matching this key is now being built. + String currentKey = offsets.get(nextValue); - // reading up to the last value (exclusive) + // Next value start boundary. + offsets.remove(nextValue); + + // Reading up to the last value (exclusive). while (!offsets.isEmpty()) { nextValue = offsets.firstKey(); String value = new String(buffer, bufferOffset, nextValue - bufferOffset); - tablePartMap.put(currentKey, value); + lastCommittedMap.put(currentKey, value); bufferOffset = nextValue; currentKey = offsets.get(nextValue); @@ -194,37 +190,81 @@ public void readFromFile() throws DBFileCorruptIOException { offsets.remove(nextValue); } - // putting the last value + // Putting the last value. String value = new String(buffer, bufferOffset, bufferSize - bufferOffset); - tablePartMap.put(currentKey, value); + lastCommittedMap.put(currentKey, value); } catch (IOException exc) { - // recover - if (cloneDBMap != null) { - tablePartMap = cloneDBMap; - } + // Recover. + lastCommittedMap = oldLastCommittedMap; throw new DBFileCorruptIOException( "Failed to read data from file: " + tablePartFilePath.toString(), exc); } - // if everything went ok - lastCommittedMap = new HashMap<>(tablePartMap); + // Everything went ok. } public String remove(String key) { - return tablePartMap.remove(key); + if (diffMap.get().containsKey(key)) { + String oldValue = diffMap.get().get(key); + // Already removed. + if (oldValue == null) { + return null; + } else { + // Postponed put will be cancelled. + diffMap.get().remove(key); + return oldValue; + } + } else { + diffMap.get().put(key, null); + return lastCommittedMap.get(key); + } } + /** + * Makes a separate up-to-date version which is a commit of the thread diff to the clone of {@link + * #lastCommittedMap}. + * @return Separate actual version. Changes in this instance have not effect on true database state. + */ + private Map makeNewActualVersion() { + return makeActualVersion(new HashMap<>(lastCommittedMap)); + } + + /** + * Convenience method for private actual version supplying. + * @param actualVersion + * Map to commit thread diffs to. + * @return actualVersion (the same instance) with committed diffs. + */ + private Map makeActualVersion(Map actualVersion) { + for (Entry e : diffMap.get().entrySet()) { + if (e.getValue() == null) { + actualVersion.remove(e.getKey()); + } else { + actualVersion.put(e.getKey(), e.getValue()); + } + } + + return actualVersion; + } + + /** + * Returns actual size for the moment (considering the thread local diff). + */ public int size() { - return tablePartMap.size(); + return makeNewActualVersion().size(); } - public void writeToFile() throws IOException { + /** + * Writes changes to the file. + * @throws java.io.IOException + */ + private void writeToFile() throws IOException { ByteArrayOutputStream stream = new ByteArrayOutputStream(1024); - Iterator keyIterator = tablePartMap.keySet().iterator(); + Iterator keyIterator = lastCommittedMap.keySet().iterator(); Charset charset = Charset.forName("UTF-8"); - int[] shiftPositions = new int[tablePartMap.size()]; + int[] shiftPositions = new int[lastCommittedMap.size()]; byte[] intZero = new byte[] {0, 0, 0, 0}; @@ -237,14 +277,14 @@ public void writeToFile() throws IOException { stream.write(intZero); } - int[] links = new int[tablePartMap.size()]; + int[] links = new int[lastCommittedMap.size()]; keyID = 0; - keyIterator = tablePartMap.keySet().iterator(); + keyIterator = lastCommittedMap.keySet().iterator(); while (keyIterator.hasNext()) { links[keyID] = stream.size(); keyID++; - stream.write(tablePartMap.get(keyIterator.next()).getBytes(charset)); + stream.write(lastCommittedMap.get(keyIterator.next()).getBytes(charset)); } byte[] bytes = stream.toByteArray(); @@ -285,7 +325,8 @@ public int commit() throws DatabaseIOException { int diffsCount = getUncommittedChangesCount(); if (diffsCount > 0) { - lastCommittedMap = new HashMap<>(tablePartMap); + makeActualVersion(lastCommittedMap); + diffMap.get().clear(); try { writeToFile(); } catch (IOException exc) { @@ -298,11 +339,11 @@ public int commit() throws DatabaseIOException { public int rollback() { int diffsCount = getUncommittedChangesCount(); - tablePartMap = new HashMap<>(lastCommittedMap); + diffMap.get().clear(); return diffsCount; } public int getUncommittedChangesCount() { - return Utility.countDifferences(lastCommittedMap, tablePartMap); + return diffMap.get().size(); } } diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/IRemoteTable.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/IRemoteTable.java new file mode 100644 index 000000000..d34acde86 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/IRemoteTable.java @@ -0,0 +1,104 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.remote; + +import ru.fizteh.fivt.storage.structured.ColumnFormatException; +import ru.fizteh.fivt.storage.structured.Storeable; + +import java.io.IOException; +import java.rmi.Remote; +import java.rmi.RemoteException; +import java.util.List; + +interface IRemoteTable extends Remote { + /** + * Возвращает название таблицы или индекса. + * @return Название таблицы. + */ + String getName() throws RemoteException; + + /** + * Получает значение по указанному ключу. + * @param key + * Ключ для поиска значения. Не может быть null. + * Для индексов по не-строковым полям аргумент представляет собой сериализованное значение + * колонки. + * Его потребуется распарсить. + * @return Значение. Если не найдено, возвращает null. + * @throws IllegalArgumentException + * Если значение параметра key является null. + */ + Storeable get(String key) throws RemoteException; + + /** + * Устанавливает значение по указанному ключу. + * @param key + * Ключ для нового значения. Не может быть null. + * @param value + * Новое значение. Не может быть null. + * @return Значение, которое было записано по этому ключу ранее. Если ранее значения не было записано, + * возвращает null. + * @throws IllegalArgumentException + * Если значение параметров key или value является null. + * @throws ru.fizteh.fivt.storage.structured.ColumnFormatException + * - при попытке передать Storeable с колонками другого типа. + */ + Storeable put(String key, Storeable value) throws ColumnFormatException, RemoteException; + + /** + * Удаляет значение по указанному ключу. + * @param key + * Ключ для поиска значения. Не может быть null. + * @return Предыдущее значение. Если не найдено, возвращает null. + * @throws IllegalArgumentException + * Если значение параметра key является null. + */ + Storeable remove(String key) throws RemoteException; + + /** + * Возвращает количество ключей в таблице. Возвращает размер текущей версии, с учётом незафиксированных + * изменений. + * @return Количество ключей в таблице. + */ + int size() throws RemoteException; + + /** + * Выводит список ключей таблицы, с учётом незафиксированных изменений. + * @return Список ключей. + */ + List list() throws RemoteException; + + /** + * Выполняет фиксацию изменений. + * @return Число записанных изменений. + * @throws java.io.IOException + * если произошла ошибка ввода/вывода. Целостность таблицы не гарантируется. + */ + int commit() throws IOException; + + /** + * Выполняет откат изменений с момента последней фиксации. + * @return Число откаченных изменений. + */ + int rollback() throws RemoteException; + + /** + * Возвращает количество изменений, ожидающих фиксации. + * @return Количество изменений, ожидающих фиксации. + */ + int getNumberOfUncommittedChanges() throws RemoteException; + + /** + * Возвращает количество колонок в таблице. + * @return Количество колонок в таблице. + */ + int getColumnsCount() throws RemoteException; + + /** + * Возвращает тип значений в колонке. + * @param columnIndex + * Индекс колонки. Начинается с нуля. + * @return Класс, представляющий тип значения. + * @throws IndexOutOfBoundsException + * - неверный индекс колонки + */ + Class getColumnType(int columnIndex) throws IndexOutOfBoundsException, RemoteException; +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/IRemoteTableProvider.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/IRemoteTableProvider.java new file mode 100644 index 000000000..7b202e273 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/IRemoteTableProvider.java @@ -0,0 +1,117 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.remote; + +import ru.fizteh.fivt.storage.structured.ColumnFormatException; +import ru.fizteh.fivt.storage.structured.Storeable; +import ru.fizteh.fivt.storage.structured.Table; + +import java.io.IOException; +import java.rmi.Remote; +import java.rmi.RemoteException; +import java.text.ParseException; +import java.util.List; + +interface IRemoteTableProvider extends Remote { + /** + * Возвращает таблицу с указанным названием. + * + * Последовательные вызовы метода с одинаковыми аргументами должны возвращать один и тот же объект + * таблицы, + * если он не был удален с помощью {@link #removeTable(String)}. + * @param name + * Название таблицы. + * @return Объект, представляющий таблицу. Если таблицы с указанным именем не существует, возвращает null. + * @throws IllegalArgumentException + * Если название таблицы null или имеет недопустимое значение. + */ + RemoteTableStub getTable(String name) throws RemoteException; + + /** + * Создаёт таблицу с указанным названием. + * Создает новую таблицу. Совершает необходимые дисковые операции. + * @param name + * Название таблицы. + * @param columnTypes + * Типы колонок таблицы. Не может быть пустой. + * @return Объект, представляющий таблицу. Если таблица с указанным именем существует, возвращает null. + * @throws IllegalArgumentException + * Если название таблицы null или имеет недопустимое значение. Если список типов + * колонок null или содержит недопустимые значения. + * @throws java.io.IOException + * При ошибках ввода/вывода. + */ + RemoteTableStub createTable(String name, List> columnTypes) throws IOException; + + /** + * Удаляет существующую таблицу с указанным названием. + * + * Объект удаленной таблицы, если был кем-то взят с помощью {@link #getTable(String)}, + * с этого момента должен бросать {@link IllegalStateException}. + * @param name + * Название таблицы. + * @throws IllegalArgumentException + * Если название таблицы null или имеет недопустимое значение. + * @throws IllegalStateException + * Если таблицы с указанным названием не существует. + * @throws java.io.IOException + * - при ошибках ввода/вывода. + */ + void removeTable(String name) throws IOException; + + /** + * Преобразовывает строку в объект {@link ru.fizteh.fivt.storage.structured.Storeable}, соответствующий + * структуре таблицы. + * @param table + * Таблица, которой должен принадлежать {@link ru.fizteh.fivt.storage.structured.Storeable}. + * @param value + * Строка, из которой нужно прочитать {@link ru.fizteh.fivt.storage.structured.Storeable}. + * @return Прочитанный {@link ru.fizteh.fivt.storage.structured.Storeable}. + * @throws java.text.ParseException + * - при каких-либо несоответстиях в прочитанных данных. + */ + Storeable deserialize(Table table, String value) throws RemoteException, ParseException; + + /** + * Преобразовывает объект {@link ru.fizteh.fivt.storage.structured.Storeable} в строку. + * @param table + * Таблица, которой должен принадлежать {@link ru.fizteh.fivt.storage.structured.Storeable}. + * @param value + * {@link ru.fizteh.fivt.storage.structured.Storeable}, который нужно записать. + * @return Строка с записанным значением. + * @throws ru.fizteh.fivt.storage.structured.ColumnFormatException + * При несоответствии типа в {@link ru.fizteh.fivt.storage.structured.Storeable} и типа колонки в + * таблице. + */ + String serialize(Table table, Storeable value) throws RemoteException, ColumnFormatException; + + /** + * Создает новый пустой {@link ru.fizteh.fivt.storage.structured.Storeable} для указанной таблицы. + * @param table + * Таблица, которой должен принадлежать {@link ru.fizteh.fivt.storage.structured.Storeable}. + * @return Пустой {@link ru.fizteh.fivt.storage.structured.Storeable}, нацеленный на использование с этой + * таблицей. + */ + Storeable createFor(Table table) throws RemoteException; + + /** + * Создает новый {@link ru.fizteh.fivt.storage.structured.Storeable} для указанной таблицы, подставляя + * туда переданные значения. + * @param table + * Таблица, которой должен принадлежать {@link ru.fizteh.fivt.storage.structured.Storeable}. + * @param values + * Список значений, которыми нужно проинициализировать поля Storeable. + * @return {@link ru.fizteh.fivt.storage.structured.Storeable}, проинициализированный переданными + * значениями. + * @throws ru.fizteh.fivt.storage.structured.ColumnFormatException + * При несоответствии типа переданного значения и колонки. + * @throws IndexOutOfBoundsException + * При несоответствии числа переданных значений и числа колонок. + */ + Storeable createFor(Table table, List values) + throws RemoteException, ColumnFormatException, IndexOutOfBoundsException; + + /** + * Возвращает имена существующих таблиц, которые могут быть получены с помощью {@link #getTable(String)}. + * @return Имена существующих таблиц. + */ + List getTableNames() throws RemoteException; +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/IRemoteTableProviderFactory.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/IRemoteTableProviderFactory.java new file mode 100644 index 000000000..c931dd8a4 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/IRemoteTableProviderFactory.java @@ -0,0 +1,16 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.remote; + +import java.io.Closeable; +import java.io.IOException; +import java.rmi.Remote; +import java.rmi.registry.Registry; + +interface IRemoteTableProviderFactory extends Remote, Closeable { + RemoteTableProviderStub obtainRemoteProvider() throws IOException; + + default void establishStorage(String localDatabaseRoot) throws IOException { + establishStorage(localDatabaseRoot, Registry.REGISTRY_PORT); + } + + void establishStorage(String localDatabaseRoot, int port) throws IOException; +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteDatabaseStorage.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteDatabaseStorage.java new file mode 100644 index 000000000..d5fbe4dc1 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteDatabaseStorage.java @@ -0,0 +1,82 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.remote; + +import ru.fizteh.fivt.storage.structured.RemoteTableProvider; +import ru.fizteh.fivt.storage.structured.RemoteTableProviderFactory; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.rmi.NotBoundException; +import java.rmi.registry.LocateRegistry; +import java.rmi.registry.Registry; +import java.util.HashMap; +import java.util.Map; + +/** + * Class used to connect to a remote database storage and obtain providers. + */ +public class RemoteDatabaseStorage implements RemoteTableProviderFactory { + // There is no read-write sync, because obtaining many providers seem to be not so popular as + // obtaining many tables. + + private final Map providerStubs = new HashMap<>(); + + private boolean providerStubClosedByMe = false; + + public RemoteDatabaseStorage() { + } + + private String makeLocation(String hostname, int port) throws UnknownHostException { + return InetAddress.getByName(hostname).getHostAddress() + ":" + port; + } + + void onProviderStubClosed(String locationInStorage) { + synchronized (providerStubs) { + if (!providerStubClosedByMe) { + providerStubs.remove(locationInStorage); + } + } + } + + @Override + protected void finalize() throws Throwable { + super.finalize(); + synchronized (providerStubs) { + providerStubClosedByMe = true; + providerStubs.values().forEach(RemoteTableProviderStub::close); + providerStubs.clear(); + } + } + + private RemoteTableProviderStub tryReplaceProviderStub(String location, + RemoteTableProviderStub providerStub) { + if (providerStub == null) { + return null; + } + + synchronized (providerStubs) { + RemoteTableProviderStub oldStub = providerStubs.get(location); + + if (oldStub == null) { + providerStubs.put(location, providerStub); + providerStub.bindToStorage(this, location); + return providerStub; + } else { + return oldStub; + } + } + } + + @Override + public RemoteTableProvider connect(String hostname, int port) throws IOException { + Registry registry = LocateRegistry.getRegistry(hostname, port); + try { + IRemoteTableProviderFactory factory = (IRemoteTableProviderFactory) registry + .lookup(RemoteTableProviderFactoryImpl.FACTORY_NAME); + return tryReplaceProviderStub(makeLocation(hostname, port), factory.obtainRemoteProvider()); + } catch (NotBoundException exc) { + // This cannot happen. + throw new RuntimeException(exc); + } + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableImpl.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableImpl.java new file mode 100644 index 000000000..d8d2e63c9 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableImpl.java @@ -0,0 +1,81 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.remote; + +import ru.fizteh.fivt.storage.structured.ColumnFormatException; +import ru.fizteh.fivt.storage.structured.Storeable; +import ru.fizteh.fivt.storage.structured.Table; + +import java.io.IOException; +import java.rmi.RemoteException; +import java.rmi.server.UnicastRemoteObject; +import java.util.List; + +/** + * Remote table object that is stored on server and available through RMI mechanism. + */ +class RemoteTableImpl extends UnicastRemoteObject implements IRemoteTable { + + private final Table table; + + public RemoteTableImpl(Table table) throws RemoteException { + this.table = table; + } + + public Table getPureTable() { + return table; + } + + @Override + public String getName() throws RemoteException { + return table.getName(); + } + + @Override + public Storeable get(String key) throws RemoteException, IllegalStateException { + return table.get(key); + } + + @Override + public Storeable put(String key, Storeable value) throws ColumnFormatException, RemoteException { + return table.put(key, value); + } + + @Override + public Storeable remove(String key) throws RemoteException { + return table.remove(key); + } + + @Override + public int size() throws RemoteException { + return table.size(); + } + + @Override + public List list() throws RemoteException { + return table.list(); + } + + @Override + public int commit() throws IOException { + return table.commit(); + } + + @Override + public int rollback() throws RemoteException { + return table.rollback(); + } + + @Override + public int getNumberOfUncommittedChanges() throws RemoteException { + return table.getNumberOfUncommittedChanges(); + } + + @Override + public int getColumnsCount() throws RemoteException { + return table.getColumnsCount(); + } + + @Override + public Class getColumnType(int columnIndex) throws RemoteException, IndexOutOfBoundsException { + return table.getColumnType(columnIndex); + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableProviderFactoryImpl.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableProviderFactoryImpl.java new file mode 100644 index 000000000..599852be9 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableProviderFactoryImpl.java @@ -0,0 +1,108 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.remote; + +import ru.fizteh.fivt.storage.structured.TableProvider; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.AutoCloseableProvider; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.ProviderWrap; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Log; + +import java.io.IOException; +import java.rmi.NotBoundException; +import java.rmi.RemoteException; +import java.rmi.registry.LocateRegistry; +import java.rmi.registry.Registry; +import java.rmi.server.UnicastRemoteObject; + +/** + * My remote factory. + */ +public class RemoteTableProviderFactoryImpl extends UnicastRemoteObject + implements IRemoteTableProviderFactory { + + static final String FACTORY_NAME = "/Database"; + /** + * Local provider that is final for all time. + */ + private final TableProvider localProvider; + private Registry registry; + /** + * Provider wrap that can be closed. + */ + private AutoCloseableProvider providerWrap; + /** + * Server-local singleton implementation of remote table providerWrap that is wrapped in stub. + */ + private IRemoteTableProvider remoteProvider; + + public RemoteTableProviderFactoryImpl(TableProvider provider) throws RemoteException { + this.localProvider = provider; + } + + private boolean isBound() { + return registry != null; + } + + private void requireBound() throws IllegalStateException { + if (!isBound()) { + throw new IllegalStateException("Factory is not established yet."); + } + } + + @Override + public synchronized void close() throws IOException { + if (!isBound()) { + return; + } + try { + providerWrap.close(); + registry.unbind(FACTORY_NAME); + } catch (NotBoundException exc) { + throw new IllegalStateException("The factory has not been established"); + } finally { + providerWrap = null; + remoteProvider = null; + registry = null; + } + } + + @Override + public synchronized RemoteTableProviderStub obtainRemoteProvider() throws IOException { + requireBound(); + return new RemoteTableProviderStub(remoteProvider); + } + + @Override + public synchronized void establishStorage(String localDatabaseRoot, int port) throws IOException { + if (isBound()) { + throw new IllegalStateException("Factory already established"); + } + + try { + try { + registry = LocateRegistry.getRegistry(port); + registry.rebind(FACTORY_NAME, this); + + } catch (RemoteException exc) { + registry = LocateRegistry.createRegistry(port); + registry.rebind(FACTORY_NAME, this); + } + + providerWrap = new ProviderWrap(localProvider); + remoteProvider = new RemoteTableProviderImpl(providerWrap); + } catch (Exception exc) { + Log.log(RemoteTableProviderFactoryImpl.class, exc, "Got exception on establishment"); + try { + providerWrap.close(); + providerWrap = null; + registry = null; + remoteProvider = null; + } catch (Exception ignored) { + Log.log( + RemoteTableProviderFactoryImpl.class, + ignored, + "Failed to cleanup after getting exception on establishment"); + } + throw exc; + } + } + +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableProviderImpl.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableProviderImpl.java new file mode 100644 index 000000000..8c8e5375c --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableProviderImpl.java @@ -0,0 +1,76 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.remote; + +import ru.fizteh.fivt.storage.structured.ColumnFormatException; +import ru.fizteh.fivt.storage.structured.Storeable; +import ru.fizteh.fivt.storage.structured.Table; +import ru.fizteh.fivt.storage.structured.TableProvider; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.UnexpectedRemoteException; + +import java.io.IOException; +import java.rmi.RemoteException; +import java.rmi.server.UnicastRemoteObject; +import java.text.ParseException; +import java.util.List; + +/** + * Remote table provider that is created at server (once) and can be exploited by clients. + */ +class RemoteTableProviderImpl extends UnicastRemoteObject implements IRemoteTableProvider { + // Even if we keep the match (pure table -> table stub), the same stubs sent to the client will be + // deserealized as different instances. So, we do not keep this match. + + private final TableProvider provider; + + public RemoteTableProviderImpl(TableProvider provider) throws RemoteException { + this.provider = provider; + } + + private RemoteTableStub wrapInStub(Table table) throws UnexpectedRemoteException { + try { + return table == null ? null : new RemoteTableStub(new RemoteTableImpl(table), table.getName()); + } catch (RemoteException exc) { + throw new UnexpectedRemoteException(exc); + } + } + + @Override + public RemoteTableStub getTable(String name) { + return wrapInStub(provider.getTable(name)); + } + + @Override + public RemoteTableStub createTable(String name, List> columnTypes) throws IOException { + return wrapInStub(provider.createTable(name, columnTypes)); + } + + @Override + public void removeTable(String name) throws IOException { + provider.removeTable(name); + } + + @Override + public Storeable deserialize(Table table, String value) throws ParseException { + return provider.deserialize(table, value); + } + + @Override + public String serialize(Table table, Storeable value) throws ColumnFormatException { + return provider.serialize(table, value); + } + + @Override + public Storeable createFor(Table table) { + return provider.createFor(table); + } + + @Override + public Storeable createFor(Table table, List values) + throws ColumnFormatException, IndexOutOfBoundsException { + return provider.createFor(table, values); + } + + @Override + public List getTableNames() { + return provider.getTableNames(); + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableProviderStub.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableProviderStub.java new file mode 100644 index 000000000..3e5809e7d --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableProviderStub.java @@ -0,0 +1,262 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.remote; + +import ru.fizteh.fivt.storage.structured.ColumnFormatException; +import ru.fizteh.fivt.storage.structured.RemoteTableProvider; +import ru.fizteh.fivt.storage.structured.Storeable; +import ru.fizteh.fivt.storage.structured.Table; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.InvalidatedObjectException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.UnexpectedRemoteException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.ValidityController; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.ValidityController.KillLock; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.ValidityController.UseLock; + +import java.io.IOException; +import java.io.Serializable; +import java.rmi.RemoteException; +import java.text.ParseException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +/** + * Stub of remote table provider that is exploited on the client side. + */ +final class RemoteTableProviderStub implements RemoteTableProvider, Serializable { + /** + * Wrapped remote table provider. + */ + private final IRemoteTableProvider remoteProvider; + /** + * Read-write sync on tableStubs. Because getTable() is very popular :) + */ + private final ReadWriteLock tableStubsLock = new ReentrantReadWriteLock(true); + /** + * For synchronization on client side. + */ + private final ValidityController validityController = new ValidityController(); + /** + * Local stubs associated with this provider. + */ + private transient Map tableStubs; + + private transient boolean tableStubClosedByMe; + + /** + * Local storage stub that manages instances of {@link ru.fizteh.fivt.students.fedorov_andrew + * .databaselibrary.db.remote.RemoteTableProviderStub}. + */ + private transient RemoteDatabaseStorage storage; + + /** + * Location on storage that identifies this provider stub. + */ + private transient String locationInStorage; + + public RemoteTableProviderStub(IRemoteTableProvider remoteProvider) { + this.remoteProvider = remoteProvider; + } + + public void bindToStorage(RemoteDatabaseStorage storage, String locationInStorage) { + this.storage = storage; + this.locationInStorage = locationInStorage; + + // Initializing transient fields. + tableStubs = new HashMap<>(); + tableStubClosedByMe = false; + } + + private RemoteTableStub tryReplaceTableStub(String name, RemoteTableStub tableStub) { + if (tableStub == null) { + return null; + } + + Lock lock = tableStubsLock.readLock(); + lock.lock(); + try { + if (!tableStubs.containsKey(name)) { + lock.unlock(); + lock = tableStubsLock.writeLock(); + lock.lock(); + if (!tableStubs.containsKey(name)) { + tableStubs.put(name, tableStub); + if (tableStub != null) { + tableStub.bindToProviderStub(this); + } + return tableStub; + } else { + return tableStubs.get(name); + } + } else { + return tableStubs.get(name); + } + } finally { + lock.unlock(); + } + } + + private void removeTableStub(String tableName) { + tableStubsLock.writeLock().lock(); + try { + RemoteTableStub tableStub = tableStubs.get(tableName); + if (tableStub != null) { + tableStub.close(); + } + } finally { + tableStubsLock.writeLock().unlock(); + } + } + + public void onTableStubClosed(String tableName) { + tableStubsLock.writeLock().lock(); + try { + if (!tableStubClosedByMe) { + tableStubs.remove(tableName); + } + } finally { + tableStubsLock.writeLock().unlock(); + } + } + + @Override + public void close() { + try (KillLock lock = validityController.useAndKill()) { + tableStubsLock.writeLock().lock(); + try { + tableStubClosedByMe = true; + tableStubs.values().forEach(RemoteTableStub::close); + storage.onProviderStubClosed(locationInStorage); + } finally { + tableStubsLock.writeLock().unlock(); + } + } + } + + @Override + protected void finalize() throws Throwable { + super.finalize(); + close(); + } + + private void forceClose(UseLock activeUseLock) { + try (KillLock killLock = activeUseLock.obtainKillLockInstead()) { + close(); + } + } + + @Override + public Table getTable(String name) { + try (UseLock lock = validityController.use()) { + try { + return tryReplaceTableStub(name, remoteProvider.getTable(name)); + } catch (InvalidatedObjectException exc) { + forceClose(lock); + throw exc; + } + } catch (RemoteException exc) { + throw new UnexpectedRemoteException(exc); + } + } + + @Override + public Table createTable(String name, List> columnTypes) throws IOException { + try (UseLock lock = validityController.use()) { + try { + return tryReplaceTableStub(name, remoteProvider.createTable(name, columnTypes)); + } catch (InvalidatedObjectException exc) { + forceClose(lock); + throw exc; + } + } catch (RemoteException exc) { + throw new UnexpectedRemoteException(exc); + } + } + + @Override + public void removeTable(String name) throws IOException { + try (UseLock lock = validityController.use()) { + try { + removeTableStub(name); + remoteProvider.removeTable(name); + } catch (InvalidatedObjectException exc) { + forceClose(lock); + throw exc; + } + } catch (RemoteException exc) { + throw new UnexpectedRemoteException(exc); + } + } + + @Override + public Storeable deserialize(Table table, String value) throws ParseException { + try (UseLock lock = validityController.use()) { + try { + return remoteProvider.deserialize(table, value); + } catch (InvalidatedObjectException exc) { + forceClose(lock); + throw exc; + } + } catch (RemoteException exc) { + throw new UnexpectedRemoteException(exc); + } + } + + @Override + public String serialize(Table table, Storeable value) throws ColumnFormatException { + try (UseLock lock = validityController.use()) { + try { + return remoteProvider.serialize(table, value); + } catch (InvalidatedObjectException exc) { + forceClose(lock); + throw exc; + } + } catch (RemoteException exc) { + throw new UnexpectedRemoteException(exc); + } + } + + @Override + public Storeable createFor(Table table) { + try (UseLock lock = validityController.use()) { + try { + return remoteProvider.createFor(table); + } catch (InvalidatedObjectException exc) { + forceClose(lock); + throw exc; + } + } catch (RemoteException exc) { + throw new UnexpectedRemoteException(exc); + } + } + + @Override + public Storeable createFor(Table table, List values) + throws ColumnFormatException, IndexOutOfBoundsException { + try (UseLock lock = validityController.use()) { + try { + return remoteProvider.createFor(table, values); + } catch (InvalidatedObjectException exc) { + forceClose(lock); + throw exc; + } + } catch (RemoteException exc) { + throw new UnexpectedRemoteException(exc); + } + } + + @Override + public List getTableNames() { + try (UseLock lock = validityController.use()) { + try { + return remoteProvider.getTableNames(); + } catch (InvalidatedObjectException exc) { + forceClose(lock); + throw exc; + } + } catch (RemoteException exc) { + throw new UnexpectedRemoteException(exc); + } + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableStub.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableStub.java new file mode 100644 index 000000000..9b5360c8e --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableStub.java @@ -0,0 +1,225 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.remote; + +import ru.fizteh.fivt.storage.structured.ColumnFormatException; +import ru.fizteh.fivt.storage.structured.Storeable; +import ru.fizteh.fivt.storage.structured.Table; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.InvalidatedObjectException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.UnexpectedRemoteException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.ValidityController; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.ValidityController.KillLock; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.ValidityController.UseLock; + +import java.io.Closeable; +import java.io.IOException; +import java.io.Serializable; +import java.rmi.RemoteException; +import java.util.List; + +/** + * Stub of remote table that is transferred from server to client.
+ * I created this stub because task API is not friendly and there are no RemoteExceptions. + */ +final class RemoteTableStub implements Table, Serializable, Closeable { + // Frankly speaking there is no need to perform forceClose() method. It it done to decrease count of + // remote requests. + + /** + * Table name is cached for correct close handling at client's local. + */ + private final String tableName; + + private final IRemoteTable remoteTable; + /** + * For validity control on client side. + */ + private final ValidityController validityController = new ValidityController(); + /** + * Local variable at the client side. + */ + private transient RemoteTableProviderStub providerStub; + + public RemoteTableStub(RemoteTableImpl remoteTable, String tableName) { + this.remoteTable = remoteTable; + this.tableName = tableName; + } + + /** + * Called at client side when we get the stub. + */ + public void bindToProviderStub(RemoteTableProviderStub providerStub) { + this.providerStub = providerStub; + } + + @Override + public void close() { + try (KillLock lock = validityController.useAndKill()) { + providerStub.onTableStubClosed(tableName); + } + } + + @Override + protected void finalize() throws Throwable { + super.finalize(); + close(); + } + + private void forceClose(UseLock activeUseLock) { + try (KillLock killLock = activeUseLock.obtainKillLockInstead()) { + close(); + } + } + + @Override + public Storeable put(String key, Storeable value) throws ColumnFormatException { + try (UseLock lock = validityController.use()) { + try { + return remoteTable.put(key, value); + } catch (InvalidatedObjectException exc) { + forceClose(lock); + throw exc; + } + } catch (RemoteException exc) { + throw new UnexpectedRemoteException(exc); + } + } + + @Override + public Storeable remove(String key) { + try (UseLock lock = validityController.use()) { + try { + return remoteTable.remove(key); + } catch (InvalidatedObjectException exc) { + forceClose(lock); + throw exc; + } + } catch (RemoteException exc) { + throw new UnexpectedRemoteException(exc); + } + } + + @Override + public int size() { + try (UseLock lock = validityController.use()) { + try { + return remoteTable.size(); + } catch (InvalidatedObjectException exc) { + forceClose(lock); + throw exc; + } + } catch (RemoteException exc) { + throw new UnexpectedRemoteException(exc); + } + } + + @Override + public List list() { + try (UseLock lock = validityController.use()) { + try { + return remoteTable.list(); + } catch (InvalidatedObjectException exc) { + forceClose(lock); + throw exc; + } + } catch (RemoteException exc) { + throw new UnexpectedRemoteException(exc); + } + } + + @Override + public int commit() throws IOException { + try (UseLock lock = validityController.use()) { + try { + return remoteTable.commit(); + } catch (InvalidatedObjectException exc) { + forceClose(lock); + throw exc; + } + } catch (RemoteException exc) { + throw new UnexpectedRemoteException(exc); + } + } + + @Override + public int rollback() { + try (UseLock lock = validityController.use()) { + try { + return remoteTable.rollback(); + } catch (InvalidatedObjectException exc) { + forceClose(lock); + throw exc; + } + } catch (RemoteException exc) { + throw new UnexpectedRemoteException(exc); + } + } + + @Override + public int getNumberOfUncommittedChanges() { + try (UseLock lock = validityController.use()) { + try { + return remoteTable.getNumberOfUncommittedChanges(); + } catch (InvalidatedObjectException exc) { + forceClose(lock); + throw exc; + } + } catch (RemoteException exc) { + throw new UnexpectedRemoteException(exc); + } + } + + @Override + public int getColumnsCount() { + try (UseLock lock = validityController.use()) { + try { + return remoteTable.getColumnsCount(); + } catch (InvalidatedObjectException exc) { + forceClose(lock); + throw exc; + } + } catch (RemoteException exc) { + throw new UnexpectedRemoteException(exc); + } + } + + @Override + public Class getColumnType(int columnIndex) throws IndexOutOfBoundsException { + try (UseLock lock = validityController.use()) { + try { + return remoteTable.getColumnType(columnIndex); + } catch (InvalidatedObjectException exc) { + forceClose(lock); + throw exc; + } + } catch (RemoteException exc) { + throw new UnexpectedRemoteException(exc); + } + } + + @Override + public String getName() { + try (UseLock lock = validityController.use()) { + try { + return remoteTable.getName(); + } catch (InvalidatedObjectException exc) { + forceClose(lock); + throw exc; + } + } catch (RemoteException exc) { + throw new UnexpectedRemoteException(exc); + } + } + + @Override + public Storeable get(String key) { + try (UseLock lock = validityController.use()) { + try { + return remoteTable.get(key); + } catch (InvalidatedObjectException exc) { + forceClose(lock); + throw exc; + } + } catch (RemoteException exc) { + throw new UnexpectedRemoteException(exc); + } + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/exception/ExecutionNotPermittedException.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/exception/ExecutionNotPermittedException.java new file mode 100644 index 000000000..5bf583b75 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/exception/ExecutionNotPermittedException.java @@ -0,0 +1,25 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception; + +public class ExecutionNotPermittedException extends Exception { + public ExecutionNotPermittedException(String message, + Throwable cause, + boolean enableSuppression, + boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } + + public ExecutionNotPermittedException() { + } + + public ExecutionNotPermittedException(String message) { + super(message); + } + + public ExecutionNotPermittedException(String message, Throwable cause) { + super(message, cause); + } + + public ExecutionNotPermittedException(Throwable cause) { + super(cause); + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/exception/ExitRequest.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/exception/ExitRequest.java index 7c24621d2..753c926fd 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/exception/ExitRequest.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/exception/ExitRequest.java @@ -4,6 +4,8 @@ * This exception describes exit request. Used in interpreter. */ public class ExitRequest extends RuntimeException { + public static final int NORMAL_TERMINATION_CODE = 0; + private final int code; public ExitRequest(int code) { diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/exception/InvalidatedObjectException.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/exception/InvalidatedObjectException.java new file mode 100644 index 000000000..b8cf6bb54 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/exception/InvalidatedObjectException.java @@ -0,0 +1,22 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception; + +/** + * This exception occurs when object can be no longer used because of {@link + * ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.ValidityController} control. + */ +public class InvalidatedObjectException extends IllegalStateException { + public InvalidatedObjectException() { + } + + public InvalidatedObjectException(String s) { + super(s); + } + + public InvalidatedObjectException(String message, Throwable cause) { + super(message, cause); + } + + public InvalidatedObjectException(Throwable cause) { + super(cause); + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/exception/UnexpectedRemoteException.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/exception/UnexpectedRemoteException.java new file mode 100644 index 000000000..cecfcccba --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/exception/UnexpectedRemoteException.java @@ -0,0 +1,9 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception; + +import java.rmi.RemoteException; + +public class UnexpectedRemoteException extends RuntimeException { + public UnexpectedRemoteException(RemoteException exc) { + super(exc.getMessage(), exc.getCause()); + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/json/JSONComplexObject.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/json/JSONComplexObject.java new file mode 100644 index 000000000..bb21ad2e8 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/json/JSONComplexObject.java @@ -0,0 +1,24 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.json; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * This annotation indicates that objects of the annotated type contain JSON fields that need to be converted + * to json as parts of these objects. + * @author Phoenix + */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +public @interface JSONComplexObject { + /** + * If true, this object will not be converted to JSON directly. Its single field will be taken instead. + */ + boolean wrapper() default false; +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/json/JSONField.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/json/JSONField.java new file mode 100644 index 000000000..6225b618c --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/json/JSONField.java @@ -0,0 +1,22 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.json; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * This annotation indicates that a field is a part of JSON object structure and should be serialized. + * @author Phoenix + */ +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface JSONField { + /** + * Name of JSON field.
+ * If you do not specify this parameter or set it an empty string, + * the default name of the field in java code will be used. + * @see java.lang.reflect.Field#getName() + */ + String name() default ""; +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/json/JSONHelper.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/json/JSONHelper.java new file mode 100644 index 000000000..c0871ffd0 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/json/JSONHelper.java @@ -0,0 +1,34 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.json; + +/** + * Contains constants and useful methods for json package. + */ +final class JSONHelper { + static final char OPENING_CURLY_BRACE = '{'; + static final char CLOSING_CURLY_BRACE = '}'; + static final char OPENING_SQUARE_BRACE = '['; + static final char CLOSING_SQUARE_BRACE = ']'; + static final char ELEMENT_SEPARATOR = ','; + static final char KEY_VALUE_SEPARATOR = ':'; + static final char QUOTES = '\"'; + static final char ESCAPE_SYMBOL = '\\'; + static final String TRUE = "true"; + static final String FALSE = "false"; + static final String NULL = "null"; + static final String CYCLIC = "cyclic"; + + private JSONHelper() { + } + + static String escape(String s) { + s = s.replace("\\", "\\\\"); + s = s.replace("\"", "\\\""); + return s; + } + + static String unescape(String s) { + s = s.replace("\\\"", "\""); + s = s.replace("\\\\", "\\"); + return s; + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/json/JSONMaker.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/json/JSONMaker.java new file mode 100644 index 000000000..c7f220bfb --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/json/JSONMaker.java @@ -0,0 +1,196 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.json; + +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Utility; + +import java.lang.reflect.Array; +import java.lang.reflect.Field; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import static ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.json.JSONHelper.*; + +/** + * This class helps to construct a JSON string from any object using special annotations. + * @author Phoenix + * @see JSONComplexObject + * @see JSONField + */ +public final class JSONMaker { + private JSONMaker() { } + + /** + * Appends JSON interpretation of obj to the given string builder. + * @param sb + * string builder for JSON. + * @param obj + * object to convert to JSON. Can be null. + * @param name + * if the object is named, name is printed before its contents. If null, object is considered not + * named. + * @param identityMap + * map to put objects to solve problem of cyclic links. + * @throws IllegalArgumentException + * @throws IllegalAccessException + */ + private static void appendJSONString(final StringBuilder sb, + final Object obj, + final String name, + final IdentityHashMap identityMap) + throws IllegalArgumentException, IllegalAccessException { + if (name != null) { + sb.append(QUOTES).append(name).append(QUOTES).append(KEY_VALUE_SEPARATOR); + } + if (obj == null) { + sb.append(NULL); + return; + } + + // Checking cyclic links. + if (identityMap.containsKey(obj)) { + sb.append(CYCLIC); + return; + } + identityMap.put(obj, Boolean.TRUE); + + Class objClass = obj.getClass(); + + if (objClass.getAnnotation(JSONComplexObject.class) != null) { + // Convenience trick. + FieldJSONAppender jsonAppender = (field, overrideName) -> { + String fieldName = field.getAnnotation(JSONField.class).name(); + boolean accessible = field.isAccessible(); + field.setAccessible(true); + + if (overrideName != null && overrideName.isEmpty()) { + if (fieldName.isEmpty()) { + overrideName = field.getName(); + } else { + overrideName = fieldName; + } + } + + appendJSONString( + sb, field.get(obj), overrideName, identityMap); + field.setAccessible(accessible); + }; + + // Fields to convert to json. + List annotatedFields = Utility.getAllAnnotatedFields(objClass, JSONField.class); + if (annotatedFields.isEmpty()) { + throw new IllegalArgumentException( + "Illegal annotation @JSONComplexObject: there are no @JSONField annotated fields"); + } else if (objClass.getAnnotation(JSONComplexObject.class).wrapper()) { + if (annotatedFields.size() > 1) { + throw new IllegalArgumentException( + "Illegal annotation @JSONComplexObject: there are more then one @JSONField"); + } + jsonAppender.appendInfo(annotatedFields.get(0), name); + + } else { + sb.append(OPENING_CURLY_BRACE); + boolean comma = false; + + for (Field field : annotatedFields) { + if (comma) { + sb.append(ELEMENT_SEPARATOR); + } + comma = true; + jsonAppender.appendInfo(field); + } + sb.append(CLOSING_CURLY_BRACE); + } + } else if (obj instanceof Map) { + sb.append(OPENING_CURLY_BRACE); + Set set = ((Map) obj).entrySet(); + + for (Entry e : set) { + appendJSONString(sb, e.getValue(), e.getKey().toString(), identityMap); + } + + sb.append(CLOSING_CURLY_BRACE); + } else if (objClass.isArray() || obj instanceof Iterable) { + sb.append(OPENING_SQUARE_BRACE); + + if (obj instanceof Iterable) { + boolean comma = false; + for (Object piece : (Iterable) obj) { + if (comma) { + sb.append(ELEMENT_SEPARATOR); + } + comma = true; + appendJSONString(sb, piece, null, identityMap); + } + } else { + int length = Array.getLength(obj); + boolean comma = false; + for (int i = 0; i < length; i++) { + if (comma) { + sb.append(ELEMENT_SEPARATOR); + } + comma = true; + appendJSONString(sb, Array.get(obj, i), null, identityMap); + } + } + + sb.append(CLOSING_SQUARE_BRACE); + } else if (obj instanceof Number) { + sb.append(obj.toString()); + } else if (obj instanceof Boolean) { + sb.append(obj.toString()); + } else { + sb.append(QUOTES).append(JSONHelper.escape(obj.toString())).append(QUOTES); + } + + identityMap.remove(obj); + } + + /** + * Serializes an object using JSON-style.
+ * If cyclic link found, 'cyclic' is printed instead of cyclic description of the object.
+ *
    + *
  • Arrays and Iterables are supported
  • + *
  • Maps are supported (keys are treated as string names)
  • + *
  • Numbers and Strings are supported
  • + *
  • Complex objects are supported
  • + *
  • {@link ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.json.JSONComplexObject} instances + * are + * supported
  • + *
+ * Objects that do not conform to any of categories above are converted to strings. + * @param object + * object to serialize. + * @return JSON string. + * @throws RuntimeException + * {@link IllegalAccessException } can be wrapped in it. + * @see Iterable + * @see Object#toString() + * @see Number + * @see JSONComplexObject + * @see JSONField + */ + public static String makeJSON(Object object) throws RuntimeException { + try { + StringBuilder sb = new StringBuilder(); + appendJSONString(sb, object, null, new IdentityHashMap<>()); + return sb.toString(); + } catch (IllegalAccessException ex) { + throw new RuntimeException(ex); + } + } + + /** + * Convenience structure used in method {@link #appendJSONString(StringBuilder, Object, String, + * java.util.IdentityHashMap)}. + */ + @FunctionalInterface + private interface FieldJSONAppender { + void appendInfo(Field field, String overrideName) throws IllegalAccessException; + + default void appendInfo(Field field) throws IllegalAccessException { + appendInfo(field, ""); + } + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/json/JSONParsedObject.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/json/JSONParsedObject.java new file mode 100644 index 000000000..3d9d6b389 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/json/JSONParsedObject.java @@ -0,0 +1,143 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.json; + +import java.util.Map; + +/** + * An object decoded from a JSON string.
+ * Can represent standard array or associative array (Map) - then operations with Map or standard array + * (correspondently).
+ * Fields/elements of this object can be of the following types: + *
    + *
  • {@link Long}
  • + *
  • {@link Double}
  • + *
  • {@link Boolean}
  • + *
  • {@link String}
  • + *
  • {@link ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.json.JSONParsedObject}
  • + *
+ */ +public interface JSONParsedObject { + /** + * Set some field. + * @param name + * name of the field. + * @param value + * value of the field. Can be null. + * @throws UnsupportedOperationException + */ + void put(String name, Object value) throws UnsupportedOperationException; + + /** + * Set some element. + * @param index + * index of the element in array. + * @param value + * value of the element. Can be null. + * @throws UnsupportedOperationException + */ + void put(int index, Object value) throws UnsupportedOperationException; + + /** + * Retrieve some field's value (for standard objects) + * @param name + * name of the field + * @return null, {@link Integer}, {@link String}, {@link Double}, {@link Boolean} or {@link + * JSONParsedObject}. + * @throws UnsupportedOperationException + */ + Object get(String name) throws UnsupportedOperationException; + + /** + * Retrieve some element (for arrays) + * @param index + * index of the element + * @return null, {@link Integer}, {@link String}, {@link Double}, {@link Boolean} or {@link + * JSONParsedObject}. + * @throws UnsupportedOperationException + */ + Object get(int index) throws UnsupportedOperationException, ClassCastException; + + default Long getLong(int index) throws UnsupportedOperationException, ClassCastException { + return (Long) get(index); + } + + default Long getLong(String name) throws UnsupportedOperationException, ClassCastException { + return (Long) get(name); + } + + default String getString(String name) throws UnsupportedOperationException, ClassCastException { + return (String) get(name); + } + + default Double getDouble(int index) throws UnsupportedOperationException, ClassCastException { + return (Double) get(index); + } + + default Double getDouble(String name) throws UnsupportedOperationException, ClassCastException { + return (Double) get(name); + } + + default String getString(int index) throws UnsupportedOperationException, ClassCastException { + return (String) get(index); + } + + default Boolean getBoolean(String name) throws UnsupportedOperationException, ClassCastException { + return (Boolean) get(name); + } + + default Boolean getBoolean(int index) throws UnsupportedOperationException, ClassCastException { + return (Boolean) get(index); + } + + default JSONParsedObject getObject(String name) throws UnsupportedOperationException, ClassCastException { + return (JSONParsedObject) get(name); + } + + default JSONParsedObject getObject(int index) throws UnsupportedOperationException, ClassCastException { + return (JSONParsedObject) get(index); + } + + /** + * Returns true if represented object contains field with the given name. + * @param name + * field name. + */ + boolean containsField(String name) throws UnsupportedOperationException; + + /** + * Returns true if the represented object is an array, false otherwise. + */ + boolean isStandardArray(); + + /** + * Performs a chain of {@link #get(String) } and {@link #get(int) } methods on the object structure. + * @param namePieces + * consists of Strings and Integers. Each name piece causes associated object retrieval. + * @return null, {@link Integer}, {@link String}, {@link Double}, {@link Boolean} or {@link + * JSONParsedObject}. + */ + Object deepGet(Object... namePieces); + + /** + * Returns count of fields/elements stored in this object/array. + */ + int size(); + + /** + * Returns this object as map. Unsupported for standard array respresentations. + * @throws UnsupportedOperationException + */ + Map asMap() throws UnsupportedOperationException; + + /** + * Returns this object as array. Unsupported for associative array representations. + * @throws UnsupportedOperationException + */ + Object[] asArray() throws UnsupportedOperationException; + + /** + * Returns string representation of this object (not in JSON format).
+ * Depending on the object method {@link java.util.Arrays#toString(boolean[])} or {@link + * java.util.Map#toString()} is used. + */ + String toString(); +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/json/JSONParser.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/json/JSONParser.java new file mode 100644 index 000000000..42019e675 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/json/JSONParser.java @@ -0,0 +1,591 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.json; + +import java.text.ParseException; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import static ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.json.JSONHelper.*; + +/** + * Powerful class that lets you parse json strings and pretend them as {@link + * ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.json.JSONParsedObject}.
+ * Note that cyclic links are not parsed! + */ +public final class JSONParser { + private JSONParser() { + } + + private static List findTokens(String s, int begin, int end) throws ParseException { + boolean inQuotes = false; + + /* + * get: indicates that the symbol at current (index) position is 100% not escaped/escaped + * set: indicates that the symbol at next (index + 1) position is 100% not escaped/escaped + */ + boolean noEscape = true; + + List tokens = new LinkedList<>(); + + // Depth level. + int level = 0; + + for (int index = begin; index < end; index++) { + switch (s.charAt(index)) { + case QUOTES: { + // True quotes. + // if (noEscape || (index > begin && s.charAt(index - 1) != ESCAPE_SYMBOL)) { + if (noEscape) { + tokens.add(new Token(TokenType.QUOTES, index, level)); + inQuotes = !inQuotes; + } else { + // Escaped quotes. + noEscape = true; + } + break; + } + case ESCAPE_SYMBOL: { + if (!inQuotes) { + throw new ParseException("Unexpected symbol " + ESCAPE_SYMBOL + " in " + s, index); + } + + noEscape = !noEscape; + break; + } + case OPENING_SQUARE_BRACE: { + noEscape = true; + if (!inQuotes) { + tokens.add(new Token(TokenType.ARRAY_START, index, level)); + level++; + } + break; + } + case CLOSING_SQUARE_BRACE: { + noEscape = true; + if (!inQuotes) { + level--; + tokens.add(new Token(TokenType.ARRAY_END, index, level)); + } + break; + } + case OPENING_CURLY_BRACE: { + noEscape = true; + if (!inQuotes) { + tokens.add(new Token(TokenType.RECORD_START, index, level)); + level++; + } + break; + } + case CLOSING_CURLY_BRACE: { + noEscape = true; + if (!inQuotes) { + level--; + tokens.add(new Token(TokenType.RECORD_END, index, level)); + } + break; + } + case KEY_VALUE_SEPARATOR: { + noEscape = true; + if (!inQuotes) { + tokens.add(new Token(TokenType.KEY_VALUE_SPLITTER, index, level)); + } + break; + } + case ELEMENT_SEPARATOR: { + noEscape = true; + if (!inQuotes) { + tokens.add(new Token(TokenType.ELEMENT_SEPARATOR, index, level)); + } + break; + } + default: { + noEscape = true; + break; + } + } + } + + if (inQuotes) { + throw new ParseException("Unclosed quotes", -1); + } + + return tokens; + } + + private static List getElementSeparators(ArrayList tokens, int startTokenIndex) { + Token start = tokens.get(startTokenIndex); + int searchLevel = start.level + 1; + + List elementSeparators = new LinkedList<>(); + + for (int i = startTokenIndex + 1, len = tokens.size(); i < len; i++) { + Token t = tokens.get(i); + if (t.level == searchLevel && t.type == TokenType.ELEMENT_SEPARATOR) { + elementSeparators.add(i); + } + if (t.level < searchLevel) { + return elementSeparators; + } + } + return elementSeparators; + } + + private static Object jsonObjectDeepGet(JSONParsedObject source, Object... pathPieces) + throws IllegalArgumentException { + Object current = source; + for (int i = 0; i < pathPieces.length; i++) { + if (!(current instanceof JSONParsedObject)) { + throw new IllegalArgumentException( + "Path piece #" + i + " cannot be applied to simple object"); + } + if (pathPieces[i] instanceof String) { + current = ((JSONParsedObject) current).get((String) pathPieces[i]); + } else if (pathPieces[i] instanceof Integer) { + current = ((JSONParsedObject) current).get((Integer) pathPieces[i]); + } else { + throw new IllegalArgumentException("Path pieces can only be strings or integers"); + } + } + return current; + } + + /** + * Parses the given json string and constructs {@link ru.fizteh.fivt.students.fedorov_andrew + * .databaselibrary.json.JSONParsedObject}.
+ * You can use escape symbol '\' to escape quotes and this symbol itself. Applying escape symbol to any + * other symbol does not affect anything. + * @param json + * json string. Must have root element: object or array. + * @return constructed object. Cannot be null. + * @throws java.text.ParseException + */ + public static JSONParsedObject parseJSON(final String json) throws ParseException { + ArrayList tokens = new ArrayList<>(findTokens(json, 0, json.length())); + // System.err.println(Arrays.toString(tokens.toArray())); + + ArrayDeque dq = new ArrayDeque<>(); + + List rootElementSeparators = getElementSeparators(tokens, 0); + rootElementSeparators.add(0, 0); + rootElementSeparators.add(tokens.size() - 1); + + dq.addLast( + new ParsingObject( + null, tokens.get(0).type == TokenType.ARRAY_START, rootElementSeparators)); + + JSONParsedObject root = null; + + while (!dq.isEmpty()) { + // Get currently parsed object. Through this iteration we will parse on of its fields/elements. + // It is like dfs but plain :) + ParsingObject currentlyParsingObject = dq.getLast(); + + // Object building over -> go one level higher. + if (currentlyParsingObject.currentElementSeparatorID + 1 >= currentlyParsingObject.dataSplitters + .size()) { + // Polling this object. It is parsed now!. + dq.pollLast(); + if (dq.isEmpty()) { + // Congratulations, we have parsed the root object. + root = currentlyParsingObject.object; + } else { + // We have parsed some complex object, but it is not over! We must assign it to its + // parent and continue with parsing the parent. + ParsingObject parent = dq.getLast(); + parent.putSafely(currentlyParsingObject.name, currentlyParsingObject.object); + } + continue; + } + + // Indices in 'tokens' array of this element boundaries. + int dataStartTokenID = currentlyParsingObject.dataSplitters + .get(currentlyParsingObject.currentElementSeparatorID); + int dataEndTokenID = currentlyParsingObject.dataSplitters + .get(currentlyParsingObject.currentElementSeparatorID + 1); + currentlyParsingObject.currentElementSeparatorID++; + + /* + * dataStartTokenID + valueOffset = first possible value token. + */ + int valueOffset = 1; + /* + * Object/Field name. It will be null, if we are in array now. + */ + String key = null; + + // If object is not array and not empty -> there must be name! + if (!currentlyParsingObject.object.isStandardArray() && dataStartTokenID + 1 < dataEndTokenID) { + valueOffset = 4; + int nameOpeningQuotesID = dataStartTokenID + 1; + int nameClosingQuotesID = nameOpeningQuotesID + 1; + if (nameClosingQuotesID >= tokens.size()) { + throw new ParseException("Quotes for field name are not found", dataStartTokenID + 1); + } + + Token nameOpeningQuotes = tokens.get(nameOpeningQuotesID); + Token nameClosingQuotes = tokens.get(nameClosingQuotesID); + if (nameOpeningQuotes.type != TokenType.QUOTES + || nameClosingQuotes.type != TokenType.QUOTES) { + throw new ParseException( + "Quotes are expected in positions " + + nameOpeningQuotes.index + + ", " + + nameClosingQuotes.index + + ", but found " + + nameOpeningQuotes.type + + " and " + + nameClosingQuotes.type, nameOpeningQuotes.index); + } + + //name, if this is not array + key = json.substring(nameOpeningQuotes.index + 1, nameClosingQuotes.index); + + int keyValueSplitterIndex = nameClosingQuotesID + 1; + if (keyValueSplitterIndex >= tokens.size()) { + throw new ParseException( + "Name-value splitter symbol not found after name", nameClosingQuotes.index + 1); + } + + Token keyValueSplitter = tokens.get(keyValueSplitterIndex); + if (keyValueSplitter.type != TokenType.KEY_VALUE_SPLITTER) { + throw new ParseException( + "Key-value splitter is expected, but found " + keyValueSplitter.type, + keyValueSplitter.index); + } + } + + int valueStartTokenID = dataStartTokenID + valueOffset; + int valueEndTokenID = dataEndTokenID - 1; + + Token valueStartToken = tokens.get(valueStartTokenID); + Token valueEndToken = tokens.get(valueEndTokenID); + + // No value tokens. expected type of value: null, number, boolean. + if (valueStartTokenID == dataEndTokenID) { + int valueStartIndex = valueEndToken.index + 1; + int valueEndIndex = tokens.get(dataEndTokenID).index; + + Object value; + String valueString = json.substring(valueStartIndex, valueEndIndex).trim().toLowerCase(); + boolean doPut = true; + + switch (valueString) { + case "": { + doPut = false; + value = null; + break; + } + case NULL: { + value = null; + break; + } + case TRUE: { + value = Boolean.TRUE; + break; + } + case FALSE: { + value = Boolean.FALSE; + break; + } + default: { + if (valueString.contains(".")) { + value = Double.parseDouble(valueString); + } else { + value = Long.parseLong(valueString); + } + break; + } + } + + if (doPut) { + currentlyParsingObject.putSafely(key, value); + } + } else if (valueStartToken.type == TokenType.QUOTES && valueEndToken.type == TokenType.QUOTES) { + // Quotes. Expected type of value: string. + if (valueStartTokenID + 1 != valueEndTokenID) { + Token extraToken = tokens.get(valueStartTokenID + 1); + throw new ParseException( + "Extra token between positions " + + valueStartToken.index + + " and " + + valueEndToken.index + + ": " + + extraToken.type, extraToken.index); + } + int valueStart = tokens.get(valueStartTokenID).index + 1; + int valueEnd = tokens.get(valueEndTokenID).index; + + String value = unescape(json.substring(valueStart, valueEnd)); + currentlyParsingObject.putSafely(key, value); + } else { + // Some complex data: record or array. + // Checking tokens are coupled and correct. + switch (valueStartToken.type) { + case ARRAY_START: { + if (valueEndToken.type != TokenType.ARRAY_END) { + throw new ParseException( + "Bad closing token for array: " + valueEndToken.type, valueEndToken.index); + } + break; + } + case RECORD_START: { + if (valueEndToken.type != TokenType.RECORD_END) { + throw new ParseException( + "Bad closing token for record: " + valueEndToken.type, valueEndToken.index); + } + break; + } + default: { + throw new ParseException( + "Bad record/array opening token: " + valueStartToken.type, valueStartToken.index); + } + } + + List elementSeparators = getElementSeparators(tokens, valueStartTokenID); + elementSeparators.add(0, valueStartTokenID); + elementSeparators.add(valueEndTokenID); + + ParsingObject next = new ParsingObject( + key, tokens.get(valueStartTokenID).type == TokenType.ARRAY_START, elementSeparators); + dq.addLast(next); + } + } + + return root; + } + + private static enum TokenType { + KEY_VALUE_SPLITTER, + ELEMENT_SEPARATOR, + RECORD_START, + RECORD_END, + ARRAY_START, + ARRAY_END, + QUOTES, + } + + private static class Token { + final TokenType type; + final int index; + /** + * Host object depth in structure. + */ + final int level; + + public Token(TokenType type, int index, int level) { + this.type = type; + this.index = index; + this.level = level; + } + + @Override + public String toString() { + return String.format("{type = %s, index = %d, level = %d}", type, index, level); + } + } + + @JSONComplexObject(wrapper = true) + private static class MapObject implements JSONParsedObject { + @JSONField + private final Map map; + + public MapObject(int size) { + map = new HashMap<>(size); + } + + @Override + public void put(String key, Object value) { + map.put(key, value); + } + + @Override + public void put(int index, Object value) throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + + @Override + public Object get(String name) { + if (!map.containsKey(name)) { + throw new IllegalArgumentException("Field missing: " + name); + } + return map.get(name); + } + + @Override + public Object get(int index) { + throw new UnsupportedOperationException("This operation not supported in associative arrays"); + } + + @Override + public boolean isStandardArray() { + return false; + } + + @Override + public boolean containsField(String name) { + return map.containsKey(name); + } + + @Override + public Object deepGet(Object... namePieces) { + return jsonObjectDeepGet(this, namePieces); + } + + @Override + public int size() { + return map.size(); + } + + @Override + public Map asMap() { + return map; + } + + @Override + public Object[] asArray() { + throw new UnsupportedOperationException("Not supported in associative arrays"); + } + + @Override + public String toString() { + return map.toString(); + } + } + + @JSONComplexObject(wrapper = true) + private static class ArrayObject implements JSONParsedObject { + @JSONField + private final List array; + + public ArrayObject(int length) { + this.array = new ArrayList<>(length); + } + + @Override + public void put(String key, Object value) throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + + @Override + public void put(int index, Object value) throws UnsupportedOperationException { + if (index == array.size()) { + array.add(value); + } else if (index < array.size()) { + array.set(index, value); + } else { + throw new IndexOutOfBoundsException( + "Index too big: " + index + ", expected (max): " + array.size()); + } + } + + @Override + public Object get(String key) { + throw new UnsupportedOperationException("This operation not supported in standard arrays"); + } + + @Override + public Object get(int index) { + return array.get(index); + } + + @Override + public boolean containsField(String name) throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isStandardArray() { + return true; + } + + @Override + public Object deepGet(Object... namePieces) { + return jsonObjectDeepGet(this, namePieces); + } + + @Override + public int size() { + return array.size(); + } + + @Override + public String toString() { + return array.stream().reduce((a, b) -> a.toString() + "," + b.toString()).get().toString(); + } + + @Override + public Map asMap() { + throw new UnsupportedOperationException("Not supported in standard arrays"); + } + + @Override + public Object[] asArray() { + return array.toArray(); + } + } + + /** + * Represents json object in stage of parsing. + */ + private static class ParsingObject { + /** + * Incomplete object. Some fields/elements can be not set. + */ + final JSONParsedObject object; + /** + * begin token index + data splitters indices + end token index + */ + final List dataSplitters; + /** + * Name of the object. + */ + final String name; + /** + * Index of element separator after which we are now. + */ + int currentElementSeparatorID; + /** + * Index of first not set element (in case of array). + */ + int index; + + /** + * @param name + * name of object + * @param isArray + * if the object represents a standard array or not. + * @param dataSplitters + * list of data splitting tokens + */ + public ParsingObject(String name, boolean isArray, List dataSplitters) { + this.name = name; + this.dataSplitters = dataSplitters; + this.currentElementSeparatorID = 0; + this.index = 0; + + if (isArray) { + this.object = new ArrayObject(this.dataSplitters.size() - 1); + } else { + this.object = new MapObject(this.dataSplitters.size() - 1); + } + } + + /** + * Put the value to named field of the object (in case of map) or set next element (in case of + * array). + */ + void putSafely(String key, Object value) { + if (object.isStandardArray()) { + if (key != null) { + throw new IllegalArgumentException("In case of array key must be null"); + } + object.put(index++, value); + } else { + object.put(key, value); + } + } + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/parallel/ControllableAgent.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/parallel/ControllableAgent.java new file mode 100644 index 000000000..b31d70799 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/parallel/ControllableAgent.java @@ -0,0 +1,14 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.parallel; + +/** + * Interface that lets the controllable runnable to notify all waiting threads that the pause has come and + * wait until any of the threads decides whether to continue or interrupt the execution of the runnable. + */ +@FunctionalInterface +public interface ControllableAgent { + /** + * Call this method if you want to make a pause. + * @throws InterruptedException + */ + void notifyAndWait() throws InterruptedException; +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/parallel/ControllableRunnable.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/parallel/ControllableRunnable.java new file mode 100644 index 000000000..2af52b2c4 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/parallel/ControllableRunnable.java @@ -0,0 +1,51 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.parallel; + +/** + * Base class for runnables served by this runner. + */ +public abstract class ControllableRunnable implements Runnable, ControllableAgent, ExceptionFreeRunnable { + private final ControllableRunner host; + + private volatile Throwable exception; + + public ControllableRunnable(ControllableRunner host) { + this.host = host; + } + + @Override + public final void run() { + synchronized (this) { + exception = null; + } + try { + runWithFreedom(this::notifyAndWait); + } catch (Exception | AssertionError exc) { + synchronized (this) { + exception = exc; + } + } + } + + /** + * Call this method after execution finishes. If an {@link Exception} or {@link + * AssertionError} has occurred during execution, it will + * be rethrown. + * @throws Exception + */ + public final synchronized void checkException() throws Exception, AssertionError { + if (exception != null) { + if (exception instanceof Exception) { + throw (Exception) exception; + } else if (exception instanceof AssertionError) { + throw (AssertionError) exception; + } else { + throw new Error("Some fatal exception occurred during thread execution", exception); + } + } + } + + @Override + public final void notifyAndWait() throws InterruptedException { + host.onControllablePause(this); + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/parallel/ControllableRunner.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/parallel/ControllableRunner.java new file mode 100644 index 000000000..663797e0d --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/parallel/ControllableRunner.java @@ -0,0 +1,226 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.parallel; + +/** + * Runnable that consumes some collections of runnables that must be executed sequentially and executes them + * so that you can track whether some part has been executed or not. + */ +public class ControllableRunner implements Runnable { + private static final int ORDER_NOT_SET = -1; + private static final int ORDER_TERMINATE = 0; + private static final int ORDER_CONTINUE = 1; + + private static final int STATUS_NOT_STARTED = -1; + private static final int STATUS_FINISHED = 0; + private static final int STATUS_STARTED = 1; + + private volatile ControllableRunnable runnable; + + /** + * Order from the observer: continue or terminate. Can be also not set. + */ + private volatile int order; + + /** + * Current status: not started/started/finished. + */ + private volatile int status; + + /** + * Creates a new instance of this runner. + */ + public ControllableRunner() { + order = ORDER_TERMINATE; + status = STATUS_NOT_STARTED; + } + + /** + * Assign the given runnable to execute it once. You can perform this action if you have never assigned + * runnable to this runner before or the last assigned runnable has finished its execution. + * @throws IllegalStateException + * If you cannot assign any runnables now. + */ + public synchronized void assignRunnable(ControllableRunnable runnable) throws IllegalStateException { + if (status == STATUS_STARTED) { + throw new IllegalStateException("Runnable has been already assigned and has not been finished"); + } + this.runnable = runnable; + status = STATUS_NOT_STARTED; + } + + /** + * Get the currently assigned controllable runnable. + */ + public ControllableRunnable getRunnable() { + return runnable; + } + + /** + * Creates and attempts to assign the alternate-kind runnable. + * @param runnable + * Alternate kind of runnable. + */ + public synchronized ControllableRunnable createAndAssign(ExceptionFreeRunnable runnable) { + ControllableRunnable controllable = createControllable(runnable); + assignRunnable(controllable); + return controllable; + } + + /** + * @throws IllegalStateException + * You can call run() only if status = unstarted. + */ + @Override + public synchronized void run() throws IllegalStateException { + checkRunnableAssigned(); + + if (status != STATUS_NOT_STARTED) { + throw new IllegalStateException("Can run only if status is: not started"); + } + + status = STATUS_STARTED; + order = ORDER_CONTINUE; + + // System.err.println("Starting"); + try { + runnable.run(); + } finally { + // System.err.println("Finishing"); + status = STATUS_FINISHED; + order = ORDER_TERMINATE; + notifyAll(); + } + } + + /** + * Creates a controllable runnable that will work this this runner. + * @param runnable + * alternate form of runnable - function that takes notification agent as an argument. + */ + public ControllableRunnable createControllable(ExceptionFreeRunnable runnable) { + return new ControllableRunnable(this) { + @Override + public void runWithFreedom(ControllableAgent agent) throws Exception { + runnable.runWithFreedom(agent); + } + }; + } + + /** + * Call this method if you want to wait until the next pause and play the role of observer.
+ * If there are no checkpoints expected in future, this method waits until execution ends. + * @throws InterruptedException + * @throws Exception + * If thrown during runnable execution. + * @throws AssertionError + * If thrown during runnable execution. + */ + public synchronized void waitUntilPause() throws InterruptedException, Exception, AssertionError { + // System.err.println(hashCode() + ": waiting until pause"); + while (order != ORDER_NOT_SET && status != STATUS_FINISHED) { + wait(); + } + runnable.checkException(); + } + + /** + * Call this method if you want to wait until execution ends. Returns immediately if the runnable is not + * set. All pauses that can be met in the future will be ignored with order {@link #continueWork()}.
+ * throws exception. + * @throws InterruptedException + * @throws Exception + * If thrown during runnable execution. + * @throws AssertionError + * If thrown during runnable execution. + */ + public synchronized void waitUntilEndOfWork() throws InterruptedException, Exception, AssertionError { + while (status != STATUS_FINISHED) { + if (order == ORDER_NOT_SET) { + continueWork(); + } + wait(); + } + runnable.checkException(); + } + + /** + * This method is called by {@link ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test.support + * .support.parallel.ControllableRunnable} + * when it wants to pause and wait until the observer decides whether to continue work or interrupt. + * @throws InterruptedException + */ + synchronized void onControllablePause(ControllableRunnable pausingRunnable) throws InterruptedException { + checkStatusIsStarted(); + if (runnable != pausingRunnable) { + throw new IllegalStateException( + "Controllable runner can handle only one runnable at a time. Do not execute more than " + + "one runnable assigned to the same controllable runner in parallel."); + } + + order = ORDER_NOT_SET; + notifyAll(); + while (order == ORDER_NOT_SET) { + wait(); + } + if (order == ORDER_TERMINATE) { + throw new InterruptedException("Terminated after pause because of user command."); + } + + // For all runners that want to wait did really wait + order = ORDER_NOT_SET; + } + + synchronized boolean isRunnableAssigned() { + return runnable != null; + } + + private synchronized void checkRunnableAssigned() { + if (!isRunnableAssigned()) { + throw new IllegalStateException("Runnable has not been assigned"); + } + } + + private synchronized void checkStatusIsStarted() throws IllegalStateException { + if (status != STATUS_STARTED) { + throw new IllegalStateException("Can perform this only if status = started"); + } + } + + /** + * Call this method after waiting in {@link #waitUntilPause()} to tell the runnable to continue work. + * @throws IllegalStateException + * If execution has finished. + */ + public synchronized void continueWork() throws IllegalStateException { + checkRunnableAssigned(); + checkStatusIsStarted(); + + // System.err.println(hashCode() + ": continue work"); + + if (order == ORDER_NOT_SET) { + order = ORDER_CONTINUE; + notifyAll(); + } else { + throw new IllegalStateException( + "Cannot manage the runnable now, because it is not in paused state."); + } + } + + /** + * Call this method after waiting in {@link #waitUntilPause()} to tell the runnable to interrupt work. + * @throws IllegalStateException + * If execution has finished. + */ + public synchronized void interruptWork() throws IllegalStateException { + checkRunnableAssigned(); + checkStatusIsStarted(); + + if (order == ORDER_NOT_SET) { + order = ORDER_TERMINATE; + notifyAll(); + } else { + throw new IllegalStateException( + "Cannot manage the runnable now, because it is not in paused state."); + } + } + +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/parallel/ExceptionFreeRunnable.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/parallel/ExceptionFreeRunnable.java new file mode 100644 index 000000000..f5a9c7c53 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/parallel/ExceptionFreeRunnable.java @@ -0,0 +1,15 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.parallel; + +/** + * Interface for runnable that can throw any exceptions. + */ +@FunctionalInterface +public interface ExceptionFreeRunnable { + /** + * Place your implementation here and do not care of exceptions. The given throwables will be caught and + * become accessible. + * @throws Exception + * @throws AssertionError + */ + void runWithFreedom(ControllableAgent agent) throws Exception, AssertionError; +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/servlet/HttpDBServer.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/servlet/HttpDBServer.java new file mode 100644 index 000000000..ed2c91458 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/servlet/HttpDBServer.java @@ -0,0 +1,262 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.servlet; + +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import ru.fizteh.fivt.storage.structured.Storeable; +import ru.fizteh.fivt.storage.structured.Table; +import ru.fizteh.fivt.storage.structured.TableProvider; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.servlet.transactions.Transaction; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.servlet.transactions.TransactionPool; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.net.InetSocketAddress; + +public class HttpDBServer { + public static final String PARAM_TABLE = "table"; + public static final String PARAM_TRANSACTION_ID = "tid"; + public static final String FIELD_DIFF = "diff"; + public static final String PARAM_KEY = "key"; + public static final String PARAM_VALUE = "value"; + private static final int TRANSACTION_ID_UPPER_BOUND = 100000; + /** + * Default value: 10 minutes. + */ + private static final long TRANSACTION_TIME_TO_LIVE = 10 * 60 * 1000L; + private final TableProvider localProvider; + private Server httpServer; + private volatile TransactionPool transactionPool; + + public HttpDBServer(TableProvider localProvider) { + this.localProvider = localProvider; + } + + public void startHttpServer(String host, int port) throws Exception { + if (isStarted()) { + throw new IllegalStateException("HttpServer is already initialized"); + } + + transactionPool = new TransactionPool(TRANSACTION_ID_UPPER_BOUND, TRANSACTION_TIME_TO_LIVE); + + httpServer = new Server(new InetSocketAddress(host, port)); + ServletContextHandler handler = new ServletContextHandler(ServletContextHandler.NO_SESSIONS); + + // Adding servlets. + handler.addServlet(new ServletHolder(new BeginServlet()), "/begin"); + handler.addServlet(new ServletHolder(new CommitServlet()), "/commit"); + handler.addServlet(new ServletHolder(new RollbackServlet()), "/rollback"); + handler.addServlet(new ServletHolder(new GetServlet()), "/get"); + handler.addServlet(new ServletHolder(new PutServlet()), "/put"); + handler.addServlet(new ServletHolder(new SizeServlet()), "/size"); + + httpServer.setHandler(handler); + + try { + httpServer.start(); + } catch (Exception exc) { + transactionPool.closePool(); + transactionPool = null; + httpServer = null; + throw exc; + } + } + + public void stopHttpServerIfStarted() throws Exception { + if (isStarted()) { + stopHttpServer(); + } + } + + public void stopHttpServer() throws Exception { + if (!isStarted()) { + throw new IllegalStateException("HttpServer not initialized"); + } + + try { + httpServer.stop(); + } finally { + transactionPool.closePool(); + httpServer = null; + transactionPool = null; + } + } + + public boolean isStarted() { + return httpServer != null; + } + + class BadRequestException extends Exception { + public BadRequestException(String message) { + super(message); + } + } + + abstract class BaseDBServlet extends HttpServlet { + protected abstract void serveGet(HttpServletRequest request, HttpServletResponse response) + throws Exception; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + try { + serveGet(req, resp); + } catch (BadRequestException exc) { + resp.reset(); + resp.setStatus(HttpServletResponse.SC_BAD_REQUEST); + resp.getWriter().println(exc.getMessage()); + } catch (Exception exc) { + resp.reset(); + resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + resp.getWriter().println(exc.getMessage()); + } + } + } + + class SizeServlet extends BaseDBServlet { + @Override + protected void serveGet(HttpServletRequest request, HttpServletResponse response) throws Exception { + int transactionID = Integer.parseInt(request.getParameter(PARAM_TRANSACTION_ID)); + Transaction
transaction = transactionPool.obtainTransaction(transactionID); + try { + transaction.executeAction( + () -> { + int size = transaction.getExtraData().size(); + response.getWriter().print(size + ""); + return null; + }); + } finally { + transactionPool.releaseTransaction(transaction); + } + } + } + + class PutServlet extends BaseDBServlet { + @Override + protected void serveGet(HttpServletRequest request, HttpServletResponse response) throws Exception { + int transactionID = Integer.parseInt(request.getParameter(PARAM_TRANSACTION_ID)); + Transaction
transaction = transactionPool.obtainTransaction(transactionID); + try { + transaction.executeAction( + () -> { + String key = request.getParameter(PARAM_KEY); + String serializedNewValue = request.getParameter(PARAM_VALUE); + + // Deserializing new value. + Storeable storeableNewValue = + localProvider.deserialize(transaction.getExtraData(), serializedNewValue); + + // Putting new value instead of old. + Storeable storeableOldValue = + transaction.getExtraData().put(key, storeableNewValue); + + // This key did not exist before. + if (storeableOldValue == null) { + throw new BadRequestException("Key not found: " + key); + } + // Now we must serialize old value. + String serializedOldValue = + localProvider.serialize(transaction.getExtraData(), storeableOldValue); + response.getWriter().print(serializedOldValue); + return null; + }); + + } finally { + transactionPool.releaseTransaction(transaction); + } + } + } + + class GetServlet extends BaseDBServlet { + @Override + protected void serveGet(HttpServletRequest request, HttpServletResponse response) throws Exception { + int transactionID = Integer.parseInt(request.getParameter(PARAM_TRANSACTION_ID)); + Transaction
transaction = transactionPool.obtainTransaction(transactionID); + try { + transaction.executeAction( + () -> { + String key = request.getParameter(PARAM_KEY); + Storeable storeableValue = transaction.getExtraData().get(key); + // Not found + if (storeableValue == null) { + throw new BadRequestException("Key not found: " + key); + } + // Now we must serialize it. + String serializedValue = + localProvider.serialize(transaction.getExtraData(), storeableValue); + response.getWriter().print(serializedValue); + return null; + }); + } finally { + transactionPool.releaseTransaction(transaction); + } + } + } + + class CommitServlet extends BaseDBServlet { + @Override + protected void serveGet(HttpServletRequest request, HttpServletResponse response) throws Exception { + int transactionID = Integer.parseInt(request.getParameter(PARAM_TRANSACTION_ID)); + Transaction
transaction = transactionPool.obtainTransaction(transactionID); + try { + transaction.executeAction( + () -> { + int diff = transaction.getExtraData().commit(); + response.getWriter().print(String.format("%s=%d", FIELD_DIFF, diff)); + return null; + }); + } finally { + transactionPool.killTransaction(transaction); + } + } + } + + class RollbackServlet extends BaseDBServlet { + @Override + protected void serveGet(HttpServletRequest request, HttpServletResponse response) throws Exception { + int transactionID = Integer.parseInt(request.getParameter(PARAM_TRANSACTION_ID)); + Transaction
transaction = transactionPool.obtainTransaction(transactionID); + try { + transaction.executeAction( + () -> { + int diff = transaction.getExtraData().rollback(); + response.getWriter().print(String.format("%s=%d", FIELD_DIFF, diff)); + return null; + }); + } finally { + transactionPool.killTransaction(transaction); + } + } + } + + class BeginServlet extends BaseDBServlet { + @Override + protected void serveGet(HttpServletRequest request, HttpServletResponse response) throws Exception { + String tableName = request.getParameter(PARAM_TABLE); + + // exception can occur here + Table table = localProvider.getTable(tableName); + if (table == null) { + throw new IllegalArgumentException("Table " + tableName + " not exists"); + } + + Transaction
transaction = transactionPool.newTransaction(); + try { + transaction.setExtraData(table); + transaction.executeAction( + () -> { + int transactionID = transaction.getTransactionID(); + response.getWriter() + .print(String.format("%s=%05d", PARAM_TRANSACTION_ID, transactionID)); + return null; + }); + } finally { + transactionPool.releaseTransaction(transaction); + } + } + + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/servlet/HttpDBServerCommands.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/servlet/HttpDBServerCommands.java new file mode 100644 index 000000000..5b45e76ed --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/servlet/HttpDBServerCommands.java @@ -0,0 +1,94 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.servlet; + +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.DatabaseIOException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.TerminalException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.AbstractCommand; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Command; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.SimpleCommandContainer; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.SingleDatabaseShellState; + +import java.util.Map; + +/** + * Container for server commands: start, stop. + */ +public class HttpDBServerCommands extends SimpleCommandContainer { + public static final Command STOPHTTP = + new AbstractCommand("stophttp", "", "stops http server", 1) { + @Override + public void executeSafely(HttpDBServerState state, String[] args) throws Exception { + int port = state.stopHttpServer(); + state.getOutputStream().println("stopped at " + port); + } + }; + public static final Command EXIT = new AbstractCommand( + "exit", "", "stops http server (if it is started) and closes the terminal", 1) { + @Override + public void execute(HttpDBServerState state, String[] args) throws TerminalException { + state.prepareToExit(0); + + // If all contracts are honoured, this line should not be reached. + throw new AssertionError("Exit request not thrown"); + } + + @Override + public void executeSafely(HttpDBServerState state, String[] args) throws Exception { + // Not used. + } + }; + public static final Command HELP = new AbstractCommand( + "help", "", "prints out description of state commands", 1, Integer.MAX_VALUE) { + @Override + public void execute(HttpDBServerState state, String[] args) { + Map> commands = state.getCommands(); + + state.getOutputStream().println( + "You can start http database server ready for new connections!"); + + state.getOutputStream().println( + String.format( + "You can set database directory to work with using environment " + + "variable '%s'", SingleDatabaseShellState.DB_DIRECTORY_PROPERTY_NAME)); + + for (Command command : commands.values()) { + state.getOutputStream().println(command.buildHelpLine()); + } + } + + @Override + public void executeSafely(HttpDBServerState state, String[] args) throws DatabaseIOException { + // not used + } + }; + private static final int DEFAULT_PORT = 8080; + public static final Command STARTHTTP = new AbstractCommand( + "starthttp", + "[port]", + "starts http server at the specified port (or, if not specified, at " + DEFAULT_PORT + ")", + 1, + 2) { + @Override + public void executeSafely(HttpDBServerState state, String[] args) throws Exception { + int port; + if (args.length == 1) { + port = DEFAULT_PORT; + } else { + port = Integer.parseInt(args[1]); + } + + state.startHttpServer(port); + state.getOutputStream().println("started at " + port); + } + }; + private static final HttpDBServerCommands INSTANCE = new HttpDBServerCommands(); + + /** + * Not for initializing. + */ + private HttpDBServerCommands() { + } + + public static HttpDBServerCommands obtainInstance() { + return INSTANCE; + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/servlet/HttpDBServerState.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/servlet/HttpDBServerState.java new file mode 100644 index 000000000..6e9c3535b --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/servlet/HttpDBServerState.java @@ -0,0 +1,48 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.servlet; + +import ru.fizteh.fivt.storage.structured.TableProvider; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.BaseShellState; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Command; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Log; + +import java.util.Map; + +public class HttpDBServerState extends BaseShellState { + private final HttpDBServer server; + + private int port = -1; + + public HttpDBServerState(TableProvider localProvider) { + server = new HttpDBServer(localProvider); + } + + public void startHttpServer(int port) throws Exception { + server.startHttpServer("localhost", port); + this.port = port; + } + + public int stopHttpServer() throws Exception { + server.stopHttpServer(); + int oldPort = port; + port = -1; + return oldPort; + } + + public boolean isStarted() { + return server.isStarted(); + } + + @Override + public void cleanup() { + try { + server.stopHttpServerIfStarted(); + } catch (Exception exc) { + Log.log(HttpDBServerState.class, exc, "Failed to cleanup"); + } + } + + @Override + public Map> getCommands() { + return HttpDBServerCommands.obtainInstance().getCommands(); + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/servlet/transactions/Transaction.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/servlet/transactions/Transaction.java new file mode 100644 index 000000000..da2cab815 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/servlet/transactions/Transaction.java @@ -0,0 +1,186 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.servlet.transactions; + +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.parallel.ControllableAgent; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.parallel.ControllableRunner; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.parallel.ExceptionFreeRunnable; + +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock; + +/** + * Transaction object that executes some action within the same thread which sleep between actions.
+ * Can be used only by one thread at a time.
+ * You can reuse this object many times. + */ +public class Transaction { + /** + * ID of this transaction. + */ + private final int transactionID; + /** + * Action performer. Run by hostRunner. Sleeps between action performing. + */ + private final ActionPerformer performer = new ActionPerformer(); + /** + * Lock for thread-safe use. + */ + private final WriteLock useLock = new ReentrantReadWriteLock(true).writeLock(); + /** + * Host runner to run the performer. + */ + private final ControllableRunner hostRunner = new ControllableRunner(); + /** + * Action to perform next. Nullable. + */ + private Action action; + /** + * Result of performed action. Can be null. + */ + private Object result; + /** + * If true, execution must be stopped. + */ + private State state = State.NotStarted; + /** + * Error occurred during action performing. Null, if all is ok. + */ + private Exception error; + /** + * Timestamp when this transaction was last accessed. {@link TransactionPool} decides when to kill this + * transaction looking at this timestamp. + */ + private long lastAccessTime; + private volatile T extraData; + + public Transaction(int transactionID) { + this.transactionID = transactionID; + } + + public int getTransactionID() { + return transactionID; + } + + public T getExtraData() { + return extraData; + } + + public void setExtraData(T extraData) { + this.extraData = extraData; + } + + /** + * This method is called by TransactionPool to decide whether to kill this transaction or not. + * @return + */ + long getLastAccessTime() { + useLock.lock(); + try { + return lastAccessTime; + } finally { + useLock.unlock(); + } + } + + /** + * This method is called by TransactionPool when this transaction is created.
+ * @throws Exception + */ + void init() throws Exception { + lastAccessTime = System.currentTimeMillis(); + if (state != State.NotStarted) { + throw new IllegalStateException("Already initialized"); + } + state = State.Active; + hostRunner.createAndAssign(performer); + new Thread(hostRunner, "Transaction " + transactionID).start(); + + hostRunner.waitUntilPause(); + } + + /** + * This method is called by TransactionPool when this transaction must be killed.
+ * Lock is expected to be obtained outside this method. + * @throws Exception + */ + void destroy() throws Exception { + if (state == State.Active) { + state = State.Stopped; + hostRunner.waitUntilEndOfWork(); + } else { + state = State.Stopped; + } + } + + void obtainWriteLock() { + useLock.lock(); + } + + void releaseWriteLock() { + useLock.unlock(); + } + + @Override + protected void finalize() throws Throwable { + super.finalize(); + destroy(); + } + + public T executeAction(Action action) throws Exception { + useLock.lock(); + try { + lastAccessTime = System.currentTimeMillis(); + if (state != State.Active) { + throw new IllegalStateException("You can execute actions only in active state"); + } + + this.action = action; + + hostRunner.continueWork(); + hostRunner.waitUntilPause(); + + if (error != null) { + throw error; + } + return (T) result; + } finally { + useLock.unlock(); + } + } + + enum State { + NotStarted, + Stopped, + Active + } + + public interface Action { + T perform() throws Exception; + } + + class ActionPerformer implements ExceptionFreeRunnable { + @Override + public void runWithFreedom(ControllableAgent agent) throws Exception, AssertionError { + while (true) { + agent.notifyAndWait(); + + if (state == State.Stopped) { + return; + } + + if (action == null) { + throw new IllegalStateException("Cannot run without action"); + } + + // Exception can occur on this step. + try { + error = null; + result = action.perform(); + } catch (Exception exc) { + error = exc; + } finally { + action = null; + } + } + } + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/servlet/transactions/TransactionPool.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/servlet/transactions/TransactionPool.java new file mode 100644 index 000000000..568691969 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/servlet/transactions/TransactionPool.java @@ -0,0 +1,195 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.servlet.transactions; + +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.InvalidatedObjectException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Log; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.ValidityController; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.ValidityController.KillLock; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.ValidityController.UseLock; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.Predicate; + +/** + * Thread-safe transaction pool. + */ +public class TransactionPool { + private static final int RANDOM_ID_ATTEMPTS = 10; + private final int idUpperBound; + private final Timer timer; + + private final ValidityController validityController = new ValidityController(); + + /** + * Mapping between transaction IDs and transactions. + */ + private final Map> transactionMap = new HashMap<>(); + + /** + * For operating with transactionMap. + */ + private final ReadWriteLock lock = new ReentrantReadWriteLock(true); + + /** + * Time to live for each transaction. If at least this time passes after a transaction was last accessed, + * it must be killed. + */ + private final long transactionTimeToLive; + + public TransactionPool(int idUpperBound, long transactionTimeToLive) { + this.idUpperBound = idUpperBound; + this.transactionTimeToLive = transactionTimeToLive; + timer = new Timer(); + timer.schedule(new PeriodicCleanup(), transactionTimeToLive, transactionTimeToLive / 2); + } + + public void closePool() { + try (KillLock killLock = validityController.useAndKill()) { + timer.cancel(); + cleanup((transaction) -> true); + } + } + + @Override + protected void finalize() throws Throwable { + super.finalize(); + try { + closePool(); + } catch (InvalidatedObjectException exc) { + // Ignore it. + } + } + + /** + * Perform pool cleanup. + * @param filter + * if true, transaction must be killed. Otherwise it is not effected. + */ + private void cleanup(Predicate> filter) { + try (UseLock useLock = validityController.use()) { + lock.writeLock().lock(); + try { + Iterator>> transactionsIter = + transactionMap.entrySet().iterator(); + while (transactionsIter.hasNext()) { + Transaction transaction = transactionsIter.next().getValue(); + transaction.obtainWriteLock(); + try { + if (filter.test(transaction)) { + try { + transactionsIter.remove(); + transaction.destroy(); + } catch (Exception exc) { + Log.log( + TransactionPool.class, + exc, + "Error while killing transaction: " + transaction.getTransactionID()); + } + } + } finally { + transaction.releaseWriteLock(); + } + } + } finally { + lock.writeLock().unlock(); + } + } + } + + public void killTransaction(Transaction transaction) throws Exception { + try (UseLock useLock = validityController.use()) { + lock.writeLock().lock(); + try { + if (transactionMap.get(transaction.getTransactionID()) != transaction) { + throw new IllegalArgumentException("Transaction not from this pool"); + } + transactionMap.remove(transaction.getTransactionID()); + try { + transaction.destroy(); + } finally { + releaseTransaction(transaction); + } + } finally { + lock.writeLock().unlock(); + } + } + } + + public void releaseTransaction(Transaction transaction) { + try (UseLock useLock = validityController.use()) { + transaction.releaseWriteLock(); + } + } + + public Transaction obtainTransaction(int transactionID) throws IllegalArgumentException { + try (UseLock useLock = validityController.use()) { + lock.readLock().lock(); + try { + Transaction transaction = transactionMap.get(transactionID); + if (transaction == null) { + throw new IllegalArgumentException("Transaction not found: " + transactionID); + } + + transaction.obtainWriteLock(); + return transaction; + } finally { + lock.readLock().unlock(); + } + } + } + + public Transaction newTransaction() throws Exception { + try (UseLock useLock = validityController.use()) { + ThreadLocalRandom random = ThreadLocalRandom.current(); + lock.writeLock().lock(); + try { + if (idUpperBound == transactionMap.size()) { + throw new IllegalStateException("No more free transaction IDs"); + } + int transactionID = random.nextInt(idUpperBound); + int times = 1; + while (transactionMap.containsKey(transactionID) && times < RANDOM_ID_ATTEMPTS) { + transactionID = random.nextInt(idUpperBound); + times++; + } + if (transactionMap.containsKey(transactionID)) { + for (transactionID = 0; transactionID < idUpperBound; transactionID++) { + if (!transactionMap.containsKey(transactionID)) { + break; + } + } + } + + Transaction transaction = new Transaction<>(transactionID); + transaction.obtainWriteLock(); + + transaction.init(); + transactionMap.put(transactionID, transaction); + return transaction; + } finally { + lock.writeLock().unlock(); + } + } + } + + class PeriodicCleanup extends TimerTask { + @Override + public void run() { + try { + long currentTime = System.currentTimeMillis(); + cleanup( + (transaction) -> currentTime - transaction.getLastAccessTime() + >= transactionTimeToLive); + } catch (Exception exc) { + Log.log(PeriodicCleanup.class, exc, "Failed to perform periodic cleanup"); + } + } + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/AbstractCommand.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/AbstractCommand.java index 70938bd33..925912f8e 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/AbstractCommand.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/AbstractCommand.java @@ -1,13 +1,17 @@ package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.ExecutionNotPermittedException; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.InvocationException; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.NoActiveTableException; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.TerminalException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.UnexpectedRemoteException; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.WrongArgsNumberException; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.AccurateExceptionHandler; import java.io.IOException; +import java.io.PrintStream; import java.text.ParseException; +import java.util.Objects; import static ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Utility.*; @@ -15,37 +19,49 @@ * Convenience class for Commands. * @author phoenix */ -public abstract class AbstractCommand implements Command { - private static final Class[] EXECUTE_SAFELY_THROWN_EXCEPTIONS = - obtainExceptionsThrownByExecuteSafely(); +public abstract class AbstractCommand> implements Command { + /** - * Used for unsafe calls. Catches and handles all exceptions thrown by {@link - * ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.AbstractCommand#executeSafely - * (SingleDatabaseShellState, String[]) } and {@link IllegalArgumentException }. + * Used for unsafe calls. Redirects handling of all exceptions to Shell. */ - protected static final AccurateExceptionHandler DATABASE_ERROR_HANDLER = - (Exception exc, SingleDatabaseShellState shell) -> { - boolean found = false; - Class actualType = exc.getClass(); - - for (Class expectedType : EXECUTE_SAFELY_THROWN_EXCEPTIONS) { - try { - actualType.asSubclass(expectedType); - found = true; - break; - } catch (ClassCastException cce) { - // Ignore it. + public static final AccurateExceptionHandler DEFAULT_EXCEPTION_HANDLER = + new AccurateExceptionHandler() { + final Class[] handledExceptions = new Class[] {IllegalArgumentException.class, + NoActiveTableException.class, + IllegalStateException.class, + NullPointerException.class, + InvocationException.class, + ParseException.class, + IOException.class, + UnexpectedRemoteException.class, + ExecutionNotPermittedException.class}; + + @Override + public void handleException(Exception exc, PrintStream ps) throws TerminalException { + Class actualType = exc.getClass(); + boolean found = false; + + for (Class expectedType : handledExceptions) { + try { + actualType.asSubclass(expectedType); + found = true; + break; + } catch (ClassCastException cce) { + // Ignore it. + } } - } - if (found) { - handleError(exc.getMessage(), exc, true); - } else if (exc instanceof RuntimeException) { - throw (RuntimeException) exc; - } else { - throw new RuntimeException("Unexpected exception", exc); + if (found) { + Shell.handleError(exc.getMessage(), exc, true, ps); + } else if (exc instanceof RuntimeException) { + throw (RuntimeException) exc; + } else { + throw new RuntimeException("Unexpected exception: " + exc.toString(), exc); + } } }; + + private final String name; private final String info; private final String invocationArgs; private final int minimalArgsCount; @@ -57,41 +73,52 @@ public abstract class AbstractCommand implements Command[] obtainExceptionsThrownByExecuteSafely() { - Class[] exceptions; + public int getMinimalArgsCount() { + return minimalArgsCount; + } - try { - exceptions = AbstractCommand.class - .getMethod("executeSafely", SingleDatabaseShellState.class, String[].class) - .getExceptionTypes(); - } catch (Exception exc) { - throw new RuntimeException("Failed to obtain exceptions thrown by executeSafely"); - } - return exceptions; + public int getMaximalArgsCount() { + return maximalArgsCount; } /** - * In implementation of {@link AbstractCommand} arguments number is checked first and then - * {@link #executeSafely(SingleDatabaseShellState, String[])} is invoked.
If you want to - * disable forced arguments number checking, override this method without invocation super - * method and put empty implementation inside {@link #executeSafely(SingleDatabaseShellState, - * String[])}. + * In implementation of {@link ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell + * .AbstractCommand} + * arguments number is checked first and then + * {@link #executeSafely(State, String[])} is invoked.
+ * If you want to disable forced arguments number checking, override this method without invocation super + * method and put empty implementation inside {@link #executeSafely(State, String[])}. */ @Override - public void execute(final SingleDatabaseShellState state, final String[] args) throws TerminalException { - checkArgsNumber(args, minimalArgsCount, maximalArgsCount); - performAccurately(() -> executeSafely(state, args), DATABASE_ERROR_HANDLER, state); + public void execute(final State state, final String[] args) throws TerminalException { + performAccurately( + () -> { + checkArgsNumber(args, minimalArgsCount, maximalArgsCount); + executeSafely(state, args); + }, DEFAULT_EXCEPTION_HANDLER, state.getOutputStream()); + } + + @Override + public String getName() { + return name; } @Override @@ -104,17 +131,11 @@ public String getInvocation() { return invocationArgs; } - public abstract void executeSafely(SingleDatabaseShellState shell, String[] args) throws - IllegalArgumentException, - NoActiveTableException, - IllegalStateException, - InvocationException, - ParseException, - IOException; + protected abstract void executeSafely(State state, String[] args) throws Exception; - protected void checkArgsNumber(String[] args, int minimal, int maximal) throws TerminalException { + void checkArgsNumber(String[] args, int minimal, int maximal) throws WrongArgsNumberException { if (args.length < minimal || args.length > maximal) { - handleError(null, new WrongArgsNumberException(this, args[0]), true); + throw new WrongArgsNumberException(this, args[0]); } } } diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/BaseShellState.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/BaseShellState.java new file mode 100644 index 000000000..47ac1b77d --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/BaseShellState.java @@ -0,0 +1,44 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell; + +import java.io.PrintStream; +import java.util.Objects; + +/** + * Base class for states with single init possibility. + * @param + */ +public abstract class BaseShellState> implements ShellState { + protected Shell host; + + protected void checkInitialized() { + Objects.requireNonNull(host, "Not initialized"); + } + + @Override + public PrintStream getOutputStream() { + checkInitialized(); + return host.getOutputStream(); + } + + @Override + public String getGreetingString() { + return "$ "; + } + + @Override + public void init(Shell host) throws Exception { + Objects.requireNonNull(host, "Host shell must not be null"); + + if (this.host != null) { + throw new IllegalStateException("Already initialized"); + } + + this.host = host; + } + + @Override + protected void finalize() throws Throwable { + super.finalize(); + cleanup(); + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/Command.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/Command.java index 5b896e041..4d64137ea 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/Command.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/Command.java @@ -9,6 +9,11 @@ public interface Command> { void execute(State state, String[] args) throws TerminalException; + /** + * Name this command can be invoked by. + */ + String getName(); + /** * Information text for the command. */ @@ -18,4 +23,9 @@ public interface Command> { * Complete formula for command invocation excluding command name. */ String getInvocation(); + + default String buildHelpLine() { + return String.format( + "\t%s%s\t%s", getName(), getInvocation() == null ? "" : (' ' + getInvocation()), getInfo()); + } } diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/Commands.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/Commands.java deleted file mode 100644 index 96732abc5..000000000 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/Commands.java +++ /dev/null @@ -1,262 +0,0 @@ -package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell; - -import ru.fizteh.fivt.storage.structured.Storeable; -import ru.fizteh.fivt.storage.structured.Table; -import ru.fizteh.fivt.storage.structured.TableProvider; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.StoreableTableImpl; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.DatabaseIOException; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.InvocationException; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.NoActiveTableException; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.TerminalException; - -import java.io.IOException; -import java.text.ParseException; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; - -public class Commands extends SimpleCommandContainer { - public static final Command COMMIT = - new AbstractCommand(null, "saves all changes made from the last commit", 1) { - @Override - public void executeSafely(SingleDatabaseShellState state, String[] args) - throws IOException, IllegalArgumentException { - int changes = state.getActiveDatabase().commit(); - System.out.println(changes); - } - }; - public static final Command ROLLBACK = - new AbstractCommand(null, "cancels all changes made from the last commit", 1) { - - @Override - public void executeSafely(SingleDatabaseShellState state, String[] args) - throws DatabaseIOException, IllegalArgumentException { - int changes = state.getActiveDatabase().rollback(); - System.out.println(changes); - } - }; - public static final Command SIZE = - new AbstractCommand(null, "prints count of stored keys in current table", 1) { - @Override - public void executeSafely(SingleDatabaseShellState state, String[] args) - throws DatabaseIOException, IllegalArgumentException, NoActiveTableException { - int size = state.getActiveDatabase().getActiveTable().size(); - System.out.println(size); - } - }; - public static final Command CREATE = new AbstractCommand( - " (type0 type1 ... typeN)", - "creates a new table with the given name and column types (must be specified inside round " - + "brackets); type can be one of the following: int, long, byte, float, double, boolean, String;", - 3, - Integer.MAX_VALUE) { - @Override - public void executeSafely(SingleDatabaseShellState state, String[] args) - throws IOException, InvocationException { - if (!args[2].startsWith("(") || !args[args.length - 1].endsWith(")")) { - throw new InvocationException( - this, args[0], "Round brackets must exist and contain types list inside them"); - } - - // Joining strings. - String typesString = String.join(" ", Arrays.asList(args).subList(2, args.length)); - - // Removing brackets. - typesString = typesString.substring(1, typesString.length() - 1).trim(); - - List> columnTypes = StoreableTableImpl.parseColumnTypes(typesString); - String tableName = args[1]; - - boolean created = state.getActiveDatabase().createTable(tableName, columnTypes); - if (created) { - System.out.println("created"); - } else { - throw new DatabaseIOException(tableName + " exists"); - } - } - }; - public static final Command DROP = new AbstractCommand( - "", "deletes table with the given name from file system", 2) { - @Override - public void executeSafely(SingleDatabaseShellState state, final String[] args) throws IOException { - state.getActiveDatabase().dropTable(args[1]); - System.out.println("dropped"); - } - }; - public static final Command EXIT = - new AbstractCommand(null, "saves all data to file system and stops interpretation", 1) { - @Override - public void execute(SingleDatabaseShellState state, String[] args) throws TerminalException { - int exitCode = 0; - - try { - state.persist(); - } catch (Exception exc) { - exitCode = 1; - DATABASE_ERROR_HANDLER.handleException(exc, state); - } finally { - state.prepareToExit(exitCode); - } - - // If all contracts are honoured, this line should not be reached. - throw new AssertionError("Exit request not thrown"); - } - - @Override - public void executeSafely(SingleDatabaseShellState shell, String[] args) - throws DatabaseIOException, IllegalArgumentException { - // Not used. - } - }; - public static final Command GET = - new AbstractCommand("", "obtains value by the key", 2) { - @Override - public void executeSafely(SingleDatabaseShellState state, final String[] args) - throws NoActiveTableException { - String key = args[1]; - - Table table = state.getActiveTable(); - TableProvider provider = state.getProvider(); - - Storeable value = table.get(key); - - if (value == null) { - throw new IllegalArgumentException("not found"); - } else { - System.out.println("found"); - System.out.println(provider.serialize(table, value)); - } - } - }; - public static final Command HELP = new AbstractCommand( - null, "prints out description of state commands", 1, Integer.MAX_VALUE) { - @Override - public void execute(SingleDatabaseShellState state, String[] args) { - Map> commands = state.getCommands(); - - System.out.println( - "DatabaseLibrary is an utility that lets you work with a simple database"); - - System.out.println( - String.format( - "You can set database directory to work with using environment " - + "variable '%s'", SingleDatabaseShellState.DB_DIRECTORY_PROPERTY_NAME)); - - for (Entry> cmdEntry : commands.entrySet()) { - String cmdName = cmdEntry.getKey(); - Command command = cmdEntry.getValue(); - - System.out.println( - String.format( - "\t%s%s\t%s", - cmdName, - command.getInvocation() == null ? "" : (' ' + command.getInvocation()), - command.getInfo())); - - } - } - - @Override - public void executeSafely(SingleDatabaseShellState state, String[] args) throws DatabaseIOException { - // not used - } - }; - public static final Command LIST = - new AbstractCommand(null, "prints all keys stored in the map", 1) { - @Override - public void executeSafely(SingleDatabaseShellState state, String[] args) - throws NoActiveTableException { - List keySet = state.getActiveTable().list(); - StringBuilder sb = new StringBuilder(); - - boolean comma = false; - - for (String key : keySet) { - sb.append(comma ? ", " : "").append(key); - comma = true; - } - - System.out.println(sb); - } - }; - public static final Command PUT = new AbstractCommand( - " [ {boolean|number|string|null}... ]", - "assigns new storeable to the key", - 3, - Integer.MAX_VALUE) { - @Override - public void executeSafely(SingleDatabaseShellState state, String[] args) - throws NoActiveTableException, ParseException { - String key = args[1]; - - Table table = state.getActiveTable(); - - String valueStr = String.join(" ", Arrays.asList(args).subList(2, args.length)); - Storeable value = state.getProvider().deserialize(table, valueStr); - - Storeable oldValue = state.getActiveTable().put(key, value); - - if (oldValue == null) { - System.out.println("new"); - } else { - String oldValueStr = state.getProvider().serialize(table, oldValue); - System.out.println("overwrite"); - System.out.println("old " + oldValueStr); - } - } - }; - public static final Command REMOVE = - new AbstractCommand("", "removes value by the key", 2) { - @Override - public void executeSafely(SingleDatabaseShellState state, String[] args) - throws DatabaseIOException, NoActiveTableException { - String key = args[1]; - - Storeable oldValue = state.getActiveTable().remove(key); - - if (oldValue == null) { - throw new DatabaseIOException("not found"); - } else { - System.out.println("removed"); - } - } - }; - public static final Command SHOW = new AbstractCommand( - "tables", "prints info on all tables assigned to the working database", 2) { - @Override - public void executeSafely(SingleDatabaseShellState state, String[] args) - throws IllegalArgumentException { - switch (args[1]) { - case "tables": { - state.getActiveDatabase().showTables(); - break; - } - default: { - throw new IllegalArgumentException("show: unexpected option: " + args[1]); - } - } - } - }; - public static final Command USE = new AbstractCommand( - "", - "saves all changes made to the current table (if present) and makes table" - + " with the given name the current one", - 2) { - @Override - public void executeSafely(final SingleDatabaseShellState state, final String[] args) - throws IOException { - state.getActiveDatabase().useTable(args[1]); - System.out.println("using " + args[1]); - } - }; - private static final Commands INSTANCE = new Commands(); - - private Commands() { - - } - - public static Commands obtainInstance() { - return INSTANCE; - } -} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/JoinedState.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/JoinedState.java new file mode 100644 index 000000000..5dae1beaf --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/JoinedState.java @@ -0,0 +1,252 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell; + +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.ExitRequest; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.TerminalException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.AccurateExceptionHandler; + +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * State that encapsulates several states that can be used. You need to create shell interpreter only for the + * joined state. You can decide when and how to react to command invocation requests to any state.
+ * If a command during execution throws {@link ru.fizteh.fivt.students.fedorov_andrew.databaselibrary + * .exception.ExitRequest} it is not handled by this default implementation. + * @param + * extending state for which interpreter is going to be created. + */ +public abstract class JoinedState> extends BaseShellState { + /** + * Wrapped commands + */ + private Map> allCommands; + private int statesCount; + + private AccurateExceptionHandler exceptionHandler; + + /** + * Pure state with no commands and exception handler. + */ + protected JoinedState() { + } + + /** + * Obtains currently used exception handler. + */ + public AccurateExceptionHandler getExceptionHandler() { + return exceptionHandler; + } + + /** + * Sets the exception handler that is used to handle errors during command executions. + * @param exceptionHandler + * exception handler with no extra data. + */ + protected void setExceptionHandler(AccurateExceptionHandler exceptionHandler) { + this.exceptionHandler = exceptionHandler; + } + + /** + * Sets all acceptable commands that can be invoked. + * @param commandsMaps + * array of command maps. Each command map is associated with state id (that is equal to the + * order + * number in this parameters sequence) which is used further to distinguish commands. + */ + protected final void setAllCommands(Map>... + commandsMaps) { + Map> allCommandsMap = new HashMap<>(); + + int idCounter = 0; + for (Map> commands : commandsMaps) { + int stateID = idCounter; + commands.forEach( + (name, command) -> { + if (allCommandsMap.containsKey(name)) { + ((CommandWrapper) allCommandsMap.get(name)).addCommandAndState(stateID, command); + } else { + allCommandsMap.put(name, new CommandWrapper(stateID, command)); + } + }); + idCounter++; + } + + allCommands = Collections.unmodifiableMap(allCommandsMap); + statesCount = commandsMaps.length; + } + + /** + * Calls {@link ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.ShellState#cleanup()} on + * each + * registered state if it is not null.
+ * States are obtained by {@link #obtainState(int)}. + */ + @Override + public void cleanup() { + for (int stateID = 0; stateID < statesCount; stateID++) { + ShellState state = obtainState(stateID); + if (state != null) { + state.cleanup(); + } + } + } + + /** + * Safely calls {@link ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell + * .ShellState#prepareToExit(int)} + * on each registered state if it is not null.
+ * States are obtained by {@link #obtainState(int)}.
+ * The resulting exit code depends on states' implementations. + */ + @Override + public void prepareToExit(int exitCode) throws ExitRequest { + int code = exitCode; + for (int stateID = 0; stateID < statesCount; stateID++) { + ShellState state = obtainState(stateID); + if (state != null) { + try { + state.prepareToExit(exitCode); + throw new AssertionError( + "prepareToExit() contract not honoured by " + state.getClass()); + } catch (ExitRequest exitRequest) { + code = exitRequest.getCode(); + } + } + } + + throw new ExitRequest(code); + } + + /** + * Returns mapping between command names and command wrappers that on {@link + * ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Command#execute(ru.fizteh.fivt.students + * .fedorov_andrew.databaselibrary.shell.ShellState, + * String[])} calls one of two methods: {@link #onExecuteConflict(java.util.List, java.util.List, + * String[])} or {@link #onExecuteRequested(int, ru.fizteh.fivt.students.fedorov_andrew.databaselibrary + * .shell.Command, + * String[])}.
+ * Exceptions thrown by these methods are handled with local exception handler.
+ * As soon as all possible state commands wrapped by one multicommand have the same name, {@link + * ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Command#getName()} returns that name, but + * both {@link ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Command#getInfo()} and {@link + * ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Command#getInvocation()} throw {@link + * UnsupportedOperationException}. + * @see #setExceptionHandler(AccurateExceptionHandler) + * @see #getExceptionHandler() + * @see #onExecuteRequested(int, ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Command, + * String[]) + * @see #onExecuteConflict(java.util.List, java.util.List, String[]) + */ + @Override + public Map> getCommands() { + return allCommands; + } + + /** + * Convenience method. + * Equivalent to {@code Objects.equals(commandA.getName(), commandB.getName())}. + */ + protected boolean areNamesEqual(Command commandA, Command commandB) { + return Objects.equals(commandA.getName(), commandB.getName()); + } + + /** + * This method is called when there are several commands with the same name from different states. You + * decide how to resolve this conflict.
+ * The default implementation throws {@link UnsupportedOperationException}.
+ * In this method you can also call {@link #onExecuteRequested(int, ru.fizteh.fivt.students + * .fedorov_andrew.databaselibrary.shell.Command, + * String[])} after you have + * chosen for which state to call the command. + * @throws Exception + */ + protected void onExecuteConflict(List stateIDs, List commands, String[] args) + throws Exception { + throw new UnsupportedOperationException("Execute conflicts are not resolved"); + } + + /** + * Runs normal execution of the given state command. The default implementation is {@code + * command.execute(obtainState(stateID), args)}. + * @throws Exception + */ + protected void executeNormally(int stateID, Command command, String[] args) throws Exception { + command.execute(obtainState(stateID), args); + } + + /** + * Obtains existing or new state for the given state id. + */ + protected abstract ShellState obtainState(int stateID); + + /** + * This method is called when one of commands is requested to invoke. You decide whether it happens.
+ * Default implementation of this method executes any given command. + * @param stateID + * id of host state for the command. + * @param command + * command that is requested to invoke + * @param args + * arguments given to the command. + */ + protected void onExecuteRequested(int stateID, Command command, String[] args) throws Exception { + executeNormally(stateID, command, args); + } + + /** + * Command for {@link JoinedState} that + * wraps + * at least one command from a state. + */ + class CommandWrapper implements Command { + private List states = new LinkedList<>(); + private List commands = new LinkedList<>(); + + public CommandWrapper(int stateID, Command wrappedCommand) { + states.add(stateID); + commands.add(wrappedCommand); + } + + public void addCommandAndState(int state, Command command) { + states.add(state); + commands.add(command); + } + + @Override + public void execute(ShellState state, String[] args) throws TerminalException { + try { + if (states.size() == 1) { + onExecuteRequested(states.get(0), commands.get(0), args); + } else { + onExecuteConflict( + Collections.unmodifiableList(states), + Collections.unmodifiableList(commands), + args); + } + } catch (TerminalException handledException) { + throw handledException; + } catch (Exception exc) { + exceptionHandler.handleException(exc, null); + } + } + + @Override + public String getName() { + return commands.get(0).getName(); + } + + @Override + public String getInfo() { + throw new UnsupportedOperationException("You cannot operate with command wrapper directly."); + } + + @Override + public String getInvocation() { + throw new UnsupportedOperationException("You cannot operate with command wrapper directly."); + } + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/Main.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/Main.java index a30e04da3..b3f70aba5 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/Main.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/Main.java @@ -1,14 +1,24 @@ package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell; +import ru.fizteh.fivt.storage.structured.TableProvider; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.AutoCloseableTableProviderFactory; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.DBTableProviderFactory; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.TerminalException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Log; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet.DBShellGeneralState; -public class Main { - //java -Dfizteh.db.dir=/home/phoenix/test/DB ru.fizteh.fivt.students.fedorov_andrew - // .databaselibrary.shell.Main +class Main { + //java -Dfizteh.db.dir=/home/phoenix/test/DB ru.fizteh.fivt.students.fedorov_andrew.databaselibrary + // .shell.Main + + private static final String PATH_PROPERTY = "fizteh.db.dir"; public static void main(String[] args) { - try { - Shell shell = new Shell<>(new SingleDatabaseShellState()); + try (AutoCloseableTableProviderFactory factory = new DBTableProviderFactory()) { + String databaseRoot = System.getProperty(SingleDatabaseShellState.DB_DIRECTORY_PROPERTY_NAME); + TableProvider provider = factory.create(databaseRoot); + + Shell shell = new Shell<>(new DBShellGeneralState(provider, databaseRoot)); int exitCode; if (args.length == 0) { exitCode = shell.run(System.in); @@ -20,6 +30,10 @@ public static void main(String[] args) { } catch (TerminalException exc) { // Already handled. System.exit(1); + } catch (Exception exc) { + Log.log(Main.class, exc); + System.out.println(exc.getMessage()); + System.exit(1); } } } diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/Shell.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/Shell.java index 261c14b65..5b82bf802 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/Shell.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/Shell.java @@ -1,20 +1,21 @@ package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.DBTableProvider; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.ExitRequest; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.TerminalException; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Log; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Utility; import java.io.BufferedReader; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.PrintStream; import java.text.ParseException; import java.util.Arrays; import java.util.LinkedList; import java.util.List; -import java.util.Map; /** * Class that represents a terminal which can execute some commands that work with some data. @@ -27,41 +28,68 @@ * @see ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.ShellState */ public class Shell> { - public static final String SPLIT_REGEX = "\\s+|$"; - public static final String USUAL_STRING_PART_REGEX = "[^\"&&\\S]+"; - public static final String QUOTED_STRING_SURROUNDING_REGEX = "[\\(\\)\\[\\]\\,]"; + private static final char QUOTE_CHARACTER = '\"'; + private static final char ESCAPE_CHARACTER = '\\'; - public static final char COMMAND_END_CHARACTER = ';'; - - public static final int READ_BUFFER_SIZE = 16 * 1024; + private static final char COMMAND_END_CHARACTER = ';'; + private static final int READ_BUFFER_SIZE = 16 * 1024; /** * Object encapsulating commands and data they work with. */ private final ShellStateImpl shellState; - - /** - * Available commands for invocation. - */ - private Map> commandMap; + private final PrintStream outputStream; + private boolean valid = true; /** * If the user is entering commands or it is package mode. */ private boolean interactive; - public Shell(ShellStateImpl shellState) throws TerminalException { + public Shell(ShellStateImpl shellState, OutputStream outputStream) throws TerminalException { this.shellState = shellState; + this.outputStream = outputStream instanceof PrintStream + ? (PrintStream) outputStream + : new PrintStream(outputStream); init(); } + public Shell(ShellStateImpl shellState) throws TerminalException { + this(shellState, System.out); + } + + /** + * Handles an occurred exception. + * @param cause + * occurred exception. If null, an {@link Exception} is constructed via {@link + * Exception#Exception(String)}. + * @param message + * message that can be reported to user and is written to log. + * @param reportToUser + * if true, message is printed to errorStream. + */ + public static void handleError(String message, + Throwable cause, + boolean reportToUser, + PrintStream errorStream) throws TerminalException { + if (reportToUser) { + errorStream.println(message == null ? cause.getMessage() : message); + } + Log.log(SingleDBCommands.class, cause, message); + if (cause == null) { + throw new TerminalException(message); + } else { + throw new TerminalException(message, cause); + } + } + /** * Splits command string into commands. * @param commandsStr * Commands split by {@link #COMMAND_END_CHARACTER}. * @return List of commands, each command is an array of its parts (space splitters are excluded from * everywhere except quoted parts). - * @throws ParseException + * @throws java.text.ParseException * In case of bad format. */ public static List splitCommandsString(String commandsStr) throws ParseException { @@ -77,13 +105,9 @@ public static List splitCommandsString(String commandsStr) throws Pars for (int start = 0, index = 0, len = commandsStr.length(); index < len; ) { char symbol = commandsStr.charAt(index); - if (symbol == DBTableProvider.QUOTE_CHARACTER) { + if (symbol == QUOTE_CHARACTER) { index = Utility.findClosingQuotes( - commandsStr, - index + 1, - len, - DBTableProvider.QUOTE_CHARACTER, - DBTableProvider.ESCAPE_CHARACTER); + commandsStr, index + 1, len, QUOTE_CHARACTER, ESCAPE_CHARACTER); if (index < 0) { throw new ParseException("Cannot find closing quotes", -1); } @@ -108,6 +132,10 @@ public static List splitCommandsString(String commandsStr) throws Pars return commands; } + public PrintStream getOutputStream() { + return outputStream; + } + /** * Executes command in this shell * @param args @@ -121,9 +149,9 @@ private void execute(String[] args) throws TerminalException, ExitRequest { Log.log(Shell.class, "Invocation request: " + Arrays.toString(args)); - Command command = commandMap.get(args[0]); + Command command = shellState.getCommands().get(args[0]); if (command == null) { - Utility.handleError(args[0] + ": command is missing", null, true); + handleError(args[0] + ": command is missing", null, true, getOutputStream()); } else { try { command.execute(shellState, args); @@ -131,7 +159,7 @@ private void execute(String[] args) throws TerminalException, ExitRequest { // If it is TerminalException, error report is already written. throw exc; } catch (Exception exc) { - Utility.handleError(args[0] + ": Method execution error", exc, true); + handleError(args[0] + ": Method execution error", exc, true, getOutputStream()); } } } @@ -145,21 +173,27 @@ private void init() throws TerminalException { try { shellState.init(this); } catch (Exception exc) { - Utility.handleError(exc.getMessage(), exc, true); + handleError(exc.getMessage(), exc, true, getOutputStream()); } - - commandMap = shellState.getCommands(); } public boolean isInteractive() { return interactive; } + private void checkValid() throws IllegalStateException { + if (!valid) { + throw new IllegalStateException("Shell has already run"); + } + } + /** * Execute commands from input stream. Commands are awaited till the-end-of-stream. */ - public int run(InputStream stream) throws TerminalException { - interactive = true; + private int run(InputStream stream, boolean interactive) throws TerminalException { + checkValid(); + valid = false; + this.interactive = interactive; if (stream == null) { throw new IllegalArgumentException("Input stream must not be null"); @@ -170,7 +204,9 @@ public int run(InputStream stream) throws TerminalException { try (BufferedReader reader = new BufferedReader( new InputStreamReader(stream), READ_BUFFER_SIZE)) { while (true) { - System.out.print(shellState.getGreetingString()); + if (interactive) { + outputStream.print(shellState.getGreetingString()); + } String str = reader.readLine(); // End of stream. @@ -178,18 +214,26 @@ public int run(InputStream stream) throws TerminalException { break; } - List commands = splitCommandsString(str); try { - for (String[] command : commands) { - execute(command); + try { + List commands = splitCommandsString(str); + for (String[] command : commands) { + execute(command); + } + } catch (ParseException exc) { + handleError("Failed to parse: " + exc.getMessage(), exc, true, getOutputStream()); } } catch (TerminalException exc) { // Exception is already handled. + if (!interactive) { + exitRequested = true; + shellState.prepareToExit(1); + } } } - } catch (IOException | ParseException exc) { + } catch (IOException exc) { exitRequested = true; - Utility.handleError("Error in input stream: " + exc.getMessage(), exc, true); + handleError("Error in input stream: " + exc.getMessage(), exc, true, getOutputStream()); // No need to cleanup - work has not been started. } catch (ExitRequest request) { exitRequested = true; @@ -197,7 +241,7 @@ public int run(InputStream stream) throws TerminalException { } finally { if (!exitRequested) { try { - persistSafelyAndPrepareToExit(); + shellState.prepareToExit(0); } catch (ExitRequest request) { return request.getCode(); } @@ -208,6 +252,10 @@ public int run(InputStream stream) throws TerminalException { throw new AssertionError("No exit request performed"); } + public int run(InputStream inputStream) throws TerminalException { + return run(inputStream, true); + } + /** * Execute commands from command line arguments. Note that command line arguments are first * concatenated into a single line then split and parsed. @@ -218,45 +266,22 @@ public int run(InputStream stream) throws TerminalException { * @return Exit code. 0 means normal status, anything else - abnormal termination (error). */ public int run(String[] args) throws TerminalException { - try { - interactive = false; - - try { - List commands = splitCommandsString(String.join(" ", args)); - - for (String[] command : commands) { - execute(command); - } - } catch (TerminalException exc) { - // Exception already handled. - shellState.prepareToExit(1); - } catch (ParseException exc) { - Utility.handleError("Cannot parse command arguments: " + exc.getMessage(), exc, true); - } - persistSafelyAndPrepareToExit(); - } catch (ExitRequest request) { - return request.getCode(); - } + return run(new ByteArrayInputStream(String.join(" ", args).getBytes()), false); + } - // If all contracts are honoured, this line is unreachable. - throw new AssertionError("No exit request performed"); + public boolean isValid() { + return valid; } - /** - * Persists shell state. If fails, calls {@link ru.fizteh.fivt.students.fedorov_andrew - * .databaselibrary.shell.ShellState#prepareToExit(int)} - * with non zero exit code. - */ - private void persistSafelyAndPrepareToExit() throws ExitRequest { - try { - shellState.persist(); - shellState.prepareToExit(0); - } catch (ExitRequest request) { - throw request; - } catch (Exception exc) { - Log.log(Shell.class, exc, "Failed to persist shell state"); - shellState.prepareToExit(1); + @Override + protected void finalize() throws Throwable { + super.finalize(); + if (valid) { + try { + shellState.prepareToExit(0); + } catch (ExitRequest req) { + // Ignore it. + } } } - } diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/ShellState.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/ShellState.java index 284122cfb..fbf448be0 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/ShellState.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/ShellState.java @@ -1,6 +1,9 @@ package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.ExitRequest; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Log; + +import java.io.PrintStream; /** * Base interface that encapsulates all data and commands to work with. @@ -8,6 +11,11 @@ * Implementation of this interface */ public interface ShellState> extends CommandContainer { + /** + * Terminal output stream for printing messages. + */ + PrintStream getOutputStream(); + /** * Performs clean up after all work is done and the shell is going to exit. */ @@ -26,16 +34,15 @@ public interface ShellState> extends CommandContainer void init(Shell host) throws Exception; /** - * Persist object's state somehow. - * @throws Exception - */ - void persist() throws Exception; - - /** - * Safely exit with cleanup. + * Safely exit with cleanup. Default implementation calls {@link #cleanup()} and throws {@link + * ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.ExitRequest} with the given code. * @throws ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.ExitRequest * you must throw this exception to indicate that you really want to exit. Do no call * {@link System#exit(int)} instead of it. */ - void prepareToExit(int exitCode) throws ExitRequest; + default void prepareToExit(int exitCode) throws ExitRequest { + Log.log(getClass(), "Preparing to exit with code " + exitCode); + cleanup(); + throw new ExitRequest(exitCode); + } } diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/SimpleCommandContainer.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/SimpleCommandContainer.java index e93649fd5..56beb9e1f 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/SimpleCommandContainer.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/SimpleCommandContainer.java @@ -1,7 +1,6 @@ package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Log; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Utility; import java.lang.reflect.Field; import java.util.HashMap; @@ -31,7 +30,10 @@ public Map> getCommands() { for (Field field : fields) { try { Command command = (Command) field.get(null); - String commandName = Utility.simplifyFieldName(field.getName()); + String commandName = command.getName(); + if (commandsMap.containsKey(commandName)) { + throw new IllegalStateException("Duplicate command name: " + commandName); + } commandsMap.put(commandName, command); Log.log("Registered command with name " + commandName); } catch (IllegalAccessException | ClassCastException exc) { diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/SingleDBCommands.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/SingleDBCommands.java new file mode 100644 index 000000000..c98c586cd --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/SingleDBCommands.java @@ -0,0 +1,260 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell; + +import ru.fizteh.fivt.storage.structured.Storeable; +import ru.fizteh.fivt.storage.structured.Table; +import ru.fizteh.fivt.storage.structured.TableProvider; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.StoreableTableImpl; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.DatabaseIOException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.InvocationException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.NoActiveTableException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.TerminalException; + +import java.io.IOException; +import java.text.ParseException; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +public class SingleDBCommands extends SimpleCommandContainer { + public static final Command COMMIT = + new AbstractCommand( + "commit", null, "saves all changes made from the last commit", 1) { + @Override + public void executeSafely(SingleDatabaseShellState state, String[] args) + throws IOException, IllegalArgumentException { + int changes = state.getActiveDatabase().commit(); + state.getOutputStream().println(changes); + } + }; + public static final Command ROLLBACK = + new AbstractCommand( + "rollback", null, "cancels all changes made from the last commit", 1) { + + @Override + public void executeSafely(SingleDatabaseShellState state, String[] args) + throws DatabaseIOException, IllegalArgumentException { + int changes = state.getActiveDatabase().rollback(); + state.getOutputStream().println(changes); + } + }; + public static final Command SIZE = + new AbstractCommand( + "size", null, "prints count of stored keys in current table", 1) { + @Override + public void executeSafely(SingleDatabaseShellState state, String[] args) + throws DatabaseIOException, IllegalArgumentException, NoActiveTableException { + int size = state.getActiveDatabase().getActiveTable().size(); + state.getOutputStream().println(size); + } + }; + public static final Command CREATE = + new AbstractCommand( + "create", + " (type0 type1 ... typeN)", + "creates a new table with the given name and column types (must be specified inside " + + "round " + + "brackets); type can be one of the following: int, long, byte, float, double, " + + "boolean, String;", + 3, + Integer.MAX_VALUE) { + @Override + public void executeSafely(SingleDatabaseShellState state, String[] args) + throws IOException, InvocationException { + if (!args[2].startsWith("(") || !args[args.length - 1].endsWith(")")) { + throw new InvocationException( + this, + args[0], + "Round brackets must exist and contain types list inside them"); + } + + // Joining strings. + String typesString = String.join(" ", Arrays.asList(args).subList(2, args.length)); + + // Removing brackets. + typesString = typesString.substring(1, typesString.length() - 1).trim(); + + List> columnTypes = StoreableTableImpl.parseColumnTypes(typesString); + String tableName = args[1]; + + boolean created = state.getActiveDatabase().createTable(tableName, columnTypes); + if (created) { + state.getOutputStream().println("created"); + } else { + throw new DatabaseIOException(tableName + " exists"); + } + } + }; + public static final Command DROP = + new AbstractCommand( + "drop", "", "deletes table with the given name from file system", 2) { + @Override + public void executeSafely(SingleDatabaseShellState state, final String[] args) + throws IOException { + state.getActiveDatabase().dropTable(args[1]); + state.getOutputStream().println("dropped"); + } + }; + public static final Command EXIT = + new AbstractCommand( + "exit", null, "rollbacks all changes and stops interpretation", 1) { + @Override + public void execute(SingleDatabaseShellState state, String[] args) throws TerminalException { + state.prepareToExit(0); + + // If all contracts are honoured, this line should not be reached. + throw new AssertionError("Exit request not thrown"); + } + + @Override + public void executeSafely(SingleDatabaseShellState state, String[] args) + throws DatabaseIOException, IllegalArgumentException { + // Not used. + } + }; + public static final Command GET = + new AbstractCommand("get", "", "obtains value by the key", 2) { + @Override + public void executeSafely(SingleDatabaseShellState state, final String[] args) + throws NoActiveTableException { + String key = args[1]; + + Table table = state.getActiveTable(); + TableProvider provider = state.getProvider(); + + Storeable value = table.get(key); + + if (value == null) { + throw new IllegalArgumentException("not found"); + } else { + state.getOutputStream().println("found"); + state.getOutputStream().println(provider.serialize(table, value)); + } + } + }; + public static final Command HELP = + new AbstractCommand( + "help", null, "prints out description of state commands", 1, Integer.MAX_VALUE) { + @Override + public void execute(SingleDatabaseShellState state, String[] args) { + Map> commands = state.getCommands(); + + state.getOutputStream().println( + "DatabaseLibrary is an utility that lets you work with a simple database"); + + state.getOutputStream().println( + String.format( + "You can set database directory to work with using environment " + + "variable '%s'", SingleDatabaseShellState.DB_DIRECTORY_PROPERTY_NAME)); + + commands.values() + .forEach((command) -> state.getOutputStream().println(command.buildHelpLine())); + } + + @Override + public void executeSafely(SingleDatabaseShellState state, String[] args) + throws DatabaseIOException { + // not used + } + }; + public static final Command LIST = + new AbstractCommand( + "list", null, "prints all keys stored in the map", 1) { + @Override + public void executeSafely(SingleDatabaseShellState state, String[] args) + throws NoActiveTableException { + List keySet = state.getActiveTable().list(); + StringBuilder sb = new StringBuilder(); + + boolean comma = false; + + for (String key : keySet) { + sb.append(comma ? ", " : "").append(key); + comma = true; + } + + state.getOutputStream().println(sb); + } + }; + public static final Command PUT = new AbstractCommand( + "put", + " [ {boolean|number|string|null}... ]", + "assigns new storeable to the key", + 3, + Integer.MAX_VALUE) { + @Override + public void executeSafely(SingleDatabaseShellState state, String[] args) + throws NoActiveTableException, ParseException { + String key = args[1]; + + Table table = state.getActiveTable(); + + String valueStr = String.join(" ", Arrays.asList(args).subList(2, args.length)); + Storeable value = state.getProvider().deserialize(table, valueStr); + + Storeable oldValue = state.getActiveTable().put(key, value); + + if (oldValue == null) { + state.getOutputStream().println("new"); + } else { + String oldValueStr = state.getProvider().serialize(table, oldValue); + state.getOutputStream().println("overwrite"); + state.getOutputStream().println("old " + oldValueStr); + } + } + }; + public static final Command REMOVE = + new AbstractCommand("remove", "", "removes value by the key", 2) { + @Override + public void executeSafely(SingleDatabaseShellState state, String[] args) + throws DatabaseIOException, NoActiveTableException { + String key = args[1]; + + Storeable oldValue = state.getActiveTable().remove(key); + + if (oldValue == null) { + throw new DatabaseIOException("not found"); + } else { + state.getOutputStream().println("removed"); + } + } + }; + public static final Command SHOW = + new AbstractCommand( + "show", "tables", "prints info on all tables assigned to the working database", 2) { + @Override + public void executeSafely(SingleDatabaseShellState state, String[] args) + throws IllegalArgumentException { + switch (args[1]) { + case "tables": { + state.getActiveDatabase().showTables(); + break; + } + default: { + throw new IllegalArgumentException("show: unexpected option: " + args[1]); + } + } + } + }; + public static final Command USE = new AbstractCommand( + "use", + "", + "saves all changes made to the current table (if present) and makes table" + + " with the given name the current one", + 2) { + @Override + public void executeSafely(final SingleDatabaseShellState state, final String[] args) + throws IOException { + state.getActiveDatabase().useTable(args[1]); + state.getOutputStream().println("using " + args[1]); + } + }; + private static final SingleDBCommands INSTANCE = new SingleDBCommands(); + + private SingleDBCommands() { + + } + + public static SingleDBCommands getInstance() { + return INSTANCE; + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/SingleDatabaseShellState.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/SingleDatabaseShellState.java index 0ef351b74..3d2d48132 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/SingleDatabaseShellState.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/SingleDatabaseShellState.java @@ -3,61 +3,34 @@ import ru.fizteh.fivt.storage.structured.Table; import ru.fizteh.fivt.storage.structured.TableProvider; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.Database; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.DatabaseIOException; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.ExitRequest; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.NoActiveTableException; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Log; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Utility; -import java.io.IOException; -import java.nio.file.DirectoryStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; +import java.io.PrintStream; import java.util.Map; -import java.util.Objects; /** * This class represents actual task implementation: work from terminal with a database, whose * location in file system is given. */ -public class SingleDatabaseShellState implements ShellState { +public abstract class SingleDatabaseShellState extends BaseShellState { /** * Name of environment property; value stored there is database location. */ public static final String DB_DIRECTORY_PROPERTY_NAME = "fizteh.db.dir"; - /** * Our proxy command container. */ - private static final Commands COMMANDS_CONTAINER = Commands.obtainInstance(); + private static final SingleDBCommands COMMANDS_CONTAINER = SingleDBCommands.getInstance(); /** * Database that user works with via terminal. */ private Database activeDatabase; - /** - * Shell representing terminal. - */ - private Shell host; - @Override - public void cleanup() { - // Delete empty files and directories inside tables' directories - Path dbDirectory = (getActiveDatabase() == null ? null : getActiveDatabase().getDbDirectory()); - - if (dbDirectory != null) { - try (DirectoryStream dirStream = Files.newDirectoryStream(dbDirectory)) { - for (Path tableDirectory : dirStream) { - Utility.removeEmptyFilesAndFolders(tableDirectory); - } - - Log.log(SingleDatabaseShellState.class, "Cleaned up successfully"); - } catch (IOException exc) { - Log.log(SingleDatabaseShellState.class, exc, "Failed to clean up"); - } - } + public PrintStream getOutputStream() { + checkInitialized(); + return host.getOutputStream(); } @Override @@ -75,41 +48,27 @@ public String getGreetingString() { } @Override - public void init(Shell host) - throws IllegalArgumentException, DatabaseIOException { - Objects.requireNonNull(host, "Host shell must not be null"); + public void init(Shell host) throws Exception { + super.init(host); - if (this.host != null) { - throw new IllegalStateException("Initialization happened already"); - } - this.host = host; - - //establishing database - String dbDirPath = System.getProperty(DB_DIRECTORY_PROPERTY_NAME); - if (dbDirPath == null) { - throw new IllegalArgumentException("Please mention database directory"); - } - - activeDatabase = new Database(Paths.get(dbDirPath)); + activeDatabase = obtainNewActiveDatabase(); } - @Override - public void persist() throws IOException { - getActiveDatabase().commit(); - } + protected abstract Database obtainNewActiveDatabase() throws Exception; @Override - public void prepareToExit(int exitCode) throws ExitRequest { - Log.log(SingleDatabaseShellState.class, "Preparing to exit with code " + exitCode); - cleanup(); - Log.close(); - throw new ExitRequest(exitCode); + public void cleanup() { + Database activeDatabase = getActiveDatabase(); + if (activeDatabase != null) { + activeDatabase.rollback(); + } } /** * Returns database user works with. */ public Database getActiveDatabase() { + checkInitialized(); return activeDatabase; } diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/SingleFactoryDatabaseShellState.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/SingleFactoryDatabaseShellState.java new file mode 100644 index 000000000..b5f92608b --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/SingleFactoryDatabaseShellState.java @@ -0,0 +1,30 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell; + +import ru.fizteh.fivt.storage.structured.TableProvider; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.DBTableProviderFactory; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.Database; + +public class SingleFactoryDatabaseShellState extends SingleDatabaseShellState { + + private final DBTableProviderFactory factory; + + public SingleFactoryDatabaseShellState() { + this.factory = new DBTableProviderFactory(); + } + + @Override + protected Database obtainNewActiveDatabase() throws Exception { + String dbPath = System.getProperty(DB_DIRECTORY_PROPERTY_NAME); + if (dbPath == null) { + throw new IllegalStateException("Please mention database directory"); + } + TableProvider provider = factory.create(dbPath); + return new Database(provider, dbPath, getOutputStream()); + } + + @Override + public void cleanup() { + super.cleanup(); + factory.close(); + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/ConvenientCollection.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/ConvenientCollection.java index 9b1fe3abc..a6bd11f81 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/ConvenientCollection.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/ConvenientCollection.java @@ -8,7 +8,8 @@ import java.util.stream.Stream; /** - * Usual collection over the given base collection with extension {@link ConvenientCollection#addNext + * Usual collection over the given base collection with extension {@link ru.fizteh.fivt.students + * .fedorov_andrew.databaselibrary.support.ConvenientCollection#chainAdd * (Object)}. */ public class ConvenientCollection implements Collection { @@ -18,7 +19,7 @@ public ConvenientCollection(Collection base) { this.collection = base; } - public ConvenientCollection addNext(T element) { + public ConvenientCollection chainAdd(T element) { collection.add(element); return this; } diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/ConvenientMap.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/ConvenientMap.java index 4fe389c04..874617cd8 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/ConvenientMap.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/ConvenientMap.java @@ -8,7 +8,9 @@ import java.util.function.Function; /** - * Usual map extended with method {@link ConvenientMap#putNext(Object, Object)}. + * Usual map extended with method {@link ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support + * .ConvenientMap#chainPut(Object, + * Object)}. */ public class ConvenientMap implements Map { private final Map baseMap; @@ -17,7 +19,7 @@ public ConvenientMap(Map baseMap) { this.baseMap = baseMap; } - public ConvenientMap putNext(K key, V value) { + public ConvenientMap chainPut(K key, V value) { baseMap.put(key, value); return this; } diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/Log.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/Log.java index 33894bf5c..808da63be 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/Log.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/Log.java @@ -30,18 +30,17 @@ public class Log { private Log() { } - private static void reopen() { + private static synchronized void reopen() { try { writer = new PrintWriter(new FileOutputStream(LOG_PATH.toAbsolutePath().toString(), !firstOpen)); firstOpen = false; } catch (IOException exc) { System.err.println(String.format("Cannot create log file: %s", LOG_PATH)); System.err.println(exc.toString()); - System.exit(1); } } - public static void close() { + public static synchronized void close() { if (writer != null) { writer.println("Log closing"); writer.close(); @@ -49,23 +48,31 @@ public static void close() { } } - public static boolean isEnableLogging() { + public static synchronized boolean isEnableLogging() { return enableLogging; } - public static void setEnableLogging(boolean enableLogging) { + public static synchronized void setEnableLogging(boolean enableLogging) { Log.enableLogging = enableLogging; } - public static void log(Class logger, String message) { + public static synchronized void log(Class logger, String message) { log(logger, null, message); } - public static void log(Class logger, Throwable throwable, String message) { - if (writer == null) { - reopen(); - } + public static synchronized void log(Class logger, Throwable throwable) { + log(logger, throwable, null); + } + + public static synchronized void log(Class logger, Throwable throwable, String message) { if (enableLogging) { + if (writer == null) { + reopen(); + if (writer == null) { + return; + } + } + StringBuilder sb = new StringBuilder(message == null ? 100 : message.length() * 2); boolean appendSpace = false; @@ -111,7 +118,7 @@ public static void log(Class logger, Throwable throwable, String message) { } } - public static void log(String message) { + public static synchronized void log(String message) { log(null, null, message); } } diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/LoggingProxyFactoryBase.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/LoggingProxyFactoryBase.java new file mode 100644 index 000000000..58a68b160 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/LoggingProxyFactoryBase.java @@ -0,0 +1,140 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support; + +import ru.fizteh.fivt.proxy.LoggingProxyFactory; + +import java.io.IOException; +import java.io.Writer; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +/** + * Base class that implements Logging proxy factory. When you extend it you have only to implement method + * that + * makes log message from given data. + */ +public abstract class LoggingProxyFactoryBase implements LoggingProxyFactory { + private volatile boolean loggingEnabled = true; + + /** + * Constructs log message from the given data. + * @param timestamp + * time when the method was invoked. + * @param invokedClass + * name of the class that contains the invoked method. + * @param invokedMethod + * name of the invoked method. + * @param arguments + * arguments given to the invoked method. + * @param thrown + * exception thrown by the invoked method. + * @param returnedValue + * value returned by the invoked method. Can be null. Is null, if method returns void. + * @param isVoid + * if the invoked method is void. + * @return message that is written to log. + */ + protected abstract String constructReport(long timestamp, + String invokedClass, + String invokedMethod, + Object[] arguments, + Throwable thrown, + Object returnedValue, + boolean isVoid); + + public boolean isLoggingEnabled() { + return loggingEnabled; + } + + public void setLoggingEnabled(boolean loggingEnabled) { + this.loggingEnabled = loggingEnabled; + } + + @Override + public Object wrap(Writer writer, Object implementation, Class interfaceClass) { + return Proxy.newProxyInstance( + implementation.getClass().getClassLoader(), + new Class[] {interfaceClass}, + new Handler(writer, implementation)); + } + + private class Handler implements InvocationHandler { + private final Writer writer; + private final Object wrappedObject; + + public Handler(Writer writer, Object wrappedObject) { + this.writer = writer; + this.wrappedObject = wrappedObject; + } + + @Override + public String toString() { + return wrappedObject.toString(); + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + boolean isObjectMethod; + + try { + Object.class.getMethod(method.getName(), method.getParameterTypes()); + isObjectMethod = true; + } catch (NoSuchMethodException exc) { + isObjectMethod = false; + } + + // We do not proxy methods of Object class. We call it on us. + if (isObjectMethod) { + try { + return method.invoke(this, args); + } catch (InvocationTargetException exc) { + throw exc.getTargetException(); + } + } + + long timestamp = System.currentTimeMillis(); + Object returnValue = null; + Throwable thrown = null; + + // We must know it before invocation. Simple reason: suppose the invoked method turns off logging. + boolean doWriteLog = isLoggingEnabled(); + + try { + boolean accessible = method.isAccessible(); + method.setAccessible(true); + returnValue = method.invoke(wrappedObject, args); + method.setAccessible(accessible); + } catch (InvocationTargetException exc) { + thrown = exc.getTargetException(); + } catch (IllegalAccessException | IllegalArgumentException exc) { + Log.log(LoggingProxyFactory.class, exc, "Error on proxy invocation"); + } finally { + if (doWriteLog) { + String report = constructReport( + timestamp, + wrappedObject.getClass().getSimpleName(), + method.getName(), + args, + thrown, + returnValue, + void.class.equals(method.getReturnType())); + + try { + writer.write(report); + writer.write(System.lineSeparator()); + writer.flush(); + } catch (IOException exc) { + Log.log(LoggingProxyFactory.class, exc, "Failed to write log report"); + } + } + } + + if (thrown != null) { + throw thrown; + } else { + return returnValue; + } + } + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/LoggingProxyFactoryJSON.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/LoggingProxyFactoryJSON.java new file mode 100644 index 000000000..f8b371361 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/LoggingProxyFactoryJSON.java @@ -0,0 +1,84 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support; + +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.json.JSONComplexObject; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.json.JSONField; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.json.JSONMaker; + +/** + * Extension which writes logs in JSON format using {@link ru.fizteh.fivt.students.fedorov_andrew + * .databaselibrary.json.JSONMaker} + */ +public class LoggingProxyFactoryJSON extends LoggingProxyFactoryBase { + + @Override + protected String constructReport(long timestamp, + String invokedClass, + String invokedMethod, + Object[] arguments, + Throwable thrown, + Object returnedValue, + boolean isVoid) { + Object report; + if (thrown != null) { + report = new LoggingReportWithThrown(timestamp, invokedClass, invokedMethod, arguments, thrown); + } else if (isVoid) { + report = new LoggingReport(timestamp, invokedClass, invokedMethod, arguments); + } else { + report = new LoggingReportWithReturnValue( + timestamp, invokedClass, invokedMethod, arguments, returnedValue); + } + return JSONMaker.makeJSON(report); + } + + @JSONComplexObject + private static class LoggingReport { + @JSONField + final long timestamp; + + @JSONField(name = "class") + final String invokeeClass; + + @JSONField(name = "method") + final String invokeeMethod; + + @JSONField + final Object[] arguments; + + public LoggingReport(long timestamp, String invokeeClass, String invokeeMethod, Object[] arguments) { + this.timestamp = timestamp; + this.invokeeClass = invokeeClass; + this.invokeeMethod = invokeeMethod; + this.arguments = arguments; + } + } + + @JSONComplexObject + private static class LoggingReportWithReturnValue extends LoggingReport { + @JSONField + final Object returnValue; + + public LoggingReportWithReturnValue(long timestamp, + String invokeeClass, + String invokeeMethod, + Object[] arguments, + Object returnValue) { + super(timestamp, invokeeClass, invokeeMethod, arguments); + this.returnValue = returnValue; + } + } + + @JSONComplexObject + private static class LoggingReportWithThrown extends LoggingReport { + @JSONField + final Throwable thrown; + + public LoggingReportWithThrown(long timestamp, + String invokeeClass, + String invokeeMethod, + Object[] arguments, + Throwable thrown) { + super(timestamp, invokeeClass, invokeeMethod, arguments); + this.thrown = thrown; + } + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/LoggingProxyFactoryXML.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/LoggingProxyFactoryXML.java new file mode 100644 index 000000000..fbe7a851d --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/LoggingProxyFactoryXML.java @@ -0,0 +1,82 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support; + +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.xml.XMLComplexObject; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.xml.XMLField; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.xml.XMLMaker; + +import java.util.Arrays; +import java.util.stream.Collectors; + +/** + * Extension which writes logs in XML format using {@link ru.fizteh.fivt.students.fedorov_andrew + * .databaselibrary.xml.XMLMaker}. + */ +public class LoggingProxyFactoryXML extends LoggingProxyFactoryBase { + + @Override + protected String constructReport(long timestamp, + String invokedClass, + String invokedMethod, + Object[] arguments, + Throwable thrown, + Object returnedValue, + boolean isVoid) { + Object report = + new LoggingReport(timestamp, invokedClass, invokedMethod, arguments, returnedValue, thrown); + return XMLMaker.makeXML(report, "invoke"); + } + + @XMLComplexObject(wrapper = true) + private static class Argument { + @XMLField + private final Object argument; + + public Argument(Object argument) { + this.argument = argument; + } + + public static Argument[] wrapObjects(Object... arguments) { + if (arguments == null) { + return null; + } + return Arrays.stream(arguments).map(Argument::new).collect(Collectors.toList()) + .toArray(new Argument[arguments.length]); + } + } + + @XMLComplexObject + private static class LoggingReport { + @XMLField(inline = true) + final long timestamp; + + @XMLField(name = "class", inline = true) + final String invokeeClass; + + @XMLField(name = "name", inline = true) + final String invokeeMethod; + + @XMLField(childName = "argument", nullPolicy = XMLField.NULLPOLICY_EMPTY_IF_NULL) + final Argument[] arguments; + + @XMLField(nullPolicy = XMLField.NULLPOLICY_IGNORE_IF_NULL) + final Throwable thrown; + + @XMLField(name = "return", nullPolicy = XMLField.NULLPOLICY_IGNORE_IF_NULL) + final Object returnValue; + + public LoggingReport(long timestamp, + String invokeeClass, + String invokeeMethod, + Object[] arguments, + Object returnValue, + Throwable thrown) { + this.timestamp = timestamp; + this.invokeeClass = invokeeClass; + this.invokeeMethod = invokeeMethod; + this.arguments = Argument.wrapObjects(arguments); + this.returnValue = returnValue; + this.thrown = thrown; + } + } + +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/Utility.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/Utility.java index 86cf21e16..e29a263d0 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/Utility.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/Utility.java @@ -1,9 +1,10 @@ package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.TerminalException; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Commands; import java.io.IOException; +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; import java.nio.file.DirectoryStream; import java.nio.file.FileVisitResult; import java.nio.file.FileVisitor; @@ -12,9 +13,12 @@ import java.nio.file.Paths; import java.nio.file.attribute.BasicFileAttributes; import java.text.ParseException; +import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -36,29 +40,6 @@ public static void checkAllTypesAreSupported(Collection> checkTypes, } } - /** - * Handles an occurred exception. - * @param cause - * occurred exception. If null, an {@link Exception} is constructed via {@link - * Exception#Exception(String)}. - * @param message - * message that can be reported to user and is written to log. - * @param reportToUser - * if true, message is printed to {@link System#err}. - */ - public static void handleError(String message, Throwable cause, boolean reportToUser) - throws TerminalException { - if (reportToUser) { - System.err.println(message == null ? cause.getMessage() : message); - } - Log.log(Commands.class, cause, message); - if (cause == null) { - throw new TerminalException(message); - } else { - throw new TerminalException(message, cause); - } - } - public static byte[] insertArray(byte[] source, int sourceOffset, int sourceSize, @@ -92,7 +73,7 @@ public static void removeEmptyFilesAndFolders(Path rootDirectory) throws IOExcep */ public static void rm(final Path removePath) throws IOException { if (Files.isDirectory(removePath)) { - Files.walkFileTree(removePath, new Utility.FileTreeRemover()); + Files.walkFileTree(removePath, new FileTreeRemover()); } else { Files.delete(removePath); } @@ -205,7 +186,7 @@ public static void checkNotNull(Object variable, String name) throws IllegalArgu * @param * Value type in the source map. * @return An inversed map. It is not guaranteed that it is instance of the same class as source map has. - * @throws java.lang.IllegalArgumentException + * @throws IllegalArgumentException * If there are two keys having the same values. * @see Object#equals(Object) */ @@ -222,20 +203,6 @@ public static Map inverseMap(Map map) throws IllegalArgumentE return inversed; } - /** - * Forms a regular expression for a string inside quotes. - * @param quotes - * Sequence of symbols that plays role of quotes. - * @param escapeSequence - * Inside quotes escapeSequence and quotes must occur only after escapeSequence. - */ - public static String getQuotedStringRegex(String quotes, String escapeSequence) { - // Regex: "((plain text)|(escaped symbols))*" - - return quotes + "([^" + quotes + escapeSequence + "]|(" + escapeSequence + escapeSequence + ")|(" - + escapeSequence + quotes + "))*" + quotes; - } - /** * Returns string between two quotes. All quote and escape sequences inside the string are escaped by * escape sequence. @@ -246,21 +213,22 @@ public static String getQuotedStringRegex(String quotes, String escapeSequence) * @param escapeSequence * Escape sequence. Quotes and this sequence occurrences will be prepended by escape sequence. * @return Endcoded string inside quotes. Returns null for null string. - * @see Utility#unquoteString(String, String, String) + * @see Utility#unquoteString(String, + * String, String) */ public static String quoteString(String s, String quoteSequence, String escapeSequence) { if (s == null) { return null; } - s = s.replaceAll( - escapeSequence, escapeSequence + escapeSequence); - s = s.replaceAll( - quoteSequence, escapeSequence + quoteSequence); + s = s.replace(escapeSequence, escapeSequence + escapeSequence); + s = s.replace(quoteSequence, escapeSequence + quoteSequence); return quoteSequence + s + quoteSequence; } /** - * Decodes a quoted via {@link Utility#quoteString(String, String, String)} method string. + * Decodes a quoted via {@link ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support + * .Utility#quoteString(String, + * String, String)} method string. * @param s * Quoted string (must start and end with quote sequence). * @param quoteSequence @@ -277,9 +245,9 @@ public static String unquoteString(String s, String quoteSequence, String escape s = s.substring(1, s.length() - 1); - s = s.replaceAll( + s = s.replace( escapeSequence + "" + quoteSequence, quoteSequence); - s = s.replaceAll( + s = s.replace( escapeSequence + escapeSequence, escapeSequence); return s; } @@ -314,6 +282,23 @@ public static int findClosingQuotes(String string, return -1; } + public static List getAllAnnotatedFields(Class searchableClass, + Class annoClass) { + List gatheredFields = new LinkedList<>(); + + while (searchableClass != Object.class) { + Arrays.stream(searchableClass.getDeclaredFields()).forEach( + (field) -> { + if (field.getAnnotation(annoClass) != null) { + gatheredFields.add(field); + } + }); + searchableClass = searchableClass.getSuperclass(); + } + + return gatheredFields; + } + /** * File visitor that deletes empty files and empty folders. * @author phoenix diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/ValidityController.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/ValidityController.java new file mode 100644 index 000000000..b827f06f0 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/ValidityController.java @@ -0,0 +1,87 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support; + +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.InvalidatedObjectException; + +import java.io.Serializable; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +/** + * Convenience class to control one's validity. + */ +public final class ValidityController implements Serializable { + private final ReadWriteLock validityLock = new ReentrantReadWriteLock(true); + private boolean valid = true; + + private void checkValid() { + if (!valid) { + throw new InvalidatedObjectException("Object has been invalidated"); + } + } + + /** + * Returns activated lock on the use of the object. While object is used, nobody can invalidate it + * (except + * the host thread of this lock). + * @throws InvalidatedObjectException + * if this object has been already invalidated. + */ + public UseLock use() throws InvalidatedObjectException { + validityLock.readLock().lock(); + try { + checkValid(); + validityLock.readLock().lock(); + return new UseLock(); + } finally { + validityLock.readLock().unlock(); + } + } + + /** + * Returns activated unique lock for the use of the object. After use object is invalidated. + * @throws InvalidatedObjectException + * if this object has been already invalidated. + */ + public KillLock useAndKill() throws InvalidatedObjectException { + validityLock.writeLock().lock(); + try { + checkValid(); + validityLock.writeLock().lock(); + return new KillLock(); + } finally { + validityLock.writeLock().unlock(); + } + } + + public interface ValidityLock extends AutoCloseable { + @Override + void close(); + } + + public class UseLock implements ValidityLock { + private boolean allowMultipleCloseAttempts = false; + + @Override + public void close() { + if (!allowMultipleCloseAttempts) { + validityLock.readLock().unlock(); + } + } + + public KillLock obtainKillLockInstead() { + close(); + allowMultipleCloseAttempts = true; + // Further close() calls (e.g., from try-with-resources) will be ignored for UseLock. + return useAndKill(); + } + } + + public class KillLock implements ValidityLock { + @Override + public void close() { + valid = false; + validityLock.writeLock().unlock(); + } + } + +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/DBShellGeneralState.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/DBShellGeneralState.java new file mode 100644 index 000000000..393e0188e --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/DBShellGeneralState.java @@ -0,0 +1,152 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet; + +import ru.fizteh.fivt.storage.structured.TableProvider; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.ExecutionNotPermittedException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.TerminalException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.servlet.HttpDBServerCommands; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.servlet.HttpDBServerState; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.AbstractCommand; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Command; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.JoinedState; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Shell; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.ShellState; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet.client.ClientCommands; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet.client.ClientGeneralState; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet.server.ServerCommands; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet.server.TelnetDBServerState; + +import java.util.List; + +public class DBShellGeneralState extends JoinedState { + private static final int CLIENT_STATE_ID = 0; + private static final int TELNET_SERVER_STATE_ID = 1; + private static final int HTTP_SERVER_STATE_ID = 2; + + private ClientGeneralState clientState; + private TelnetDBServerState telnetServerState; + private HttpDBServerState httpServerState; + + public DBShellGeneralState(TableProvider provider, String databaseRoot) throws TerminalException { + //TODO: we need remote provider here. + + clientState = new ClientGeneralState(); + telnetServerState = new TelnetDBServerState(provider, databaseRoot); + httpServerState = new HttpDBServerState(provider); + + // Fake initialization. + new Shell<>(clientState); + new Shell<>(telnetServerState); + new Shell<>(httpServerState); + + setAllCommands( + clientState.getCommands(), telnetServerState.getCommands(), httpServerState.getCommands()); + setExceptionHandler( + (exception, noData) -> AbstractCommand.DEFAULT_EXCEPTION_HANDLER + .handleException(exception, getOutputStream())); + } + + @Override + protected void onExecuteConflict(List stateIDs, List commands, String[] args) + throws Exception { + Command telnetServerCommand = null; + Command clientCommand = null; + Command httpServerCommand = null; + + for (int i = 0; i < stateIDs.size(); i++) { + switch (stateIDs.get(i)) { + case CLIENT_STATE_ID: { + clientCommand = commands.get(i); + break; + + } + case TELNET_SERVER_STATE_ID: { + telnetServerCommand = commands.get(i); + break; + } + case HTTP_SERVER_STATE_ID: { + httpServerCommand = commands.get(i); + break; + } + default: { + throw new IllegalArgumentException("Illegal state ID: " + stateIDs.get(i)); + } + } + } + + boolean telnetServerActive = telnetServerState.isStarted(); + boolean clientActive = clientState.isConnected(); + boolean httpServerActive = httpServerState.isStarted(); + + boolean nobodyActive = !telnetServerActive && !clientActive && !httpServerActive; + + boolean helpCommand = areNamesEqual(telnetServerCommand, ServerCommands.HELP) && areNamesEqual( + clientCommand, ClientCommands.HELP) && areNamesEqual( + httpServerCommand, HttpDBServerCommands.HELP); + boolean exitCommand = areNamesEqual(telnetServerCommand, ServerCommands.EXIT) && areNamesEqual( + clientCommand, ClientCommands.EXIT) && areNamesEqual( + httpServerCommand, HttpDBServerCommands.EXIT); + + // Conflict between server and client EXIT or HELP command. + // if (telnetServerActive || httpServerActive) { + // if (telnetServerActive) { + // // Telnet Server variant + // onExecuteRequested(TELNET_SERVER_STATE_ID, telnetServerCommand, args); + // } + // if (httpServerActive) { + // onExecuteRequested(HTTP_SERVER_STATE_ID, httpServerCommand, args); + // } + // } else if (clientActive) { + // // Client variant + // onExecuteRequested(CLIENT_STATE_ID, clientCommand, args); + // } else + if (helpCommand) { + if (clientActive || nobodyActive) { + executeNormally(CLIENT_STATE_ID, clientCommand, args); + } + if (!clientActive || nobodyActive) { + executeNormally(HTTP_SERVER_STATE_ID, httpServerCommand, args); + executeNormally(TELNET_SERVER_STATE_ID, telnetServerCommand, args); + } + } else if (exitCommand) { + prepareToExit(0); + } else { + throw new UnsupportedOperationException( + "Cannot resolve command conflict: " + + telnetServerCommand.getName() + + " and " + + clientCommand.getName()); + } + } + + @Override + protected ShellState obtainState(int stateID) { + switch (stateID) { + case CLIENT_STATE_ID: + return clientState; + case TELNET_SERVER_STATE_ID: + return telnetServerState; + case HTTP_SERVER_STATE_ID: + return httpServerState; + default: + throw new IllegalArgumentException("Illegal state ID: " + stateID); + } + } + + @Override + protected void onExecuteRequested(int stateID, Command command, String[] args) throws Exception { + if (stateID == CLIENT_STATE_ID) { + if (telnetServerState.isStarted() || httpServerState.isStarted()) { + throw new ExecutionNotPermittedException("You cannot execute this command in server mode"); + } else { + executeNormally(stateID, command, args); + } + } else { + if (clientState.isConnected()) { + throw new ExecutionNotPermittedException( + "You cannot execute this command when connected as client"); + } else { + executeNormally(stateID, command, args); + } + } + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/client/ClientCommands.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/client/ClientCommands.java new file mode 100644 index 000000000..bf31bb26b --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/client/ClientCommands.java @@ -0,0 +1,117 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet.client; + +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.DatabaseIOException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.InvocationException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.NoActiveTableException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.TerminalException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.AbstractCommand; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Command; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.SimpleCommandContainer; + +import java.io.IOException; +import java.net.InetAddress; +import java.text.ParseException; +import java.util.Map; + +public final class ClientCommands extends SimpleCommandContainer { + public static final Command CONNECT = new AbstractCommand( + "connect", " ", "connects to a database storage server", 3) { + @Override + public void executeSafely(DBClientState state, String[] args) throws + IllegalArgumentException, + NoActiveTableException, + IllegalStateException, + NullPointerException, + InvocationException, + ParseException, + IOException { + int port; + + try { + port = Integer.parseInt(args[2]); + } catch (NumberFormatException exc) { + throw new NumberFormatException("not connected: " + exc.getMessage()); + } + state.connect(args[1], port); + state.getOutputStream().println("connected"); + } + }; + public static final Command DISCONNECT = + new AbstractCommand("disconnect", "", "disconnects from database storage", 1) { + @Override + public void executeSafely(DBClientState state, String[] args) throws + IllegalArgumentException, + NoActiveTableException, + IllegalStateException, + NullPointerException, + InvocationException, + ParseException, + IOException { + state.disconnect(); + state.getOutputStream().println("disconnected"); + } + }; + public static final Command WHEREAMI = new AbstractCommand( + "whereami", "", "prints host and port you are connected to", 1) { + @Override + public void executeSafely(DBClientState state, String[] args) throws + IllegalArgumentException, + NoActiveTableException, + IllegalStateException, + NullPointerException, + InvocationException, + ParseException, + IOException { + String address = state.getHost(); + int port = state.getPort(); + + InetAddress inetAddress = InetAddress.getByName(address); + if (inetAddress.isLoopbackAddress()) { + state.getOutputStream().println("local " + port); + } else { + state.getOutputStream().println("remote " + inetAddress.getHostAddress() + ":" + port); + } + } + }; + public static final Command HELP = new AbstractCommand( + "help", null, "prints out description of state commands", 1, Integer.MAX_VALUE) { + @Override + public void execute(DBClientState state, String[] args) { + Map> commands = state.getCommands(); + + state.getOutputStream().println( + "You can connect to a database storage server"); + + commands.values().forEach((command) -> state.getOutputStream().println(command.buildHelpLine())); + } + + @Override + public void executeSafely(DBClientState state, String[] args) throws DatabaseIOException { + // not used + } + }; + public static final Command EXIT = new AbstractCommand( + "exit", null, "saves all data to file system and stops interpretation", 1) { + @Override + public void execute(DBClientState state, String[] args) throws TerminalException { + state.prepareToExit(0); + + // If all contracts are honoured, this line should not be reached. + throw new AssertionError("Exit request not thrown"); + } + + @Override + public void executeSafely(DBClientState state, String[] args) + throws DatabaseIOException, IllegalArgumentException { + // Not used. + } + }; + private static final ClientCommands INSTANCE = new ClientCommands(); + + private ClientCommands() { + } + + public static ClientCommands getInstance() { + return INSTANCE; + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/client/ClientGeneralState.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/client/ClientGeneralState.java new file mode 100644 index 000000000..8f58bb37f --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/client/ClientGeneralState.java @@ -0,0 +1,125 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet.client; + +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.Database; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.ExecutionNotPermittedException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.AbstractCommand; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Command; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.JoinedState; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Shell; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.ShellState; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.SingleDBCommands; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.SingleDatabaseShellState; + +import java.util.List; + +public class ClientGeneralState extends JoinedState { + private static final int DB_STATE_ID = 0; + private static final int CLIENT_STATE_ID = 1; + + private final DBClientState clientState = new DBClientState(); + private SingleDatabaseShellState databaseState; + + public ClientGeneralState() { + setAllCommands( + SingleDBCommands.getInstance().getCommands(), ClientCommands.getInstance().getCommands()); + } + + public boolean isConnected() { + return clientState.isConnected(); + } + + protected Database obtainNewActiveDatabase() throws Exception { + return new Database(clientState.getRemoteProvider(), clientState.getHost(), getOutputStream()); + } + + @Override + public void init(Shell host) throws Exception { + super.init(host); + clientState.init(host); + setExceptionHandler( + (exception, noData) -> AbstractCommand.DEFAULT_EXCEPTION_HANDLER + .handleException(exception, getOutputStream())); + } + + @Override + protected void onExecuteConflict(List stateIDs, List commands, String[] args) + throws Exception { + Command clientCommand = null; + Command databaseCommand = null; + + for (int i = 0; i < stateIDs.size(); i++) { + switch (stateIDs.get(i)) { + case CLIENT_STATE_ID: { + clientCommand = commands.get(i); + break; + } + case DB_STATE_ID: { + databaseCommand = commands.get(i); + break; + } + default: { + throw new IllegalArgumentException("Illegal state ID: " + stateIDs.get(i)); + } + } + } + + if (areNamesEqual(clientCommand, ClientCommands.HELP) && areNamesEqual( + databaseCommand, SingleDBCommands.HELP)) { + onExecuteRequested(CLIENT_STATE_ID, clientCommand, args); + if (clientState.isConnected()) { + onExecuteRequested(DB_STATE_ID, databaseCommand, args); + } + } else if (areNamesEqual(clientCommand, ClientCommands.EXIT) && areNamesEqual( + databaseCommand, SingleDBCommands.EXIT)) { + prepareToExit(0); + } else { + throw new UnsupportedOperationException( + "Cannot resolve command conflict: " + clientCommand.getName()); + } + } + + @Override + protected ShellState obtainState(int stateID) { + if (stateID == CLIENT_STATE_ID) { + return clientState; + } else if (stateID == DB_STATE_ID) { + return databaseState; + } else { + throw new IllegalArgumentException("Illegal state id: " + stateID); + } + } + + @Override + protected void onExecuteRequested(int stateID, Command command, String[] args) throws Exception { + if (stateID == CLIENT_STATE_ID) { + executeNormally(stateID, command, args); + + if (areNamesEqual(ClientCommands.CONNECT, command)) { + ClientGeneralState generalState = this; + + databaseState = new SingleDatabaseShellState() { + + @Override + protected Database obtainNewActiveDatabase() throws Exception { + return generalState.obtainNewActiveDatabase(); + } + }; + try { + new Shell<>(databaseState); + } catch (Exception exc) { + throw new RuntimeException( + "Failed to build joined state: " + exc.getMessage(), exc.getCause()); + } + } else if (areNamesEqual(ClientCommands.DISCONNECT, command)) { + databaseState.cleanup(); + databaseState = null; + } + } else if (stateID == DB_STATE_ID) { + if (!clientState.isConnected()) { + throw new ExecutionNotPermittedException("You should connect to a database storage at first"); + } else { + executeNormally(stateID, command, args); + } + } + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/client/DBClientState.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/client/DBClientState.java new file mode 100644 index 000000000..3c416d1ad --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/client/DBClientState.java @@ -0,0 +1,81 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet.client; + +import ru.fizteh.fivt.storage.structured.RemoteTableProvider; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.remote.RemoteDatabaseStorage; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.BaseShellState; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Command; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Log; + +import java.io.IOException; +import java.util.Map; + +public class DBClientState extends BaseShellState { + private final RemoteDatabaseStorage storage = new RemoteDatabaseStorage(); + private RemoteTableProvider remoteProvider; + private String host; + private int port = -1; + + public void connect(String host, int port) throws IOException { + checkInitialized(); + if (remoteProvider == null) { + try { + remoteProvider = storage.connect(host, port); + this.host = host; + this.port = port; + } catch (IOException exc) { + throw new IOException("not connected: " + exc.getMessage(), exc.getCause()); + } + } else { + throw new IllegalStateException("not connected: already connected"); + } + } + + public RemoteTableProvider getRemoteProvider() { + return remoteProvider; + } + + public String getHost() { + requireConnected(); + return host; + } + + public int getPort() { + requireConnected(); + return port; + } + + public void disconnect() throws IllegalStateException, IOException { + requireConnected(); + try { + remoteProvider.close(); + } finally { + remoteProvider = null; + } + } + + private void requireConnected() { + if (!isConnected()) { + throw new IllegalStateException("not connected"); + } + } + + public boolean isConnected() { + return remoteProvider != null; + } + + @Override + public void cleanup() { + try { + if (isConnected()) { + disconnect(); + } + } catch (IOException exc) { + Log.log(DBClientState.class, exc, "Exception occurred on cleanup"); + } + } + + @Override + public Map> getCommands() { + return ClientCommands.getInstance().getCommands(); + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/server/Server.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/server/Server.java new file mode 100644 index 000000000..628a92764 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/server/Server.java @@ -0,0 +1,168 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet.server; + +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.TerminalException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.ShellState; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Log; + +import java.io.Closeable; +import java.io.IOException; +import java.io.PrintStream; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.function.Supplier; + +/** + * Server that listens to connections and creates communicator threads. + */ +public class Server implements Closeable { + private final PrintStream reportStream; + private final Set users; + private Supplier shellStateSupplier; + private ServerSocket serverSocket; + private boolean deletingFromUsers = false; + + public Server(PrintStream reportStream) { + this.reportStream = reportStream; + users = new HashSet<>(); + } + + public synchronized Supplier getShellStateSupplier() { + return shellStateSupplier; + } + + public synchronized void setShellStateSupplier(Supplier shellStateSupplier) { + if (isStarted()) { + throw new IllegalStateException("Cannot set state supplier: already started"); + } + this.shellStateSupplier = shellStateSupplier; + } + + public PrintStream getReportStream() { + return reportStream; + } + + public synchronized ServerSocket getServerSocket() { + return serverSocket; + } + + /** + * Called by ServerCommunicator after it is closed. + */ + synchronized void onConnectionClosed(ServerCommunicator communicator) { + Log.log(Server.class, "Disconnected: " + communicator); + if (!deletingFromUsers) { + users.remove(communicator); + } + } + + public synchronized boolean isStarted() { + return serverSocket != null; + } + + public synchronized void startServer(int port) throws IOException, IllegalStateException { + if (isStarted()) { + throw new IllegalStateException("not started: already started"); + } + + if (shellStateSupplier == null) { + throw new IllegalStateException("not started: state supplier not defined"); + } + + try { + serverSocket = new ServerSocket(port); + } catch (IOException exc) { + throw new IOException("not started: " + exc.getMessage(), exc.getCause()); + } + + Thread serverThread = new Thread(this::run, this + ": server thread"); + serverThread.setDaemon(true); + serverThread.setPriority(Thread.MIN_PRIORITY); + serverThread.start(); + } + + public synchronized void stopServerIfStarted() throws IOException { + if (isStarted()) { + close(); + } + } + + public synchronized int stopServer() throws IOException, IllegalStateException { + requireStarted(); + int port = getServerSocket().getLocalPort(); + close(); + return port; + } + + public synchronized List listUsers() { + requireStarted(); + return new LinkedList<>(users); + } + + private void run() { + try { + while (!Thread.currentThread().isInterrupted()) { + ServerSocket serverSocketTemp; + + synchronized (this) { + serverSocketTemp = serverSocket; + } + + if (serverSocketTemp == null) { + return; + } + + Socket socket = serverSocketTemp.accept(); + ServerCommunicator communicator = new ServerCommunicator(socket, this, shellStateSupplier); + Thread communicatorThread = new Thread(communicator, "Communicator with " + communicator); + communicatorThread.setDaemon(true); + communicatorThread.start(); + + synchronized (this) { + users.add(communicator); + } + } + } catch (IOException exc) { + Log.log(Server.class, exc, "Error on listening to incoming connections"); + } catch (TerminalException exc) { + // Already handled. + } finally { + try { + close(); + } catch (IOException exc) { + Log.log(TelnetDBServerState.class, exc, this + ": failed to close server socket"); + } + } + } + + private void requireStarted() throws IllegalStateException { + if (!isStarted()) { + throw new IllegalStateException("not started"); + } + } + + @Override + public synchronized void close() throws IOException { + if (serverSocket != null) { + deletingFromUsers = true; + for (ServerCommunicator communicator : users) { + try { + communicator.close(); + } catch (IOException exc) { + Log.log(Server.class, exc, "Error on closing socket: " + communicator); + } + } + users.clear(); + deletingFromUsers = false; + + try { + serverSocket.close(); + } finally { + serverSocket = null; + } + } + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/server/ServerCommands.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/server/ServerCommands.java new file mode 100644 index 000000000..5d37eeb56 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/server/ServerCommands.java @@ -0,0 +1,141 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet.server; + +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.DatabaseIOException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.InvocationException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.NoActiveTableException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.TerminalException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.AbstractCommand; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Command; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.SimpleCommandContainer; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.SingleDatabaseShellState; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet.server.TelnetDBServerState.User; + +import java.io.IOException; +import java.text.ParseException; +import java.util.List; +import java.util.Map; + +/** + * Container for server commands: start, stop, listusers. + */ +public class ServerCommands extends SimpleCommandContainer { + public static final Command STOP = + new AbstractCommand("stop", "", "stops server", 1) { + @Override + public void executeSafely(TelnetDBServerState state, String[] args) throws + IllegalArgumentException, + NoActiveTableException, + IllegalStateException, + NullPointerException, + InvocationException, + ParseException, + IOException { + int port = state.stopServer(); + state.getOutputStream().println("stopped at " + port); + } + }; + public static final Command LISTUSERS = new AbstractCommand( + "listusers", "", "prints list of ip addresses and ports of connected users", 1) { + @Override + public void executeSafely(TelnetDBServerState state, String[] args) throws + IllegalArgumentException, + NoActiveTableException, + IllegalStateException, + NullPointerException, + InvocationException, + ParseException, + IOException { + List users = state.listUsers(); + + StringBuilder sb = new StringBuilder(); + for (User user : users) { + sb.append(user); + sb.append(System.lineSeparator()); + } + state.getOutputStream().print(sb.toString()); + } + }; + public static final Command EXIT = new AbstractCommand( + "exit", "", "stops server (if it is started) and closes the terminal", 1) { + @Override + public void execute(TelnetDBServerState state, String[] args) throws TerminalException { + state.prepareToExit(0); + + // If all contracts are honoured, this line should not be reached. + throw new AssertionError("Exit request not thrown"); + } + + @Override + public void executeSafely(TelnetDBServerState state, String[] args) throws + IllegalArgumentException, + NoActiveTableException, + IllegalStateException, + NullPointerException, + InvocationException, + ParseException, + IOException { + // Not used. + } + }; + public static final Command HELP = new AbstractCommand( + "help", "", "prints out description of state commands", 1, Integer.MAX_VALUE) { + @Override + public void execute(TelnetDBServerState state, String[] args) { + Map> commands = state.getCommands(); + + state.getOutputStream().println( + "You can start telnet database server ready for new connections!"); + + state.getOutputStream().println( + String.format( + "You can set database directory to work with using environment " + + "variable '%s'", SingleDatabaseShellState.DB_DIRECTORY_PROPERTY_NAME)); + + for (Command command : commands.values()) { + state.getOutputStream().println(command.buildHelpLine()); + } + } + + @Override + public void executeSafely(TelnetDBServerState state, String[] args) throws DatabaseIOException { + // not used + } + }; + private static final int DEFAULT_PORT = 10001; + public static final Command START = new AbstractCommand( + "start", + "[port]", + "starts server at the specified port (or, if not specified, at " + DEFAULT_PORT + ")", + 1, + 2) { + @Override + public void executeSafely(TelnetDBServerState state, String[] args) throws + IllegalArgumentException, + NoActiveTableException, + IllegalStateException, + InvocationException, + ParseException, + IOException { + int port; + if (args.length == 1) { + port = DEFAULT_PORT; + } else { + port = Integer.parseInt(args[1]); + } + + state.startServer(port); + state.getOutputStream().println("started at " + port); + } + }; + private static final ServerCommands INSTANCE = new ServerCommands(); + + /** + * Not for initializing. + */ + private ServerCommands() { + } + + public static ServerCommands obtainInstance() { + return INSTANCE; + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/server/ServerCommunicator.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/server/ServerCommunicator.java new file mode 100644 index 000000000..ec409ef08 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/server/ServerCommunicator.java @@ -0,0 +1,89 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet.server; + +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.TerminalException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Shell; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.ShellState; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Log; + +import java.io.Closeable; +import java.io.IOException; +import java.io.PrintStream; +import java.net.Socket; +import java.util.function.Supplier; + +public class ServerCommunicator implements Runnable, Closeable { + private final Socket socket; + + private final Shell interpreter; + + private final PrintStream reportStream; + + private final Server server; + + private boolean closed = false; + + public ServerCommunicator(Socket socket, Server server, Supplier shellStateSupplier) + throws IOException, TerminalException { + this.socket = socket; + this.server = server; + this.reportStream = server.getReportStream(); + this.interpreter = new Shell<>(shellStateSupplier.get(), socket.getOutputStream()); + } + + public Socket getSocket() { + return socket; + } + + @Override + public void run() { + try { + interpreter.run(socket.getInputStream()); + } catch (TerminalException exc) { + Log.log(ServerCommunicator.class, exc, "Interpreter run terminated"); + // Already handled. + } catch (IOException exc) { + reportStream.println(this + ": Error on obtaining socket input stream: " + exc.getMessage()); + } finally { + try { + close(); + } catch (IOException exc) { + Log.log( + TelnetDBServerState.class, + this + " failed to close connection after all commands execution"); + } + + } + } + + private void notifyConnectionClosed() { + server.onConnectionClosed(this); + } + + @Override + public synchronized void close() throws IOException { + if (!closed) { + closed = true; + socket.close(); + notifyConnectionClosed(); + } + } + + @Override + public int hashCode() { + return socket.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof ServerCommunicator) { + ServerCommunicator user = (ServerCommunicator) obj; + return socket.equals(user.socket); + } + return false; + } + + @Override + public String toString() { + return socket.toString(); + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/server/TelnetDBServerState.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/server/TelnetDBServerState.java new file mode 100644 index 000000000..c4c0484d5 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/server/TelnetDBServerState.java @@ -0,0 +1,152 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet.server; + +import ru.fizteh.fivt.storage.structured.RemoteTableProvider; +import ru.fizteh.fivt.storage.structured.RemoteTableProviderFactory; +import ru.fizteh.fivt.storage.structured.TableProvider; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.Database; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.remote.RemoteDatabaseStorage; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.remote.RemoteTableProviderFactoryImpl; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.BaseShellState; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Command; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Shell; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.ShellState; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.SingleDatabaseShellState; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Log; + +import java.io.IOException; +import java.net.InetAddress; +import java.rmi.registry.Registry; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +public class TelnetDBServerState extends BaseShellState { + private static final ServerCommands COMMANDS = ServerCommands.obtainInstance(); + private final String databaseRoot; + private final TableProvider localProvider; + private Supplier clientShellStateSupplier; + private Server server; + private RemoteTableProviderFactoryImpl factory; + + public TelnetDBServerState(TableProvider localProvider, String databaseRoot) { + this.localProvider = localProvider; + this.databaseRoot = databaseRoot; + RemoteTableProviderFactory storage = new RemoteDatabaseStorage(); + TelnetDBServerState serverState = this; + this.clientShellStateSupplier = () -> new SingleDatabaseShellState() { + @Override + protected Database obtainNewActiveDatabase() throws Exception { + RemoteTableProvider provider = storage.connect("127.0.0.1", Registry.REGISTRY_PORT); + return new Database( + provider, serverState.getDatabaseRoot(), getOutputStream()); + } + }; + } + + public String getDatabaseRoot() { + return databaseRoot; + } + + public void startServer(int port) throws IOException, IllegalStateException { + checkInitialized(); + if (server.isStarted()) { + throw new IllegalStateException("not started: already started"); + } + + try { + factory = new RemoteTableProviderFactoryImpl(localProvider); + factory.establishStorage(databaseRoot); + + server.setShellStateSupplier(clientShellStateSupplier); + server.startServer(port); + } catch (Exception exc) { + try { + if (factory != null) { + factory.close(); + } + } catch (Exception ignored) { + Log.log( + TelnetDBServerState.class, + ignored, + "Failed to close factory after server install failure"); + } + throw exc; + } + } + + public boolean isStarted() { + return server.isStarted(); + } + + private void closeFactory() throws IOException { + factory.close(); + factory = null; + } + + public int stopServer() throws IOException { + checkInitialized(); + int port = server.stopServer(); + closeFactory(); + return port; + } + + public void stopServerIfStarted() throws IOException { + checkInitialized(); + server.stopServerIfStarted(); + if (factory != null) { + closeFactory(); + } + } + + public List listUsers() { + checkInitialized(); + return server.listUsers().stream().map( + (ServerCommunicator communicator) -> new User( + communicator.getSocket().getInetAddress(), communicator.getSocket().getPort())) + .collect(Collectors.toList()); + } + + @Override + public void cleanup() { + try { + stopServerIfStarted(); + } catch (Exception exc) { + Log.log(TelnetDBServerState.class, exc); + } + } + + @Override + public void init(Shell host) throws Exception { + super.init(host); + server = new Server(host.getOutputStream()); + } + + @Override + public Map> getCommands() { + return COMMANDS.getCommands(); + } + + public static class User { + private final InetAddress address; + private final int port; + + public User(InetAddress address, int port) { + this.address = address; + this.port = port; + } + + public InetAddress getAddress() { + return address; + } + + public int getPort() { + return port; + } + + @Override + public String toString() { + return address.getHostAddress() + ":" + port; + } + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/ClientGeneralStateTest.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/ClientGeneralStateTest.java new file mode 100644 index 000000000..6e8cdc8d6 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/ClientGeneralStateTest.java @@ -0,0 +1,110 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import ru.fizteh.fivt.storage.structured.RemoteTableProvider; +import ru.fizteh.fivt.storage.structured.RemoteTableProviderFactory; +import ru.fizteh.fivt.storage.structured.TableProvider; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.AutoCloseableTableProviderFactory; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.DBTableProviderFactory; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.Database; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.remote.RemoteDatabaseStorage; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.TerminalException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Shell; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet.client.ClientGeneralState; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet.server.TelnetDBServerState; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test.support.RegexMatcher; + +import java.io.IOException; +import java.io.PrintStream; +import java.rmi.registry.Registry; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +@RunWith(JUnit4.class) +public class ClientGeneralStateTest extends InterpreterTestBase { + private TelnetDBServerState serverState; + private ClientGeneralState clientState; + + private AutoCloseableTableProviderFactory factory; + + @Override + protected Shell constructInterpreter() throws TerminalException { + this.clientState = new ClientGeneralState() { + @Override + protected Database obtainNewActiveDatabase() throws Exception { + RemoteTableProviderFactory factory = new RemoteDatabaseStorage(); + RemoteTableProvider provider = factory.connect("localhost", Registry.REGISTRY_PORT); + return new Database(provider, "", getOutputStream()); + } + + @Override + public PrintStream getOutputStream() { + return System.out; + } + }; + return new Shell<>(clientState); + } + + @Before + public void prepareRemoteAPITest() throws Exception { + factory = new DBTableProviderFactory(); + TableProvider provider = factory.create(DB_ROOT.toString()); + + serverState = new TelnetDBServerState(provider, DB_ROOT.toString()); + new Shell(serverState); + serverState.startServer(10001); + } + + @After + public void cleanupRemoteAPITest() throws IOException { + serverState.stopServerIfStarted(); + factory.close(); + } + + @Test + public void testWhereAmI() throws IOException, TerminalException { + runBatchExpectZero("connect localhost 1099", "whereami"); + assertEquals(makeTerminalExpectedMessage("connected", "local 1099"), getOutput()); + } + + @Test + public void testCreateTableAndPutSmth() throws IOException, TerminalException { + runBatchExpectZero( + "connect localhost 1099", "create t1 (String)", "use t1", "put a [\"b\"]", "commit"); + assertEquals( + makeTerminalExpectedMessage("connected", "created", "using t1", "new", "1"), getOutput()); + runBatchExpectZero("connect localhost 1099", "use t1", "get a"); + assertEquals(makeTerminalExpectedMessage("connected", "using t1", "found", "[\"b\"]"), getOutput()); + } + + @Test + public void testConnectToNotExistentServer() throws IOException, TerminalException { + runBatchExpectNonZero("connect 127.0.0.1 10500"); + assertThat(getOutput(), startsWith("not connected")); + } + + @Test + public void testConnectAndCallNotExistentCommand() throws IOException, TerminalException { + runInteractiveExpectZero("connect 127.0.0.1 1099", "not_exists_yeah?", "disconnect"); + assertThat( + getOutput(), new RegexMatcher( + makeTerminalExpectedRegex( + "\\Q" + clientState.getGreetingString() + "\\E", + "connected", + "\\Qnot_exists_yeah?: command is missing\\E", + "disconnected"))); + } + + @Test + public void testCallShowTablesWithoutConnect() throws IOException, TerminalException { + runBatchExpectNonZero("show tables"); + assertEquals( + getOutput(), makeTerminalExpectedMessage( + "You should connect to a database storage at first")); + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/ControllableRunnerTest.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/ControllableRunnerTest.java new file mode 100644 index 000000000..facf329ec --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/ControllableRunnerTest.java @@ -0,0 +1,116 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.parallel.ControllableAgent; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.parallel.ControllableRunnable; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.parallel.ControllableRunner; + +import static org.junit.Assert.*; + +@RunWith(JUnit4.class) +public class ControllableRunnerTest extends DuplicatedIOTestBase { + private static final String NEW_LINE = System.lineSeparator(); + + @Test + public void testWaitForEndOfWork() throws Exception { + ControllableRunner runner = new ControllableRunner(); + ControllableRunnable runnable = runner.createControllable( + (ControllableAgent agent) -> { + try { + Thread.sleep(2000L); + } catch (InterruptedException exc) { + throw new AssertionError(exc); + } + System.err.println("Hello from runnable"); + }); + runner.assignRunnable(runnable); + + new Thread(runner, "Runnable").start(); + runner.waitUntilPause(); + + System.err.println("After execution"); + + assertEquals("Hello from runnable" + NEW_LINE + "After execution" + NEW_LINE, getOutput()); + } + + @Test + public void testWaitForEndOfWork1() throws Exception { + ControllableRunner runner = new ControllableRunner(); + ControllableRunnable runnable = runner.createControllable( + (ControllableAgent agent) -> System.err.println("Hello from runnable")); + runner.assignRunnable(runnable); + + new Thread(runner, "Runnable").start(); + + // Possibly giving time to finish; + Thread.sleep(2000L); + runner.waitUntilPause(); + + System.err.println("After execution"); + + assertEquals("Hello from runnable" + NEW_LINE + "After execution" + NEW_LINE, getOutput()); + } + + @Test + public void testContinueInCheckpoint() throws Throwable { + ControllableRunner runner = new ControllableRunner(); + ControllableRunnable runnable = runner.createControllable( + (ControllableAgent agent) -> { + System.err.println("Before checkpoint"); + try { + agent.notifyAndWait(); + } catch (InterruptedException exc) { + throw new AssertionError(exc); + } + System.err.println("After checkpoint"); + }); + runner.assignRunnable(runnable); + + new Thread(runner, "Runnable").start(); + runner.waitUntilPause(); + System.err.println("In checkpoint"); + runner.continueWork(); + runner.waitUntilPause(); + System.err.println("After execution"); + + assertEquals( + String.join( + NEW_LINE, + "Before checkpoint", + "In checkpoint", + "After checkpoint", + "After execution", + ""), getOutput()); + } + + @Test + public void testInterruptInCheckpoint() throws InterruptedException, Exception { + ControllableRunner runner = new ControllableRunner(); + ControllableRunnable runnable = runner.createControllable( + (ControllableAgent agent) -> { + System.err.println("Before checkpoint"); + try { + agent.notifyAndWait(); + } catch (InterruptedException exc) { + System.err.println("Interrupted"); + return; + } + System.err.println("Must not be written"); + }); + runner.assignRunnable(runnable); + + new Thread(runner, "Runnable").start(); + runner.waitUntilPause(); + System.err.println("In checkpoint"); + runner.interruptWork(); + runner.waitUntilPause(); // Waiting until execution ends. + System.err.println("After execution"); + + assertEquals( + String.join( + NEW_LINE, "Before checkpoint", "In checkpoint", "Interrupted", "After execution", ""), + getOutput()); + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/DBServerTest.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/DBServerTest.java new file mode 100644 index 000000000..9cb2cbd9b --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/DBServerTest.java @@ -0,0 +1,91 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import ru.fizteh.fivt.storage.structured.TableProvider; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.AutoCloseableTableProviderFactory; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.DBTableProviderFactory; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Shell; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet.server.TelnetDBServerState; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; + +import static org.junit.Assert.*; + +@RunWith(JUnit4.class) +public class DBServerTest extends TestBase { + @Rule + public ExpectedException exception = ExpectedException.none(); + private TelnetDBServerState serverState; + + private AutoCloseableTableProviderFactory factory; + + @Before + public void prepareRemoteAPITest() throws Exception { + factory = new DBTableProviderFactory(); + TableProvider provider = factory.create(DB_ROOT.toString()); + + serverState = new TelnetDBServerState(provider, DB_ROOT.toString()); + new Shell(serverState); + } + + @After + public void cleanupRemoteAPITest() throws IOException { + serverState.stopServerIfStarted(); + factory.close(); + } + + private String collectFromInputStream(InputStream inputStream) throws IOException { + StringBuilder sb = new StringBuilder(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) { + while (reader.ready()) { + sb.append(reader.readLine()).append(System.lineSeparator()); + } + } + return sb.toString(); + } + + @Test + public void testDoubleStart() throws IOException { + serverState.startServer(10001); + exception.expect(IllegalStateException.class); + exception.expectMessage("not started: already started"); + serverState.startServer(10002); + } + + @Test + public void testDisconnectNotConnected() throws IOException { + exception.expect(IllegalStateException.class); + exception.expectMessage("not started"); + serverState.stopServer(); + } + + @Test + public void testStartStop() throws IOException { + serverState.startServer(10001); + assertTrue(serverState.isStarted()); + serverState.stopServer(); + assertFalse(serverState.isStarted()); + } + + @Test + public void testStopIfConnected() throws IOException { + serverState.stopServerIfStarted(); + } + + @Test + public void testStopIfConnected2() throws IOException { + serverState.startServer(10001); + assertTrue(serverState.isStarted()); + serverState.stopServerIfStarted(); + assertFalse(serverState.isStarted()); + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/DatabaseShellTest.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/DatabaseShellTest.java index 7f80fcde6..f5ce3b786 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/DatabaseShellTest.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/DatabaseShellTest.java @@ -5,9 +5,11 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.DatabaseIOException; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.TerminalException; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Shell; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.SingleDatabaseShellState; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.SingleFactoryDatabaseShellState; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test.support.MutatedSDSS; import java.io.FileOutputStream; @@ -27,41 +29,31 @@ */ @RunWith(JUnit4.class) public class DatabaseShellTest extends InterpreterTestBase { + @BeforeClass public static void globalPrepareDatabaseShellTest() { System.setProperty(SingleDatabaseShellState.DB_DIRECTORY_PROPERTY_NAME, DB_ROOT.toString()); } - private void createAndUseTable(String table) throws TerminalException { - runBatchExpectZero(String.format("create %1$s (String); use %1$s", table)); + private void createTableWithStringColumn(String table) throws TerminalException { + runBatchExpectZero(String.format("create %1$s (String)", table)); assertEquals( - "Creating and using table", String.format("created%nusing %1$s%n", table), getOutput()); - } - - @Test - public void testFailPersistOnExit() throws TerminalException { - MutatedSDSS state = new MutatedSDSS(0); - interpreter = new Shell<>(state); - - runBatchExpectNonZero("exit"); - assertEquals( - "Failing on persist() call", - makeTerminalExpectedMessage("Fail on commit [test mode]"), - getOutput()); + "Creating table", makeTerminalExpectedMessage("created"), getOutput()); } @Test public void testPutCompositeStoreable() throws TerminalException { - runBatchExpectZero("create t1 (String int boolean)", "use t1"); - runBatchExpectZero("put key [ \"hello\", 2, null ]"); + runBatchExpectZero( + "create t1 (String int boolean)", "use t1", "put key [ \"hello\", 2, null ]", "commit"); - assertEquals("Output after put", makeTerminalExpectedMessage("new"), getOutput()); + assertThat("Output after put", getOutput(), containsString(makeTerminalExpectedMessage("new"))); - runBatchExpectZero(true, "use t1"); - runBatchExpectZero("get key"); + runBatchExpectZero("use t1", "get key"); - assertEquals( - "Output after get", makeTerminalExpectedMessage("found", "[\"hello\",2,null]"), getOutput()); + assertThat( + "Output after get", getOutput(), containsString( + makeTerminalExpectedMessage( + "found", "[\"hello\",2,null]"))); } @Test @@ -76,7 +68,7 @@ public void testNotExistingCommand() throws TerminalException { @Override protected Shell constructInterpreter() throws TerminalException { - return new Shell<>(new SingleDatabaseShellState()); + return new Shell<>(new SingleFactoryDatabaseShellState()); } @After @@ -91,13 +83,15 @@ public void testUseWithUncommittedChanges() throws TerminalException { String tableA = "tableA"; String tableB = "tableB"; - createAndUseTable(tableB); - createAndUseTable(tableA); + createTableWithStringColumn(tableB); + createTableWithStringColumn(tableA); String command = String.format( - "put a [\"b\"]; put b [\"c\"]; put c [\"d\"]; remove b; put a [\"bbb\"]; use %s", tableB); + "use " + + tableA + + "; put a [\"b\"]; put b [\"c\"]; put c [\"d\"]; remove b; put a [\"bbb\"]; use %s", tableB); String expectedReply = String.format( - "new%nnew%nnew%nremoved%noverwrite%nold [\"b\"]%n2 unsaved changes%n"); + "using tableA%nnew%nnew%nnew%nremoved%noverwrite%nold [\"b\"]%n2 unsaved changes%n"); runBatchExpectNonZero(command); @@ -109,56 +103,57 @@ public void testUseWithTheSameTableAndUncommittedChanges() throws TerminalExcept String tableA = "tableA"; String tableB = "tableB"; - createAndUseTable(tableB); - createAndUseTable(tableA); - - String command = String.format( - "put a [\"b\"]; put b [\"c\"]; put c [\"d\"]; remove b; put a [\"bbb\"]; use %1$s", tableA); - String expectedReply = String.format( - "new%nnew%nnew%nremoved%noverwrite%nold [\"b\"]%nusing %1$s%n", tableA); + createTableWithStringColumn(tableB); + createTableWithStringColumn(tableA); - runBatchExpectZero(false, command); + runBatchExpectZero( + "use " + tableA, + "put a [\"b\"]; put b [\"c\"]; put c [\"d\"]; remove b; put a [\"bbb\"];", + "use " + tableA); assertEquals( - "Attempt to use the same table with uncommitted changes", expectedReply, getOutput()); + makeTerminalExpectedMessage( + "using " + tableA, + "new", + "new", + "new", + "removed", + "overwrite", + "old [\"b\"]", + "using " + tableA), getOutput()); } @Test public void testCommit() throws TerminalException { String table = "table"; - String command = String.format( - "put a [\"b\"]; commit; put b [\"c\"]; remove a; commit"); - String expectedReply = String.format("new%n1%nnew%nremoved%n2%n"); - - createAndUseTable(table); - runBatchExpectZero(command); + createTableWithStringColumn(table); + runBatchExpectZero("use " + table, "put a [\"b\"]; commit; put b [\"c\"]; remove a; commit"); - assertEquals("Changes count test in commit", expectedReply, getOutput()); + assertEquals( + makeTerminalExpectedMessage("using " + table, "new", "1", "new", "removed", "2"), + getOutput()); } @Test - public void testCommitFail() throws TerminalException { + public void testCommitFail() throws TerminalException, DatabaseIOException { interpreter = new Shell<>(new MutatedSDSS(0)); - runBatchExpectNonZero("create t1 (String)", "use t1", "put a [\"b\"]"); - runBatchExpectNonZero("commit"); + runBatchExpectNonZero("create t1 (String)", "use t1", "put a [\"b\"]", "commit"); - assertEquals(makeTerminalExpectedMessage("Fail on commit [test mode]"), getOutput()); + assertThat(getOutput(), containsString("Fail on commit [test mode]")); } @Test public void testRollback() throws TerminalException { String table = "table"; - String command = String.format( - "put a [\"b\"]; commit; put b [\"c\"]; remove a; rollback"); - String expectedReply = String.format("new%n1%nnew%nremoved%n2%n"); - - createAndUseTable(table); - runBatchExpectZero(command); + createTableWithStringColumn(table); + runBatchExpectZero("use table", "put a [\"b\"]", "commit", "put b [\"c\"];", "remove a", "rollback"); - assertEquals("Changes count test in rollback", expectedReply, getOutput()); + assertEquals( + makeTerminalExpectedMessage("using " + table, "new", "1", "new", "removed", "2"), + getOutput()); } @Test @@ -166,13 +161,10 @@ public void testGetNotExistent() throws TerminalException { String table = "table"; String key = "not_existent_key"; - String command = String.format("get %2$s", table, key); - String expectedReply = String.format("not found%n"); - - createAndUseTable(table); - runBatchExpectNonZero(command); + createTableWithStringColumn(table); + runBatchExpectNonZero("use " + table, "get " + key); - assertEquals("Getting not existent key must raise error", expectedReply, getOutput()); + assertEquals(makeTerminalExpectedMessage("using " + table, "not found"), getOutput()); } @Test @@ -182,28 +174,24 @@ public void testGetExistent() throws TerminalException { String key = "key"; String value = "value"; - String command = String.format("put %1$s [\"%2$s\"]; get %1$s", key, value); - String expectedReply = String.format("new%nfound%n[\"%s\"]%n", value); - - createAndUseTable(table); - runBatchExpectZero(command); + createTableWithStringColumn(table); + runBatchExpectZero("use " + table, "put " + key + " [\"" + value + "\"]", "get " + key); - assertEquals("Getting existent key", expectedReply, getOutput()); + assertEquals( + makeTerminalExpectedMessage( + "using " + table, "new", "found", "[\"" + value + "\"]"), getOutput()); } @Test public void testRemoveNotExistent() throws TerminalException { String table = "table"; - createAndUseTable(table); + createTableWithStringColumn(table); String key = "key"; - String command = String.format("remove %s", key); - String expectedReply = String.format("not found%n"); - - runBatchExpectNonZero(command); + runBatchExpectNonZero("use " + table, "remove " + key); - assertEquals("Removing not existent key", expectedReply, getOutput()); + assertEquals(makeTerminalExpectedMessage("using " + table, "not found"), getOutput()); } @Test @@ -227,10 +215,12 @@ public void testInteractiveMode() throws TerminalException { public void testInteractiveMode1() throws TerminalException { String table = "table"; String fakeTable = "fake_table"; - createAndUseTable(table); + createTableWithStringColumn(table); runBatchExpectZero( - false, "put a [\"b\"]; put b [\"c\"]; put c [\"d\"]; put d [\"e\"]; put e [\"a\"]; exit"); + "use " + table, + "put a [\"b\"]; put b [\"c\"]; put c [\"d\"]; put d [\"e\"]; put e [\"a\"]; commit", + "exit"); runInteractiveExpectZero( "use " + table, "show tables", "use " + fakeTable + "; list", "use " + table + "; list"); @@ -253,8 +243,8 @@ public void testRunShellWithNullStream() throws TerminalException { @Test public void testDropExistingTable() throws TerminalException { String table = "table"; - createAndUseTable(table); - runBatchExpectZero(true, "drop " + table); + createTableWithStringColumn(table); + runBatchExpectZero("drop " + table); assertEquals( "When an existing table is dropped, a good report must be made", String.format("dropped%n"), @@ -273,11 +263,13 @@ public void testListWithoutActiveTable() throws TerminalException { @Test public void testSize() throws TerminalException { String table = "table"; - createAndUseTable(table); + createTableWithStringColumn(table); runBatchExpectZero( - "put a [\"b\"]; put c [\"d\"]; put d [\"e\"]; remove c; put d [\"dd\"]; put k [\"a\"]"); - runBatchExpectZero("size"); - assertEquals("Improper size printed", String.format("3%n"), getOutput()); + "use " + table, + "put a [\"b\"]; put c [\"d\"]; put d [\"e\"]; remove c; put d [\"dd\"]; put k [\"a\"]; " + + "commit"); + runBatchExpectZero("use " + table, "size"); + assertEquals(makeTerminalExpectedMessage("using " + table, "3"), getOutput()); } @Test @@ -291,7 +283,7 @@ public void testShowUnexpectedOption() throws TerminalException { } @Test - public void testInitWithFarNotExistingDir() throws TerminalException { + public void testInitWithFarNotExistingDir() throws TerminalException, DatabaseIOException { System.setProperty( SingleDatabaseShellState.DB_DIRECTORY_PROPERTY_NAME, DB_ROOT.resolve("path1").resolve("path2").toString()); @@ -301,7 +293,7 @@ public void testInitWithFarNotExistingDir() throws TerminalException { "Database directory parent path does not exist or is not a " + "directory"); try { - interpreter = new Shell<>(new SingleDatabaseShellState()); + constructInterpreter(); } finally { System.setProperty( SingleDatabaseShellState.DB_DIRECTORY_PROPERTY_NAME, DB_ROOT.toString()); @@ -309,14 +301,14 @@ public void testInitWithFarNotExistingDir() throws TerminalException { } @Test - public void testInitWithNullDir() throws TerminalException { + public void testInitWithNullDir() throws TerminalException, DatabaseIOException { System.getProperties().remove(SingleDatabaseShellState.DB_DIRECTORY_PROPERTY_NAME); exception.expect(TerminalException.class); exception.expectMessage("Please mention database directory"); try { - interpreter = new Shell<>(new SingleDatabaseShellState()); + constructInterpreter(); } finally { System.setProperty( SingleDatabaseShellState.DB_DIRECTORY_PROPERTY_NAME, DB_ROOT.toString()); @@ -363,11 +355,12 @@ public void testShowCorruptTables() throws Exception { String tableB = "tableB"; String corruptTable = "corruptTable"; - createAndUseTable(corruptTable); - createAndUseTable(tableB); - createAndUseTable(tableA); + createTableWithStringColumn(corruptTable); + createTableWithStringColumn(tableB); + createTableWithStringColumn(tableA); runBatchExpectZero( + "use " + tableA, "put a [\"b\"]; put c [\"d\"]", "commit", "use " + corruptTable, @@ -381,7 +374,7 @@ public void testShowCorruptTables() throws Exception { fos.write("failure".getBytes()); } - runInteractiveExpectZero(true, "show tables"); + runInteractiveExpectZero("show tables"); // Table order can be different. String lineRegex = String.format("(%s 2|%s 0|%s corrupt)", tableA, tableB, corruptTable); @@ -396,8 +389,8 @@ public void testShowCorruptTables() throws Exception { public void testUseCorruptTable() throws IOException, TerminalException { String table = "corruptTable"; - createAndUseTable(table); - runBatchExpectZero("put a [\"b\"]; put c [\"d\"]; put e [\"f\"]; put k [\"j\"]"); + createTableWithStringColumn(table); + runBatchExpectZero("use " + table, "put a [\"b\"]; put c [\"d\"]; put e [\"f\"]; put k [\"j\"]"); Path corruptPath = DB_ROOT.resolve(table).resolve("1.dir").resolve("1.dat"); if (!Files.exists(corruptPath.getParent())) { @@ -407,7 +400,7 @@ public void testUseCorruptTable() throws IOException, TerminalException { fos.write(new byte[] {1, 2, 5, 0}); } - runBatchExpectNonZero(true, "use " + table); + runBatchExpectNonZero("use " + table); assertThat( "Using corrupt table must raise error", getOutput(), @@ -431,26 +424,11 @@ public void testCreateTableWithInvalidName() throws TerminalException { getOutput()); } - @Test - public void testRunInteractiveWithUnparseableStream() throws TerminalException { - exception.expect(TerminalException.class); - exception.expectMessage(containsString("Error in input stream")); - - runInteractiveExpectNonZero("do smth \""); - } - - @Test - public void testRunWithUnparseableStream() throws TerminalException { - exception.expect(TerminalException.class); - exception.expectMessage(containsString("Cannot parse command arguments")); - - runBatchExpectNonZero("do smth \""); // Unclosed quotes. - } - @Test public void testCreateTableWithInvalidName1() throws TerminalException { runBatchExpectNonZero( - "create " + Paths.get("..", DB_ROOT.getFileName().toString(), "table").toString() + "create " + + Paths.get("..", DB_ROOT.getFileName().toString(), "table").toString() + " (String)"); assertEquals( "Illegal table name error must be raised", @@ -503,10 +481,10 @@ public void testCreateTable() throws TerminalException { String name = "existing_table"; String command = "create " + name + " (String)"; - runBatchExpectZero(true, command); + runBatchExpectZero(command); assertEquals( "Attempt to create not existing table", String.format("created%n"), getOutput()); - runBatchExpectNonZero(true, command); + runBatchExpectNonZero(command); assertEquals( "Attempt to create existing table", String.format("%s exists%n", name), getOutput()); } diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/DuplicatedIOTestBase.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/DuplicatedIOTestBase.java new file mode 100644 index 000000000..86ae6da46 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/DuplicatedIOTestBase.java @@ -0,0 +1,94 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test; + +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Ignore; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test.support.BAOSDuplicator; + +import java.io.PrintStream; + +/** + * Test base for convenient output tracking. {@link ru.fizteh.fivt.students.fedorov_andrew.databaselibrary + * .test.support.BAOSDuplicator} + * is used for output duplicating. + */ +@Ignore +public class DuplicatedIOTestBase { + private static PrintStream stdErr; + // Standard out and error streams are stored here. + private static PrintStream stdOut; + + private static BAOSDuplicator out; + + /** + * Sets standard output and error stream as {@link ru.fizteh.fivt.students.fedorov_andrew + * .databaselibrary.test.support.BAOSDuplicator}. + */ + @BeforeClass + public static void globalPrepareDuplicatedIOTestBase() { + stdOut = System.out; + stdErr = System.err; + out = new BAOSDuplicator(stdOut); + + // Wrap over {@link #out} that is used as {@link System#out} and {@link System#err}. + PrintStream outAndErrPrintStream = new PrintStream(out); + System.setOut(outAndErrPrintStream); + System.setErr(outAndErrPrintStream); + } + + /** + * Recovers standard output and error streams. + */ + @AfterClass + public static void globalCleanupDuplicatedIOTestBase() { + System.setOut(stdOut); + System.setErr(stdErr); + } + + /** + * Obtains output from the buffer. + */ + public String getOutput() { + return out.toString(); + } + + public void printDirectlyToStdOut(Object obj) { + stdOut.print(obj); + } + + public void printlnDirectlyToStdOut(Object obj) { + stdOut.println(obj); + } + + public void printDirectlyToStdOut(String str) { + stdOut.print(str); + } + + public void printlnDirectlyToStdOut(String str) { + stdOut.println(str); + } + + void printlnDirectlyToStdOut() { + stdOut.println(); + } + + /** + * Resets output in the buffer. + */ + @Before + public void prepare() { + out.reset(); + } + + /** + * Prints to the standard output test separator string. + */ + @After + public void cleanup() { + printlnDirectlyToStdOut(); + printlnDirectlyToStdOut("-------------------------------------------------"); + } + +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/InterpreterTest.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/InterpreterTest.java index 3fd4c30b5..18d74392c 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/InterpreterTest.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/InterpreterTest.java @@ -20,6 +20,13 @@ protected Shell constructInterpreter() throws TerminalExc return new Shell<>(state); } + @Test + public void testUnclosedQuotes() throws TerminalException { + runBatchExpectNonZero("echo [\"quotes']"); + + assertEquals(makeTerminalExpectedMessage("Failed to parse: Cannot find closing quotes"), getOutput()); + } + @Test public void testUnexpectedMethodError() throws TerminalException { runBatchExpectNonZero(AlternativeShellState.THROW_RUNTIME.getName()); @@ -41,15 +48,6 @@ public void testInteractiveContinueWorkAfterError() throws TerminalException { assertTrue("Interactive: should be okay after error", getOutput().matches(regex)); } - @Test - public void testErrorInPersistOnExit() throws TerminalException { - state.setMakeExceptionOnPersist(true); - - runBatchExpectNonZero(); - - assertEquals("Expecting empty output", getOutput(), makeTerminalExpectedMessage()); - } - @Test public void testErrorOnInit() { state.setMakeExceptionOnInit(true); diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/InterpreterTestBase.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/InterpreterTestBase.java index b635ccc48..47553d1bf 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/InterpreterTestBase.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/InterpreterTestBase.java @@ -6,15 +6,13 @@ import org.junit.BeforeClass; import org.junit.Rule; import org.junit.rules.ExpectedException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.DatabaseIOException; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.TerminalException; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Shell; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.ShellState; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test.support.BAOSDuplicator; import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.PrintStream; import java.util.Arrays; import static org.junit.Assert.*; @@ -27,60 +25,23 @@ public abstract class InterpreterTestBase interpreter; + Shell interpreter; @BeforeClass public static void globalPrepareInterpreterTestBase() { - stdOut = System.out; - stdErr = System.err; - out = new BAOSDuplicator(stdOut); - - /* - Wrap over {@link #out} that is used as {@link System#out} and {@link System#err}. - */ - PrintStream outAndErrPrintStream = new PrintStream(out); - System.setOut(outAndErrPrintStream); - System.setErr(outAndErrPrintStream); + DuplicatedIOTestBase.globalPrepareDuplicatedIOTestBase(); } @AfterClass public static void globalCleanupInterpreterTestBase() { - System.setOut(stdOut); - System.setErr(stdErr); - } - - protected abstract Shell constructInterpreter() throws TerminalException; - - /** - * Initializes {@link #interpreter}. - * @throws TerminalException - */ - @Before - public void prepare() throws TerminalException { - interpreter = constructInterpreter(); - } - - /** - * Removes all files under {@link #DB_ROOT}. - * @throws java.io.IOException - */ - @After - public void cleanup() throws IOException { - interpreter = null; - stdOut.println(); - stdOut.println("-------------------------------------------------"); + DuplicatedIOTestBase.globalCleanupDuplicatedIOTestBase(); } /** @@ -101,7 +62,7 @@ public void cleanup() throws IOException { * @return Regex for full interpreter answer. * @see java.util.regex.Pattern */ - protected String makeTerminalExpectedRegex(String greetingRegex, String... reports) { + public static String makeTerminalExpectedRegex(String greetingRegex, String... reports) { StringBuilder sb = new StringBuilder(String.format("(?m)^%s", greetingRegex)); for (String s : reports) { sb.append(String.format("%s$%n^%s", s, greetingRegex)); @@ -109,28 +70,57 @@ protected String makeTerminalExpectedRegex(String greetingRegex, String... repor return sb.toString(); } + /** + * Constructs a multiline message that expected output must be equal to.
+ * Recommended to be used to test batch mode.
+ * Each report is considered to be a separate line. Lines are separated using {@link + * System#lineSeparator()}. + */ + public static String makeTerminalExpectedMessage(String... reports) { + StringBuilder sb = new StringBuilder(); + for (String s : reports) { + sb.append(String.format("%s%n", s)); + } + return sb.toString(); + } + + protected abstract Shell constructInterpreter() throws TerminalException; + + @Before + public void prepare() throws TerminalException, DatabaseIOException { + interpreter = constructInterpreter(); + } + + /** + * Removes all files under {@link #DB_ROOT}. + * @throws java.io.IOException + */ + @After + public void cleanup() throws IOException { + interpreter = null; + IO_DUPLICATOR.cleanup(); + } + /** * Obtains everything that was output by the interpreter.
*/ - protected String getOutput() { - return out.toString(); + String getOutput() { + return IO_DUPLICATOR.getOutput(); } /** * Runs batch mode with given array of commands.
* Output is reset before run. - * @param reinit - * if true, {@link #prepare()} method is called before run. * @param commands * List of commands. Semicolons are appended to each command that does not end with a * semicolon. * @return Exit code. */ - protected int runBatch(boolean reinit, String... commands) throws TerminalException { + int runBatch(String... commands) throws TerminalException { // Clean what has been output before. - out.reset(); + IO_DUPLICATOR.prepare(); - stdOut.println(Arrays.toString(commands)); + IO_DUPLICATOR.printlnDirectlyToStdOut(Arrays.toString(commands)); for (int i = 0, len = commands.length; i < len; i++) { commands[i] = commands[i].trim(); if (!commands[i].endsWith(";")) { @@ -138,12 +128,8 @@ protected int runBatch(boolean reinit, String... commands) throws TerminalExcept } } - if (reinit) { - try { - prepare(); - } catch (TerminalException exc) { - throw new AssertionError(exc); - } + if (interpreter == null || !interpreter.isValid()) { + interpreter = constructInterpreter(); } return interpreter.run(commands); } @@ -151,77 +137,45 @@ protected int runBatch(boolean reinit, String... commands) throws TerminalExcept /** * Runs interpreter mode with given list of lines.
* Output is reset before run. - * @param reinit - * If true, {@link #prepare()} method is called before run. * @param lines * List of lines. {@link System#lineSeparator()} is appended to each line. * @return Exit code. * @throws TerminalException */ - protected int runInteractive(boolean reinit, String... lines) throws TerminalException { - out.reset(); + int runInteractive(String... lines) throws TerminalException { + IO_DUPLICATOR.prepare(); for (String cmd : lines) { - stdOut.println(cmd); + IO_DUPLICATOR.printlnDirectlyToStdOut(cmd); } StringBuilder sb = new StringBuilder(); for (String cmd : lines) { sb.append(String.format("%s%n", cmd)); } - if (reinit) { - try { - prepare(); - } catch (TerminalException exc) { - throw new AssertionError(exc); + try { + if (interpreter == null || !interpreter.isValid()) { + interpreter = constructInterpreter(); } + } catch (TerminalException exc) { + throw new AssertionError(exc); } return interpreter.run(new ByteArrayInputStream(sb.toString().getBytes())); } - protected void runInteractiveExpectZero(String... lines) throws TerminalException { - runInteractiveExpectZero(false, lines); - } - - protected void runInteractiveExpectNonZero(String... lines) throws TerminalException { - runInteractiveExpectNonZero(false, lines); - } - - protected void runBatchExpectZero(String... commands) throws TerminalException { - runBatchExpectZero(false, commands); - } - - protected void runBatchExpectNonZero(String... commands) throws TerminalException { - runBatchExpectNonZero(false, commands); - } - - protected void runInteractiveExpectZero(boolean reinit, String... lines) throws TerminalException { - assertEquals("Exit status 0 expected", 0, runInteractive(reinit, lines)); + void runInteractiveExpectZero(String... lines) throws TerminalException { + assertEquals("Exit status 0 expected", 0, runInteractive(lines)); } - protected void runInteractiveExpectNonZero(boolean reinit, String... lines) throws TerminalException { - assertNotEquals("Non-zero exit status expected", 0, runInteractive(reinit, lines)); + void runInteractiveExpectNonZero(String... lines) throws TerminalException { + assertNotEquals("Non-zero exit status expected", 0, runInteractive(lines)); } - protected void runBatchExpectZero(boolean reinit, String... commands) throws TerminalException { - assertEquals("Exit status 0 expected", 0, runBatch(reinit, commands)); + void runBatchExpectZero(String... commands) throws TerminalException { + assertEquals("Exit status 0 expected", 0, runBatch(commands)); } - protected void runBatchExpectNonZero(boolean reinit, String... commands) throws TerminalException { - assertNotEquals("Non-zero exit status expected", 0, runBatch(reinit, commands)); - } - - /** - * Constructs a multiline message that expected output must be equal to.
- * Recommended to be used to test batch mode.
- * Each report is considered to be a separate line. Lines are separated using {@link - * System#lineSeparator()}. - */ - protected String makeTerminalExpectedMessage(String... reports) { - StringBuilder sb = new StringBuilder(); - for (String s : reports) { - sb.append(String.format("%s%n", s)); - } - return sb.toString(); + void runBatchExpectNonZero(String... commands) throws TerminalException { + assertNotEquals("Non-zero exit status expected", 0, runBatch(commands)); } } diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/JSONTest.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/JSONTest.java new file mode 100644 index 000000000..a681dbbf8 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/JSONTest.java @@ -0,0 +1,113 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.json.JSONComplexObject; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.json.JSONField; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.json.JSONMaker; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.json.JSONParsedObject; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.json.JSONParser; + +import java.text.ParseException; +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.*; + +@RunWith(JUnit4.class) +public class JSONTest { + @Test + public void testSimpleObject() throws ParseException { + @JSONComplexObject + class MyObj { + @JSONField(name = "changedName") + String stringField; + + @JSONField + int intField; + + @JSONField + double doubleField; + + @JSONField + Boolean booleanField; + + @JSONField + String stringField2; + + @JSONField + boolean booleanField2; + + Object notRecordableField; + } + + MyObj obj = new MyObj(); + obj.stringField = "hello"; + obj.intField = 123; + obj.doubleField = -1.23; + obj.booleanField = null; + obj.stringField2 = "\"\\\\\\\"\""; + obj.booleanField2 = false; + + String json = JSONMaker.makeJSON(obj); + System.err.println(json); + JSONParsedObject parsedObject = JSONParser.parseJSON(json); + + assertEquals(6, parsedObject.size()); + + assertEquals(obj.stringField, parsedObject.getString("changedName")); + assertEquals(obj.intField, (long) parsedObject.getLong("intField")); + assertEquals(obj.doubleField, parsedObject.getDouble("doubleField"), 0.0); + assertEquals(obj.booleanField, parsedObject.getBoolean("booleanField")); + assertEquals(obj.stringField2, parsedObject.getString("stringField2")); + assertEquals(obj.booleanField2, parsedObject.getBoolean("booleanField2")); + assertFalse(parsedObject.containsField("notRecordableField")); + } + + @Test + public void testSimpleArray() throws ParseException { + Object[] array = new Object[6]; + + array[0] = "String"; + array[1] = true; + array[2] = 12345678901234L; + array[3] = 14.08; + array[4] = new String[] {"a", "sdf", "asdfbs", "{}:,][[]]]"}; + array[5] = Arrays.asList(2L, false, -1.0, "string"); + + String json = JSONMaker.makeJSON(array); + JSONParsedObject parsedObject = JSONParser.parseJSON(json); + + assertEquals(array[0], parsedObject.get(0)); + assertEquals(array[1], parsedObject.get(1)); + assertEquals(array[2], parsedObject.get(2)); + assertEquals(array[3], parsedObject.get(3)); + assertArrayEquals((Object[]) array[4], parsedObject.getObject(4).asArray()); + assertArrayEquals(((List) array[5]).toArray(), parsedObject.getObject(5).asArray()); + } + + @Test + public void testCyclicLinks() throws ParseException { + CyclicA a = new CyclicA(); + CyclicB b = new CyclicB(); + + a.b = b; + b.a = a; + + String json = JSONMaker.makeJSON(a); + assertEquals("{\"b\":{\"a\":cyclic}}", json); + } + + @JSONComplexObject + class CyclicA { + @JSONField + CyclicB b; + } + + @JSONComplexObject + class CyclicB { + @JSONField + CyclicA a; + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/LoggingProxyFactoryJSONTest.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/LoggingProxyFactoryJSONTest.java new file mode 100644 index 000000000..79357f8f0 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/LoggingProxyFactoryJSONTest.java @@ -0,0 +1,137 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test; + +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import ru.fizteh.fivt.proxy.LoggingProxyFactory; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.json.JSONMaker; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.json.JSONParsedObject; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.json.JSONParser; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.LoggingProxyFactoryJSON; + +import java.text.ParseException; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +@RunWith(JUnit4.class) +public class LoggingProxyFactoryJSONTest extends LoggingProxyFactoryTestBase { + + /** + * Matcher that always returns true. + */ + private static final Matcher EXISTS_MATCHER = new BaseMatcher() { + @Override + public boolean matches(Object item) { + return true; + } + + @Override + public void describeTo(Description description) { + description.appendText("exists"); + } + }; + /** + * Requires presence of some sections and forbids situation when 'thrown' and 'returnValue" exist both. + */ + private static final Matcher MIN_REQUIREMENTS = allOf( + new LogPieceMatcher(TIMESTAMP, EXISTS_MATCHER), + new LogPieceMatcher(CLASS, EXISTS_MATCHER), + new LogPieceMatcher(METHOD, EXISTS_MATCHER), + new LogPieceMatcher(ARGUMENTS, EXISTS_MATCHER), + not( + allOf( + new LogPieceMatcher(THROWN, EXISTS_MATCHER), + new LogPieceMatcher(RETURN_VALUE, EXISTS_MATCHER)))); + + private static Matcher makeRequirements(Matcher... restrictions) { + return allOf(MIN_REQUIREMENTS, allOf(restrictions)); + } + + @Override + protected LoggingProxyFactory obtainFreshFactory() { + return new LoggingProxyFactoryJSON(); + } + + @Test + public void testDoNothingJSON() throws ParseException { + wrapped.doNothing(); + JSONParsedObject parsed = JSONParser.parseJSON(getOutput()); + assertThat(parsed, makeRequirements()); + } + + @Test + public void testBoolMethodJSON() throws ParseException { + wrapped.boolMethod(1, null); + JSONParsedObject parsedObject = JSONParser.parseJSON(getOutput()); + assertThat(parsedObject, makeRequirements()); + assertThat(parsedObject.getObject(ARGUMENTS).asArray(), equalTo(new Object[] {1L, null})); + assertThat(parsedObject, new LogPieceMatcher("returnValue", equalTo(Boolean.TRUE))); + } + + @Test + public void testThrowingMethodJSON() throws ParseException { + List> iterable = new LinkedList<>(); + iterable.add(Arrays.asList("1_1", "1_2", "1_3")); + iterable.add(Arrays.asList("2_1", "2_2")); + iterable.add(null); + + try { + wrapped.throwingMethod(iterable); + } catch (Exception exc) { + // Ignore it. + } + + JSONParsedObject parsedObject = JSONParser.parseJSON(getOutput()); + + assertThat(parsedObject, makeRequirements()); + assertEquals( + JSONMaker.makeJSON(new Object[] {iterable}), JSONMaker.makeJSON(parsedObject.get(ARGUMENTS))); + } + + @Test + public void testEqualsNotProxiedJSON() { + wrapped.equals(null); + assertEquals("", getOutput()); + } + + protected static class LogPieceMatcher extends BaseMatcher { + private final String fieldName; + private final Matcher valueMatcher; + + public LogPieceMatcher(String fieldName, Matcher valueMatcher) { + this.fieldName = fieldName; + this.valueMatcher = valueMatcher; + } + + @Override + public boolean matches(Object item) { + if (item instanceof JSONParsedObject) { + JSONParsedObject obj = (JSONParsedObject) item; + if (valueMatcher == null) { + return !obj.containsField(fieldName); + } else { + return obj.containsField(fieldName) && valueMatcher.matches(obj.get(fieldName)); + } + } + return false; + } + + @Override + public void describeTo(Description description) { + description.appendText("Field " + fieldName); + if (valueMatcher == null) { + description.appendText(" must not exist."); + } else { + description.appendText(" must match: "); + description.appendDescriptionOf(valueMatcher); + } + } + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/LoggingProxyFactoryTestBase.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/LoggingProxyFactoryTestBase.java new file mode 100644 index 000000000..098cd9b35 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/LoggingProxyFactoryTestBase.java @@ -0,0 +1,49 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test; + +import org.junit.Before; +import ru.fizteh.fivt.proxy.LoggingProxyFactory; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test.support.BAOSDuplicator; + +import java.io.OutputStreamWriter; +import java.io.Writer; + +public abstract class LoggingProxyFactoryTestBase { + protected static final String TIMESTAMP = "timestamp"; + protected static final String CLASS = "class"; + protected static final String METHOD = "method"; + protected static final String ARGUMENTS = "arguments"; + protected static final String THROWN = "thrown"; + protected static final String RETURN_VALUE = "returnValue"; + protected final BAOSDuplicator out = new BAOSDuplicator(System.out); + protected final Writer writer = new OutputStreamWriter(out); + protected LoggingProxyFactory factory; + protected TestFace wrapped; + + protected String getOutput() { + return out.toString(); + } + + @Before + public void prepare() { + out.reset(); + factory = obtainFreshFactory(); + wrapped = (TestFace) factory.wrap(writer, new TestFaceImpl(), TestFace.class); + } + + protected abstract LoggingProxyFactory obtainFreshFactory(); + + protected interface TestFace { + default void doNothing() { + } + + default boolean boolMethod(int a, Integer b) { + return true; + } + + default String throwingMethod(Iterable iterable) throws Exception { + throw new Exception(); + } + } + + protected static class TestFaceImpl implements TestFace {} +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/LoggingProxyFactoryXMLTest.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/LoggingProxyFactoryXMLTest.java new file mode 100644 index 000000000..91b5ad8cf --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/LoggingProxyFactoryXMLTest.java @@ -0,0 +1,85 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test; + +import org.junit.Test; +import ru.fizteh.fivt.proxy.LoggingProxyFactory; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.LoggingProxyFactoryXML; + +import javax.xml.stream.XMLEventReader; +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamException; +import java.io.StringReader; +import java.text.ParseException; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +public class LoggingProxyFactoryXMLTest extends LoggingProxyFactoryTestBase { + private static final String NEW_LINE = System.lineSeparator(); + + @Override + protected LoggingProxyFactory obtainFreshFactory() { + return new LoggingProxyFactoryXML(); + } + + @Test + public void testDoNothingJSON() throws ParseException, XMLStreamException { + wrapped.doNothing(); + XMLInputFactory xmlInputFactory = XMLInputFactory.newFactory(); + XMLEventReader reader = xmlInputFactory.createXMLEventReader( + new StringReader(getOutput())); + + reader.nextTag(); + + assertThat( + getOutput(), allOf( + startsWith("" + + NEW_LINE))); + } + + @Test + public void testBoolMethodJSON() throws ParseException { + wrapped.boolMethod(1, null); + assertThat( + getOutput(), allOf( + startsWith("1true" + + NEW_LINE))); + } + + @Test + public void testThrowingMethodJSON() throws ParseException { + List> iterable = new LinkedList<>(); + iterable.add(Arrays.asList("1_1", "1_2", "1_3")); + iterable.add(Arrays.asList("2_1", "2_2")); + iterable.add(null); + + try { + wrapped.throwingMethod(iterable); + } catch (Exception exc) { + // Ignore it. + } + assertThat( + getOutput(), allOf( + startsWith( + "1_11_21_32_12_2java.lang" + + ".Exception" + + NEW_LINE))); + } + + @Test + public void testEqualsNotProxiedJSON() { + wrapped.equals(null); + assertEquals("", getOutput()); + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/ReadWriteTest.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/ReadWriteTest.java index 1a5d9a41a..5ebccace6 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/ReadWriteTest.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/ReadWriteTest.java @@ -49,13 +49,15 @@ private void performReadWriteFileMapTest(int keysMin, } Path testPath = Paths.get(System.getProperty("user.home"), "test", "java_test.dat"); + Files.deleteIfExists(testPath); + TablePart testFileMap = new TablePart(testPath); for (Entry e : map.entrySet()) { testFileMap.put(e.getKey(), e.getValue()); } - testFileMap.writeToFile(); + testFileMap.commit(); testFileMap = new TablePart(testPath); diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/RemoteDataStorageTest.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/RemoteDataStorageTest.java new file mode 100644 index 000000000..b5754e782 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/RemoteDataStorageTest.java @@ -0,0 +1,204 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import ru.fizteh.fivt.storage.structured.RemoteTableProvider; +import ru.fizteh.fivt.storage.structured.Storeable; +import ru.fizteh.fivt.storage.structured.Table; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.AutoCloseableProvider; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.AutoCloseableTableProviderFactory; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.DBTableProviderFactory; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.remote.RemoteDatabaseStorage; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.remote.RemoteTableProviderFactoryImpl; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.InvalidatedObjectException; + +import java.io.IOException; +import java.rmi.registry.Registry; +import java.util.Arrays; + +import static org.junit.Assert.*; + +@RunWith(JUnit4.class) +public class RemoteDataStorageTest extends TestBase { + @Rule + public ExpectedException exception = ExpectedException.none(); + private RemoteTableProviderFactoryImpl remoteFactory; + + private AutoCloseableTableProviderFactory factory; + private AutoCloseableProvider localProvider; + + @Before + public void prepare() throws Exception { + factory = new DBTableProviderFactory(); + localProvider = factory.create(DB_ROOT.toString()); + + remoteFactory = new RemoteTableProviderFactoryImpl(localProvider); + remoteFactory.establishStorage(DB_ROOT.toString()); + } + + @After + public void cleanup() throws Exception { + remoteFactory.close(); + factory.close(); + cleanDBRoot(); + } + + @Test + public void testProviderStubsAreSame() throws IOException { + RemoteDatabaseStorage storage = new RemoteDatabaseStorage(); + RemoteTableProvider providerA = storage.connect("localhost", Registry.REGISTRY_PORT); + RemoteTableProvider providerB = storage.connect("127.0.0.1", Registry.REGISTRY_PORT); + assertSame(providerA, providerB); + } + + @Test + public void testProviderStubsClosedProperly() throws IOException { + RemoteDatabaseStorage storage = new RemoteDatabaseStorage(); + RemoteTableProvider providerA = storage.connect("localhost", Registry.REGISTRY_PORT); + providerA.close(); + RemoteTableProvider providerB = storage.connect("localhost", Registry.REGISTRY_PORT); + + assertNotSame(providerA, providerB); + + // Check it is actual. + providerB.getTableNames(); + + exception.expect(InvalidatedObjectException.class); + + // It was closed = invalidated. + providerA.getTableNames(); + } + + @Test + public void testTableStubsAreSame() throws Exception { + RemoteDatabaseStorage storage = new RemoteDatabaseStorage(); + RemoteTableProvider provider = storage.connect("localhost", Registry.REGISTRY_PORT); + + String tableName = "table"; + + Table remoteTableA = provider.createTable(tableName, Arrays.asList(String.class)); + Table remoteTableB = provider.getTable(tableName); + Table remoteTableC = provider.getTable(tableName); + + assertSame(remoteTableA, remoteTableB); + assertSame(remoteTableB, remoteTableC); + } + + @Test + public void testServerTableCloseInvalidatesRemoteTable() throws Exception { + RemoteDatabaseStorage storage = new RemoteDatabaseStorage(); + RemoteTableProvider provider = storage.connect("localhost", Registry.REGISTRY_PORT); + + String tableName = "table"; + + Table remoteTable = provider.createTable(tableName, Arrays.asList(String.class)); + + Table serverTable = localProvider.getTable(tableName); + ((AutoCloseable) serverTable).close(); + + exception.expect(InvalidatedObjectException.class); + remoteTable.getName(); + } + + @Test + public void testServerFactoryCloseInvalidatesRemoteTable() throws Exception { + RemoteDatabaseStorage storage = new RemoteDatabaseStorage(); + RemoteTableProvider provider = storage.connect("localhost", Registry.REGISTRY_PORT); + + String tableName = "table"; + + Table remoteTable = provider.createTable(tableName, Arrays.asList(String.class)); + + remoteFactory.close(); + + exception.expect(InvalidatedObjectException.class); + remoteTable.getName(); + } + + @Test + public void testRemoteTableClosesAfterTableIsInvalidated() throws Exception { + RemoteDatabaseStorage storage = new RemoteDatabaseStorage(); + RemoteTableProvider remoteProvider = storage.connect("localhost", Registry.REGISTRY_PORT); + + String tableName = "table"; + + Table remoteTable = remoteProvider.createTable(tableName, Arrays.asList(String.class)); + + Table serverTable = localProvider.getTable(tableName); + ((AutoCloseable) serverTable).close(); + + try { + remoteTable.list(); + } catch (InvalidatedObjectException exc) { + // Ignore it. + } + + // Now client's remoteTable must have been completely closed. + Table remoteTable2 = remoteProvider.getTable(tableName); + + // Checking availability. + remoteTable2.getName(); + + exception.expect(InvalidatedObjectException.class); + remoteTable.getName(); + } + + @Test + public void testTwoCreatesOfTheSameTable() throws IOException { + RemoteDatabaseStorage storage = new RemoteDatabaseStorage(); + RemoteTableProvider providerA = storage.connect("localhost", Registry.REGISTRY_PORT); + RemoteTableProvider providerB = storage.connect("localhost", Registry.REGISTRY_PORT); + + String tableName = "table"; + + Table remoteTableA = providerA.createTable(tableName, Arrays.asList(String.class)); + Table remoteTableB = providerB.createTable(tableName, Arrays.asList(String.class)); + + assertNull(remoteTableB); + } + + @Test + public void testPutValue() throws IOException { + RemoteDatabaseStorage storage = new RemoteDatabaseStorage(); + RemoteTableProvider providerA = storage.connect("localhost", Registry.REGISTRY_PORT); + RemoteTableProvider providerB = storage.connect("localhost", Registry.REGISTRY_PORT); + + String tableName = "table"; + String key = "key"; + String value = "value"; + + providerA.createTable(tableName, Arrays.asList(String.class)); + + Table tableFromA = providerA.getTable(tableName); + Table tableFromB = providerB.getTable(tableName); + + tableFromA.put(key, providerA.createFor(tableFromA, Arrays.asList(value))); + Storeable storeable = tableFromB.get(key); + + assertEquals(value, storeable.getStringAt(0)); + + tableFromB.remove(key); + assertNull(tableFromA.get(key)); + assertNull(tableFromB.get(key)); + } + + @Test + public void testRemoveTable() throws IOException { + RemoteDatabaseStorage storage = new RemoteDatabaseStorage(); + RemoteTableProvider provider = storage.connect("localhost", Registry.REGISTRY_PORT); + + String tableName = "table"; + + Table table = provider.createTable(tableName, Arrays.asList(String.class)); + + provider.removeTable(tableName); + + exception.expect(InvalidatedObjectException.class); + table.list(); + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/StoreableTest.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/StoreableTest.java index 479323169..47166c401 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/StoreableTest.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/StoreableTest.java @@ -1,6 +1,7 @@ package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test; import org.junit.After; +import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Rule; @@ -17,6 +18,7 @@ import java.io.IOException; import java.util.Arrays; +import java.util.List; import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; @@ -24,6 +26,8 @@ @RunWith(JUnit4.class) public class StoreableTest extends TestBase { private static final String TABLE_NAME = "table"; + private static final List> TABLE_COLUMN_TYPES = Arrays.asList( + String.class, Integer.class, Double.class, Float.class, Boolean.class, Byte.class, Long.class); private static TableProviderFactory factory; @Rule public ExpectedException exception = ExpectedException.none(); @@ -36,26 +40,52 @@ public static void globalPrepare() { factory = new DBTableProviderFactory(); } + @AfterClass + public static void globalCleanup() throws Exception { + if (factory instanceof AutoCloseable) { + ((AutoCloseable) factory).close(); + } + } + @Before public void prepare() throws IOException { provider = factory.create(DB_ROOT.toString()); - table = provider.createTable( - TABLE_NAME, Arrays.asList( - String.class, - Integer.class, - Double.class, - Float.class, - Boolean.class, - Byte.class, - Long.class)); + table = provider.createTable(TABLE_NAME, TABLE_COLUMN_TYPES); storeable = provider.createFor(table); } @After - public void cleanup() throws IOException { - cleanDBRoot(); + public void cleanup() throws Exception { + if (provider instanceof AutoCloseable) { + ((AutoCloseable) provider).close(); + } provider = null; table = null; + cleanDBRoot(); + } + + @Test + public void testToString() { + storeable.setColumnAt(0, "value"); + storeable.setColumnAt(2, 194.1); + storeable.setColumnAt(4, true); + assertEquals("StoreableImpl[value,,194.1,,true,,]", storeable.toString()); + } + + @Test + public void testStoreableEquals() throws IOException { + Table table2 = provider.createTable(TABLE_NAME + "2", TABLE_COLUMN_TYPES); + assertEquals(storeable, provider.createFor(table2)); + } + + @Test + public void testStoreableEquals1() throws IOException { + Storeable storeable2 = provider.createFor(table); + + storeable.setColumnAt(0, "string1"); + storeable2.setColumnAt(0, "string2"); + + assertNotEquals(storeable, storeable2); } @Test diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/TableProviderFactoryTest.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/TableProviderFactoryTest.java index f13dd5462..f67b21120 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/TableProviderFactoryTest.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/TableProviderFactoryTest.java @@ -16,7 +16,7 @@ import static org.hamcrest.CoreMatchers.*; /** - * Tests {@link TableProviderFactory } mostly for error cases. + * Tests {@link ru.fizteh.fivt.storage.structured.TableProviderFactory } mostly for error cases. * @author phoenix */ @RunWith(org.junit.runners.JUnit4.class) @@ -31,9 +31,11 @@ public void prepare() { } @After - public void cleanup() throws IOException { + public void cleanup() throws Exception { + if (factory instanceof AutoCloseable) { + ((AutoCloseable) factory).close(); + } cleanDBRoot(); - factory = null; } @Test diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/TableProviderTest.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/TableProviderTest.java index 248c790a0..1a6e7a72f 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/TableProviderTest.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/TableProviderTest.java @@ -1,7 +1,9 @@ package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test; +import junit.framework.AssertionFailedError; import org.hamcrest.Matcher; import org.junit.After; +import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Rule; @@ -16,6 +18,11 @@ import ru.fizteh.fivt.storage.structured.TableProviderFactory; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.DBTableProviderFactory; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.StringTableImpl; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.InvalidatedObjectException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.parallel.ControllableAgent; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.parallel.ControllableRunnable; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.parallel.ControllableRunner; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test.support.TestUtils; import java.io.IOException; import java.io.PrintWriter; @@ -30,6 +37,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Objects; +import java.util.concurrent.ThreadLocalRandom; import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; @@ -48,15 +56,25 @@ public static void globalPrepare() { factory = new DBTableProviderFactory(); } + @AfterClass + public static void globalCleanup() throws Exception { + if (factory instanceof AutoCloseable) { + ((AutoCloseable) factory).close(); + } + } + @Before - public void prepareProvider() throws IOException { + public void prepareProvider() throws Exception { provider = factory.create(DB_ROOT.toString()); } @After - public void cleanupProvider() throws IOException { - cleanDBRoot(); + public void cleanupProvider() throws Exception { + if (provider instanceof AutoCloseable) { + ((AutoCloseable) provider).close(); + } provider = null; + cleanDBRoot(); } private Table createTable(Class... columnTypes) throws IOException { @@ -69,6 +87,11 @@ private void checkSerialize(Table table, Storeable storeable) throws ParseExcept assertTrue(Objects.equals(storeable, deserialized)); } + @Test + public void testToString() { + assertEquals("DBTableProvider[" + DB_ROOT.normalize().toString() + "]", provider.toString()); + } + @Test public void testSerializeDeserializeSync() throws IOException, ParseException { Table table = createTable(String.class, Double.class, Boolean.class); @@ -83,44 +106,91 @@ public void testSerializeDeserializeSyncNulls() throws IOException, ParseExcepti checkSerialize(table, storeable); } - private void expectJSONRegexMatchFailure() { - exception.expect(ParseException.class); - exception.expectMessage( - wrongTypeMatcherAndAllOf( - containsString( - "Does not match JSON simple list regular expression"))); - } - @Test - public void testDeserialize() throws IOException, ParseException { - Table table = createTable(Long.class, Byte.class, String.class); - expectJSONRegexMatchFailure(); + public void testConcurrentCreateTable() throws Exception { + final String tableName = "table"; - provider.deserialize(table, "3, 1, null"); - } + class TableCreator extends ControllableRunnable { + volatile Table createdTable; + volatile Table gotTable; - @Test - public void testDeserialize1() throws IOException, ParseException { - Table table = createTable(Long.class, Byte.class, String.class); - expectJSONRegexMatchFailure(); + public TableCreator(ControllableRunner host) { + super(host); + } + + @Override + public void runWithFreedom(ControllableAgent agent) throws Exception, AssertionError { + TestUtils.consumeCPU(ThreadLocalRandom.current().nextInt(20, 40)); + System.err.println("Attempt to create table"); + createdTable = provider.createTable(tableName, DEFAULT_COLUMN_TYPES); + gotTable = provider.getTable(tableName); + } + } + + int threadsCount = 26; + + ControllableRunner[] runners = new ControllableRunner[threadsCount]; + + for (int i = 0; i < threadsCount; i++) { + runners[i] = new ControllableRunner(); + runners[i].assignRunnable(new TableCreator(runners[i])); + } - provider.deserialize(table, "[,,]"); + for (int i = 0; i < threadsCount; i++) { + new Thread(runners[i], "Runner " + i).start(); + } + + for (int i = 0; i < threadsCount; i++) { + runners[i].waitUntilEndOfWork(); + } + + // All gotTable must be equal. + // One createdTable must be equal to any gotTable, all other createdTables must be null. + + Table gotTable = ((TableCreator) runners[0].getRunnable()).gotTable; + + for (int i = 1; i < threadsCount; i++) { + assertSame( + "All links for gotTable must be the same", + gotTable, + ((TableCreator) runners[i].getRunnable()).gotTable); + } + + boolean foundCreated = false; + + for (int i = 0; i < threadsCount; i++) { + Table createdTable = ((TableCreator) runners[i].getRunnable()).createdTable; + if (createdTable != null) { + if (foundCreated) { + throw new AssertionFailedError("More then one created table"); + } else { + foundCreated = true; + } + } + } + + assertTrue("Must be one created table", foundCreated); } @Test - public void testDeserialize2() throws IOException, ParseException { + public void testDeserialize() throws IOException, ParseException { Table table = createTable(Long.class, Byte.class, String.class); - expectJSONRegexMatchFailure(); + exception.expect(ParseException.class); + exception.expectMessage( + wrongTypeMatcherAndAllOf( + containsString( + "Arguments must be inside square brackets"))); - provider.deserialize(table, "[\"]"); + provider.deserialize(table, "3, 1, null"); } @Test - public void testDeserialize3() throws IOException, ParseException { + public void testDeserialize2() throws IOException, ParseException { Table table = createTable(Long.class, Byte.class, String.class); - expectJSONRegexMatchFailure(); + exception.expect(ParseException.class); + exception.expectMessage("Unclosed quotes"); - provider.deserialize(table, "[\"/say\"]"); + provider.deserialize(table, "[\"]"); } @Test @@ -131,7 +201,7 @@ public void testDeserializeTooManyElements() throws IOException, ParseException exception.expectMessage( wrongTypeMatcherAndAllOf( containsString( - "Too many elements in the list; expected: " + table.getColumnsCount()))); + "Irregular number of arguments given"))); provider.deserialize(table, "[ 1, 2, 3, 4, 5, \"6\" ]"); } @@ -143,8 +213,7 @@ public void testDeserializeTooFewElements() throws IOException, ParseException { exception.expect(ParseException.class); exception.expectMessage( wrongTypeMatcherAndAllOf( - containsString( - "Too few elements in the list; expected: " + table.getColumnsCount()))); + containsString("Irregular number of arguments given"))); provider.deserialize(table, "[ \"lonely element\" ]"); } @@ -162,13 +231,14 @@ public void testSerialize() throws IOException { @Test public void testSerialize1() throws IOException { Table table = createTable(Boolean.class, Double.class, String.class, String.class); - Storeable storable = provider.createFor(table, Arrays.asList(false, -2.41, null, "a//b ///\" \" c")); + Storeable storable = provider.createFor(table, Arrays.asList(false, -2.41, null, "a\\b \\\" c")); String serialized = provider.serialize(table, storable); System.err.println(serialized); String regex = - "(\\s*)\\[(\\s*)false,(\\s*)-2\\.41,null,(\\s*)\"a////b ///////\" /\" c\"(\\s*)\\](\\s*)"; + "(\\s*)\\[(\\s*)false,(\\s*)-2\\.41,null,(\\s*)\"a\\\\\\\\b \\\\\\\\\\\\\" c\"(\\s*)\\]" + + "(\\s*)"; assertTrue(serialized.matches(regex)); } @@ -190,7 +260,7 @@ private void expectTableCorruptAndAllOf(String tableName, Matcher... mat } @Test - public void testObtainTableWithInvalidSignatureFile() throws IOException { + public void testObtainTableWithInvalidSignatureFile() throws Exception { String tableName = "table"; Table table = provider.createTable(tableName, DEFAULT_COLUMN_TYPES); @@ -211,9 +281,11 @@ public void testObtainTableWithInvalidSignatureFile() throws IOException { } @Test - public void testObtainTableWithExtraFile() throws IOException { + public void testObtainTableWithExtraFile() throws Exception { String tableName = "table"; + cleanupProvider(); + Files.createDirectories(DB_ROOT.resolve(tableName).resolve("1.dir")); Files.createFile(DB_ROOT.resolve(tableName).resolve("1.dir").resolve("file.txt")); @@ -224,7 +296,7 @@ public void testObtainTableWithExtraFile() throws IOException { } @Test - public void testObtainTableWithMissingSignatureFile() throws IOException { + public void testObtainTableWithMissingSignatureFile() throws Exception { String tableName = "table"; Files.createDirectory(DB_ROOT.resolve(tableName)); @@ -237,9 +309,11 @@ public void testObtainTableWithMissingSignatureFile() throws IOException { } @Test - public void testObtainTableWithBadIDOfPartFile() throws IOException { + public void testObtainTableWithBadIDOfPartFile() throws Exception { String tableName = "table"; + cleanupProvider(); + Files.createDirectories(DB_ROOT.resolve(tableName).resolve("1.dir")); Files.createFile(DB_ROOT.resolve(tableName).resolve("1.dir").resolve("10000.dat")); @@ -316,8 +390,8 @@ public void testRemoveTableExistent() throws IOException { Table table = provider.createTable(name, DEFAULT_COLUMN_TYPES); provider.removeTable(name); - exception.expect(IllegalStateException.class); - exception.expectMessage(name + " is invalidated"); + exception.expect(InvalidatedObjectException.class); + exception.expectMessage("Object has been invalidated"); // Even calling to this simple method can cause IllegalStateException if the table has been removed. table.getName(); @@ -378,10 +452,12 @@ public void testDeserializeStringsWithCommas() throws IOException, ParseExceptio } @Test - public void testObtainTableWithEmptySignatureFile() throws IOException { + public void testObtainTableWithEmptySignatureFile() throws Exception { String tableName = "table"; - Files.createDirectory(DB_ROOT.resolve(tableName)); + cleanupProvider(); + + Files.createDirectories(DB_ROOT.resolve(tableName)); Files.createFile(DB_ROOT.resolve(tableName).resolve("signature.tsv")); prepareProvider(); @@ -415,7 +491,9 @@ public void testPutOneStoreableToAnotherTable2() throws IOException { exception.expectMessage( wrongTypeMatcherAndAllOf( containsString( - "Column #0 expected to have type " + INT_TYPE + ", but actual type is " + "Column #0 expected to have type " + + INT_TYPE + + ", but actual type is " + LONG_TYPE))); tableA.put("key", storeableB); diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/TableTest.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/TableTest.java index b067444ab..4338a93d3 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/TableTest.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/TableTest.java @@ -13,6 +13,9 @@ import ru.fizteh.fivt.storage.structured.TableProvider; import ru.fizteh.fivt.storage.structured.TableProviderFactory; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.StoreableTableImpl; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.parallel.ControllableAgent; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.parallel.ControllableRunnable; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.parallel.ControllableRunner; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test.support.TestUtils; import java.io.IOException; @@ -20,6 +23,8 @@ import java.text.ParseException; import java.util.Arrays; import java.util.List; +import java.util.concurrent.ThreadLocalRandom; +import java.util.function.Consumer; import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; @@ -79,6 +84,11 @@ private String remove(String key) { return extractString(oldValue); } + @Test + public void testToString() { + assertEquals("StoreableTableImpl[" + DB_ROOT.resolve(TABLE_NAME).normalize() + "]", table.toString()); + } + @Test public void testPutWithNullKey() { exception.expect(IllegalArgumentException.class); @@ -153,7 +163,109 @@ public void testNumberOfUncommittedChanges4() throws ParseException, IOException remove("key"); put("key", "value"); - assertEquals(0, table.getNumberOfUncommittedChanges()); + assertEquals(1, table.getNumberOfUncommittedChanges()); + } + + @Test + public void testRollbackConcurrent() throws Throwable { + ControllableRunner runnerA = new ControllableRunner(); + ControllableRunner runnerB = new ControllableRunner(); + + // Scheme: + // runnerA: put values + // runnerB: put values, rollback + // runnerA: check own changes still exist + + ControllableRunnable contrA = runnerA.createAndAssign( + (ControllableAgent agent) -> { + // runnerA: put values + put("a", "b"); + put("b", "c"); + + agent.notifyAndWait(); + + // runnerA: check own changes still exist + int changes = table.getNumberOfUncommittedChanges(); + assertEquals("Changes of another thread must not have been rolled back", 2, changes); + + }); + + ControllableRunnable contrB = runnerB.createAndAssign( + (ControllableAgent agent) -> { + // runnerB: put values, rollback + put("a", "b"); + put("c", "d"); + int changes = table.rollback(); + assertEquals("Must have rolled back my changes", 2, changes); + }); + + new Thread(runnerA, "runnerA").start(); + new Thread(runnerB, "runnerB").start(); + + runnerA.waitUntilPause(); // Wait until pause. + runnerB.waitUntilEndOfWork(); // Wait until end of work. + runnerA.continueWork(); + runnerA.waitUntilEndOfWork(); // Wait until end of work. + } + + @Test + public void testCommitGlobalUpdate() throws Throwable { + ControllableRunner runnerA = new ControllableRunner(); + ControllableRunner runnerB = new ControllableRunner(); + + ControllableRunnable contrA = runnerA.createAndAssign( + (ControllableAgent agent) -> { + // runnerA: put values + put("a", "b"); + put("b", "c"); + + agent.notifyAndWait(); + + // runnerA: commit + // System.err.println("A: commit"); + int changes = table.commit(); + assertEquals("Must have committed my changes.", 2, changes); + + agent.notifyAndWait(); + + // runnerA: get values + assertNull("Must have been removed.", get("b")); + assertEquals("Committed from another thread.", "d", get("c")); + assertEquals("Committed from me earlier.", "b", get("a")); + }); + + ControllableRunnable contrB = runnerB.createAndAssign( + (ControllableAgent agent) -> { + // runnerB: get values, put values, remove values + assertNull("Must not have been committed.", get("a")); + assertNull("Must not have been committed.", get("b")); + + put("c", "d"); + remove("b"); + + agent.notifyAndWait(); + + // runnerB: get values, commit + assertNull("Committed but replaced", get("b")); + assertEquals("Committed from another thread.", "b", get("a")); + int changes = table.commit(); + assertEquals("Must have committed my changes.", 2, changes); + }); + + // runnerA: put values + // runnerB: get values, put values, remove values + // runnerA: commit + // runnerB: get values, commit + // runnerA: get values + + new Thread(runnerA, "runnerA").start(); + runnerA.waitUntilPause(); + new Thread(runnerB, "runnerB").start(); + runnerB.waitUntilPause(); + runnerA.continueWork(); + runnerA.waitUntilPause(); + runnerB.waitUntilEndOfWork(); + runnerA.waitUntilEndOfWork(); } @Test @@ -167,14 +279,68 @@ public void testNumberOfUncommittedChanges5() throws ParseException, IOException } @Test - public void testPutOneStoreableToAnotherTable() throws IOException { - Table table2 = provider.createTable(TABLE_NAME + "2", DEFAULT_COLUMN_TYPES); - Storeable storeable = provider.createFor(table); + public void testManyConcurrentCommits() throws Exception { + int threadsNumber = TestUtils.ALPHABET.length; + + ControllableRunner[] runners = new ControllableRunner[threadsNumber]; + + for (int i = 0; i < threadsNumber; i++) { + final int id = i; + runners[i] = new ControllableRunner(); + runners[i].createAndAssign( + (ControllableAgent agent) -> { + final ThreadLocalRandom random = ThreadLocalRandom.current(); + + Consumer trashFiller = (actionsCount) -> { + try { + System.err.println( + Thread.currentThread().getName() + + ": doing " + + actionsCount + + " actions"); + for (int j = 0; j < actionsCount; j++) { + put( + String.valueOf(random.nextInt(100)), + String.valueOf(random.nextInt())); + if (random.nextInt(10) < 5) { + table.commit(); + } + } + } catch (IOException | ParseException exc) { + throw new AssertionError(exc); + } + }; + + trashFiller.accept(random.nextInt(20, 40)); + + String mainKey = "key" + TestUtils.ALPHABET[id]; + String mainValue = "value " + mainKey; + + put(mainKey, mainValue); + table.commit(); + assertEquals("Main key-value not matches", mainValue, get(mainKey)); + + trashFiller.accept(random.nextInt(20, 40)); + + agent.notifyAndWait(); + + assertEquals("Main key-value not matches", mainValue, get(mainKey)); + }); + } - exception.expect(IllegalStateException.class); - exception.expectMessage("Cannot put storeable assigned to one table to another table"); + for (int i = 0; i < threadsNumber; i++) { + new Thread(runners[i], "runner " + i).start(); + } + + // Going to the finish line... + for (ControllableRunner runner : runners) { + runner.waitUntilPause(); + } - table2.put("key", storeable); + // Total check in the end. + for (ControllableRunner runner : runners) { + runner.waitUntilEndOfWork(); + } } @Test diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/TestBase.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/TestBase.java index 1580de64e..413fb08c2 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/TestBase.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/TestBase.java @@ -1,8 +1,8 @@ package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test; import org.hamcrest.Matcher; +import org.junit.AfterClass; import org.junit.BeforeClass; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Utility; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test.support.RegexMatcher; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test.support.TestUtils; @@ -18,24 +18,24 @@ */ public abstract class TestBase { public static final String WRONG_TYPE_MESSAGE_REGEX = "wrong type \\([^\\(\\)]+\\)"; - - /** - * Accepts strings of this type: "wrong type ( ...description without round brackets... )" - */ - public static final Matcher WRONG_TYPE_MESSAGE_MATCHER = - new RegexMatcher(WRONG_TYPE_MESSAGE_REGEX); - protected static final Path DB_ROOT = Paths.get(System.getProperty("user.home"), "test", "JUnitDB"); + static final Path DB_ROOT = Paths.get(System.getProperty("user.home"), "test", "JUnitDB"); static final String INT_TYPE = Integer.class.getSimpleName(); static final String STRING_TYPE = String.class.getSimpleName(); static final String DOUBLE_TYPE = Double.class.getSimpleName(); static final String FLOAT_TYPE = Float.class.getSimpleName(); static final String BOOLEAN_TYPE = Boolean.class.getSimpleName(); static final String LONG_TYPE = Long.class.getSimpleName(); + /** + * Accepts strings of this type: "wrong type ( ...description without round brackets... )" + */ + private static final Matcher WRONG_TYPE_MESSAGE_MATCHER = + new RegexMatcher(WRONG_TYPE_MESSAGE_REGEX); @BeforeClass - public static void globalPrepareTestBase() throws IOException { + @AfterClass + public static void globalCleanupBeforeAndAfterClass() throws IOException { if (Files.exists(DB_ROOT)) { - Utility.rm(DB_ROOT); + TestUtils.removeFileSubtree(DB_ROOT); } } @@ -43,9 +43,7 @@ public static Matcher wrongTypeMatcherAndAllOf(Matcher... matche return allOf(WRONG_TYPE_MESSAGE_MATCHER, allOf(matchers)); } - protected void cleanDBRoot() throws IOException { - if (Files.exists(DB_ROOT)) { - TestUtils.removeFileSubtree(DB_ROOT); - } + void cleanDBRoot() throws IOException { + globalCleanupBeforeAndAfterClass(); } } diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/TransactionPoolTest.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/TransactionPoolTest.java new file mode 100644 index 000000000..4ad6342b0 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/TransactionPoolTest.java @@ -0,0 +1,78 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.servlet.transactions.Transaction; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.servlet.transactions.TransactionPool; + +import static org.junit.Assert.*; + +public class TransactionPoolTest { + @Rule + public ExpectedException exception = ExpectedException.none(); + + @Test + public void testTooManyTransactions() throws Exception { + int count = 10; + TransactionPool pool = new TransactionPool(count, 100000L); + + Transaction[] transactions = new Transaction[count]; + + for (int i = 0; i < 10; i++) { + Transaction transaction = pool.newTransaction(); + int id = transaction.getTransactionID(); + assertNull("Duplicate id", transactions[id]); + transactions[id] = transaction; + pool.releaseTransaction(transaction); + } + + exception.expect(IllegalStateException.class); + exception.expectMessage("No more free transaction IDs"); + pool.newTransaction(); + } + + @Test + public void testRemoveTransactionAndPerformAction() throws Exception { + TransactionPool pool = new TransactionPool(100500, 100000L); + Transaction tr = pool.newTransaction(); + pool.killTransaction(tr); + + exception.expect(IllegalStateException.class); + exception.expectMessage("You can execute actions only in active state"); + + tr.executeAction(null); + } + + @Test + public void testTransactionDiesAfterSomeTime() throws Exception { + TransactionPool pool = new TransactionPool(100, 100L); + Transaction tr = pool.newTransaction(); + pool.releaseTransaction(tr); + + Thread.sleep(500L); + + exception.expect(IllegalStateException.class); + exception.expectMessage("You can execute actions only in active state"); + tr.executeAction(null); + } + + @Test + public void testTransactionDiesAfterSomeTime1() throws Exception { + TransactionPool pool = new TransactionPool(100, 100L); + Transaction tr = pool.newTransaction(); + int id = tr.getTransactionID(); + pool.releaseTransaction(tr); + + Thread.sleep(500L); + + exception.expect(IllegalArgumentException.class); + exception.expectMessage("Transaction not found: " + id); + tr = pool.obtainTransaction(id); + try { + tr.executeAction(null); + } finally { + pool.releaseTransaction(tr); + } + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/UtilityTest.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/UtilityTest.java index ede72c41c..48165ee6c 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/UtilityTest.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/UtilityTest.java @@ -6,7 +6,6 @@ import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.DBTableProvider; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Shell; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.ConvenientMap; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Utility; @@ -20,7 +19,9 @@ @RunWith(JUnit4.class) public class UtilityTest { - private static final String QUOTED_STRING_REGEX = Utility.getQuotedStringRegex("\"", "/"); + private static final char QUOTE_CHARACTER = '"'; + private static final char ESCAPE_CHARACTER = '\\'; + @Rule public ExpectedException exception = ExpectedException.none(); @@ -41,34 +42,18 @@ private static int findQuotesEnd(String s) { } private static String quoteString(String s) { - return Utility - .quoteString(s, DBTableProvider.QUOTE_CHARACTER + "", DBTableProvider.ESCAPE_CHARACTER + ""); + return Utility.quoteString(s, QUOTE_CHARACTER + "", ESCAPE_CHARACTER + ""); } private static String unquoteString(String s) { return Utility.unquoteString( - s, DBTableProvider.QUOTE_CHARACTER + "", DBTableProvider.ESCAPE_CHARACTER + ""); + s, QUOTE_CHARACTER + "", ESCAPE_CHARACTER + ""); } private static String[] supplyArray(String... strings) { return strings; } - @Test - public void testMatchQuotedStringRegex() { - assertFalse("\"/\"".matches(QUOTED_STRING_REGEX)); - } - - @Test - public void testMatchQuotedStringRegex1() { - assertTrue("\"//\"".matches(QUOTED_STRING_REGEX)); - } - - @Test - public void testMatchQuotedStringRegex2() { - assertFalse("\"//\"\"".matches(QUOTED_STRING_REGEX)); - } - @Test public void testSplitString() { assertArrayEquals( @@ -111,7 +96,7 @@ public void testSplitString4() { @Test public void testSplitString5() { - String part = quoteString("\"/ word \"/\""); + String part = quoteString("\"\\ word \"\\\""); assertArrayEquals( "Invalid string split", supplyArray("create", "table", "(" + part + "," + "1,", "null", ")"), @@ -155,7 +140,7 @@ public void testInverseMapWithTwoDuplicateValues() { exception.expectMessage("Source map contains at least two duplicate values"); Utility.inverseMap( - new ConvenientMap(new HashMap()).putNext(1, 2).putNext( + new ConvenientMap<>(new HashMap<>()).chainPut(1, 2).chainPut( 2, 2)); } diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/XMLTest.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/XMLTest.java new file mode 100644 index 000000000..957ecdadb --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/XMLTest.java @@ -0,0 +1,81 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.xml.XMLComplexObject; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.xml.XMLField; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.xml.XMLMaker; + +import static org.junit.Assert.*; + +@RunWith(JUnit4.class) +public class XMLTest { + @Test + public void testCyclicLinks() { + MyObject5 objA = new MyObject5(); + MyObject6 objB = new MyObject6(); + objB.a = objA; + objA.b = objB; + + assertEquals("cyclic", XMLMaker.makeXML(objA, "obj5")); + } + + @Test + public void testSimpleTag() { + assertEquals("1323", XMLMaker.makeXML(new MyObject1(), "obj")); + } + + @Test + public void testRenamedField() { + assertEquals("123", XMLMaker.makeXML(new MyObject2(), "obj")); + } + + @Test + public void testInlineAttribute() { + assertEquals("", XMLMaker.makeXML(new MyObject3(), "obj")); + } + + @Test + public void testArray() { + assertEquals( + "truefalseString", + XMLMaker.makeXML(new MyObject4(), "obj")); + } + + @XMLComplexObject + class MyObject1 { + @XMLField + private String field1 = "1323"; + } + + @XMLComplexObject + class MyObject2 { + @XMLField(name = "value") + private int field2 = 123; + } + + @XMLComplexObject + class MyObject3 { + @XMLField(inline = true) + private boolean tag = true; + } + + @XMLComplexObject + class MyObject4 { + @XMLField + private Object[] objects = new Object[] {"true", "false", "String"}; + } + + @XMLComplexObject + class MyObject5 { + @XMLField + MyObject6 b; + } + + @XMLComplexObject + class MyObject6 { + @XMLField + MyObject5 a; + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/AlternativeShellState.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/AlternativeShellState.java index 9bf24050a..b80371993 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/AlternativeShellState.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/AlternativeShellState.java @@ -7,6 +7,7 @@ import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.ShellState; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.SimpleCommandContainer; +import java.io.PrintStream; import java.util.Arrays; import java.util.Map; @@ -42,22 +43,12 @@ public void execute(AlternativeShellState state, String[] args) throws TerminalE private final Map> commandMap; - private boolean makeExceptionOnPersist; private boolean makeExceptionOnInit; - private boolean makeRuntimeExceptionOnCleanup; public AlternativeShellState() { commandMap = super.getCommands(); } - public boolean isMakeExceptionOnPersist() { - return makeExceptionOnPersist; - } - - public void setMakeExceptionOnPersist(boolean makeExceptionOnPersist) { - this.makeExceptionOnPersist = makeExceptionOnPersist; - } - public boolean isMakeExceptionOnInit() { return makeExceptionOnInit; } @@ -66,19 +57,14 @@ public void setMakeExceptionOnInit(boolean makeExceptionOnInit) { this.makeExceptionOnInit = makeExceptionOnInit; } - public boolean isMakeRuntimeExceptionOnCleanup() { - return makeRuntimeExceptionOnCleanup; - } - - public void setMakeRuntimeExceptionOnCleanup(boolean makeRuntimeExceptionOnCleanup) { - this.makeRuntimeExceptionOnCleanup = makeRuntimeExceptionOnCleanup; + @Override + public PrintStream getOutputStream() { + return System.out; } @Override public void cleanup() { - if (makeRuntimeExceptionOnCleanup) { - throw new SpontaniousRuntimeException(); - } + } @Override @@ -93,18 +79,6 @@ public void init(Shell host) throws Exception { } } - @Override - public void persist() throws Exception { - if (makeExceptionOnPersist) { - throw new SpontaniousException(); - } - } - - @Override - public void prepareToExit(int exitCode) throws ExitRequest { - throw new ExitRequest(exitCode); - } - @Override public Map> getCommands() { return commandMap; diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/MutatedDatabase.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/MutatedDatabase.java index 2f3864a36..937412b65 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/MutatedDatabase.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/MutatedDatabase.java @@ -1,16 +1,16 @@ package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test.support; +import ru.fizteh.fivt.storage.structured.TableProvider; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.Database; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.DatabaseIOException; import java.io.IOException; -import java.nio.file.Path; public class MutatedDatabase extends Database { private int commitCallsLeft; - public MutatedDatabase(Path dbDirectory, int commitCallsLeft) throws DatabaseIOException { - super(dbDirectory); + public MutatedDatabase(TableProvider provider, String dbDirectory, int commitCallsLeft) { + super(provider, dbDirectory, System.out); this.commitCallsLeft = commitCallsLeft; } diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/MutatedSDSS.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/MutatedSDSS.java index c2575414c..acba8b52c 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/MutatedSDSS.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/MutatedSDSS.java @@ -1,11 +1,14 @@ package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test.support; +import ru.fizteh.fivt.storage.structured.TableProvider; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.DBTableProviderFactory; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.Database; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.DatabaseIOException; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Shell; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.SingleDatabaseShellState; -import java.nio.file.Paths; +import java.io.IOException; +import java.io.PrintStream; /** * Mutated SingleDatabaseShellState that limits calls to {@link ru.fizteh.fivt.students.fedorov_andrew @@ -13,22 +16,40 @@ * method. */ public class MutatedSDSS extends SingleDatabaseShellState { + private final DBTableProviderFactory factory; + private final TableProvider provider; + private final String databasePath; private int commitCallsLeft; - private MutatedDatabase mutatedActiveDatabase; - public MutatedSDSS(int commitCallsLeft) { + public MutatedSDSS(int commitCallsLeft) throws DatabaseIOException { if (commitCallsLeft < 0) { throw new IllegalArgumentException("commitCallsLeft must be positive or 0"); } this.commitCallsLeft = commitCallsLeft; + factory = new DBTableProviderFactory(); + databasePath = System.getProperty(DB_DIRECTORY_PROPERTY_NAME); + provider = factory.create(databasePath); + } + + @Override + public PrintStream getOutputStream() { + return System.out; + } + + @Override + public void init(Shell host) throws IllegalArgumentException, IOException { + this.mutatedActiveDatabase = new MutatedDatabase(provider, databasePath, commitCallsLeft); + } + + @Override + protected Database obtainNewActiveDatabase() throws Exception { + return null; } @Override - public void init(Shell host) - throws IllegalArgumentException, DatabaseIOException { - this.mutatedActiveDatabase = new MutatedDatabase( - Paths.get(System.getProperty(DB_DIRECTORY_PROPERTY_NAME)), commitCallsLeft); + public void cleanup() { + factory.close(); } @Override diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/ProbableActionSet.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/ProbableActionSet.java index 492add62f..1fa750ded 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/ProbableActionSet.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/ProbableActionSet.java @@ -13,7 +13,7 @@ public class ProbableActionSet { private final LinkedList actions; private int probCasesSum; - public ProbableActionSet() { + private ProbableActionSet() { actions = new LinkedList<>(); probCasesSum = 0; } @@ -41,7 +41,7 @@ public static > ProbableActionSet makeEquallyDistributedSet return set; } - public ProbableActionSet add(Action action, int probCases) { + ProbableActionSet add(Action action, int probCases) { if (probCases <= 0) { throw new IllegalArgumentException("probCases must be positive integer"); } diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/SpontaniousException.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/SpontaniousException.java index 5644294b2..6f9900613 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/SpontaniousException.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/SpontaniousException.java @@ -1,6 +1,6 @@ package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test.support; -public class SpontaniousException extends Exception { +class SpontaniousException extends Exception { public SpontaniousException() { super("Spontanious exception"); } diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/SpontaniousRuntimeException.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/SpontaniousRuntimeException.java index 7cbefcdb8..69f2ebb5c 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/SpontaniousRuntimeException.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/SpontaniousRuntimeException.java @@ -1,6 +1,6 @@ package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test.support; -public class SpontaniousRuntimeException extends RuntimeException { +class SpontaniousRuntimeException extends RuntimeException { public SpontaniousRuntimeException() { super("Spontanious runtime exception"); } diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/TestUtils.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/TestUtils.java index 0c8f8597c..beb46509b 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/TestUtils.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/TestUtils.java @@ -10,6 +10,7 @@ import java.util.Iterator; import java.util.Map; import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; /** * This class provides some utility methods for testing.
Note that some methods are linking to @@ -18,7 +19,7 @@ * directly. */ public class TestUtils { - private static final char[] ALPHABET = "abcdefghijklmnopqrstuvwxyz".toCharArray(); + public static final char[] ALPHABET = "abcdefghijklmnopqrstuvwxyz".toCharArray(); private static final Random RANDOM = new Random(); // Not for constructing @@ -29,7 +30,7 @@ public static int randInt(int a, int b) { return RANDOM.nextInt(b - a + 1) + a; } - public static int randInt(int n) { + private static int randInt(int n) { return RANDOM.nextInt(n); } @@ -41,6 +42,15 @@ public static String randString(int length) { return String.valueOf(data); } + public static void consumeCPU(int actionsCount) { + int sum = 0; + ThreadLocalRandom random = ThreadLocalRandom.current(); + for (int i = 0; i < actionsCount; i++) { + sum += random.nextInt(123512); + } + random.nextInt(sum); + } + public static TableProviderFactory obtainFactory() { return new DBTableProviderFactory(); } diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/xml/XMLComplexObject.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/xml/XMLComplexObject.java new file mode 100644 index 000000000..198120fbc --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/xml/XMLComplexObject.java @@ -0,0 +1,22 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.xml; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marker for objects that contain some fields that must be xml-serialized. + */ +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@Target(ElementType.TYPE) +public @interface XMLComplexObject { + /** + * If true, object's single field is written directly skipping this object's serialization. + */ + boolean wrapper() default false; +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/xml/XMLField.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/xml/XMLField.java new file mode 100644 index 000000000..32fa3da7c --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/xml/XMLField.java @@ -0,0 +1,45 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.xml; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marker for fields that must be xml-serialized. + */ +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface XMLField { + String DEFAULT_NAME = ""; + + int NULLPOLICY_EMPTY_IF_NULL = 0; + int NULLPOLICY_IGNORE_IF_NULL = 1; + int NULLPOLICY_FULL_IF_NULL = 2; + + /** + * Used for iterables, arrays to specify element tag alternative to 'value'.
+ * If empty, default tag is applied. + */ + String childName() default DEFAULT_NAME; + + /** + * Specifies behaviour if element that is going to be serialized is null.
+ * @see #NULLPOLICY_EMPTY_IF_NULL + * @see #NULLPOLICY_FULL_IF_NULL + * @see #NULLPOLICY_IGNORE_IF_NULL + */ + int nullPolicy() default NULLPOLICY_FULL_IF_NULL; + + /** + * Name of tag/attribute of this element (replaces declared field's name, if not empty). + */ + String name() default DEFAULT_NAME; + + /** + * If true, the value is written as attribute inside host tag.
+ * Otherwise the value is written inside inner node.
+ * If this element is set true for an object that cannot be inlined, exception is raised. + */ + boolean inline() default false; +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/xml/XMLMaker.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/xml/XMLMaker.java new file mode 100644 index 000000000..585d8f5d5 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/xml/XMLMaker.java @@ -0,0 +1,221 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.xml; + +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Utility; + +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; +import java.io.StringWriter; +import java.lang.reflect.Array; +import java.lang.reflect.Field; +import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +public class XMLMaker { + private static final String LIST_TAG = "list"; + private static final String LIST_ELEMENT = "value"; + private static final String CYCLIC_LINK = "cyclic"; + private static final String NULL_ELEMENT = "null"; + + /** + * Writes some xml. + * @param writer + * where to write. + * @param object + * what to serialize. Can be null. + * @param tagName + * name of the tag. Can be null. + * @param inline + * if this is an attribute or not. + * @param childName + * name of each child node (only for iterables/arrays). Can be null or empty. + * @param identityMap + * map to determine cyclic links and handle them in proper way. + * @throws javax.xml.stream.XMLStreamException + * @throws IllegalAccessException + */ + private static void writeXML(XMLStreamWriter writer, + Object object, + String tagName, + boolean inline, + String childName, + IdentityHashMap identityMap) + throws XMLStreamException, IllegalAccessException { + ConditionVerifier inlineUseVerifier = () -> { + if (inline) { + throw new IllegalArgumentException("This object cannot be inlined: " + object); + } + }; + + if (tagName != null && !inline) { + writer.writeStartElement(tagName); + } + + if (object == null) { + writer.writeEmptyElement(NULL_ELEMENT); + } else { + Class objClass = object.getClass(); + + if (identityMap.containsKey(object)) { + writer.writeCharacters(CYCLIC_LINK); + } else { + identityMap.put(object, Boolean.TRUE); + + if (objClass.getAnnotation(XMLComplexObject.class) != null) { + inlineUseVerifier.verify(); + + // Convenience trick. + FieldXMLAppender xmlAppender = (field, overrideName) -> { + XMLField fieldAnno = field.getAnnotation(XMLField.class); + + String fieldName = fieldAnno.name(); + boolean accessible = field.isAccessible(); + field.setAccessible(true); + + if (overrideName != null && overrideName.isEmpty()) { + if (fieldName.isEmpty()) { + overrideName = field.getName(); + } else { + overrideName = fieldName; + } + } + + Object fieldValue = field.get(object); + if (fieldValue != null + || fieldAnno.nullPolicy() == XMLField.NULLPOLICY_FULL_IF_NULL) { + writeXML( + writer, + field.get(object), + overrideName, + fieldAnno.inline(), + fieldAnno.childName(), + identityMap); + } else if (fieldAnno.nullPolicy() == XMLField.NULLPOLICY_EMPTY_IF_NULL) { + writer.writeEmptyElement(overrideName); + } + field.setAccessible(accessible); + }; + + List annotatedFields = Utility.getAllAnnotatedFields(objClass, XMLField.class); + + if (annotatedFields.isEmpty()) { + throw new IllegalArgumentException( + "Class " + + objClass.getSimpleName() + + " does not have any XML fields and cannot be serialized"); + } else if (objClass.getAnnotation(XMLComplexObject.class).wrapper()) { + if (annotatedFields.size() != 1) { + throw new IllegalArgumentException( + "Class " + + objClass.getSimpleName() + + " has more then one XML fields, thus it cannot be annotated as " + + "'wrapper'"); + } + xmlAppender.appendInfo(annotatedFields.get(0), tagName); + } else { + // Attributes first. + for (Field field : annotatedFields) { + XMLField fieldAnno = field.getAnnotation(XMLField.class); + if (fieldAnno.inline()) { + xmlAppender.appendInfo(field); + } + } + + // Tags then. + for (Field field : annotatedFields) { + XMLField fieldAnno = field.getAnnotation(XMLField.class); + if (!fieldAnno.inline()) { + xmlAppender.appendInfo(field); + } + } + } + } else if (object instanceof Iterable || objClass.isArray()) { + inlineUseVerifier.verify(); + + Iterator iter = (object instanceof Iterable + ? ((Iterable) object).iterator() + : new Iterator() { + final int size = Array.getLength(object); + int index = 0; + + @Override + public boolean hasNext() { + return index < size; + } + + @Override + public Object next() { + if (!hasNext()) { + throw new NoSuchElementException( + "No more elements in the array"); + } + return Array.get(object, index++); + } + }); + + if (tagName == null) { + writer.writeStartElement(LIST_TAG); + } + while (iter.hasNext()) { + Object next = iter.next(); + writer.writeStartElement( + childName == null || childName.isEmpty() ? LIST_ELEMENT : childName); + writeXML(writer, next, null, false, null, identityMap); + writer.writeEndElement(); + } + if (tagName == null) { + writer.writeEndElement(); + } + } else { + String value = object.toString(); + if (inline) { + writer.writeAttribute(tagName, value); + } else { + writer.writeCharacters(value); + } + } + + identityMap.remove(object); + } + } + + if (tagName != null && !inline) { + writer.writeEndElement(); + } + } + + public static String makeXML(Object object, String tagName) { + try { + XMLOutputFactory factory = XMLOutputFactory.newFactory(); + StringWriter stringWriter = new StringWriter(); + XMLStreamWriter xmlWriter = factory.createXMLStreamWriter(stringWriter); + + writeXML(xmlWriter, object, tagName, false, null, new IdentityHashMap<>()); + + return stringWriter.toString(); + } catch (IllegalAccessException | XMLStreamException exc) { + throw new RuntimeException(exc); + } + } + + /** + * Convenience structure used in method {@link #writeXML(javax.xml.stream.XMLStreamWriter, Object, + * String, + * boolean, String, java.util.IdentityHashMap)}. + */ + @FunctionalInterface + private interface FieldXMLAppender { + void appendInfo(Field field, String overrideName) throws IllegalAccessException, XMLStreamException; + + default void appendInfo(Field field) throws IllegalAccessException, XMLStreamException { + appendInfo(field, ""); + } + } + + @FunctionalInterface + interface ConditionVerifier { + void verify() throws RuntimeException; + } +}