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..42056da21 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/AutoCloseableProvider.java @@ -0,0 +1,8 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db; + +import ru.fizteh.fivt.storage.structured.TableProvider; + +interface AutoCloseableProvider extends TableProvider, AutoCloseable { + @Override + void close(); +} 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..065c79588 --- /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; + +interface AutoCloseableTable extends Table, AutoCloseable { + @Override + void close(); +} 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 54fa92cda..6d4e102b8 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,17 @@ 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.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,18 +26,14 @@ 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) @@ -52,318 +53,333 @@ public class DBTableProvider implements TableProvider { } }).putNext(Double.class, Double::parseDouble).putNext(Float.class, Float::parseFloat) .putNext( - String.class, - s -> Utility.unquoteString(s, QUOTE_CHARACTER + "", ESCAPE_CHARACTER + "")); + 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 * 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); + public AutoCloseableTable getTable(String name) throws IllegalArgumentException { + try (UseLock useLock = validityController.use()) { + Utility.checkTableNameIsCorrect(name); - persistenceLock.readLock().lock(); - try { - if (tables.containsKey(name)) { - StoreableTableImpl table = tables.get(name); - if (table == null) { - DatabaseIOException corruptionReason = corruptTables.get(name); - throw new IllegalArgumentException( - corruptionReason.getMessage(), corruptionReason); + 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); + } + } } - return table; - } else { - return null; + + 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(); } - } finally { - persistenceLock.readLock().unlock(); } } @Override - public StoreableTableImpl createTable(String name, List> columnTypes) + public AutoCloseableTable createTable(String name, List> columnTypes) throws IllegalArgumentException, DatabaseIOException { - Utility.checkTableNameIsCorrect(name); + 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); + 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); + Path tablePath = databaseRoot.resolve(name); - persistenceLock.writeLock().lock(); - try { - if (tables.containsKey(name) && tables.get(name) != null) { - return null; - } + persistenceLock.writeLock().lock(); + try { + if (tables.containsKey(name) && tables.get(name) != null) { + return null; + } - StoreableTableImpl newTable = StoreableTableImpl.createTable(this, tablePath, columnTypes); - tables.put(name, newTable); - return newTable; - } finally { - persistenceLock.writeLock().unlock(); + AutoCloseableTable newTable = + StoreableTableImpl.createTable(this, this::onTableClosed, tablePath, columnTypes); + tables.put(name, newTable); + return newTable; + } finally { + persistenceLock.writeLock().unlock(); + } } } @Override public void removeTable(String name) throws IllegalArgumentException, IllegalStateException, DatabaseIOException { - Utility.checkTableNameIsCorrect(name); - Path tablePath = databaseRoot.resolve(name); + 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"); - } + persistenceLock.writeLock().lock(); + try { + if (!tables.containsKey(name)) { + throw new IllegalStateException(name + " not exists"); + } - StoreableTableImpl 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.invalidate(); - } + 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(); + } - corruptTables.remove(name); + corruptTables.remove(name); - if (!Files.exists(tablePath)) { - return; - } + if (!Files.exists(tablePath)) { + return; + } - 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; + 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(); } - } 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; + try (UseLock useLock = validityController.use()) { + Utility.checkNotNull(table, "Table"); + Utility.checkNotNull(value, "Value"); - for (int col = 0, colsCount = table.getColumnsCount(); col < colsCount; col++) { + StoreableTableImpl.checkStoreableAppropriate(table, value); - String colValueStr; // If null, write null. - - 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))); + } + } + + /** + * Loads tables from file system that are not registered.
+ * Not thread-safe. + * @throws DatabaseIOException */ + private void loadMissingTables() throws DatabaseIOException { + loadTables(tables::containsKey); + } - private void reloadTables() throws DatabaseIOException { + 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) { @@ -371,4 +387,53 @@ 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(); + } finally { + persistenceLock.writeLock().unlock(); + } + factory.onProviderClosed(this); + } finally { + tableClosedByMe = false; + } + } } 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..9fc03d3e9 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,74 @@ package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db; +import ru.fizteh.fivt.proxy.LoggingProxyFactory; import ru.fizteh.fivt.storage.structured.TableProviderFactory; 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.LoggingProxyFactoryImpl; 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 TableProviderFactory, AutoCloseable { + private static final LoggingProxyFactory LOGGING_PROXY_FACTORY = new LoggingProxyFactoryImpl(); + 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<>(); + + 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()) { + for (AutoCloseableProvider provider : generatedProviders.keySet()) { + provider.close(); + } + generatedProviders.clear(); + } + } + + /** + * Unregisters the given provider. + * @param provider + * Pure (not proxied) link to the closed provider. + */ + synchronized void onProviderClosed(AutoCloseableProvider provider) { + try (UseLock useLock = validityController.use()) { + generatedProviders.remove(provider); + } + } private void checkDatabaseDirectory(final Path databaseRoot) throws DatabaseIOException { if (!Files.isDirectory(databaseRoot)) { @@ -31,25 +89,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..0e90fb129 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 @@ -13,8 +13,8 @@ * Database class responsible for a set of tables assigned to it. * @author phoenix */ -public class Database { - protected final TableProvider provider; +public class Database implements AutoCloseable { + private final TableProvider provider; /** * Root directory of all database files */ @@ -69,6 +69,13 @@ public void dropTable(String tableName) throws IllegalArgumentException, IOExcep } } + @Override + public void close() throws Exception { + if (provider instanceof AutoCloseable) { + ((AutoCloseable) provider).close(); + } + } + public Table getActiveTable() throws NoActiveTableException { checkCurrentTableIsOpen(); return activeTable; 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 9b5fa3520..390975c7e 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,17 +3,24 @@ 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.util.ArrayList; +import java.util.List; import java.util.Objects; /** * Implementation of Storeable that can be put to the table it is assigned to as a value.
- * Not thread-safe. + * Not thread-safe.
+ * Not bound to any table. */ -public class StoreableImpl implements Storeable { +@JSONComplexObject(wrapper = true) +public final class StoreableImpl implements Storeable { + @JSONField private final Object[] values; - private Table host; + private final List> types; /** * Creates a new instance of Storeable with null values as default. @@ -21,21 +28,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())); @@ -110,7 +124,7 @@ private T getTypedValue(int columnIndex, Class clazz) @Override public int hashCode() { - return host.hashCode(); + return values.length; } @Override @@ -120,11 +134,11 @@ public boolean equals(Object obj) { } StoreableImpl storeable = (StoreableImpl) obj; - if (host != storeable.host) { + if (values.length != storeable.values.length) { 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; } @@ -135,9 +149,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 0c436737b..fdebf0f65 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,9 +28,9 @@ import java.util.List; import java.util.Map; import java.util.Scanner; -import java.util.concurrent.atomic.AtomicBoolean; +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") @@ -48,17 +51,24 @@ public class StoreableTableImpl implements Table { private final List> columnTypes; - private AtomicBoolean 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 = new AtomicBoolean(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); @@ -77,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); } /** @@ -105,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; @@ -137,7 +151,8 @@ static StoreableTableImpl getTable(TableProvider provider, Path tablePath) throw } } - 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(); @@ -151,7 +166,7 @@ static StoreableTableImpl getTable(TableProvider provider, Path tablePath) throw } } - return table; + return DBTableProviderFactory.wrapImplementation(table, AutoCloseableTable.class); } /** @@ -164,7 +179,7 @@ static StoreableTableImpl getTable(TableProvider provider, Path tablePath) throw * 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"); @@ -185,104 +200,87 @@ 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 IllegalStateException}). - */ - void invalidate() { - // We need table's write lock here to sync with file system. - // Remember the table becomes invalidated before being deleted. - store.getPersistenceLock().writeLock().lock(); - try { - invalidated.set(true); - } finally { - store.getPersistenceLock().writeLock().unlock(); - } - } - - private void checkValidity() throws IllegalStateException { - if (invalidated.get()) { - 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(); + } } /** @@ -290,37 +288,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); + } } /** @@ -339,4 +342,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 037c3c400..d40bafbeb 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 @@ -144,16 +144,6 @@ public static StringTableImpl getTable(Path tableRoot, Predicate extraFile return table; } - /** - * Get read-write lock for the table.
- * Write lock is acquired when something is going to be written to the file system (before this data is - * updated using the diff for the calling thread).
- * In all other cases read lock is acquired. - */ - ReadWriteLock getPersistenceLock() { - return persistenceLock; - } - public Path getTableRoot() { return tableRoot; } @@ -229,7 +219,7 @@ private void checkFileSystem(Predicate filter) throws DatabaseIOException } } - public void readFromFileSystem() throws DBFileCorruptIOException, TableCorruptIOException { + void readFromFileSystem() throws DBFileCorruptIOException, TableCorruptIOException { persistenceLock.writeLock().lock(); try { @@ -376,7 +366,7 @@ private Path makeTablePartFilePath(int hash) { } /** - * Gets {@link ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.TablePart} instance assigned to + * Gets {@link TablePart} instance assigned to * this {@code hash} from memory. Not thread-safe. * @param key * key that is hold by desired table. 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 1dd810783..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 @@ -27,12 +27,11 @@ * @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(() -> new HashMap()); + private final ThreadLocal> diffMap = ThreadLocal.withInitial(HashMap::new); private Path tablePartFilePath; /** * Map with last changes that are written to the file system.
@@ -63,8 +62,7 @@ public TablePart(Path tablePartFilePath) { public String get(String key) { if (diffMap.get().containsKey(key)) { - String value = diffMap.get().get(key); - return value; + return diffMap.get().get(key); } else { return lastCommittedMap.get(key); } @@ -228,7 +226,7 @@ public String remove(String key) { * @return Separate actual version. Changes in this instance have not effect on true database state. */ private Map makeNewActualVersion() { - return makeActualVersion(new HashMap(lastCommittedMap)); + return makeActualVersion(new HashMap<>(lastCommittedMap)); } /** @@ -249,13 +247,16 @@ private Map makeActualVersion(Map actualVersion) return actualVersion; } + /** + * Returns actual size for the moment (considering the thread local diff). + */ public int size() { return makeNewActualVersion().size(); } /** * Writes changes to the file. - * @throws IOException + * @throws java.io.IOException */ private void writeToFile() throws IOException { ByteArrayOutputStream stream = new ByteArrayOutputStream(1024); 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..e01485f34 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/json/JSONMaker.java @@ -0,0 +1,214 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.json; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Array; +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.IdentityHashMap; +import java.util.LinkedList; +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() { } + + private static List getAnnotatedFields(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; + } + + /** + * 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 (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 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 = getAnnotatedFields(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 { + 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..ef3b10790 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/json/JSONParsedObject.java @@ -0,0 +1,145 @@ +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 java.lang.Long}
  • + *
  • {@link java.lang.Double}
  • + *
  • {@link java.lang.Boolean}
  • + *
  • {@link java.lang.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 java.lang.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 java.lang.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 java.lang.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 java.lang.UnsupportedOperationException + */ + Map asMap() throws UnsupportedOperationException; + + /** + * Returns this object as array. Unsupported for associative array representations. + * @throws java.lang.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..e736aa77c --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/json/JSONParser.java @@ -0,0 +1,567 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.json; + +import java.text.ParseException; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +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)) { + 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(String json) throws ParseException { + ArrayList tokens = new ArrayList<>(findTokens(json, 0, json.length())); + 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 (!currentlyParsingObject.object.isStandardArray()) { + 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(); + + switch (valueString) { + case "": { + throw new ParseException("Empty elements are not allowed in json", -1); + } + 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; + } + } + + 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 Object[] array; + + public ArrayObject(int length) { + this.array = new Object[length]; + } + + @Override + public void put(String key, Object value) throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + + @Override + public void put(int index, Object value) throws UnsupportedOperationException { + array[index] = value; + } + + @Override + public Object get(String key) { + throw new UnsupportedOperationException("This operation not supported in standard arrays"); + } + + @Override + public Object get(int index) { + return array[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.length; + } + + @Override + public String toString() { + return Arrays.toString(array); + } + + @Override + public Map asMap() { + throw new UnsupportedOperationException("Not supported in standard arrays"); + } + + @Override + public Object[] asArray() { + return array; + } + } + + /** + * 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/shell/AbstractCommand.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/AbstractCommand.java index 5faced748..a46e16168 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 @@ -20,10 +20,10 @@ public abstract class AbstractCommand implements Command DATABASE_ERROR_HANDLER = + static final AccurateExceptionHandler DATABASE_ERROR_HANDLER = (Exception exc, SingleDatabaseShellState shell) -> { boolean found = false; Class actualType = exc.getClass(); @@ -114,7 +114,7 @@ public abstract void executeSafely(SingleDatabaseShellState shell, String[] args ParseException, IOException; - protected void checkArgsNumber(String[] args, int minimal, int maximal) throws TerminalException { + void checkArgsNumber(String[] args, int minimal, int maximal) throws TerminalException { if (args.length < minimal || args.length > maximal) { handleError(null, new WrongArgsNumberException(this, args[0]), true); } diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/CommandContainer.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/CommandContainer.java index 499ec99b9..348e75d0b 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/CommandContainer.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/CommandContainer.java @@ -6,7 +6,7 @@ * Base interface for class that has a variety of commands suitable for given shell state. * @param * Some class extending ShellState - * @see ShellState + * @see ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.ShellState */ public interface CommandContainer> { Map> getCommands(); 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..e105c25fb 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 @@ -2,7 +2,7 @@ import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.TerminalException; -public class Main { +class Main { //java -Dfizteh.db.dir=/home/phoenix/test/DB ru.fizteh.fivt.students.fedorov_andrew // .databaselibrary.shell.Main 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 616dccb85..6c7701d44 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,6 +1,5 @@ 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; @@ -27,19 +26,17 @@ * @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; - + private boolean valid = true; /** * Available commands for invocation. */ @@ -77,13 +74,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); } @@ -155,10 +148,19 @@ 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 { + checkValid(); + valid = false; + interactive = true; if (stream == null) { @@ -218,6 +220,9 @@ 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 { + checkValid(); + valid = false; + try { interactive = false; @@ -259,4 +264,19 @@ private void persistSafelyAndPrepareToExit() throws ExitRequest { } } + public boolean isValid() { + return valid; + } + + @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/SingleDatabaseShellState.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/SingleDatabaseShellState.java index 0ef351b74..774fc94e2 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 @@ -102,6 +102,11 @@ public void persist() throws IOException { public void prepareToExit(int exitCode) throws ExitRequest { Log.log(SingleDatabaseShellState.class, "Preparing to exit with code " + exitCode); cleanup(); + try { + activeDatabase.close(); + } catch (Exception exc) { + Log.log(SingleDatabaseShellState.class, exc, "Failed to close database properly"); + } Log.close(); throw new ExitRequest(exitCode); } 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..2f88a8289 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 @@ -19,7 +19,7 @@ public class Log { /** * If logging is disabled, no messages are output */ - private static boolean enableLogging = true; + private static boolean enableLogging = false; /** * Writer to the log file. */ @@ -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,27 @@ 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, 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 +114,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/LoggingProxyFactoryImpl.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/LoggingProxyFactoryImpl.java new file mode 100644 index 000000000..d3fb1ebdf --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/LoggingProxyFactoryImpl.java @@ -0,0 +1,194 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support; + +import ru.fizteh.fivt.proxy.LoggingProxyFactory; +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 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; + +public class LoggingProxyFactoryImpl implements LoggingProxyFactory { + private volatile boolean loggingEnabled = true; + + private static LoggingReport constructReport(long timestamp, + String invokeeClass, + String invokeeMethod, + Object[] arguments, + Throwable thrown, + Object returnedValue, + boolean isVoid) { + if (thrown != null) { + return new LoggingReportWithThrown(timestamp, invokeeClass, invokeeMethod, arguments, thrown); + } + if (isVoid) { + return new LoggingReport(timestamp, invokeeClass, invokeeMethod, arguments); + } else { + return new LoggingReportWithReturnValue( + timestamp, invokeeClass, invokeeMethod, arguments, returnedValue); + } + } + + 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)); + } + + @JSONComplexObject + private static class LoggingReport { + @JSONField + long timestamp; + + @JSONField(name = "class") + String invokeeClass; + + @JSONField(name = "method") + String invokeeMethod; + + @JSONField + Object[] arguments; + + public LoggingReport() { + + } + + 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 + Object returnValue; + + public LoggingReportWithReturnValue() { + + } + + 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 + Throwable thrown; + + public LoggingReportWithThrown() { + + } + + public LoggingReportWithThrown(long timestamp, + String invokeeClass, + String invokeeMethod, + Object[] arguments, + Throwable thrown) { + super(timestamp, invokeeClass, invokeeMethod, arguments); + this.thrown = thrown; + } + } + + 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) { + LoggingReport report = constructReport( + timestamp, + wrappedObject.getClass().getSimpleName(), + method.getName(), + args, + thrown, + returnValue, + void.class.equals(method.getReturnType())); + String str = JSONMaker.makeJSON(report); + + try { + writer.write(str + 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/Utility.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/Utility.java index b478cd8a9..aef753f42 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 @@ -222,20 +222,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,17 +232,15 @@ 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 ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Utility#unquoteString(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; } @@ -280,9 +264,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; } 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..71953f070 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/ValidityController.java @@ -0,0 +1,81 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support; + +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +/** + * Convenience class to control one's validity. + */ +public final class ValidityController { + private final ReadWriteLock validityLock = new ReentrantReadWriteLock(true); + private boolean valid = true; + + private void checkValid() { + if (!valid) { + throw new IllegalStateException("This object has been invalidated: "); + } + } + + /** + * Invalidates + */ + private void invalidate() { + validityLock.writeLock().lock(); + try { + checkValid(); + valid = false; + } finally { + validityLock.writeLock().unlock(); + } + } + + /** + * 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 java.lang.IllegalStateException + * if this object has been already invalidated. + */ + public UseLock use() throws IllegalStateException { + 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 java.lang.IllegalStateException + * if this object has been already invalidated. + */ + public KillLock useAndKill() throws IllegalStateException { + validityLock.writeLock().lock(); + try { + checkValid(); + validityLock.writeLock().lock(); + return new KillLock(); + } finally { + validityLock.writeLock().unlock(); + } + } + + public class UseLock implements AutoCloseable { + @Override + public void close() { + validityLock.readLock().unlock(); + } + } + + public class KillLock implements AutoCloseable { + @Override + public void close() { + invalidate(); + validityLock.writeLock().unlock(); + } + } + +} 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 index 1b4243b58..af5f8b71e 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/ControllableRunnerTest.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/ControllableRunnerTest.java @@ -39,9 +39,7 @@ public void testWaitForEndOfWork() throws Exception { public void testWaitForEndOfWork1() throws Exception { ControllableRunner runner = new ControllableRunner(); ControllableRunnable runnable = runner.createControllable( - (ControllableAgent agent) -> { - System.err.println("Hello from runnable"); - }); + (ControllableAgent agent) -> System.err.println("Hello from runnable")); runner.assignRunnable(runnable); new Thread(runner, "Runnable").start(); 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 9f1d6a323..9bd0fab1d 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 @@ -32,10 +32,10 @@ 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()); + "Creating table", makeTerminalExpectedMessage("created"), getOutput()); } @Test @@ -52,16 +52,16 @@ public void testFailPersistOnExit() throws TerminalException { @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 ]"); - 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 @@ -91,13 +91,14 @@ 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 +110,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"); + createTableWithStringColumn(table); + runBatchExpectZero("use " + table, "put a [\"b\"]; commit; put b [\"c\"]; remove a; commit"); - createAndUseTable(table); - runBatchExpectZero(command); - - assertEquals("Changes count test in commit", expectedReply, getOutput()); + assertEquals( + makeTerminalExpectedMessage("using " + table, "new", "1", "new", "removed", "2"), + getOutput()); } @Test public void testCommitFail() throws TerminalException { 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"); + createTableWithStringColumn(table); + runBatchExpectZero("use table", "put a [\"b\"]", "commit", "put b [\"c\"];", "remove a", "rollback"); - createAndUseTable(table); - runBatchExpectZero(command); - - assertEquals("Changes count test in rollback", expectedReply, getOutput()); + assertEquals( + makeTerminalExpectedMessage("using " + table, "new", "1", "new", "removed", "2"), + getOutput()); } @Test @@ -166,13 +168,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 +181,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); + createTableWithStringColumn(table); + runBatchExpectZero("use " + table, "put " + key + " [\"" + value + "\"]", "get " + key); - createAndUseTable(table); - runBatchExpectZero(command); - - 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,16 +222,14 @@ public void testInteractiveMode() throws TerminalException { public void testInteractiveMode1() throws TerminalException { String table = "table"; String fakeTable = "fake_table"; - createAndUseTable(table); + createTableWithStringColumn(table); runBatchExpectZero( - "put a [\"b\"]; put b [\"c\"]; put c [\"d\"]; put d [\"e\"]; put e [\"a\"]; exit"); - runInteractiveExpectZero( - true, "use " + table, - "show tables", - "use " + fakeTable + "; list", - "use " + table + "; list"); + "put a [\"b\"]; put b [\"c\"]; put c [\"d\"]; put d [\"e\"]; put e [\"a\"];", + "exit"); + runInteractiveExpectZero( + "use " + table, "show tables", "use " + fakeTable + "; list", "use " + table + "; list"); String regex = makeTerminalExpectedRegex( GREETING_REGEX, @@ -257,8 +250,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"), @@ -277,11 +270,12 @@ public void testListWithoutActiveTable() throws TerminalException { @Test public void testSize() throws TerminalException { String table = "table"; - createAndUseTable(table); + createTableWithStringColumn(table); runBatchExpectZero( + "use " + table, "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()); + runBatchExpectZero("use " + table, "size"); + assertEquals(makeTerminalExpectedMessage("using " + table, "3"), getOutput()); } @Test @@ -367,11 +361,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, @@ -385,7 +380,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); @@ -400,8 +395,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())) { @@ -411,7 +406,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(), @@ -507,10 +502,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 index 22d2edbe9..86ae6da46 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/DuplicatedIOTestBase.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/DuplicatedIOTestBase.java @@ -16,7 +16,7 @@ */ @Ignore public class DuplicatedIOTestBase { - protected static PrintStream stdErr; + private static PrintStream stdErr; // Standard out and error streams are stored here. private static PrintStream stdOut; @@ -70,7 +70,7 @@ public void printlnDirectlyToStdOut(String str) { stdOut.println(str); } - public void printlnDirectlyToStdOut() { + void printlnDirectlyToStdOut() { stdOut.println(); } 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 29119072b..1fd4b20fa 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 @@ -24,14 +24,14 @@ public abstract class InterpreterTestBase interpreter; + Shell interpreter; @BeforeClass public static void globalPrepareInterpreterTestBase() { @@ -45,10 +45,6 @@ public static void globalCleanupInterpreterTestBase() { protected abstract Shell constructInterpreter() throws TerminalException; - /** - * Initializes {@link #interpreter}. - * @throws TerminalException - */ @Before public void prepare() throws TerminalException { interpreter = constructInterpreter(); @@ -82,7 +78,7 @@ public void cleanup() throws IOException { * @return Regex for full interpreter answer. * @see java.util.regex.Pattern */ - protected String makeTerminalExpectedRegex(String greetingRegex, String... reports) { + 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)); @@ -93,21 +89,19 @@ protected String makeTerminalExpectedRegex(String greetingRegex, String... repor /** * Obtains everything that was output by the interpreter.
*/ - protected String getOutput() { + 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. IO_DUPLICATOR.prepare(); @@ -119,12 +113,12 @@ protected int runBatch(boolean reinit, String... commands) throws TerminalExcept } } - 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(commands); } @@ -132,14 +126,12 @@ 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 { + int runInteractive(String... lines) throws TerminalException { IO_DUPLICATOR.prepare(); for (String cmd : lines) { @@ -150,46 +142,30 @@ protected int runInteractive(boolean reinit, String... lines) throws TerminalExc 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)); + void runBatchExpectNonZero(String... commands) throws TerminalException { + assertNotEquals("Non-zero exit status expected", 0, runBatch(commands)); } /** @@ -198,7 +174,7 @@ protected void runBatchExpectNonZero(boolean reinit, String... commands) throws * Each report is considered to be a separate line. Lines are separated using {@link * System#lineSeparator()}. */ - protected String makeTerminalExpectedMessage(String... reports) { + String makeTerminalExpectedMessage(String... reports) { StringBuilder sb = new StringBuilder(); for (String s : reports) { sb.append(String.format("%s%n", s)); 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/LoggingProxyFactoryTest.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/LoggingProxyFactoryTest.java new file mode 100644 index 000000000..b79c9a9eb --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/LoggingProxyFactoryTest.java @@ -0,0 +1,179 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test; + +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.junit.Before; +import org.junit.BeforeClass; +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.LoggingProxyFactoryImpl; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test.support.BAOSDuplicator; + +import java.io.OutputStreamWriter; +import java.io.Writer; +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 LoggingProxyFactoryTest { + private static final String TIMESTAMP = "timestamp"; + private static final String CLASS = "class"; + private static final String METHOD = "method"; + private static final String ARGUMENTS = "arguments"; + private static final String THROWN = "thrown"; + private static final String RETURN_VALUE = "returnValue"; + + /** + * 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 LoggingProxyFactory factory; + private final BAOSDuplicator out = new BAOSDuplicator(System.out); + private final Writer writer = new OutputStreamWriter(out); + private TestFace wrapped; + + private static Matcher makeRequirements(Matcher... restrictions) { + return allOf(MIN_REQUIREMENTS, allOf(restrictions)); + } + + @BeforeClass + public static void globalPrepare() { + factory = new LoggingProxyFactoryImpl(); + } + + private String getOutput() { + return out.toString(); + } + + @Before + public void prepare() { + out.reset(); + wrapped = (TestFace) factory.wrap(writer, new TestFaceImpl(), TestFace.class); + } + + @Test + public void testDoNothing() throws ParseException { + wrapped.doNothing(); + JSONParsedObject parsed = JSONParser.parseJSON(getOutput()); + assertThat(parsed, makeRequirements()); + } + + @Test + public void testBoolMethod() 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 testThrowingMethod() 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 testEqualsNotProxied() { + wrapped.equals(null); + assertEquals("", getOutput()); + } + + private interface TestFace { + default void doNothing() { + } + + default boolean boolMethod(int a, Integer b) { + return true; + } + + default String throwingMethod(Iterable iterable) throws Exception { + throw new Exception(); + } + } + + private static class TestFaceImpl implements TestFace { + + } + + private 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/StoreableTest.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/StoreableTest.java index a516ea20e..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; @@ -39,6 +40,13 @@ 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()); @@ -47,16 +55,27 @@ public void prepare() throws IOException { } @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); - assertNotEquals(storeable, provider.createFor(table2)); + assertEquals(storeable, provider.createFor(table2)); } @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 b6cd80c05..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 @@ -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 d84630bb2..0f80b9a8e 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 @@ -3,6 +3,7 @@ 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; @@ -54,15 +55,28 @@ 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 { + if (provider != null && provider instanceof AutoCloseable) { + ((AutoCloseable) provider).close(); + } 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 { @@ -75,6 +89,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); @@ -89,14 +108,6 @@ 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 testConcurrentCreateTable() throws Exception { final String tableName = "table"; @@ -141,9 +152,10 @@ public void runWithFreedom(ControllableAgent agent) throws Exception, AssertionE Table gotTable = ((TableCreator) runners[0].getRunnable()).gotTable; for (int i = 1; i < threadsCount; i++) { - assertTrue( + assertSame( "All links for gotTable must be the same", - ((TableCreator) runners[i].getRunnable()).gotTable == gotTable); + gotTable, + ((TableCreator) runners[i].getRunnable()).gotTable); } boolean foundCreated = false; @@ -165,7 +177,11 @@ public void runWithFreedom(ControllableAgent agent) throws Exception, AssertionE @Test 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, "3, 1, null"); } @@ -173,7 +189,10 @@ public void testDeserialize() throws IOException, ParseException { @Test public void testDeserialize1() throws IOException, ParseException { Table table = createTable(Long.class, Byte.class, String.class); - expectJSONRegexMatchFailure(); + exception.expect(ParseException.class); + exception.expectMessage( + wrongTypeMatcherAndAllOf( + containsString("Empty elements are not allowed in json"))); provider.deserialize(table, "[,,]"); } @@ -181,19 +200,12 @@ public void testDeserialize1() throws IOException, ParseException { @Test 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, "[\"]"); } - @Test - public void testDeserialize3() throws IOException, ParseException { - Table table = createTable(Long.class, Byte.class, String.class); - expectJSONRegexMatchFailure(); - - provider.deserialize(table, "[\"/say\"]"); - } - @Test public void testDeserializeTooManyElements() throws IOException, ParseException { Table table = createTable(Long.class, Byte.class, Integer.class); @@ -202,7 +214,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\" ]"); } @@ -214,8 +226,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\" ]"); } @@ -233,13 +244,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)); } @@ -261,7 +273,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); @@ -282,7 +294,7 @@ public void testObtainTableWithInvalidSignatureFile() throws IOException { } @Test - public void testObtainTableWithExtraFile() throws IOException { + public void testObtainTableWithExtraFile() throws Exception { String tableName = "table"; Files.createDirectories(DB_ROOT.resolve(tableName).resolve("1.dir")); @@ -295,7 +307,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)); @@ -308,7 +320,7 @@ public void testObtainTableWithMissingSignatureFile() throws IOException { } @Test - public void testObtainTableWithBadIDOfPartFile() throws IOException { + public void testObtainTableWithBadIDOfPartFile() throws Exception { String tableName = "table"; Files.createDirectories(DB_ROOT.resolve(tableName).resolve("1.dir")); @@ -388,7 +400,7 @@ public void testRemoveTableExistent() throws IOException { provider.removeTable(name); exception.expect(IllegalStateException.class); - exception.expectMessage(name + " is invalidated"); + exception.expectMessage("This object has been invalidated"); // Even calling to this simple method can cause IllegalStateException if the table has been removed. table.getName(); @@ -449,7 +461,7 @@ 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)); 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 3426ba318..f6324c118 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 @@ -84,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); @@ -336,17 +341,6 @@ public void testManyConcurrentCommits() throws Exception { } } - @Test - public void testPutOneStoreableToAnotherTable() throws IOException { - Table table2 = provider.createTable(TABLE_NAME + "2", DEFAULT_COLUMN_TYPES); - Storeable storeable = provider.createFor(table); - - exception.expect(IllegalStateException.class); - exception.expectMessage("Cannot put storeable assigned to one table to another table"); - - table2.put("key", storeable); - } - @Test public void testGetColumnTypeAtBadIndex() { exception.expect(IndexOutOfBoundsException.class); 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/UtilityTest.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/UtilityTest.java index ede72c41c..93e2c4824 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<>()).putNext(1, 2).putNext( 2, 2)); } 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 9f1fb92ea..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 @@ -30,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); } diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/parallel/ControllableRunnable.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/parallel/ControllableRunnable.java index 04250cbbb..3bf903c4e 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/parallel/ControllableRunnable.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/parallel/ControllableRunnable.java @@ -27,8 +27,8 @@ public final void run() { } /** - * Call this method after execution finishes. If an {@link java.lang.Exception} or {@link - * java.lang.AssertionError} has occurred during execution, it will + * Call this method after execution finishes. If an {@link Exception} or {@link + * AssertionError} has occurred during execution, it will * be rethrown. * @throws Exception */ diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/parallel/ControllableRunner.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/parallel/ControllableRunner.java index 16effbb9b..d71b1ba8a 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/parallel/ControllableRunner.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/parallel/ControllableRunner.java @@ -36,7 +36,7 @@ public ControllableRunner() { /** * 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 java.lang.IllegalStateException + * @throws IllegalStateException * If you cannot assign any runnables now. */ public synchronized void assignRunnable(ControllableRunnable runnable) throws IllegalStateException { @@ -109,9 +109,9 @@ public void runWithFreedom(ControllableAgent agent) throws Exception { * 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 java.lang.Exception + * @throws Exception * If thrown during runnable execution. - * @throws java.lang.AssertionError + * @throws AssertionError * If thrown during runnable execution. */ public synchronized void waitUntilPause() throws InterruptedException, Exception, AssertionError { @@ -127,9 +127,9 @@ public synchronized void waitUntilPause() throws InterruptedException, Exception * set. All pauses that can be met in the future will be ignored with order {@link #continueWork()}.
* throws exception. * @throws InterruptedException - * @throws java.lang.Exception + * @throws Exception * If thrown during runnable execution. - * @throws java.lang.AssertionError + * @throws AssertionError * If thrown during runnable execution. */ public synchronized void waitUntilEndOfWork() throws InterruptedException, Exception, AssertionError { @@ -169,7 +169,7 @@ synchronized void onControllablePause(ControllableRunnable pausingRunnable) thro order = ORDER_NOT_SET; } - public synchronized boolean isRunnableAssigned() { + synchronized boolean isRunnableAssigned() { return runnable != null; } @@ -189,10 +189,6 @@ private synchronized void checkStatusIsStarted() throws IllegalStateException { * Call this method after waiting in {@link #waitUntilPause()} to tell the runnable to continue work. * @throws IllegalStateException * If execution has finished. - * @throws java.lang.Exception - * If thrown during runnable execution. - * @throws java.lang.AssertionError - * If thrown during runnable execution. */ public synchronized void continueWork() throws IllegalStateException { checkRunnableAssigned(); diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/parallel/ExceptionFreeRunnable.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/parallel/ExceptionFreeRunnable.java index abc9aedfb..4204d625c 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/parallel/ExceptionFreeRunnable.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/parallel/ExceptionFreeRunnable.java @@ -9,7 +9,7 @@ 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 java.lang.AssertionError + * @throws AssertionError */ void runWithFreedom(ControllableAgent agent) throws Exception, AssertionError; }