From a679d323209a7073296aa8c1ab010bf509fc3885 Mon Sep 17 00:00:00 2001 From: AndreosGhost Date: Fri, 21 Nov 2014 18:59:20 +0300 Subject: [PATCH 01/14] Task06-Parallel --- .../databaselibrary/db/DBTableProvider.java | 99 +++++--- .../databaselibrary/db/StoreableImpl.java | 14 +- .../db/StoreableTableImpl.java | 25 +- .../databaselibrary/db/StringTableImpl.java | 150 ++++++++---- .../databaselibrary/db/TablePart.java | 168 ++++++++----- .../shell/AbstractCommand.java | 6 +- .../shell/CommandContainer.java | 2 +- .../databaselibrary/shell/Shell.java | 2 +- .../support/ConvenientCollection.java | 3 +- .../support/ConvenientMap.java | 4 +- .../databaselibrary/support/Utility.java | 11 +- .../test/ControllableRunnerTest.java | 118 +++++++++ .../test/DatabaseShellTest.java | 8 +- .../test/DuplicatedIOTestBase.java | 94 +++++++ .../test/InterpreterTestBase.java | 39 +-- .../databaselibrary/test/ReadWriteTest.java | 4 +- .../databaselibrary/test/StoreableTest.java | 29 ++- .../test/TableProviderFactoryTest.java | 2 +- .../test/TableProviderTest.java | 71 ++++++ .../databaselibrary/test/TableTest.java | 172 ++++++++++++- .../test/support/TestUtils.java | 12 +- .../support/parallel/ControllableAgent.java | 14 ++ .../parallel/ControllableRunnable.java | 51 ++++ .../support/parallel/ControllableRunner.java | 230 ++++++++++++++++++ .../parallel/ExceptionFreeRunnable.java | 15 ++ 25 files changed, 1120 insertions(+), 223 deletions(-) create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/ControllableRunnerTest.java create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/DuplicatedIOTestBase.java create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/parallel/ControllableAgent.java create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/parallel/ControllableRunnable.java create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/parallel/ControllableRunner.java create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/parallel/ExceptionFreeRunnable.java diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/DBTableProvider.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/DBTableProvider.java index edfda2a76..54fa92cda 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 @@ -21,6 +21,8 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.function.Function; public class DBTableProvider implements TableProvider { @@ -65,10 +67,14 @@ public class DBTableProvider implements TableProvider { */ private final Map corruptTables; + /** + * Lock for getting/creating/removing tables access management. + */ + private final ReadWriteLock persistenceLock = new ReentrantReadWriteLock(true); + /** * Constructs a database table provider. - * @throws ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception - * .DatabaseIOException + * @throws ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.DatabaseIOException * If failed to scan database directory. */ DBTableProvider(Path databaseRoot) throws DatabaseIOException { @@ -81,16 +87,22 @@ public class DBTableProvider implements TableProvider { @Override public StoreableTableImpl getTable(String name) throws IllegalArgumentException { Utility.checkTableNameIsCorrect(name); - if (tables.containsKey(name)) { - StoreableTableImpl table = tables.get(name); - if (table == null) { - DatabaseIOException corruptionReason = corruptTables.get(name); - throw new IllegalArgumentException( - corruptionReason.getMessage(), corruptionReason); + + 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); + } + return table; + } else { + return null; } - return table; - } else { - return null; + } finally { + persistenceLock.readLock().unlock(); } } @@ -109,13 +121,18 @@ public StoreableTableImpl createTable(String name, List> columnTypes) Path tablePath = databaseRoot.resolve(name); - 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; + StoreableTableImpl newTable = StoreableTableImpl.createTable(this, tablePath, columnTypes); + tables.put(name, newTable); + return newTable; + } finally { + persistenceLock.writeLock().unlock(); + } } @Override @@ -124,31 +141,39 @@ public void removeTable(String name) Utility.checkTableNameIsCorrect(name); Path tablePath = databaseRoot.resolve(name); - 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) { - removed.invalidate(); - } + 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(); + } - 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); + 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; + TableCorruptIOException corruptionReason = new TableCorruptIOException( + name, "Failed to drop table: " + exc.toString(), exc); + corruptTables.put(name, corruptionReason); + throw corruptionReason; + } + } finally { + persistenceLock.writeLock().unlock(); } } diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/StoreableImpl.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/StoreableImpl.java index 8a407bf51..9b5fa3520 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 @@ -6,6 +6,10 @@ import java.util.Objects; +/** + * Implementation of Storeable that can be put to the table it is assigned to as a value.
+ * Not thread-safe. + */ public class StoreableImpl implements Storeable { private final Object[] values; @@ -120,16 +124,6 @@ public boolean equals(Object obj) { return false; } - if (host.getColumnsCount() != storeable.host.getColumnsCount()) { - return false; - } - - for (int col = 0; col < host.getColumnsCount(); col++) { - if (!host.getColumnType(col).equals(storeable.host.getColumnType(col))) { - return false; - } - } - for (int col = 0; col < host.getColumnsCount(); col++) { if (!Objects.equals(values[col], storeable.values[col])) { return false; diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/StoreableTableImpl.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/StoreableTableImpl.java index 3ebc0de7a..0c436737b 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 @@ -25,6 +25,7 @@ import java.util.List; import java.util.Map; import java.util.Scanner; +import java.util.concurrent.atomic.AtomicBoolean; public class StoreableTableImpl implements Table { private static final Map, String> CLASSES_TO_NAMES_MAP = @@ -47,11 +48,11 @@ public class StoreableTableImpl implements Table { private final List> columnTypes; - private boolean invalidated; + private AtomicBoolean invalidated; private StoreableTableImpl(TableProvider provider, StringTableImpl store, List> columnTypes) { this.provider = provider; - this.invalidated = false; + this.invalidated = new AtomicBoolean(false); this.store = store; this.columnTypes = Collections.unmodifiableList(new ArrayList>(columnTypes)); } @@ -155,12 +156,13 @@ static StoreableTableImpl getTable(TableProvider provider, Path tablePath) throw /** * Checks whether the given storeable can be stored in the given table as a value. - * @throws ColumnFormatException + * @throws ru.fizteh.fivt.storage.structured.ColumnFormatException * If columns count differs or some column has wrong type. Note that if some column has null * value, its type cannot be determined. - * @throws java.lang.IllegalStateException + * @throws IllegalStateException * If the given storeable is already assigned to another table. This check can be performed only - * for instances of {@link StoreableTableImpl}. + * for instances of {@link ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db + * .StoreableTableImpl}. */ public static void checkStoreableAppropriate(Table table, Storeable storeable) throws ColumnFormatException, IllegalStateException { @@ -215,14 +217,21 @@ TableProvider getProvider() { } /** - * Mark this table as invalidated (all further operations throw {@link java.lang.IllegalStateException}). + * Mark this table as invalidated (all further operations throw {@link IllegalStateException}). */ void invalidate() { - invalidated = true; + // 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) { + if (invalidated.get()) { throw new IllegalStateException(store.getName() + " is invalidated"); } } diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/StringTableImpl.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/StringTableImpl.java index 3388c3384..037c3c400 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/StringTableImpl.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/StringTableImpl.java @@ -14,8 +14,10 @@ import java.util.HashMap; import java.util.LinkedList; import java.util.List; -import java.util.Map.Entry; +import java.util.Map; import java.util.Set; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.function.Predicate; /** @@ -33,11 +35,15 @@ public final class StringTableImpl { private final Path tableRoot; private final String tableName; + /** + * Lock for exploit of table and writing to the file system. + */ + private final ReadWriteLock persistenceLock = new ReentrantReadWriteLock(true); /** * Mapping between table parts and local hashes of keys that can be stored inside them. * @see #getHash(String) */ - private HashMap tableParts; + private Map tableParts; /** * Constructor for cloning and safe table creation/obtaining. @@ -138,6 +144,16 @@ 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; } @@ -214,50 +230,42 @@ private void checkFileSystem(Predicate filter) throws DatabaseIOException } public void readFromFileSystem() throws DBFileCorruptIOException, TableCorruptIOException { - StringTableImpl thisClone = clone(); - tableParts.clear(); - + persistenceLock.writeLock().lock(); try { - for (int dir = 0; dir < DIRECTORIES_COUNT; dir++) { - for (int file = 0; file < FILES_COUNT; file++) { - int partHash = buildHash(dir, file); - TablePart fmap = new TablePart(makeTablePartFilePath(partHash)); - if (Files.exists(fmap.getTablePartFilePath())) { - fmap.readFromFile(); - } + Map oldTableParts = tableParts; + tableParts = new HashMap<>(); + + try { + for (int dir = 0; dir < DIRECTORIES_COUNT; dir++) { + for (int file = 0; file < FILES_COUNT; file++) { + int partHash = buildHash(dir, file); - // checking keys' hashes - Set keySet = fmap.keySet(); - for (String key : keySet) { - int keyHash = getHash(key); - if (keyHash != partHash) { - throw new TableCorruptIOException( - tableName, "Some keys are stored in improper places"); + TablePart tablePart = new TablePart(makeTablePartFilePath(partHash)); + if (Files.exists(tablePart.getTablePartFilePath())) { + tablePart.readFromFile(); + } + + // checking keys' hashes + Set keySet = tablePart.keySet(); + for (String key : keySet) { + int keyHash = getHash(key); + if (keyHash != partHash) { + throw new TableCorruptIOException( + tableName, "Some keys are stored in improper places"); + } } - } - tableParts.put(partHash, fmap); + tableParts.put(partHash, tablePart); + } } + } catch (Exception exc) { + this.tableParts = oldTableParts; + throw exc; } - } catch (Exception exc) { - this.tableParts = thisClone.tableParts; - throw exc; - } - } - - /** - * Clones the whole table - */ - @Override - protected StringTableImpl clone() { - StringTableImpl cloneTable = new StringTableImpl(tableRoot); - - for (Entry entry : tableParts.entrySet()) { - cloneTable.tableParts.put(entry.getKey(), entry.getValue().clone()); + } finally { + persistenceLock.writeLock().unlock(); } - - return cloneTable; } public String getName() { @@ -265,16 +273,31 @@ public String getName() { } public String get(String key) { - return obtainTablePart(key).get(key); + persistenceLock.readLock().lock(); + try { + return obtainTablePart(key).get(key); + } finally { + persistenceLock.readLock().unlock(); + } } public String put(String key, String value) { Utility.checkNotNull(value, "Value"); - return obtainTablePart(key).put(key, value); + persistenceLock.readLock().lock(); + try { + return obtainTablePart(key).put(key, value); + } finally { + persistenceLock.readLock().unlock(); + } } public String remove(String key) { - return obtainTablePart(key).remove(key); + persistenceLock.readLock().lock(); + try { + return obtainTablePart(key).remove(key); + } finally { + persistenceLock.readLock().unlock(); + } } /** @@ -283,8 +306,13 @@ public String remove(String key) { public int size() { int rowsNumber = 0; - for (TablePart part : tableParts.values()) { - rowsNumber += part.size(); + persistenceLock.readLock().lock(); + try { + for (TablePart part : tableParts.values()) { + rowsNumber += part.size(); + } + } finally { + persistenceLock.readLock().unlock(); } return rowsNumber; @@ -292,16 +320,29 @@ public int size() { public int commit() throws DatabaseIOException { int diffsCount = 0; - for (TablePart part : tableParts.values()) { - diffsCount += part.commit(); + + persistenceLock.writeLock().lock(); + try { + for (TablePart part : tableParts.values()) { + diffsCount += part.commit(); + } + } finally { + persistenceLock.writeLock().unlock(); } return diffsCount; } public int rollback() { int diffsCount = 0; - for (TablePart part : tableParts.values()) { - diffsCount += part.rollback(); + + persistenceLock.readLock().lock(); + try { + for (TablePart part : tableParts.values()) { + diffsCount += part.rollback(); + } + + } finally { + persistenceLock.readLock().unlock(); } return diffsCount; } @@ -312,8 +353,13 @@ public int rollback() { public List list() { List keySet = new LinkedList<>(); - for (TablePart part : tableParts.values()) { - keySet.addAll(part.keySet()); + persistenceLock.readLock().lock(); + try { + for (TablePart part : tableParts.values()) { + keySet.addAll(part.keySet()); + } + } finally { + persistenceLock.readLock().unlock(); } return keySet; @@ -330,7 +376,8 @@ private Path makeTablePartFilePath(int hash) { } /** - * Gets {@link TablePart} instance assigned to this {@code hash} from memory + * Gets {@link ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.TablePart} instance assigned to + * this {@code hash} from memory. Not thread-safe. * @param key * key that is hold by desired table. */ @@ -339,6 +386,9 @@ private TablePart obtainTablePart(String key) { return tableParts.get(getHash(key)); } + /** + * Counts number of uncommitted changes for this thread. + */ public int getNumberOfUncommittedChanges() { int diffsCount = 0; for (TablePart part : tableParts.values()) { diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/TablePart.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/TablePart.java index ae272f0eb..1dd810783 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/TablePart.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/TablePart.java @@ -15,25 +15,32 @@ import java.nio.file.Path; import java.util.HashMap; import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; import java.util.Set; import java.util.TreeMap; /** * This class represents a table part implemented as usual {@link java.util.HashMap} and stored in a separate - * file. + * file.
+ * This class is not thread-safe. * @author phoenix */ public class TablePart { public static final int READ_BUFFER_SIZE = 16 * 1024; - + /** + * A pair (key, value) describes put. A pair (key, null) describes removal. + */ + private final ThreadLocal> diffMap = + ThreadLocal.withInitial(() -> new HashMap()); private Path tablePartFilePath; - - private HashMap tablePartMap; - - private HashMap lastCommittedMap; + /** + * Map with last changes that are written to the file system.
+ */ + private Map lastCommittedMap; /** - * Private constructor for cloning + * Private constructor for cloning. */ private TablePart() { @@ -51,25 +58,16 @@ public TablePart(Path tablePartFilePath) { this.tablePartFilePath = tablePartFilePath; - tablePartMap = new HashMap<>(); lastCommittedMap = new HashMap<>(); } - /** - * Silently clones the object - no changes in file system are made. - */ - @SuppressWarnings("unchecked") - @Override - public TablePart clone() { - TablePart fmap = new TablePart(); - fmap.tablePartMap = (HashMap) this.tablePartMap.clone(); - fmap.lastCommittedMap = (HashMap) this.lastCommittedMap.clone(); - fmap.tablePartFilePath = this.tablePartFilePath; - return fmap; - } - public String get(String key) { - return tablePartMap.get(key); + if (diffMap.get().containsKey(key)) { + String value = diffMap.get().get(key); + return value; + } else { + return lastCommittedMap.get(key); + } } public Path getTablePartFilePath() { @@ -77,35 +75,33 @@ public Path getTablePartFilePath() { } public Set keySet() { - return tablePartMap.keySet(); + return makeNewActualVersion().keySet(); } public String put(String key, String value) { - return tablePartMap.put(key, value); + String oldValue = get(key); + diffMap.get().put(key, value); + return oldValue; } /** - * Reads database from file system (all previous data is purged).
If an error occurs the - * state before this operation is recovered. + * Reads database from file system (all previous data is purged).
+ * If an error occurs the state before this operation is recovered.
+ * Thread-local uncommitted diffs are not effected.
* @throws ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.DBFileCorruptIOException */ @SuppressWarnings("unchecked") public void readFromFile() throws DBFileCorruptIOException { - /* - * if an exception occurs and database is cloned, recover if cloned - * object is null - no recover is performed. - */ - HashMap cloneDBMap = (HashMap) tablePartMap.clone(); - tablePartMap.clear(); + // For recover purposes. + Map oldLastCommittedMap = lastCommittedMap; + lastCommittedMap = new HashMap<>(); try (DataInputStream stream = new DataInputStream( new FileInputStream( tablePartFilePath.toString()))) { - /* - * structure: (no spaces or newlines) 00<4 - * bytes:offset> 00<4 bytes:offset> ... - * ... - */ + // Structure: (no spaces or newlines) 00<4 + // bytes:offset> 00<4 bytes:offset> ... + // ... byte[] buffer = new byte[1024]; int bufferSize = 0; @@ -170,23 +166,25 @@ public void readFromFile() throws DBFileCorruptIOException { } } - // empty map + // Empty map. if (offsets.isEmpty()) { return; } - // reading values - String currentKey = offsets.get(nextValue); // value matching this - // key is now being - // built - offsets.remove(nextValue); // next value start boundary + // Reading values. + + // Value matching this key is now being built. + String currentKey = offsets.get(nextValue); + + // Next value start boundary. + offsets.remove(nextValue); - // reading up to the last value (exclusive) + // Reading up to the last value (exclusive). while (!offsets.isEmpty()) { nextValue = offsets.firstKey(); String value = new String(buffer, bufferOffset, nextValue - bufferOffset); - tablePartMap.put(currentKey, value); + lastCommittedMap.put(currentKey, value); bufferOffset = nextValue; currentKey = offsets.get(nextValue); @@ -194,37 +192,78 @@ public void readFromFile() throws DBFileCorruptIOException { offsets.remove(nextValue); } - // putting the last value + // Putting the last value. String value = new String(buffer, bufferOffset, bufferSize - bufferOffset); - tablePartMap.put(currentKey, value); + lastCommittedMap.put(currentKey, value); } catch (IOException exc) { - // recover - if (cloneDBMap != null) { - tablePartMap = cloneDBMap; - } + // Recover. + lastCommittedMap = oldLastCommittedMap; throw new DBFileCorruptIOException( "Failed to read data from file: " + tablePartFilePath.toString(), exc); } - // if everything went ok - lastCommittedMap = new HashMap<>(tablePartMap); + // Everything went ok. } public String remove(String key) { - return tablePartMap.remove(key); + if (diffMap.get().containsKey(key)) { + String oldValue = diffMap.get().get(key); + // Already removed. + if (oldValue == null) { + return null; + } else { + // Postponed put will be cancelled. + diffMap.get().remove(key); + return oldValue; + } + } else { + diffMap.get().put(key, null); + return lastCommittedMap.get(key); + } + } + + /** + * Makes a separate up-to-date version which is a commit of the thread diff to the clone of {@link + * #lastCommittedMap}. + * @return Separate actual version. Changes in this instance have not effect on true database state. + */ + private Map makeNewActualVersion() { + return makeActualVersion(new HashMap(lastCommittedMap)); + } + + /** + * Convenience method for private actual version supplying. + * @param actualVersion + * Map to commit thread diffs to. + * @return actualVersion (the same instance) with committed diffs. + */ + private Map makeActualVersion(Map actualVersion) { + for (Entry e : diffMap.get().entrySet()) { + if (e.getValue() == null) { + actualVersion.remove(e.getKey()); + } else { + actualVersion.put(e.getKey(), e.getValue()); + } + } + + return actualVersion; } public int size() { - return tablePartMap.size(); + return makeNewActualVersion().size(); } - public void writeToFile() throws IOException { + /** + * Writes changes to the file. + * @throws IOException + */ + private void writeToFile() throws IOException { ByteArrayOutputStream stream = new ByteArrayOutputStream(1024); - Iterator keyIterator = tablePartMap.keySet().iterator(); + Iterator keyIterator = lastCommittedMap.keySet().iterator(); Charset charset = Charset.forName("UTF-8"); - int[] shiftPositions = new int[tablePartMap.size()]; + int[] shiftPositions = new int[lastCommittedMap.size()]; byte[] intZero = new byte[] {0, 0, 0, 0}; @@ -237,14 +276,14 @@ public void writeToFile() throws IOException { stream.write(intZero); } - int[] links = new int[tablePartMap.size()]; + int[] links = new int[lastCommittedMap.size()]; keyID = 0; - keyIterator = tablePartMap.keySet().iterator(); + keyIterator = lastCommittedMap.keySet().iterator(); while (keyIterator.hasNext()) { links[keyID] = stream.size(); keyID++; - stream.write(tablePartMap.get(keyIterator.next()).getBytes(charset)); + stream.write(lastCommittedMap.get(keyIterator.next()).getBytes(charset)); } byte[] bytes = stream.toByteArray(); @@ -285,7 +324,8 @@ public int commit() throws DatabaseIOException { int diffsCount = getUncommittedChangesCount(); if (diffsCount > 0) { - lastCommittedMap = new HashMap<>(tablePartMap); + makeActualVersion(lastCommittedMap); + diffMap.get().clear(); try { writeToFile(); } catch (IOException exc) { @@ -298,11 +338,11 @@ public int commit() throws DatabaseIOException { public int rollback() { int diffsCount = getUncommittedChangesCount(); - tablePartMap = new HashMap<>(lastCommittedMap); + diffMap.get().clear(); return diffsCount; } public int getUncommittedChangesCount() { - return Utility.countDifferences(lastCommittedMap, tablePartMap); + return diffMap.get().size(); } } diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/AbstractCommand.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/AbstractCommand.java index 70938bd33..5faced748 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,7 +20,7 @@ public abstract class AbstractCommand implements Command DATABASE_ERROR_HANDLER = @@ -82,7 +82,9 @@ private static Class[] obtainExceptionsThrownByExecuteSafely() { } /** - * In implementation of {@link AbstractCommand} arguments number is checked first and then + * In implementation of {@link ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell + * .AbstractCommand} + * arguments number is checked first and then * {@link #executeSafely(SingleDatabaseShellState, String[])} is invoked.
If you want to * disable forced arguments number checking, override this method without invocation super * method and put empty implementation inside {@link #executeSafely(SingleDatabaseShellState, 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 348e75d0b..499ec99b9 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 ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.ShellState + * @see ShellState */ public interface CommandContainer> { Map> getCommands(); diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/Shell.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/Shell.java index 261c14b65..616dccb85 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 @@ -61,7 +61,7 @@ public Shell(ShellStateImpl shellState) throws TerminalException { * Commands split by {@link #COMMAND_END_CHARACTER}. * @return List of commands, each command is an array of its parts (space splitters are excluded from * everywhere except quoted parts). - * @throws ParseException + * @throws java.text.ParseException * In case of bad format. */ public static List splitCommandsString(String commandsStr) throws ParseException { diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/ConvenientCollection.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/ConvenientCollection.java index 9b1fe3abc..64b490865 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/ConvenientCollection.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/ConvenientCollection.java @@ -8,7 +8,8 @@ import java.util.stream.Stream; /** - * Usual collection over the given base collection with extension {@link ConvenientCollection#addNext + * Usual collection over the given base collection with extension {@link ru.fizteh.fivt.students + * .fedorov_andrew.databaselibrary.support.ConvenientCollection#addNext * (Object)}. */ public class ConvenientCollection implements Collection { diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/ConvenientMap.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/ConvenientMap.java index 4fe389c04..0240c6248 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/ConvenientMap.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/ConvenientMap.java @@ -8,7 +8,9 @@ import java.util.function.Function; /** - * Usual map extended with method {@link ConvenientMap#putNext(Object, Object)}. + * Usual map extended with method {@link ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support + * .ConvenientMap#putNext(Object, + * Object)}. */ public class ConvenientMap implements Map { private final Map baseMap; diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/Utility.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/Utility.java index 86cf21e16..b478cd8a9 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 @@ -92,7 +92,7 @@ public static void removeEmptyFilesAndFolders(Path rootDirectory) throws IOExcep */ public static void rm(final Path removePath) throws IOException { if (Files.isDirectory(removePath)) { - Files.walkFileTree(removePath, new Utility.FileTreeRemover()); + Files.walkFileTree(removePath, new FileTreeRemover()); } else { Files.delete(removePath); } @@ -205,7 +205,7 @@ public static void checkNotNull(Object variable, String name) throws IllegalArgu * @param * Value type in the source map. * @return An inversed map. It is not guaranteed that it is instance of the same class as source map has. - * @throws java.lang.IllegalArgumentException + * @throws IllegalArgumentException * If there are two keys having the same values. * @see Object#equals(Object) */ @@ -246,7 +246,8 @@ public static String getQuotedStringRegex(String quotes, String escapeSequence) * @param escapeSequence * Escape sequence. Quotes and this sequence occurrences will be prepended by escape sequence. * @return Endcoded string inside quotes. Returns null for null string. - * @see Utility#unquoteString(String, String, String) + * @see ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Utility#unquoteString(String, + * String, String) */ public static String quoteString(String s, String quoteSequence, String escapeSequence) { if (s == null) { @@ -260,7 +261,9 @@ public static String quoteString(String s, String quoteSequence, String escapeSe } /** - * Decodes a quoted via {@link Utility#quoteString(String, String, String)} method string. + * Decodes a quoted via {@link ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support + * .Utility#quoteString(String, + * String, String)} method string. * @param s * Quoted string (must start and end with quote sequence). * @param quoteSequence diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/ControllableRunnerTest.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/ControllableRunnerTest.java new file mode 100644 index 000000000..1b4243b58 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/ControllableRunnerTest.java @@ -0,0 +1,118 @@ +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.test.support.parallel.ControllableAgent; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test.support.parallel.ControllableRunnable; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test.support.parallel.ControllableRunner; + +import static org.junit.Assert.*; + +@RunWith(JUnit4.class) +public class ControllableRunnerTest extends DuplicatedIOTestBase { + private static final String NEW_LINE = System.lineSeparator(); + + @Test + public void testWaitForEndOfWork() throws Exception { + ControllableRunner runner = new ControllableRunner(); + ControllableRunnable runnable = runner.createControllable( + (ControllableAgent agent) -> { + try { + Thread.sleep(2000L); + } catch (InterruptedException exc) { + throw new AssertionError(exc); + } + System.err.println("Hello from runnable"); + }); + runner.assignRunnable(runnable); + + new Thread(runner, "Runnable").start(); + runner.waitUntilPause(); + + System.err.println("After execution"); + + assertEquals("Hello from runnable" + NEW_LINE + "After execution" + NEW_LINE, getOutput()); + } + + @Test + public void testWaitForEndOfWork1() throws Exception { + ControllableRunner runner = new ControllableRunner(); + ControllableRunnable runnable = runner.createControllable( + (ControllableAgent agent) -> { + System.err.println("Hello from runnable"); + }); + runner.assignRunnable(runnable); + + new Thread(runner, "Runnable").start(); + + // Possibly giving time to finish; + Thread.sleep(2000L); + runner.waitUntilPause(); + + System.err.println("After execution"); + + assertEquals("Hello from runnable" + NEW_LINE + "After execution" + NEW_LINE, getOutput()); + } + + @Test + public void testContinueInCheckpoint() throws Throwable { + ControllableRunner runner = new ControllableRunner(); + ControllableRunnable runnable = runner.createControllable( + (ControllableAgent agent) -> { + System.err.println("Before checkpoint"); + try { + agent.notifyAndWait(); + } catch (InterruptedException exc) { + throw new AssertionError(exc); + } + System.err.println("After checkpoint"); + }); + runner.assignRunnable(runnable); + + new Thread(runner, "Runnable").start(); + runner.waitUntilPause(); + System.err.println("In checkpoint"); + runner.continueWork(); + runner.waitUntilPause(); + System.err.println("After execution"); + + assertEquals( + String.join( + NEW_LINE, + "Before checkpoint", + "In checkpoint", + "After checkpoint", + "After execution", + ""), getOutput()); + } + + @Test + public void testInterruptInCheckpoint() throws InterruptedException, Exception { + ControllableRunner runner = new ControllableRunner(); + ControllableRunnable runnable = runner.createControllable( + (ControllableAgent agent) -> { + System.err.println("Before checkpoint"); + try { + agent.notifyAndWait(); + } catch (InterruptedException exc) { + System.err.println("Interrupted"); + return; + } + System.err.println("Must not be written"); + }); + runner.assignRunnable(runnable); + + new Thread(runner, "Runnable").start(); + runner.waitUntilPause(); + System.err.println("In checkpoint"); + runner.interruptWork(); + runner.waitUntilPause(); // Waiting until execution ends. + System.err.println("After execution"); + + assertEquals( + String.join( + NEW_LINE, "Before checkpoint", "In checkpoint", "Interrupted", "After execution", ""), + getOutput()); + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/DatabaseShellTest.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/DatabaseShellTest.java index 7f80fcde6..9f1d6a323 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 @@ -230,9 +230,13 @@ public void testInteractiveMode1() throws TerminalException { createAndUseTable(table); runBatchExpectZero( - false, "put a [\"b\"]; put b [\"c\"]; put c [\"d\"]; put d [\"e\"]; put e [\"a\"]; exit"); + "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"); + true, + "use " + table, + "show tables", + "use " + fakeTable + "; list", + "use " + table + "; list"); String regex = makeTerminalExpectedRegex( GREETING_REGEX, diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/DuplicatedIOTestBase.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/DuplicatedIOTestBase.java new file mode 100644 index 000000000..22d2edbe9 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/DuplicatedIOTestBase.java @@ -0,0 +1,94 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test; + +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Ignore; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test.support.BAOSDuplicator; + +import java.io.PrintStream; + +/** + * Test base for convenient output tracking. {@link ru.fizteh.fivt.students.fedorov_andrew.databaselibrary + * .test.support.BAOSDuplicator} + * is used for output duplicating. + */ +@Ignore +public class DuplicatedIOTestBase { + protected static PrintStream stdErr; + // Standard out and error streams are stored here. + private static PrintStream stdOut; + + private static BAOSDuplicator out; + + /** + * Sets standard output and error stream as {@link ru.fizteh.fivt.students.fedorov_andrew + * .databaselibrary.test.support.BAOSDuplicator}. + */ + @BeforeClass + public static void globalPrepareDuplicatedIOTestBase() { + stdOut = System.out; + stdErr = System.err; + out = new BAOSDuplicator(stdOut); + + // Wrap over {@link #out} that is used as {@link System#out} and {@link System#err}. + PrintStream outAndErrPrintStream = new PrintStream(out); + System.setOut(outAndErrPrintStream); + System.setErr(outAndErrPrintStream); + } + + /** + * Recovers standard output and error streams. + */ + @AfterClass + public static void globalCleanupDuplicatedIOTestBase() { + System.setOut(stdOut); + System.setErr(stdErr); + } + + /** + * Obtains output from the buffer. + */ + public String getOutput() { + return out.toString(); + } + + public void printDirectlyToStdOut(Object obj) { + stdOut.print(obj); + } + + public void printlnDirectlyToStdOut(Object obj) { + stdOut.println(obj); + } + + public void printDirectlyToStdOut(String str) { + stdOut.print(str); + } + + public void printlnDirectlyToStdOut(String str) { + stdOut.println(str); + } + + public void printlnDirectlyToStdOut() { + stdOut.println(); + } + + /** + * Resets output in the buffer. + */ + @Before + public void prepare() { + out.reset(); + } + + /** + * Prints to the standard output test separator string. + */ + @After + public void cleanup() { + printlnDirectlyToStdOut(); + printlnDirectlyToStdOut("-------------------------------------------------"); + } + +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/InterpreterTestBase.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/InterpreterTestBase.java index b635ccc48..29119072b 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 @@ -9,12 +9,9 @@ import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.TerminalException; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Shell; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.ShellState; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test.support.BAOSDuplicator; import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.PrintStream; import java.util.Arrays; import static org.junit.Assert.*; @@ -28,13 +25,8 @@ public abstract class InterpreterTestBase constructInterpreter() throws TerminalException; @@ -79,8 +61,7 @@ public void prepare() throws TerminalException { @After public void cleanup() throws IOException { interpreter = null; - stdOut.println(); - stdOut.println("-------------------------------------------------"); + IO_DUPLICATOR.cleanup(); } /** @@ -113,7 +94,7 @@ protected String makeTerminalExpectedRegex(String greetingRegex, String... repor * Obtains everything that was output by the interpreter.
*/ protected String getOutput() { - return out.toString(); + return IO_DUPLICATOR.getOutput(); } /** @@ -128,9 +109,9 @@ protected String getOutput() { */ protected int runBatch(boolean reinit, String... commands) throws TerminalException { // Clean what has been output before. - out.reset(); + IO_DUPLICATOR.prepare(); - stdOut.println(Arrays.toString(commands)); + IO_DUPLICATOR.printlnDirectlyToStdOut(Arrays.toString(commands)); for (int i = 0, len = commands.length; i < len; i++) { commands[i] = commands[i].trim(); if (!commands[i].endsWith(";")) { @@ -159,10 +140,10 @@ protected int runBatch(boolean reinit, String... commands) throws TerminalExcept * @throws TerminalException */ protected int runInteractive(boolean reinit, String... lines) throws TerminalException { - out.reset(); + IO_DUPLICATOR.prepare(); for (String cmd : lines) { - stdOut.println(cmd); + IO_DUPLICATOR.printlnDirectlyToStdOut(cmd); } StringBuilder sb = new StringBuilder(); for (String cmd : lines) { diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/ReadWriteTest.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/ReadWriteTest.java index 1a5d9a41a..5ebccace6 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/ReadWriteTest.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/ReadWriteTest.java @@ -49,13 +49,15 @@ private void performReadWriteFileMapTest(int keysMin, } Path testPath = Paths.get(System.getProperty("user.home"), "test", "java_test.dat"); + Files.deleteIfExists(testPath); + TablePart testFileMap = new TablePart(testPath); for (Entry e : map.entrySet()) { testFileMap.put(e.getKey(), e.getValue()); } - testFileMap.writeToFile(); + testFileMap.commit(); testFileMap = new TablePart(testPath); diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/StoreableTest.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/StoreableTest.java index 479323169..a516ea20e 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 @@ -17,6 +17,7 @@ import java.io.IOException; import java.util.Arrays; +import java.util.List; import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; @@ -24,6 +25,8 @@ @RunWith(JUnit4.class) public class StoreableTest extends TestBase { private static final String TABLE_NAME = "table"; + private static final List> TABLE_COLUMN_TYPES = Arrays.asList( + String.class, Integer.class, Double.class, Float.class, Boolean.class, Byte.class, Long.class); private static TableProviderFactory factory; @Rule public ExpectedException exception = ExpectedException.none(); @@ -39,15 +42,7 @@ public static void globalPrepare() { @Before public void prepare() throws IOException { provider = factory.create(DB_ROOT.toString()); - table = provider.createTable( - TABLE_NAME, Arrays.asList( - String.class, - Integer.class, - Double.class, - Float.class, - Boolean.class, - Byte.class, - Long.class)); + table = provider.createTable(TABLE_NAME, TABLE_COLUMN_TYPES); storeable = provider.createFor(table); } @@ -58,6 +53,22 @@ public void cleanup() throws IOException { table = null; } + @Test + public void testStoreableEquals() throws IOException { + Table table2 = provider.createTable(TABLE_NAME + "2", TABLE_COLUMN_TYPES); + assertNotEquals(storeable, provider.createFor(table2)); + } + + @Test + public void testStoreableEquals1() throws IOException { + Storeable storeable2 = provider.createFor(table); + + storeable.setColumnAt(0, "string1"); + storeable2.setColumnAt(0, "string2"); + + assertNotEquals(storeable, storeable2); + } + @Test public void testPutStringToInt() { exception.expect(ColumnFormatException.class); diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/TableProviderFactoryTest.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/TableProviderFactoryTest.java index f13dd5462..b6cd80c05 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/TableProviderFactoryTest.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/TableProviderFactoryTest.java @@ -16,7 +16,7 @@ import static org.hamcrest.CoreMatchers.*; /** - * Tests {@link TableProviderFactory } mostly for error cases. + * Tests {@link ru.fizteh.fivt.storage.structured.TableProviderFactory } mostly for error cases. * @author phoenix */ @RunWith(org.junit.runners.JUnit4.class) diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/TableProviderTest.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/TableProviderTest.java index 248c790a0..d84630bb2 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/TableProviderTest.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/TableProviderTest.java @@ -1,5 +1,6 @@ package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test; +import junit.framework.AssertionFailedError; import org.hamcrest.Matcher; import org.junit.After; import org.junit.Before; @@ -16,6 +17,10 @@ import ru.fizteh.fivt.storage.structured.TableProviderFactory; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.DBTableProviderFactory; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.StringTableImpl; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test.support.TestUtils; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test.support.parallel.ControllableAgent; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test.support.parallel.ControllableRunnable; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test.support.parallel.ControllableRunner; import java.io.IOException; import java.io.PrintWriter; @@ -30,6 +35,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Objects; +import java.util.concurrent.ThreadLocalRandom; import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; @@ -91,6 +97,71 @@ private void expectJSONRegexMatchFailure() { "Does not match JSON simple list regular expression"))); } + @Test + public void testConcurrentCreateTable() throws Exception { + final String tableName = "table"; + + class TableCreator extends ControllableRunnable { + volatile Table createdTable; + volatile Table gotTable; + + public TableCreator(ControllableRunner host) { + super(host); + } + + @Override + public void runWithFreedom(ControllableAgent agent) throws Exception, AssertionError { + TestUtils.consumeCPU(ThreadLocalRandom.current().nextInt(20, 40)); + System.err.println("Attempt to create table"); + createdTable = provider.createTable(tableName, DEFAULT_COLUMN_TYPES); + gotTable = provider.getTable(tableName); + } + } + + int threadsCount = 26; + + ControllableRunner[] runners = new ControllableRunner[threadsCount]; + + for (int i = 0; i < threadsCount; i++) { + runners[i] = new ControllableRunner(); + runners[i].assignRunnable(new TableCreator(runners[i])); + } + + for (int i = 0; i < threadsCount; i++) { + new Thread(runners[i], "Runner " + i).start(); + } + + for (int i = 0; i < threadsCount; i++) { + runners[i].waitUntilEndOfWork(); + } + + // All gotTable must be equal. + // One createdTable must be equal to any gotTable, all other createdTables must be null. + + Table gotTable = ((TableCreator) runners[0].getRunnable()).gotTable; + + for (int i = 1; i < threadsCount; i++) { + assertTrue( + "All links for gotTable must be the same", + ((TableCreator) runners[i].getRunnable()).gotTable == gotTable); + } + + boolean foundCreated = false; + + for (int i = 0; i < threadsCount; i++) { + Table createdTable = ((TableCreator) runners[i].getRunnable()).createdTable; + if (createdTable != null) { + if (foundCreated) { + throw new AssertionFailedError("More then one created table"); + } else { + foundCreated = true; + } + } + } + + assertTrue("Must be one created table", foundCreated); + } + @Test public void testDeserialize() throws IOException, ParseException { Table table = createTable(Long.class, Byte.class, String.class); diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/TableTest.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/TableTest.java index b067444ab..3426ba318 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 @@ -14,12 +14,17 @@ import ru.fizteh.fivt.storage.structured.TableProviderFactory; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.StoreableTableImpl; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test.support.TestUtils; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test.support.parallel.ControllableAgent; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test.support.parallel.ControllableRunnable; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test.support.parallel.ControllableRunner; import java.io.IOException; import java.io.PrintWriter; import java.text.ParseException; import java.util.Arrays; import java.util.List; +import java.util.concurrent.ThreadLocalRandom; +import java.util.function.Consumer; import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; @@ -153,7 +158,109 @@ public void testNumberOfUncommittedChanges4() throws ParseException, IOException remove("key"); put("key", "value"); - assertEquals(0, table.getNumberOfUncommittedChanges()); + assertEquals(1, table.getNumberOfUncommittedChanges()); + } + + @Test + public void testRollbackConcurrent() throws Throwable { + ControllableRunner runnerA = new ControllableRunner(); + ControllableRunner runnerB = new ControllableRunner(); + + // Scheme: + // runnerA: put values + // runnerB: put values, rollback + // runnerA: check own changes still exist + + ControllableRunnable contrA = runnerA.createAndAssign( + (ControllableAgent agent) -> { + // runnerA: put values + put("a", "b"); + put("b", "c"); + + agent.notifyAndWait(); + + // runnerA: check own changes still exist + int changes = table.getNumberOfUncommittedChanges(); + assertEquals("Changes of another thread must not have been rolled back", 2, changes); + + }); + + ControllableRunnable contrB = runnerB.createAndAssign( + (ControllableAgent agent) -> { + // runnerB: put values, rollback + put("a", "b"); + put("c", "d"); + int changes = table.rollback(); + assertEquals("Must have rolled back my changes", 2, changes); + }); + + new Thread(runnerA, "runnerA").start(); + new Thread(runnerB, "runnerB").start(); + + runnerA.waitUntilPause(); // Wait until pause. + runnerB.waitUntilEndOfWork(); // Wait until end of work. + runnerA.continueWork(); + runnerA.waitUntilEndOfWork(); // Wait until end of work. + } + + @Test + public void testCommitGlobalUpdate() throws Throwable { + ControllableRunner runnerA = new ControllableRunner(); + ControllableRunner runnerB = new ControllableRunner(); + + ControllableRunnable contrA = runnerA.createAndAssign( + (ControllableAgent agent) -> { + // runnerA: put values + put("a", "b"); + put("b", "c"); + + agent.notifyAndWait(); + + // runnerA: commit + // System.err.println("A: commit"); + int changes = table.commit(); + assertEquals("Must have committed my changes.", 2, changes); + + agent.notifyAndWait(); + + // runnerA: get values + assertNull("Must have been removed.", get("b")); + assertEquals("Committed from another thread.", "d", get("c")); + assertEquals("Committed from me earlier.", "b", get("a")); + }); + + ControllableRunnable contrB = runnerB.createAndAssign( + (ControllableAgent agent) -> { + // runnerB: get values, put values, remove values + assertNull("Must not have been committed.", get("a")); + assertNull("Must not have been committed.", get("b")); + + put("c", "d"); + remove("b"); + + agent.notifyAndWait(); + + // runnerB: get values, commit + assertNull("Committed but replaced", get("b")); + assertEquals("Committed from another thread.", "b", get("a")); + int changes = table.commit(); + assertEquals("Must have committed my changes.", 2, changes); + }); + + // runnerA: put values + // runnerB: get values, put values, remove values + // runnerA: commit + // runnerB: get values, commit + // runnerA: get values + + new Thread(runnerA, "runnerA").start(); + runnerA.waitUntilPause(); + new Thread(runnerB, "runnerB").start(); + runnerB.waitUntilPause(); + runnerA.continueWork(); + runnerA.waitUntilPause(); + runnerB.waitUntilEndOfWork(); + runnerA.waitUntilEndOfWork(); } @Test @@ -166,6 +273,69 @@ public void testNumberOfUncommittedChanges5() throws ParseException, IOException assertEquals(1, table.getNumberOfUncommittedChanges()); } + @Test + public void testManyConcurrentCommits() throws Exception { + int threadsNumber = TestUtils.ALPHABET.length; + + ControllableRunner[] runners = new ControllableRunner[threadsNumber]; + + for (int i = 0; i < threadsNumber; i++) { + final int id = i; + runners[i] = new ControllableRunner(); + runners[i].createAndAssign( + (ControllableAgent agent) -> { + final ThreadLocalRandom random = ThreadLocalRandom.current(); + + Consumer trashFiller = (actionsCount) -> { + try { + System.err.println( + Thread.currentThread().getName() + ": doing " + actionsCount + + " actions"); + for (int j = 0; j < actionsCount; j++) { + put( + String.valueOf(random.nextInt(100)), + String.valueOf(random.nextInt())); + if (random.nextInt(10) < 5) { + table.commit(); + } + } + } catch (IOException | ParseException exc) { + throw new AssertionError(exc); + } + }; + + trashFiller.accept(random.nextInt(20, 40)); + + String mainKey = "key" + TestUtils.ALPHABET[id]; + String mainValue = "value " + mainKey; + + put(mainKey, mainValue); + table.commit(); + assertEquals("Main key-value not matches", mainValue, get(mainKey)); + + trashFiller.accept(random.nextInt(20, 40)); + + agent.notifyAndWait(); + + assertEquals("Main key-value not matches", mainValue, get(mainKey)); + }); + } + + for (int i = 0; i < threadsNumber; i++) { + new Thread(runners[i], "runner " + i).start(); + } + + // Going to the finish line... + for (ControllableRunner runner : runners) { + runner.waitUntilPause(); + } + + // Total check in the end. + for (ControllableRunner runner : runners) { + runner.waitUntilEndOfWork(); + } + } + @Test public void testPutOneStoreableToAnotherTable() throws IOException { Table table2 = provider.createTable(TABLE_NAME + "2", DEFAULT_COLUMN_TYPES); diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/TestUtils.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/TestUtils.java index 0c8f8597c..9f1fb92ea 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/TestUtils.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/TestUtils.java @@ -10,6 +10,7 @@ import java.util.Iterator; import java.util.Map; import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; /** * This class provides some utility methods for testing.
Note that some methods are linking to @@ -18,7 +19,7 @@ * directly. */ public class TestUtils { - private static final char[] ALPHABET = "abcdefghijklmnopqrstuvwxyz".toCharArray(); + public static final char[] ALPHABET = "abcdefghijklmnopqrstuvwxyz".toCharArray(); private static final Random RANDOM = new Random(); // Not for constructing @@ -41,6 +42,15 @@ public static String randString(int length) { return String.valueOf(data); } + public static void consumeCPU(int actionsCount) { + int sum = 0; + ThreadLocalRandom random = ThreadLocalRandom.current(); + for (int i = 0; i < actionsCount; i++) { + sum += random.nextInt(123512); + } + random.nextInt(sum); + } + public static TableProviderFactory obtainFactory() { return new DBTableProviderFactory(); } diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/parallel/ControllableAgent.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/parallel/ControllableAgent.java new file mode 100644 index 000000000..0cd93277c --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/parallel/ControllableAgent.java @@ -0,0 +1,14 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test.support.parallel; + +/** + * Interface that lets the controllable runnable to notify all waiting threads that the pause has come and + * wait until any of the threads decides whether to continue or interrupt the execution of the runnable. + */ +@FunctionalInterface +public interface ControllableAgent { + /** + * Call this method if you want to make a pause. + * @throws InterruptedException + */ + void notifyAndWait() throws InterruptedException; +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/parallel/ControllableRunnable.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/parallel/ControllableRunnable.java new file mode 100644 index 000000000..04250cbbb --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/parallel/ControllableRunnable.java @@ -0,0 +1,51 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test.support.parallel; + +/** + * Base class for runnables served by this runner. + */ +public abstract class ControllableRunnable implements Runnable, ControllableAgent, ExceptionFreeRunnable { + private final ControllableRunner host; + + private volatile Throwable exception; + + public ControllableRunnable(ControllableRunner host) { + this.host = host; + } + + @Override + public final void run() { + synchronized (this) { + exception = null; + } + try { + runWithFreedom(this::notifyAndWait); + } catch (Exception | AssertionError exc) { + synchronized (this) { + exception = exc; + } + } + } + + /** + * Call this method after execution finishes. If an {@link java.lang.Exception} or {@link + * java.lang.AssertionError} has occurred during execution, it will + * be rethrown. + * @throws Exception + */ + public final synchronized void checkException() throws Exception, AssertionError { + if (exception != null) { + if (exception instanceof Exception) { + throw (Exception) exception; + } else if (exception instanceof AssertionError) { + throw (AssertionError) exception; + } else { + throw new Error("Some fatal exception occurred during thread execution", exception); + } + } + } + + @Override + public final void notifyAndWait() throws InterruptedException { + host.onControllablePause(this); + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/parallel/ControllableRunner.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/parallel/ControllableRunner.java new file mode 100644 index 000000000..16effbb9b --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/parallel/ControllableRunner.java @@ -0,0 +1,230 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test.support.parallel; + +/** + * Runnable that consumes some collections of runnables that must be executed sequentially and executes them + * so that you can track whether some part has been executed or not. + */ +public class ControllableRunner implements Runnable { + private static final int ORDER_NOT_SET = -1; + private static final int ORDER_TERMINATE = 0; + private static final int ORDER_CONTINUE = 1; + + private static final int STATUS_NOT_STARTED = -1; + private static final int STATUS_FINISHED = 0; + private static final int STATUS_STARTED = 1; + + private volatile ControllableRunnable runnable; + + /** + * Order from the observer: continue or terminate. Can be also not set. + */ + private volatile int order; + + /** + * Current status: not started/started/finished. + */ + private volatile int status; + + /** + * Creates a new instance of this runner. + */ + public ControllableRunner() { + order = ORDER_TERMINATE; + status = STATUS_NOT_STARTED; + } + + /** + * Assign the given runnable to execute it once. You can perform this action if you have never assigned + * runnable to this runner before or the last assigned runnable has finished its execution. + * @throws java.lang.IllegalStateException + * If you cannot assign any runnables now. + */ + public synchronized void assignRunnable(ControllableRunnable runnable) throws IllegalStateException { + if (status == STATUS_STARTED) { + throw new IllegalStateException("Runnable has been already assigned and has not been finished"); + } + this.runnable = runnable; + status = STATUS_NOT_STARTED; + } + + /** + * Get the currently assigned controllable runnable. + */ + public ControllableRunnable getRunnable() { + return runnable; + } + + /** + * Creates and attempts to assign the alternate-kind runnable. + * @param runnable + * Alternate kind of runnable. + */ + public synchronized ControllableRunnable createAndAssign(ExceptionFreeRunnable runnable) { + ControllableRunnable controllable = createControllable(runnable); + assignRunnable(controllable); + return controllable; + } + + /** + * @throws IllegalStateException + * You can call run() only if status = unstarted. + */ + @Override + public synchronized void run() throws IllegalStateException { + checkRunnableAssigned(); + + if (status != STATUS_NOT_STARTED) { + throw new IllegalStateException("Can run only if status is: not started"); + } + + status = STATUS_STARTED; + order = ORDER_CONTINUE; + + // System.err.println("Starting"); + try { + runnable.run(); + } finally { + // System.err.println("Finishing"); + status = STATUS_FINISHED; + order = ORDER_TERMINATE; + notifyAll(); + } + } + + /** + * Creates a controllable runnable that will work this this runner. + * @param runnable + * alternate form of runnable - function that takes notification agent as an argument. + */ + public ControllableRunnable createControllable(ExceptionFreeRunnable runnable) { + return new ControllableRunnable(this) { + @Override + public void runWithFreedom(ControllableAgent agent) throws Exception { + runnable.runWithFreedom(agent); + } + }; + } + + /** + * Call this method if you want to wait until the next pause and play the role of observer.
+ * If there are no checkpoints expected in future, this method waits until execution ends. + * @throws InterruptedException + * @throws java.lang.Exception + * If thrown during runnable execution. + * @throws java.lang.AssertionError + * If thrown during runnable execution. + */ + public synchronized void waitUntilPause() throws InterruptedException, Exception, AssertionError { + // System.err.println(hashCode() + ": waiting until pause"); + while (order != ORDER_NOT_SET && status != STATUS_FINISHED) { + wait(); + } + runnable.checkException(); + } + + /** + * Call this method if you want to wait until execution ends. Returns immediately if the runnable is not + * set. All pauses that can be met in the future will be ignored with order {@link #continueWork()}.
+ * throws exception. + * @throws InterruptedException + * @throws java.lang.Exception + * If thrown during runnable execution. + * @throws java.lang.AssertionError + * If thrown during runnable execution. + */ + public synchronized void waitUntilEndOfWork() throws InterruptedException, Exception, AssertionError { + while (status != STATUS_FINISHED) { + if (order == ORDER_NOT_SET) { + continueWork(); + } + wait(); + } + runnable.checkException(); + } + + /** + * This method is called by {@link ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test.support + * .support.parallel.ControllableRunnable} + * when it wants to pause and wait until the observer decides whether to continue work or interrupt. + * @throws InterruptedException + */ + synchronized void onControllablePause(ControllableRunnable pausingRunnable) throws InterruptedException { + checkStatusIsStarted(); + if (runnable != pausingRunnable) { + throw new IllegalStateException( + "Controllable runner can handle only one runnable at a time. Do not execute more than " + + "one runnable assigned to the same controllable runner in parallel."); + } + + order = ORDER_NOT_SET; + notifyAll(); + while (order == ORDER_NOT_SET) { + wait(); + } + if (order == ORDER_TERMINATE) { + throw new InterruptedException("Terminated after pause because of user command."); + } + + // For all runners that want to wait did really wait + order = ORDER_NOT_SET; + } + + public synchronized boolean isRunnableAssigned() { + return runnable != null; + } + + private synchronized void checkRunnableAssigned() { + if (!isRunnableAssigned()) { + throw new IllegalStateException("Runnable has not been assigned"); + } + } + + private synchronized void checkStatusIsStarted() throws IllegalStateException { + if (status != STATUS_STARTED) { + throw new IllegalStateException("Can perform this only if status = started"); + } + } + + /** + * Call this method after waiting in {@link #waitUntilPause()} to tell the runnable to continue work. + * @throws IllegalStateException + * If execution has finished. + * @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(); + checkStatusIsStarted(); + + // System.err.println(hashCode() + ": continue work"); + + if (order == ORDER_NOT_SET) { + order = ORDER_CONTINUE; + notifyAll(); + } else { + throw new IllegalStateException( + "Cannot manage the runnable now, because it is not in paused state."); + } + } + + /** + * Call this method after waiting in {@link #waitUntilPause()} to tell the runnable to interrupt work. + * @throws IllegalStateException + * If execution has finished. + */ + public synchronized void interruptWork() throws IllegalStateException { + checkRunnableAssigned(); + checkStatusIsStarted(); + + if (order == ORDER_NOT_SET) { + order = ORDER_TERMINATE; + notifyAll(); + } else { + throw new IllegalStateException( + "Cannot manage the runnable now, because it is not in paused state."); + } + } + +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/parallel/ExceptionFreeRunnable.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/parallel/ExceptionFreeRunnable.java new file mode 100644 index 000000000..abc9aedfb --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/parallel/ExceptionFreeRunnable.java @@ -0,0 +1,15 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test.support.parallel; + +/** + * Interface for runnable that can throw any exceptions. + */ +@FunctionalInterface +public interface ExceptionFreeRunnable { + /** + * Place your implementation here and do not care of exceptions. The given throwables will be caught and + * become accessible. + * @throws Exception + * @throws java.lang.AssertionError + */ + void runWithFreedom(ControllableAgent agent) throws Exception, AssertionError; +} From 24fdfb0cc149a479a13966a9eaa6ef3366187dfe Mon Sep 17 00:00:00 2001 From: AndreosGhost Date: Thu, 4 Dec 2014 23:59:51 +0300 Subject: [PATCH 02/14] Task 07-Proxy + optimizations for Task 06-Parallel --- .../db/AutoCloseableProvider.java | 8 + .../db/AutoCloseableTable.java | 8 + .../databaselibrary/db/DBTableProvider.java | 593 +++++++++++------- .../db/DBTableProviderFactory.java | 106 +++- .../databaselibrary/db/Database.java | 11 +- .../databaselibrary/db/StoreableImpl.java | 55 +- .../db/StoreableTableImpl.java | 208 +++--- .../databaselibrary/db/StringTableImpl.java | 14 +- .../databaselibrary/db/TablePart.java | 15 +- .../json/JSONComplexObject.java | 23 + .../databaselibrary/json/JSONField.java | 22 + .../databaselibrary/json/JSONHelper.java | 34 + .../databaselibrary/json/JSONMaker.java | 214 +++++++ .../json/JSONParsedObject.java | 145 +++++ .../databaselibrary/json/JSONParser.java | 567 +++++++++++++++++ .../shell/AbstractCommand.java | 6 +- .../shell/CommandContainer.java | 2 +- .../databaselibrary/shell/Main.java | 2 +- .../databaselibrary/shell/Shell.java | 48 +- .../shell/SingleDatabaseShellState.java | 5 + .../databaselibrary/support/ArrayMatcher.java | 54 ++ .../databaselibrary/support/Log.java | 18 +- .../support/LoggingProxyFactoryImpl.java | 194 ++++++ .../databaselibrary/support/Utility.java | 12 +- .../support/ValidityController.java | 81 +++ .../test/ControllableRunnerTest.java | 4 +- .../test/DatabaseShellTest.java | 153 +++-- .../test/DuplicatedIOTestBase.java | 4 +- .../test/InterpreterTestBase.java | 74 +-- .../databaselibrary/test/JSONTest.java | 113 ++++ .../test/LoggingProxyFactoryTest.java | 179 ++++++ .../databaselibrary/test/StoreableTest.java | 25 +- .../test/TableProviderFactoryTest.java | 6 +- .../test/TableProviderTest.java | 82 +-- .../databaselibrary/test/TableTest.java | 16 +- .../databaselibrary/test/TestBase.java | 26 +- .../databaselibrary/test/UtilityTest.java | 14 +- .../test/support/ProbableActionSet.java | 4 +- .../test/support/SpontaniousException.java | 2 +- .../support/SpontaniousRuntimeException.java | 2 +- .../test/support/TestUtils.java | 2 +- .../parallel/ControllableRunnable.java | 4 +- .../support/parallel/ControllableRunner.java | 16 +- .../parallel/ExceptionFreeRunnable.java | 2 +- 44 files changed, 2551 insertions(+), 622 deletions(-) create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/AutoCloseableProvider.java create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/AutoCloseableTable.java create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/json/JSONComplexObject.java create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/json/JSONField.java create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/json/JSONHelper.java create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/json/JSONMaker.java create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/json/JSONParsedObject.java create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/json/JSONParser.java create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/ArrayMatcher.java create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/LoggingProxyFactoryImpl.java create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/ValidityController.java create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/JSONTest.java create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/LoggingProxyFactoryTest.java 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..7d2ad5226 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,19 +26,18 @@ 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; - -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 + ""); - +import java.util.function.Predicate; + +final class DBTableProvider implements AutoCloseableProvider { + public static final char ESCAPE_CHARACTER = '\\'; + private static final char QUOTE_CHARACTER = '\"'; + // public static final String QUOTED_STRING_REGEX = + // Utility.getQuotedStringRegex(QUOTE_CHARACTER + "", ESCAPE_CHARACTER + "" + + // ESCAPE_CHARACTER); private static final Collection> SUPPORTED_TYPES = new ConvenientCollection<>( new HashSet>()).addNext(Integer.class).addNext(Long.class).addNext(Byte.class) .addNext(Double.class).addNext(Float.class).addNext(Boolean.class) @@ -52,318 +56,416 @@ 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); - } - - 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); + try (UseLock useLock = validityController.use()) { + 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); + // } + // + int leftBound = value.indexOf('['); + int rightBound = value.lastIndexOf(']'); + + 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; + // int currentColumn = 0; + // + // int index = leftBound + 1; + // + // for (; index < rightBound; ) { + // char currentChar = value.charAt(index); + // + // if (Character.isSpaceChar(currentChar)) { + // // Space that does not mean anything. + // + // index++; + // } else if (LIST_SEPARATOR_CHARACTER == currentChar) { + // // Next list element. + // + // currentColumn++; + // if (currentColumn >= columnsCount) { + // throw new ParseException( + // "wrong type (Too many elements in the list; expected: " + + // columnsCount + ")", + // index); + // } + // index++; + // } else { + // // Boolean, Number, Null, String. + // + // // End of element (exclusive). + // int elementEnd; + // + // 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. + // + // 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; + // } + // } + // + // // 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(currentColumn, elementObj); + // index = elementEnd; + // } + // } + // + // 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"); + try (UseLock useLock = validityController.use()) { + Utility.checkNotNull(table, "Table"); + Utility.checkNotNull(value, "Value"); - StoreableTableImpl.checkStoreableAppropriate(table, value); + StoreableTableImpl.checkStoreableAppropriate(table, value); - StringBuilder sb = new StringBuilder(); - sb.append("["); - boolean comma = false; - - for (int col = 0, colsCount = table.getColumnsCount(); col < colsCount; col++) { - - 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 +473,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..b19458ada 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(singleField = 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..cb3f178b4 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)); } - 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..1866a469d --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/json/JSONComplexObject.java @@ -0,0 +1,23 @@ +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 multiple JSON fields. + * @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 singleField() 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..91689b543 --- /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).singleField()) { + 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..01b19c468 --- /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}
  • + *
+ */ +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..a809a4302 --- /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(singleField = 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(singleField = 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/ArrayMatcher.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/ArrayMatcher.java new file mode 100644 index 000000000..324c6e681 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/ArrayMatcher.java @@ -0,0 +1,54 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support; + +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.hamcrest.Matcher; + +import java.lang.reflect.Array; + +/** + * Matcher for array that stores own matcher for each element. + * @deprecated Developed but alternative way has been found for the moment. + */ +@Deprecated +class ArrayMatcher extends BaseMatcher { + private final Matcher[] matchers; + + /** + * Constructs array matcher from array of matchers. + * @param matchers + * array of matchers. Note that this array is not copied, so changes are backed. + */ + public ArrayMatcher(Matcher... matchers) { + this.matchers = matchers; + } + + @Override + public boolean matches(Object item) { + int length = Array.getLength(item); + if (matchers.length != length) { + return false; + } + + for (int i = 0; i < length; i++) { + if (!matchers[i].matches(Array.get(item, i))) { + return false; + } + } + + return true; + } + + @Override + public void describeTo(Description description) { + description.appendText("Array must have length " + matchers.length + " and match: "); + boolean comma = false; + for (Matcher matcher : matchers) { + if (comma) { + description.appendText(", "); + } + comma = true; + description.appendDescriptionOf(matcher); + } + } +} 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..3161fe15e 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/Log.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/Log.java @@ -30,18 +30,17 @@ public class Log { private Log() { } - private static void reopen() { + private static synchronized void reopen() { try { writer = new PrintWriter(new FileOutputStream(LOG_PATH.toAbsolutePath().toString(), !firstOpen)); firstOpen = false; } catch (IOException exc) { System.err.println(String.format("Cannot create log file: %s", LOG_PATH)); System.err.println(exc.toString()); - System.exit(1); } } - public static void close() { + public static synchronized void close() { if (writer != null) { writer.println("Log closing"); writer.close(); @@ -49,21 +48,24 @@ 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) { + public static synchronized void log(Class logger, Throwable throwable, String message) { if (writer == null) { reopen(); + if (writer == null) { + return; + } } if (enableLogging) { StringBuilder sb = new StringBuilder(message == null ? 100 : message.length() * 2); @@ -111,7 +113,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..3dc7a00a0 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 @@ -225,9 +225,9 @@ public static Map inverseMap(Map map) throws IllegalArgumentE /** * Forms a regular expression for a string inside quotes. * @param quotes - * Sequence of symbols that plays role of quotes. + * Sequence of symbols that plays role of quotes [regex-style]. * @param escapeSequence - * Inside quotes escapeSequence and quotes must occur only after escapeSequence. + * Inside quotes escapeSequence and quotes must occur only after escapeSequence [regex-style]. */ public static String getQuotedStringRegex(String quotes, String escapeSequence) { // Regex: "((plain text)|(escaped symbols))*" @@ -246,14 +246,14 @@ 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( + s = s.replace( escapeSequence, escapeSequence + escapeSequence); s = s.replaceAll( quoteSequence, escapeSequence + quoteSequence); @@ -280,9 +280,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..37e481a32 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,11 @@ @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 = '\\'; + + private static final String QUOTED_STRING_REGEX = + Utility.getQuotedStringRegex(QUOTE_CHARACTER + "", ESCAPE_CHARACTER + "" + ESCAPE_CHARACTER); @Rule public ExpectedException exception = ExpectedException.none(); @@ -41,13 +44,12 @@ 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) { @@ -155,7 +157,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; } From b3f88c1f869973a9705ddabd4d75ae207af4b087 Mon Sep 17 00:00:00 2001 From: AndreosGhost Date: Fri, 5 Dec 2014 00:02:58 +0300 Subject: [PATCH 03/14] Fixed minor misprinting --- .../fedorov_andrew/databaselibrary/json/JSONParsedObject.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 01b19c468..ef3b10790 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/json/JSONParsedObject.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/json/JSONParsedObject.java @@ -15,7 +15,7 @@ *
  • {@link ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.json.JSONParsedObject}
  • * */ -interface JSONParsedObject { +public interface JSONParsedObject { /** * Set some field. * @param name From 9fe6eb1ce71b96d429f2f6a475570be7f45b5c12 Mon Sep 17 00:00:00 2001 From: AndreosGhost Date: Mon, 8 Dec 2014 18:03:50 +0300 Subject: [PATCH 04/14] Fixed 18:05 08.12.2014 --- .../databaselibrary/db/DBTableProvider.java | 88 +------------------ .../databaselibrary/db/StoreableImpl.java | 2 +- .../db/StoreableTableImpl.java | 2 +- .../json/JSONComplexObject.java | 5 +- .../databaselibrary/json/JSONMaker.java | 2 +- .../databaselibrary/json/JSONParser.java | 4 +- .../databaselibrary/support/ArrayMatcher.java | 54 ------------ .../databaselibrary/support/Log.java | 13 +-- .../databaselibrary/support/Utility.java | 20 +---- .../databaselibrary/test/UtilityTest.java | 19 +--- 10 files changed, 19 insertions(+), 190 deletions(-) delete mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/ArrayMatcher.java 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 7d2ad5226..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 @@ -33,11 +33,8 @@ import java.util.function.Predicate; final class DBTableProvider implements AutoCloseableProvider { - public static final char ESCAPE_CHARACTER = '\\'; private static final char QUOTE_CHARACTER = '\"'; - // public static final String QUOTED_STRING_REGEX = - // Utility.getQuotedStringRegex(QUOTE_CHARACTER + "", ESCAPE_CHARACTER + "" + - // ESCAPE_CHARACTER); + private static final Collection> SUPPORTED_TYPES = new ConvenientCollection<>( new HashSet>()).addNext(Integer.class).addNext(Long.class).addNext(Byte.class) .addNext(Double.class).addNext(Float.class).addNext(Boolean.class) @@ -222,16 +219,6 @@ 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); - // } - // int leftBound = value.indexOf('['); int rightBound = value.lastIndexOf(']'); @@ -281,79 +268,6 @@ public Storeable deserialize(Table table, String value) throws ParseException { } return storeable; - // int currentColumn = 0; - // - // int index = leftBound + 1; - // - // for (; index < rightBound; ) { - // char currentChar = value.charAt(index); - // - // if (Character.isSpaceChar(currentChar)) { - // // Space that does not mean anything. - // - // index++; - // } else if (LIST_SEPARATOR_CHARACTER == currentChar) { - // // Next list element. - // - // currentColumn++; - // if (currentColumn >= columnsCount) { - // throw new ParseException( - // "wrong type (Too many elements in the list; expected: " + - // columnsCount + ")", - // index); - // } - // index++; - // } else { - // // Boolean, Number, Null, String. - // - // // End of element (exclusive). - // int elementEnd; - // - // 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. - // - // 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; - // } - // } - // - // // 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(currentColumn, elementObj); - // index = elementEnd; - // } - // } - // - // if (currentColumn + 1 != columnsCount) { - // throw new ParseException( - // "wrong type (Too few elements in the list; expected: " + columnsCount - // + ")", -1); - // } - // - // return storeable; } } 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 b19458ada..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 @@ -15,7 +15,7 @@ * Not thread-safe.
    * Not bound to any table. */ -@JSONComplexObject(singleField = true) +@JSONComplexObject(wrapper = true) public final class StoreableImpl implements Storeable { @JSONField private final Object[] values; 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 cb3f178b4..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 @@ -62,7 +62,7 @@ private StoreableTableImpl(TableProvider provider, this.provider = provider; this.onTableClosedListener = onTableClosedListener; this.store = store; - this.columnTypes = Collections.unmodifiableList(new ArrayList>(columnTypes)); + this.columnTypes = Collections.unmodifiableList(new ArrayList<>(columnTypes)); } static AutoCloseableTable createTable(TableProvider provider, 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 index 1866a469d..bb21ad2e8 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/json/JSONComplexObject.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/json/JSONComplexObject.java @@ -8,7 +8,8 @@ import java.lang.annotation.Target; /** - * This annotation indicates that objects of the annotated type contain multiple JSON fields. + * 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}) @@ -19,5 +20,5 @@ /** * If true, this object will not be converted to JSON directly. Its single field will be taken instead. */ - boolean singleField() default false; + boolean wrapper() default false; } 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 index 91689b543..e01485f34 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/json/JSONMaker.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/json/JSONMaker.java @@ -138,7 +138,7 @@ private static void appendJSONString(final StringBuilder sb, if (annotatedFields.isEmpty()) { throw new IllegalArgumentException( "Illegal annotation @JSONComplexObject: there are no @JSONField annotated fields"); - } else if (objClass.getAnnotation(JSONComplexObject.class).singleField()) { + } else if (objClass.getAnnotation(JSONComplexObject.class).wrapper()) { if (annotatedFields.size() > 1) { throw new IllegalArgumentException( "Illegal annotation @JSONComplexObject: there are more then one @JSONField"); 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 index a809a4302..e736aa77c 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/json/JSONParser.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/json/JSONParser.java @@ -370,7 +370,7 @@ public String toString() { } } - @JSONComplexObject(singleField = true) + @JSONComplexObject(wrapper = true) private static class MapObject implements JSONParsedObject { @JSONField private final Map map; @@ -438,7 +438,7 @@ public String toString() { } } - @JSONComplexObject(singleField = true) + @JSONComplexObject(wrapper = true) private static class ArrayObject implements JSONParsedObject { @JSONField private final Object[] array; diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/ArrayMatcher.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/ArrayMatcher.java deleted file mode 100644 index 324c6e681..000000000 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/ArrayMatcher.java +++ /dev/null @@ -1,54 +0,0 @@ -package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support; - -import org.hamcrest.BaseMatcher; -import org.hamcrest.Description; -import org.hamcrest.Matcher; - -import java.lang.reflect.Array; - -/** - * Matcher for array that stores own matcher for each element. - * @deprecated Developed but alternative way has been found for the moment. - */ -@Deprecated -class ArrayMatcher extends BaseMatcher { - private final Matcher[] matchers; - - /** - * Constructs array matcher from array of matchers. - * @param matchers - * array of matchers. Note that this array is not copied, so changes are backed. - */ - public ArrayMatcher(Matcher... matchers) { - this.matchers = matchers; - } - - @Override - public boolean matches(Object item) { - int length = Array.getLength(item); - if (matchers.length != length) { - return false; - } - - for (int i = 0; i < length; i++) { - if (!matchers[i].matches(Array.get(item, i))) { - return false; - } - } - - return true; - } - - @Override - public void describeTo(Description description) { - description.appendText("Array must have length " + matchers.length + " and match: "); - boolean comma = false; - for (Matcher matcher : matchers) { - if (comma) { - description.appendText(", "); - } - comma = true; - description.appendDescriptionOf(matcher); - } - } -} 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 3161fe15e..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. */ @@ -61,13 +61,14 @@ public static synchronized void log(Class logger, String message) { } public static synchronized void log(Class logger, Throwable throwable, String message) { - if (writer == null) { - reopen(); + if (enableLogging) { if (writer == null) { - return; + reopen(); + if (writer == null) { + return; + } } - } - if (enableLogging) { + StringBuilder sb = new StringBuilder(message == null ? 100 : message.length() * 2); boolean appendSpace = false; 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 3dc7a00a0..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 [regex-style]. - * @param escapeSequence - * Inside quotes escapeSequence and quotes must occur only after escapeSequence [regex-style]. - */ - 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. @@ -253,10 +239,8 @@ public static String quoteString(String s, String quoteSequence, String escapeSe if (s == null) { return null; } - s = s.replace( - 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; } 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 37e481a32..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 @@ -22,8 +22,6 @@ public class UtilityTest { private static final char QUOTE_CHARACTER = '"'; private static final char ESCAPE_CHARACTER = '\\'; - private static final String QUOTED_STRING_REGEX = - Utility.getQuotedStringRegex(QUOTE_CHARACTER + "", ESCAPE_CHARACTER + "" + ESCAPE_CHARACTER); @Rule public ExpectedException exception = ExpectedException.none(); @@ -56,21 +54,6 @@ 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( @@ -113,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", ")"), From b099971935eb11a0015eed0a1c23d4ea3436baf2 Mon Sep 17 00:00:00 2001 From: AndreosGhost Date: Wed, 17 Dec 2014 22:20:53 +0300 Subject: [PATCH 05/14] Fixed comments to task06-parallel, changed JSON to XML in task07-proxy, implemented Telnet server --- .../db/AutoCloseableProvider.java | 2 +- .../db/AutoCloseableTable.java | 2 +- .../databaselibrary/db/DBTableProvider.java | 33 ++- .../db/DBTableProviderFactory.java | 13 +- .../databaselibrary/db/Database.java | 30 ++- .../db/StoreableTableImpl.java | 14 +- .../databaselibrary/db/StringTableImpl.java | 13 +- .../databaselibrary/json/JSONMaker.java | 106 ++++----- .../json/JSONParsedObject.java | 32 ++- .../databaselibrary/json/JSONParser.java | 41 ++-- .../shell/AbstractCommand.java | 51 ++-- .../databaselibrary/shell/BaseShellState.java | 39 ++++ .../databaselibrary/shell/Command.java | 5 + .../shell/CommandContainer.java | 2 +- .../databaselibrary/shell/Commands.java | 210 +++++++++-------- .../shell/DatabaseFactory.java | 35 +++ .../databaselibrary/shell/Main.java | 22 +- .../databaselibrary/shell/Shell.java | 55 ++++- .../databaselibrary/shell/ShellState.java | 7 + .../shell/SingleDatabaseShellState.java | 59 +---- .../SingleFactoryDatabaseShellState.java | 30 +++ .../support/AccurateExceptionHandler.java | 4 +- .../support/ConvenientCollection.java | 4 +- .../support/ConvenientMap.java | 4 +- .../databaselibrary/support/Log.java | 6 +- .../support/LoggingProxyFactoryBase.java | 140 +++++++++++ .../support/LoggingProxyFactoryJSON.java | 84 +++++++ .../support/LoggingProxyFactoryXML.java | 82 +++++++ .../databaselibrary/support/Utility.java | 48 ++-- .../support/ValidityController.java | 4 +- .../databaselibrary/telnet/Server.java | 153 ++++++++++++ .../telnet/ServerCommands.java | 143 ++++++++++++ .../telnet/ServerCommunicator.java | 85 +++++++ .../databaselibrary/telnet/ServerState.java | 110 +++++++++ .../test/DatabaseShellTest.java | 17 +- .../test/InterpreterTestBase.java | 11 +- .../test/LoggingProxyFactoryJSONTest.java | 137 +++++++++++ .../test/LoggingProxyFactoryTestBase.java | 49 ++++ .../test/LoggingProxyFactoryXMLTest.java | 83 +++++++ .../test/TableProviderTest.java | 22 +- .../databaselibrary/test/UtilityTest.java | 2 +- .../databaselibrary/test/XMLTest.java | 81 +++++++ .../test/support/AlternativeShellState.java | 18 +- .../test/support/MutatedDatabase.java | 6 +- .../test/support/MutatedSDSS.java | 35 ++- .../databaselibrary/xml/XMLComplexObject.java | 22 ++ .../databaselibrary/xml/XMLField.java | 45 ++++ .../databaselibrary/xml/XMLMaker.java | 219 ++++++++++++++++++ 48 files changed, 2012 insertions(+), 403 deletions(-) create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/BaseShellState.java create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/DatabaseFactory.java create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/SingleFactoryDatabaseShellState.java create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/LoggingProxyFactoryBase.java create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/LoggingProxyFactoryJSON.java create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/LoggingProxyFactoryXML.java create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/Server.java create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/ServerCommands.java create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/ServerCommunicator.java create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/ServerState.java create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/LoggingProxyFactoryJSONTest.java create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/LoggingProxyFactoryTestBase.java create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/LoggingProxyFactoryXMLTest.java create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/XMLTest.java create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/xml/XMLComplexObject.java create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/xml/XMLField.java create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/xml/XMLMaker.java 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 index 42056da21..887456bf9 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/AutoCloseableProvider.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/AutoCloseableProvider.java @@ -2,7 +2,7 @@ import ru.fizteh.fivt.storage.structured.TableProvider; -interface AutoCloseableProvider extends TableProvider, AutoCloseable { +public 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 index 065c79588..8e4c67aff 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/AutoCloseableTable.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/AutoCloseableTable.java @@ -2,7 +2,7 @@ import ru.fizteh.fivt.storage.structured.Table; -interface AutoCloseableTable extends Table, AutoCloseable { +public 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 6d4e102b8..cded1d74f 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 @@ -10,6 +10,7 @@ import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.json.JSONParser; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.ConvenientCollection; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.ConvenientMap; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Log; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Utility; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.ValidityController; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.ValidityController.KillLock; @@ -36,14 +37,14 @@ final class DBTableProvider implements AutoCloseableProvider { private static final char QUOTE_CHARACTER = '\"'; private static final Collection> SUPPORTED_TYPES = new ConvenientCollection<>( - new HashSet>()).addNext(Integer.class).addNext(Long.class).addNext(Byte.class) - .addNext(Double.class).addNext(Float.class).addNext(Boolean.class) - .addNext(String.class); + new HashSet>()).chainAdd(Integer.class).chainAdd(Long.class).chainAdd(Byte.class) + .chainAdd(Double.class).chainAdd(Float.class).chainAdd(Boolean.class) + .chainAdd(String.class); private static final Map, Function> PARSERS = new ConvenientMap<>(new HashMap, Function>()) - .putNext(Integer.class, Integer::parseInt).putNext(Long.class, Long::parseLong) - .putNext(Byte.class, Byte::parseByte).putNext( + .chainPut(Integer.class, Integer::parseInt).chainPut(Long.class, Long::parseLong) + .chainPut(Byte.class, Byte::parseByte).chainPut( Boolean.class, str -> { Utility.checkNotNull(str, "String to parse"); if (str.matches("(?i)true|false")) { @@ -51,8 +52,8 @@ final class DBTableProvider implements AutoCloseableProvider { } else { throw new ColumnFormatException("Expected 'true' or 'false' as boolean"); } - }).putNext(Double.class, Double::parseDouble).putNext(Float.class, Float::parseFloat) - .putNext( + }).chainPut(Double.class, Double::parseDouble).chainPut(Float.class, Float::parseFloat) + .chainPut( String.class, str -> { if (!str.startsWith(QUOTE_CHARACTER + "") || !str .endsWith(QUOTE_CHARACTER + "")) { @@ -94,6 +95,12 @@ final class DBTableProvider implements AutoCloseableProvider { reloadAllTables(); } + public Path getDatabaseRoot() { + try (UseLock useLock = validityController.use()) { + return databaseRoot; + } + } + @Override public AutoCloseableTable getTable(String name) throws IllegalArgumentException { try (UseLock useLock = validityController.use()) { @@ -428,9 +435,21 @@ public void close() { tables.values().stream().filter(table -> table != null).forEach(AutoCloseableTable::close); tables.clear(); corruptTables.clear(); + + // Deleting empty files and empty folders. + try (DirectoryStream dirStream = Files.newDirectoryStream(databaseRoot)) { + for (Path tableDirectory : dirStream) { + Utility.removeEmptyFilesAndFolders(tableDirectory); + } + + Log.log(DBTableProvider.class, "Cleaned up successfully"); + } catch (IOException exc) { + Log.log(DBTableProvider.class, exc, "Failed to clean up"); + } } finally { persistenceLock.writeLock().unlock(); } + factory.onProviderClosed(this); } finally { tableClosedByMe = false; 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 9fc03d3e9..3874d21f8 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 @@ -4,7 +4,7 @@ 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.LoggingProxyFactoryJSON; 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; @@ -21,7 +21,7 @@ import java.util.IdentityHashMap; public final class DBTableProviderFactory implements TableProviderFactory, AutoCloseable { - private static final LoggingProxyFactory LOGGING_PROXY_FACTORY = new LoggingProxyFactoryImpl(); + private static final LoggingProxyFactory LOGGING_PROXY_FACTORY = new LoggingProxyFactoryJSON(); private static final Writer LOG_WRITER; static { @@ -41,6 +41,8 @@ public final class DBTableProviderFactory implements TableProviderFactory, AutoC private final IdentityHashMap generatedProviders = new IdentityHashMap<>(); + private boolean providerClosedByMe = false; + static T wrapImplementation(T implementation, Class interfaceClass) { if (LOG_WRITER != null) { return (T) LOGGING_PROXY_FACTORY.wrap(LOG_WRITER, implementation, interfaceClass); @@ -52,10 +54,13 @@ static T wrapImplementation(T implementation, Class interfaceClass) { @Override public synchronized void close() { try (KillLock lock = validityController.useAndKill()) { + providerClosedByMe = true; for (AutoCloseableProvider provider : generatedProviders.keySet()) { provider.close(); } generatedProviders.clear(); + } finally { + providerClosedByMe = false; } } @@ -66,7 +71,9 @@ public synchronized void close() { */ synchronized void onProviderClosed(AutoCloseableProvider provider) { try (UseLock useLock = validityController.use()) { - generatedProviders.remove(provider); + if (!providerClosedByMe) { + generatedProviders.remove(provider); + } } } 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 0e90fb129..00aa23a7f 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 @@ -6,19 +6,20 @@ import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.NoActiveTableException; import java.io.IOException; -import java.nio.file.Path; +import java.io.PrintStream; import java.util.List; /** * Database class responsible for a set of tables assigned to it. * @author phoenix */ -public class Database implements AutoCloseable { +public class Database { private final TableProvider provider; /** * Root directory of all database files */ - private final Path dbDirectory; + private final String dbDirectory; + private final PrintStream outputStream; /** * Table in use.
    All operations (like {@code put}, {@code get}, etc.) are performed with * this table. @@ -28,12 +29,11 @@ public class Database implements AutoCloseable { /** * Establishes a database instance on given folder.
    If the folder exists, the old database * is used.
    If the folder does not exist, a new database is created within the folder. - * @throws ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.DatabaseIOException */ - public Database(Path dbDirectory) throws DatabaseIOException { + public Database(TableProvider provider, String dbDirectory, PrintStream outputStream) { + this.outputStream = outputStream; this.dbDirectory = dbDirectory; - DBTableProviderFactory factory = new DBTableProviderFactory(); - this.provider = factory.create(dbDirectory.toString()); + this.provider = provider; } public TableProvider getProvider() { @@ -69,19 +69,12 @@ 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; } - public Path getDbDirectory() { + public String getDbDirectory() { return dbDirectory; } @@ -106,9 +99,10 @@ public int rollback() { } public void showTables() { - System.out.println("table_name row_count"); + outputStream.println("table_name row_count"); List tableNames = provider.getTableNames(); + StringBuilder sb = new StringBuilder(); for (String tableName : tableNames) { boolean valid; int changesCount = 0; @@ -121,8 +115,10 @@ public void showTables() { valid = false; } - System.out.println(String.format("%s %s", tableName, valid ? changesCount : "corrupt")); + sb.append(String.format("%s %s", tableName, valid ? changesCount : "corrupt")); + sb.append(System.lineSeparator()); } + outputStream.print(sb.toString()); } /** 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 fdebf0f65..191b67e1b 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 @@ -32,13 +32,13 @@ public final class StoreableTableImpl implements AutoCloseableTable { private static final Map, String> CLASSES_TO_NAMES_MAP = - new ConvenientMap<>(new HashMap, String>()).putNext(Integer.class, "int") - .putNext(Long.class, "long") - .putNext(Byte.class, "byte") - .putNext(Double.class, "double") - .putNext(Float.class, "float") - .putNext(Boolean.class, "boolean") - .putNext(String.class, "String"); + new ConvenientMap<>(new HashMap, String>()).chainPut(Integer.class, "int") + .chainPut(Long.class, "long") + .chainPut(Byte.class, "byte") + .chainPut(Double.class, "double") + .chainPut(Float.class, "float") + .chainPut(Boolean.class, "boolean") + .chainPut(String.class, "String"); private static final Map> NAMES_TO_CLASSES_MAP = Utility.inverseMap(CLASSES_TO_NAMES_MAP); 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 d40bafbeb..fd4f4d97b 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 @@ -325,15 +325,10 @@ public int commit() throws DatabaseIOException { public int rollback() { int diffsCount = 0; - persistenceLock.readLock().lock(); - try { - for (TablePart part : tableParts.values()) { - diffsCount += part.rollback(); - } - - } finally { - persistenceLock.readLock().unlock(); + for (TablePart part : tableParts.values()) { + diffsCount += part.rollback(); } + return diffsCount; } @@ -366,7 +361,7 @@ private Path makeTablePartFilePath(int hash) { } /** - * Gets {@link TablePart} instance assigned to + * Gets {@link ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.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/json/JSONMaker.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/json/JSONMaker.java index e01485f34..1c15b2f8c 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/json/JSONMaker.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/json/JSONMaker.java @@ -1,11 +1,10 @@ package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.json; -import java.lang.annotation.Annotation; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Utility; + 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; @@ -17,28 +16,11 @@ * This class helps to construct a JSON string from any object using special annotations. * @author Phoenix * @see JSONComplexObject - * @see JSONField + * @see ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.json.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 @@ -75,45 +57,7 @@ private static void appendJSONString(final StringBuilder sb, 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) { + if (objClass.getAnnotation(JSONComplexObject.class) != null) { // Convenience trick. FieldJSONAppender jsonAppender = (field, overrideName) -> { String fieldName = field.getAnnotation(JSONField.class).name(); @@ -134,7 +78,7 @@ private static void appendJSONString(final StringBuilder sb, }; // Fields to convert to json. - List annotatedFields = getAnnotatedFields(objClass, JSONField.class); + List annotatedFields = Utility.getAllAnnotatedFields(objClass, JSONField.class); if (annotatedFields.isEmpty()) { throw new IllegalArgumentException( "Illegal annotation @JSONComplexObject: there are no @JSONField annotated fields"); @@ -158,6 +102,44 @@ private static void appendJSONString(final StringBuilder sb, } sb.append(CLOSING_CURLY_BRACE); } + } else if (obj instanceof Map) { + sb.append(OPENING_CURLY_BRACE); + Set set = ((Map) obj).entrySet(); + + for (Entry e : set) { + appendJSONString(sb, e.getValue(), e.getKey().toString(), identityMap); + } + + sb.append(CLOSING_CURLY_BRACE); + } else if (objClass.isArray() || obj instanceof Iterable) { + sb.append(OPENING_SQUARE_BRACE); + + if (obj instanceof Iterable) { + boolean comma = false; + for (Object piece : (Iterable) obj) { + if (comma) { + sb.append(ELEMENT_SEPARATOR); + } + comma = true; + appendJSONString(sb, piece, null, identityMap); + } + } else { + int length = Array.getLength(obj); + boolean comma = false; + for (int i = 0; i < length; i++) { + if (comma) { + sb.append(ELEMENT_SEPARATOR); + } + comma = true; + appendJSONString(sb, Array.get(obj, i), null, identityMap); + } + } + + sb.append(CLOSING_SQUARE_BRACE); + } else if (obj instanceof Number) { + sb.append(obj.toString()); + } else if (obj instanceof Boolean) { + sb.append(obj.toString()); } else { sb.append(QUOTES).append(JSONHelper.escape(obj.toString())).append(QUOTES); } @@ -187,7 +169,7 @@ private static void appendJSONString(final StringBuilder sb, * @see Object#toString() * @see Number * @see JSONComplexObject - * @see JSONField + * @see ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.json.JSONField */ public static String makeJSON(Object object) throws RuntimeException { try { 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 index ef3b10790..ef8325608 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/json/JSONParsedObject.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/json/JSONParsedObject.java @@ -8,11 +8,11 @@ * (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}
    • + *
    • {@link Long}
    • + *
    • {@link Double}
    • + *
    • {@link Boolean}
    • + *
    • {@link String}
    • + *
    • {@link JSONParsedObject}
    • *
    */ public interface JSONParsedObject { @@ -22,7 +22,7 @@ public interface JSONParsedObject { * name of the field. * @param value * value of the field. Can be null. - * @throws java.lang.UnsupportedOperationException + * @throws UnsupportedOperationException */ void put(String name, Object value) throws UnsupportedOperationException; @@ -41,8 +41,8 @@ public interface JSONParsedObject { * @param name * name of the field * @return null, {@link Integer}, {@link String}, {@link Double}, {@link Boolean} or {@link - * JSONParsedObject}. - * @throws java.lang.UnsupportedOperationException + * ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.json.JSONParsedObject}. + * @throws UnsupportedOperationException */ Object get(String name) throws UnsupportedOperationException; @@ -51,8 +51,8 @@ public interface JSONParsedObject { * @param index * index of the element * @return null, {@link Integer}, {@link String}, {@link Double}, {@link Boolean} or {@link - * JSONParsedObject}. - * @throws java.lang.UnsupportedOperationException + * ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.json.JSONParsedObject}. + * @throws UnsupportedOperationException */ Object get(int index) throws UnsupportedOperationException, ClassCastException; @@ -88,13 +88,11 @@ default Boolean getBoolean(int index) throws UnsupportedOperationException, Clas return (Boolean) get(index); } - default JSONParsedObject getObject(String name) - throws UnsupportedOperationException, ClassCastException { + default JSONParsedObject getObject(String name) throws UnsupportedOperationException, ClassCastException { return (JSONParsedObject) get(name); } - default JSONParsedObject getObject(int index) - throws UnsupportedOperationException, ClassCastException { + default JSONParsedObject getObject(int index) throws UnsupportedOperationException, ClassCastException { return (JSONParsedObject) get(index); } @@ -115,7 +113,7 @@ default JSONParsedObject getObject(int index) * @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}. + * ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.json.JSONParsedObject}. */ Object deepGet(Object... namePieces); @@ -126,13 +124,13 @@ default JSONParsedObject getObject(int index) /** * Returns this object as map. Unsupported for standard array respresentations. - * @throws java.lang.UnsupportedOperationException + * @throws UnsupportedOperationException */ Map asMap() throws UnsupportedOperationException; /** * Returns this object as array. Unsupported for associative array representations. - * @throws java.lang.UnsupportedOperationException + * @throws UnsupportedOperationException */ Object[] asArray() throws UnsupportedOperationException; 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 index e736aa77c..1b4fe9ea3 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/json/JSONParser.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/json/JSONParser.java @@ -3,7 +3,6 @@ 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; @@ -38,7 +37,8 @@ private static List findTokens(String s, int begin, int end) throws Parse switch (s.charAt(index)) { case QUOTES: { // True quotes. - if (noEscape || (index > begin && s.charAt(index - 1) != ESCAPE_SYMBOL)) { + // if (noEscape || (index > begin && s.charAt(index - 1) != ESCAPE_SYMBOL)) { + if (noEscape) { tokens.add(new Token(TokenType.QUOTES, index, level)); inQuotes = !inQuotes; } else { @@ -162,8 +162,10 @@ private static Object jsonObjectDeepGet(JSONParsedObject source, Object... pathP * @return constructed object. Cannot be null. * @throws java.text.ParseException */ - public static JSONParsedObject parseJSON(String json) throws ParseException { + public static JSONParsedObject parseJSON(final String json) throws ParseException { ArrayList tokens = new ArrayList<>(findTokens(json, 0, json.length())); + // System.err.println(Arrays.toString(tokens.toArray())); + ArrayDeque dq = new ArrayDeque<>(); List rootElementSeparators = getElementSeparators(tokens, 0); @@ -214,7 +216,8 @@ public static JSONParsedObject parseJSON(String json) throws ParseException { */ String key = null; - if (!currentlyParsingObject.object.isStandardArray()) { + // If object is not array and not empty -> there must be name! + if (!currentlyParsingObject.object.isStandardArray() && dataStartTokenID + 1 < dataEndTokenID) { valueOffset = 4; int nameOpeningQuotesID = dataStartTokenID + 1; int nameClosingQuotesID = nameOpeningQuotesID + 1; @@ -262,10 +265,13 @@ public static JSONParsedObject parseJSON(String json) throws ParseException { Object value; String valueString = json.substring(valueStartIndex, valueEndIndex).trim().toLowerCase(); + boolean doPut = true; switch (valueString) { case "": { - throw new ParseException("Empty elements are not allowed in json", -1); + doPut = false; + value = null; + break; } case NULL: { value = null; @@ -289,7 +295,9 @@ public static JSONParsedObject parseJSON(String json) throws ParseException { } } - currentlyParsingObject.putSafely(key, value); + if (doPut) { + currentlyParsingObject.putSafely(key, value); + } } else if (valueStartToken.type == TokenType.QUOTES && valueEndToken.type == TokenType.QUOTES) { // Quotes. Expected type of value: string. if (valueStartTokenID + 1 != valueEndTokenID) { @@ -441,10 +449,10 @@ public String toString() { @JSONComplexObject(wrapper = true) private static class ArrayObject implements JSONParsedObject { @JSONField - private final Object[] array; + private final List array; public ArrayObject(int length) { - this.array = new Object[length]; + this.array = new ArrayList<>(length); } @Override @@ -454,7 +462,14 @@ public void put(String key, Object value) throws UnsupportedOperationException { @Override public void put(int index, Object value) throws UnsupportedOperationException { - array[index] = value; + if (index == array.size()) { + array.add(value); + } else if (index < array.size()) { + array.set(index, value); + } else { + throw new IndexOutOfBoundsException( + "Index too big: " + index + ", expected (max): " + array.size()); + } } @Override @@ -464,7 +479,7 @@ public Object get(String key) { @Override public Object get(int index) { - return array[index]; + return array.get(index); } @Override @@ -484,12 +499,12 @@ public Object deepGet(Object... namePieces) { @Override public int size() { - return array.length; + return array.size(); } @Override public String toString() { - return Arrays.toString(array); + return array.stream().reduce((a, b) -> a.toString() + "," + b.toString()).get().toString(); } @Override @@ -499,7 +514,7 @@ public Map asMap() { @Override public Object[] asArray() { - return array; + return array.toArray(); } } 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 a46e16168..1f7eebfef 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 @@ -7,6 +7,7 @@ import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.AccurateExceptionHandler; import java.io.IOException; +import java.io.PrintStream; import java.text.ParseException; import static ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Utility.*; @@ -15,16 +16,16 @@ * Convenience class for Commands. * @author phoenix */ -public abstract class AbstractCommand implements Command { +public abstract class AbstractCommand> implements Command { private static final Class[] EXECUTE_SAFELY_THROWN_EXCEPTIONS = obtainExceptionsThrownByExecuteSafely(); /** * Used for unsafe calls. Catches and handles all exceptions thrown by {@link - * ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.AbstractCommand#executeSafely + * AbstractCommand#executeSafely * (SingleDatabaseShellState, String[]) } and {@link IllegalArgumentException }. */ - static final AccurateExceptionHandler DATABASE_ERROR_HANDLER = - (Exception exc, SingleDatabaseShellState shell) -> { + static final AccurateExceptionHandler DATABASE_ERROR_HANDLER = + (Exception exc, PrintStream ps) -> { boolean found = false; Class actualType = exc.getClass(); @@ -39,7 +40,7 @@ public abstract class AbstractCommand implements Command[] obtainExceptionsThrownByExecuteSafely() { Class[] exceptions; try { - exceptions = AbstractCommand.class - .getMethod("executeSafely", SingleDatabaseShellState.class, String[].class) - .getExceptionTypes(); + exceptions = AbstractCommand.class.getMethod("executeSafely", ShellState.class, String[].class) + .getExceptionTypes(); } catch (Exception exc) { throw new RuntimeException("Failed to obtain exceptions thrown by executeSafely"); } @@ -85,15 +85,17 @@ private static Class[] obtainExceptionsThrownByExecuteSafely() { * In implementation of {@link ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell * .AbstractCommand} * arguments number is checked first and then - * {@link #executeSafely(SingleDatabaseShellState, String[])} is invoked.
    If you want to - * disable forced arguments number checking, override this method without invocation super - * method and put empty implementation inside {@link #executeSafely(SingleDatabaseShellState, - * String[])}. + * {@link #executeSafely(State, String[])} is invoked.
    + * If you want to disable forced arguments number checking, override this method without invocation super + * method and put empty implementation inside {@link #executeSafely(State, String[])}. */ @Override - public void execute(final SingleDatabaseShellState state, final String[] args) throws TerminalException { - checkArgsNumber(args, minimalArgsCount, maximalArgsCount); - performAccurately(() -> executeSafely(state, args), DATABASE_ERROR_HANDLER, state); + public void execute(final State state, final String[] args) throws TerminalException { + performAccurately( + () -> { + checkArgsNumber(args, minimalArgsCount, maximalArgsCount); + executeSafely(state, args); + }, DATABASE_ERROR_HANDLER, state.getOutputStream()); } @Override @@ -106,17 +108,18 @@ public String getInvocation() { return invocationArgs; } - public abstract void executeSafely(SingleDatabaseShellState shell, String[] args) throws - IllegalArgumentException, - NoActiveTableException, - IllegalStateException, - InvocationException, - ParseException, - IOException; + public abstract void executeSafely(State state, String[] args) throws + IllegalArgumentException, + NoActiveTableException, + IllegalStateException, + NullPointerException, + InvocationException, + ParseException, + IOException; - void checkArgsNumber(String[] args, int minimal, int maximal) throws TerminalException { + void checkArgsNumber(String[] args, int minimal, int maximal) throws WrongArgsNumberException { if (args.length < minimal || args.length > maximal) { - handleError(null, new WrongArgsNumberException(this, args[0]), true); + throw new WrongArgsNumberException(this, args[0]); } } } diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/BaseShellState.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/BaseShellState.java new file mode 100644 index 000000000..bd615a3ee --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/BaseShellState.java @@ -0,0 +1,39 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell; + +import java.io.PrintStream; +import java.util.Objects; + +/** + * Base class for states with single init possibility. + * @param + */ +public abstract class BaseShellState> implements ShellState { + protected Shell host; + + protected void checkInitialized() { + Objects.requireNonNull(host, "Not initialized"); + } + + @Override + public PrintStream getOutputStream() { + checkInitialized(); + return host.getOutputStream(); + } + + @Override + public String getGreetingString() { + return "$ "; + } + + @Override + public void init(Shell host) throws Exception { + Objects.requireNonNull(host, "Host shell must not be null"); + + if (this.host != null) { + throw new IllegalStateException("Already initialized"); + } + + this.host = host; + } + +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/Command.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/Command.java index 5b896e041..80680ff76 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/Command.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/Command.java @@ -18,4 +18,9 @@ public interface Command> { * Complete formula for command invocation excluding command name. */ String getInvocation(); + + default String buildHelpLine(String commandName) { + return String.format( + "\t%s%s\t%s", commandName, getInvocation() == null ? "" : (' ' + getInvocation()), getInfo()); + } } 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 348e75d0b..499ec99b9 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 ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.ShellState + * @see ShellState */ public interface CommandContainer> { Map> getCommands(); diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/Commands.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/Commands.java index 96732abc5..013228069 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/Commands.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/Commands.java @@ -18,74 +18,85 @@ public class Commands extends SimpleCommandContainer { public static final Command COMMIT = - new AbstractCommand(null, "saves all changes made from the last commit", 1) { + new AbstractCommand( + null, "saves all changes made from the last commit", 1) { @Override public void executeSafely(SingleDatabaseShellState state, String[] args) throws IOException, IllegalArgumentException { int changes = state.getActiveDatabase().commit(); - System.out.println(changes); + state.getOutputStream().println(changes); } }; public static final Command ROLLBACK = - new AbstractCommand(null, "cancels all changes made from the last commit", 1) { + new AbstractCommand( + null, "cancels all changes made from the last commit", 1) { @Override public void executeSafely(SingleDatabaseShellState state, String[] args) throws DatabaseIOException, IllegalArgumentException { int changes = state.getActiveDatabase().rollback(); - System.out.println(changes); + state.getOutputStream().println(changes); } }; public static final Command SIZE = - new AbstractCommand(null, "prints count of stored keys in current table", 1) { + new AbstractCommand( + null, "prints count of stored keys in current table", 1) { @Override public void executeSafely(SingleDatabaseShellState state, String[] args) throws DatabaseIOException, IllegalArgumentException, NoActiveTableException { int size = state.getActiveDatabase().getActiveTable().size(); - System.out.println(size); + state.getOutputStream().println(size); } }; - public static final Command CREATE = new AbstractCommand( - " (type0 type1 ... typeN)", - "creates a new table with the given name and column types (must be specified inside round " - + "brackets); type can be one of the following: int, long, byte, float, double, boolean, String;", - 3, - Integer.MAX_VALUE) { - @Override - public void executeSafely(SingleDatabaseShellState state, String[] args) - throws IOException, InvocationException { - if (!args[2].startsWith("(") || !args[args.length - 1].endsWith(")")) { - throw new InvocationException( - this, args[0], "Round brackets must exist and contain types list inside them"); - } + public static final Command CREATE = + new AbstractCommand( + " (type0 type1 ... typeN)", + "creates a new table with the given name and column types (must be specified inside " + + "round " + + "brackets); type can be one of the following: int, long, byte, float, double, " + + "boolean, String;", + 3, + Integer.MAX_VALUE) { + @Override + public void executeSafely(SingleDatabaseShellState state, String[] args) + throws IOException, InvocationException { + if (!args[2].startsWith("(") || !args[args.length - 1].endsWith(")")) { + throw new InvocationException( + this, + args[0], + "Round brackets must exist and contain types list inside them"); + } - // Joining strings. - String typesString = String.join(" ", Arrays.asList(args).subList(2, args.length)); + // Joining strings. + String typesString = String.join(" ", Arrays.asList(args).subList(2, args.length)); - // Removing brackets. - typesString = typesString.substring(1, typesString.length() - 1).trim(); + // Removing brackets. + typesString = typesString.substring(1, typesString.length() - 1).trim(); - List> columnTypes = StoreableTableImpl.parseColumnTypes(typesString); - String tableName = args[1]; + List> columnTypes = StoreableTableImpl.parseColumnTypes(typesString); + String tableName = args[1]; - boolean created = state.getActiveDatabase().createTable(tableName, columnTypes); - if (created) { - System.out.println("created"); - } else { - throw new DatabaseIOException(tableName + " exists"); - } - } - }; - public static final Command DROP = new AbstractCommand( - "", "deletes table with the given name from file system", 2) { - @Override - public void executeSafely(SingleDatabaseShellState state, final String[] args) throws IOException { - state.getActiveDatabase().dropTable(args[1]); - System.out.println("dropped"); - } - }; + boolean created = state.getActiveDatabase().createTable(tableName, columnTypes); + if (created) { + state.getOutputStream().println("created"); + } else { + throw new DatabaseIOException(tableName + " exists"); + } + } + }; + public static final Command DROP = + new AbstractCommand( + "", "deletes table with the given name from file system", 2) { + @Override + public void executeSafely(SingleDatabaseShellState state, final String[] args) + throws IOException { + state.getActiveDatabase().dropTable(args[1]); + state.getOutputStream().println("dropped"); + } + }; public static final Command EXIT = - new AbstractCommand(null, "saves all data to file system and stops interpretation", 1) { + new AbstractCommand( + null, "saves all data to file system and stops interpretation", 1) { @Override public void execute(SingleDatabaseShellState state, String[] args) throws TerminalException { int exitCode = 0; @@ -94,7 +105,7 @@ public void execute(SingleDatabaseShellState state, String[] args) throws Termin state.persist(); } catch (Exception exc) { exitCode = 1; - DATABASE_ERROR_HANDLER.handleException(exc, state); + DATABASE_ERROR_HANDLER.handleException(exc, state.getOutputStream()); } finally { state.prepareToExit(exitCode); } @@ -104,13 +115,13 @@ public void execute(SingleDatabaseShellState state, String[] args) throws Termin } @Override - public void executeSafely(SingleDatabaseShellState shell, String[] args) + public void executeSafely(SingleDatabaseShellState state, String[] args) throws DatabaseIOException, IllegalArgumentException { // Not used. } }; public static final Command GET = - new AbstractCommand("", "obtains value by the key", 2) { + new AbstractCommand("", "obtains value by the key", 2) { @Override public void executeSafely(SingleDatabaseShellState state, final String[] args) throws NoActiveTableException { @@ -124,46 +135,42 @@ public void executeSafely(SingleDatabaseShellState state, final String[] args) if (value == null) { throw new IllegalArgumentException("not found"); } else { - System.out.println("found"); - System.out.println(provider.serialize(table, value)); + state.getOutputStream().println("found"); + state.getOutputStream().println(provider.serialize(table, value)); } } }; - public static final Command HELP = new AbstractCommand( - null, "prints out description of state commands", 1, Integer.MAX_VALUE) { - @Override - public void execute(SingleDatabaseShellState state, String[] args) { - Map> commands = state.getCommands(); - - System.out.println( - "DatabaseLibrary is an utility that lets you work with a simple database"); + public static final Command HELP = + new AbstractCommand( + null, "prints out description of state commands", 1, Integer.MAX_VALUE) { + @Override + public void execute(SingleDatabaseShellState state, String[] args) { + Map> commands = state.getCommands(); - System.out.println( - String.format( - "You can set database directory to work with using environment " - + "variable '%s'", SingleDatabaseShellState.DB_DIRECTORY_PROPERTY_NAME)); + state.getOutputStream().println( + "DatabaseLibrary is an utility that lets you work with a simple database"); - for (Entry> cmdEntry : commands.entrySet()) { - String cmdName = cmdEntry.getKey(); - Command command = cmdEntry.getValue(); + state.getOutputStream().println( + String.format( + "You can set database directory to work with using environment " + + "variable '%s'", SingleDatabaseShellState.DB_DIRECTORY_PROPERTY_NAME)); - System.out.println( - String.format( - "\t%s%s\t%s", - cmdName, - command.getInvocation() == null ? "" : (' ' + command.getInvocation()), - command.getInfo())); + for (Entry> cmdEntry : commands.entrySet()) { + String cmdName = cmdEntry.getKey(); + Command command = cmdEntry.getValue(); - } - } + state.getOutputStream().println(command.buildHelpLine(cmdName)); + } + } - @Override - public void executeSafely(SingleDatabaseShellState state, String[] args) throws DatabaseIOException { - // not used - } - }; + @Override + public void executeSafely(SingleDatabaseShellState state, String[] args) + throws DatabaseIOException { + // not used + } + }; public static final Command LIST = - new AbstractCommand(null, "prints all keys stored in the map", 1) { + new AbstractCommand(null, "prints all keys stored in the map", 1) { @Override public void executeSafely(SingleDatabaseShellState state, String[] args) throws NoActiveTableException { @@ -177,10 +184,10 @@ public void executeSafely(SingleDatabaseShellState state, String[] args) comma = true; } - System.out.println(sb); + state.getOutputStream().println(sb); } }; - public static final Command PUT = new AbstractCommand( + public static final Command PUT = new AbstractCommand( " [ {boolean|number|string|null}... ]", "assigns new storeable to the key", 3, @@ -198,16 +205,16 @@ public void executeSafely(SingleDatabaseShellState state, String[] args) Storeable oldValue = state.getActiveTable().put(key, value); if (oldValue == null) { - System.out.println("new"); + state.getOutputStream().println("new"); } else { String oldValueStr = state.getProvider().serialize(table, oldValue); - System.out.println("overwrite"); - System.out.println("old " + oldValueStr); + state.getOutputStream().println("overwrite"); + state.getOutputStream().println("old " + oldValueStr); } } }; public static final Command REMOVE = - new AbstractCommand("", "removes value by the key", 2) { + new AbstractCommand("", "removes value by the key", 2) { @Override public void executeSafely(SingleDatabaseShellState state, String[] args) throws DatabaseIOException, NoActiveTableException { @@ -218,27 +225,28 @@ public void executeSafely(SingleDatabaseShellState state, String[] args) if (oldValue == null) { throw new DatabaseIOException("not found"); } else { - System.out.println("removed"); + state.getOutputStream().println("removed"); } } }; - public static final Command SHOW = new AbstractCommand( - "tables", "prints info on all tables assigned to the working database", 2) { - @Override - public void executeSafely(SingleDatabaseShellState state, String[] args) - throws IllegalArgumentException { - switch (args[1]) { - case "tables": { - state.getActiveDatabase().showTables(); - break; - } - default: { - throw new IllegalArgumentException("show: unexpected option: " + args[1]); - } - } - } - }; - public static final Command USE = new AbstractCommand( + public static final Command SHOW = + new AbstractCommand( + "tables", "prints info on all tables assigned to the working database", 2) { + @Override + public void executeSafely(SingleDatabaseShellState state, String[] args) + throws IllegalArgumentException { + switch (args[1]) { + case "tables": { + state.getActiveDatabase().showTables(); + break; + } + default: { + throw new IllegalArgumentException("show: unexpected option: " + args[1]); + } + } + } + }; + public static final Command USE = new AbstractCommand( "", "saves all changes made to the current table (if present) and makes table" + " with the given name the current one", @@ -247,7 +255,7 @@ public void executeSafely(SingleDatabaseShellState state, String[] args) public void executeSafely(final SingleDatabaseShellState state, final String[] args) throws IOException { state.getActiveDatabase().useTable(args[1]); - System.out.println("using " + args[1]); + state.getOutputStream().println("using " + args[1]); } }; private static final Commands INSTANCE = new Commands(); diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/DatabaseFactory.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/DatabaseFactory.java new file mode 100644 index 000000000..c9594f78c --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/DatabaseFactory.java @@ -0,0 +1,35 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell; + +import ru.fizteh.fivt.storage.structured.TableProvider; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.DBTableProviderFactory; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.Database; + +import java.io.Closeable; +import java.io.PrintStream; + +public final class DatabaseFactory implements Closeable { + private final DBTableProviderFactory factory; + private final TableProvider provider; + private final String databasePath; + + public DatabaseFactory() throws Exception { + factory = new DBTableProviderFactory(); + databasePath = System.getProperty(SingleDatabaseShellState.DB_DIRECTORY_PROPERTY_NAME); + provider = factory.create(databasePath); + } + + public Database obtainDatabase(PrintStream outputStream) { + return new Database(provider, databasePath, outputStream); + } + + @Override + public void close() { + factory.close(); + } + + @Override + protected void finalize() throws Throwable { + super.finalize(); + close(); + } +} 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 e105c25fb..cc01d4aad 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/Main.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/Main.java @@ -1,14 +1,25 @@ package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.Database; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.TerminalException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet.ServerState; class Main { - //java -Dfizteh.db.dir=/home/phoenix/test/DB ru.fizteh.fivt.students.fedorov_andrew - // .databaselibrary.shell.Main + //java -Dfizteh.db.dir=/home/phoenix/test/DB ru.fizteh.fivt.students.fedorov_andrew.databaselibrary + // .shell.Main + + private static final String PATH_PROPERTY = "fizteh.db.dir"; public static void main(String[] args) { - try { - Shell shell = new Shell<>(new SingleDatabaseShellState()); + try (DatabaseFactory factory = new DatabaseFactory()) { + Shell shell = new Shell<>( + new ServerState( + () -> new SingleDatabaseShellState() { + @Override + protected Database obtainNewActiveDatabase() throws Exception { + return factory.obtainDatabase(getOutputStream()); + } + })); int exitCode; if (args.length == 0) { exitCode = shell.run(System.in); @@ -20,6 +31,9 @@ public static void main(String[] args) { } catch (TerminalException exc) { // Already handled. System.exit(1); + } catch (Exception exc) { + System.out.println(exc.getMessage()); + System.exit(1); } } } diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/Shell.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/Shell.java index 6c7701d44..6ad096588 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 @@ -9,6 +9,8 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.PrintStream; import java.text.ParseException; import java.util.Arrays; import java.util.LinkedList; @@ -36,22 +38,54 @@ public class Shell> { * Object encapsulating commands and data they work with. */ private final ShellStateImpl shellState; + private final PrintStream outputStream; private boolean valid = true; /** * Available commands for invocation. */ private Map> commandMap; - /** * If the user is entering commands or it is package mode. */ private boolean interactive; - public Shell(ShellStateImpl shellState) throws TerminalException { + public Shell(ShellStateImpl shellState, OutputStream outputStream) throws TerminalException { this.shellState = shellState; + this.outputStream = outputStream instanceof PrintStream + ? (PrintStream) outputStream + : new PrintStream(outputStream); init(); } + public Shell(ShellStateImpl shellState) throws TerminalException { + this(shellState, System.out); + } + + /** + * Handles an occurred exception. + * @param cause + * occurred exception. If null, an {@link Exception} is constructed via {@link + * Exception#Exception(String)}. + * @param message + * message that can be reported to user and is written to log. + * @param reportToUser + * if true, message is printed to errorStream. + */ + public static void handleError(String message, + Throwable cause, + boolean reportToUser, + PrintStream errorStream) throws TerminalException { + if (reportToUser) { + errorStream.println(message == null ? cause.getMessage() : message); + } + Log.log(Commands.class, cause, message); + if (cause == null) { + throw new TerminalException(message); + } else { + throw new TerminalException(message, cause); + } + } + /** * Splits command string into commands. * @param commandsStr @@ -101,6 +135,10 @@ public static List splitCommandsString(String commandsStr) throws Pars return commands; } + public PrintStream getOutputStream() { + return outputStream; + } + /** * Executes command in this shell * @param args @@ -116,7 +154,7 @@ private void execute(String[] args) throws TerminalException, ExitRequest { Command command = commandMap.get(args[0]); if (command == null) { - Utility.handleError(args[0] + ": command is missing", null, true); + handleError(args[0] + ": command is missing", null, true, getOutputStream()); } else { try { command.execute(shellState, args); @@ -124,7 +162,7 @@ private void execute(String[] args) throws TerminalException, ExitRequest { // If it is TerminalException, error report is already written. throw exc; } catch (Exception exc) { - Utility.handleError(args[0] + ": Method execution error", exc, true); + handleError(args[0] + ": Method execution error", exc, true, getOutputStream()); } } } @@ -138,7 +176,7 @@ private void init() throws TerminalException { try { shellState.init(this); } catch (Exception exc) { - Utility.handleError(exc.getMessage(), exc, true); + handleError(exc.getMessage(), exc, true, getOutputStream()); } commandMap = shellState.getCommands(); @@ -172,7 +210,7 @@ public int run(InputStream stream) throws TerminalException { try (BufferedReader reader = new BufferedReader( new InputStreamReader(stream), READ_BUFFER_SIZE)) { while (true) { - System.out.print(shellState.getGreetingString()); + outputStream.print(shellState.getGreetingString()); String str = reader.readLine(); // End of stream. @@ -191,7 +229,7 @@ public int run(InputStream stream) throws TerminalException { } } catch (IOException | ParseException exc) { exitRequested = true; - Utility.handleError("Error in input stream: " + exc.getMessage(), exc, true); + handleError("Error in input stream: " + exc.getMessage(), exc, true, getOutputStream()); // No need to cleanup - work has not been started. } catch (ExitRequest request) { exitRequested = true; @@ -236,7 +274,8 @@ public int run(String[] args) throws TerminalException { // Exception already handled. shellState.prepareToExit(1); } catch (ParseException exc) { - Utility.handleError("Cannot parse command arguments: " + exc.getMessage(), exc, true); + handleError( + "Cannot parse command arguments: " + exc.getMessage(), exc, true, getOutputStream()); } persistSafelyAndPrepareToExit(); } catch (ExitRequest request) { diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/ShellState.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/ShellState.java index 284122cfb..410ab39f9 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/ShellState.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/ShellState.java @@ -2,12 +2,19 @@ import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.ExitRequest; +import java.io.PrintStream; + /** * Base interface that encapsulates all data and commands to work with. * @param * Implementation of this interface */ public interface ShellState> extends CommandContainer { + /** + * Terminal output stream for printing messages. + */ + PrintStream getOutputStream(); + /** * Performs clean up after all work is done and the shell is going to exit. */ 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 774fc94e2..14126e4ee 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/SingleDatabaseShellState.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/SingleDatabaseShellState.java @@ -3,30 +3,23 @@ import ru.fizteh.fivt.storage.structured.Table; import ru.fizteh.fivt.storage.structured.TableProvider; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.Database; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.DatabaseIOException; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.ExitRequest; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.NoActiveTableException; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Log; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Utility; import java.io.IOException; -import java.nio.file.DirectoryStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; +import java.io.PrintStream; import java.util.Map; -import java.util.Objects; /** * This class represents actual task implementation: work from terminal with a database, whose * location in file system is given. */ -public class SingleDatabaseShellState implements ShellState { +public abstract class SingleDatabaseShellState extends BaseShellState { /** * Name of environment property; value stored there is database location. */ public static final String DB_DIRECTORY_PROPERTY_NAME = "fizteh.db.dir"; - /** * Our proxy command container. */ @@ -37,27 +30,9 @@ public class SingleDatabaseShellState implements ShellState host; - @Override - public void cleanup() { - // Delete empty files and directories inside tables' directories - Path dbDirectory = (getActiveDatabase() == null ? null : getActiveDatabase().getDbDirectory()); - - if (dbDirectory != null) { - try (DirectoryStream dirStream = Files.newDirectoryStream(dbDirectory)) { - for (Path tableDirectory : dirStream) { - Utility.removeEmptyFilesAndFolders(tableDirectory); - } - - Log.log(SingleDatabaseShellState.class, "Cleaned up successfully"); - } catch (IOException exc) { - Log.log(SingleDatabaseShellState.class, exc, "Failed to clean up"); - } - } + public PrintStream getOutputStream() { + return host.getOutputStream(); } @Override @@ -75,22 +50,17 @@ public String getGreetingString() { } @Override - public void init(Shell host) - throws IllegalArgumentException, DatabaseIOException { - Objects.requireNonNull(host, "Host shell must not be null"); + public void init(Shell host) throws Exception { + super.init(host); - if (this.host != null) { - throw new IllegalStateException("Initialization happened already"); - } - this.host = host; + activeDatabase = obtainNewActiveDatabase(); + } - //establishing database - String dbDirPath = System.getProperty(DB_DIRECTORY_PROPERTY_NAME); - if (dbDirPath == null) { - throw new IllegalArgumentException("Please mention database directory"); - } + protected abstract Database obtainNewActiveDatabase() throws Exception; + + @Override + public void cleanup() { - activeDatabase = new Database(Paths.get(dbDirPath)); } @Override @@ -102,11 +72,6 @@ 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/shell/SingleFactoryDatabaseShellState.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/SingleFactoryDatabaseShellState.java new file mode 100644 index 000000000..b5f92608b --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/SingleFactoryDatabaseShellState.java @@ -0,0 +1,30 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell; + +import ru.fizteh.fivt.storage.structured.TableProvider; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.DBTableProviderFactory; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.Database; + +public class SingleFactoryDatabaseShellState extends SingleDatabaseShellState { + + private final DBTableProviderFactory factory; + + public SingleFactoryDatabaseShellState() { + this.factory = new DBTableProviderFactory(); + } + + @Override + protected Database obtainNewActiveDatabase() throws Exception { + String dbPath = System.getProperty(DB_DIRECTORY_PROPERTY_NAME); + if (dbPath == null) { + throw new IllegalStateException("Please mention database directory"); + } + TableProvider provider = factory.create(dbPath); + return new Database(provider, dbPath, getOutputStream()); + } + + @Override + public void cleanup() { + super.cleanup(); + factory.close(); + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/AccurateExceptionHandler.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/AccurateExceptionHandler.java index e602a3bc4..c2c9a8cf2 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/AccurateExceptionHandler.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/AccurateExceptionHandler.java @@ -1,7 +1,5 @@ package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.TerminalException; - /** * Interface for exception handlers that must intercept at least all checked exceptions. * @param @@ -11,5 +9,5 @@ */ @FunctionalInterface public interface AccurateExceptionHandler { - void handleException(Exception exc, T additionalData) throws TerminalException; + void handleException(Exception exc, T additionalData); } diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/ConvenientCollection.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/ConvenientCollection.java index 64b490865..a6bd11f81 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/ConvenientCollection.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/ConvenientCollection.java @@ -9,7 +9,7 @@ /** * Usual collection over the given base collection with extension {@link ru.fizteh.fivt.students - * .fedorov_andrew.databaselibrary.support.ConvenientCollection#addNext + * .fedorov_andrew.databaselibrary.support.ConvenientCollection#chainAdd * (Object)}. */ public class ConvenientCollection implements Collection { @@ -19,7 +19,7 @@ public ConvenientCollection(Collection base) { this.collection = base; } - public ConvenientCollection addNext(T element) { + public ConvenientCollection chainAdd(T element) { collection.add(element); return this; } diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/ConvenientMap.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/ConvenientMap.java index 0240c6248..874617cd8 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/ConvenientMap.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/ConvenientMap.java @@ -9,7 +9,7 @@ /** * Usual map extended with method {@link ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support - * .ConvenientMap#putNext(Object, + * .ConvenientMap#chainPut(Object, * Object)}. */ public class ConvenientMap implements Map { @@ -19,7 +19,7 @@ public ConvenientMap(Map baseMap) { this.baseMap = baseMap; } - public ConvenientMap putNext(K key, V value) { + public ConvenientMap chainPut(K key, V value) { baseMap.put(key, value); return this; } diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/Log.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/Log.java index 2f88a8289..808da63be 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/Log.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/Log.java @@ -19,7 +19,7 @@ public class Log { /** * If logging is disabled, no messages are output */ - private static boolean enableLogging = false; + private static boolean enableLogging = true; /** * Writer to the log file. */ @@ -60,6 +60,10 @@ public static synchronized void log(Class logger, String message) { log(logger, null, message); } + public static synchronized void log(Class logger, Throwable throwable) { + log(logger, throwable, null); + } + public static synchronized void log(Class logger, Throwable throwable, String message) { if (enableLogging) { if (writer == null) { diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/LoggingProxyFactoryBase.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/LoggingProxyFactoryBase.java new file mode 100644 index 000000000..58a68b160 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/LoggingProxyFactoryBase.java @@ -0,0 +1,140 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support; + +import ru.fizteh.fivt.proxy.LoggingProxyFactory; + +import java.io.IOException; +import java.io.Writer; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +/** + * Base class that implements Logging proxy factory. When you extend it you have only to implement method + * that + * makes log message from given data. + */ +public abstract class LoggingProxyFactoryBase implements LoggingProxyFactory { + private volatile boolean loggingEnabled = true; + + /** + * Constructs log message from the given data. + * @param timestamp + * time when the method was invoked. + * @param invokedClass + * name of the class that contains the invoked method. + * @param invokedMethod + * name of the invoked method. + * @param arguments + * arguments given to the invoked method. + * @param thrown + * exception thrown by the invoked method. + * @param returnedValue + * value returned by the invoked method. Can be null. Is null, if method returns void. + * @param isVoid + * if the invoked method is void. + * @return message that is written to log. + */ + protected abstract String constructReport(long timestamp, + String invokedClass, + String invokedMethod, + Object[] arguments, + Throwable thrown, + Object returnedValue, + boolean isVoid); + + public boolean isLoggingEnabled() { + return loggingEnabled; + } + + public void setLoggingEnabled(boolean loggingEnabled) { + this.loggingEnabled = loggingEnabled; + } + + @Override + public Object wrap(Writer writer, Object implementation, Class interfaceClass) { + return Proxy.newProxyInstance( + implementation.getClass().getClassLoader(), + new Class[] {interfaceClass}, + new Handler(writer, implementation)); + } + + private class Handler implements InvocationHandler { + private final Writer writer; + private final Object wrappedObject; + + public Handler(Writer writer, Object wrappedObject) { + this.writer = writer; + this.wrappedObject = wrappedObject; + } + + @Override + public String toString() { + return wrappedObject.toString(); + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + boolean isObjectMethod; + + try { + Object.class.getMethod(method.getName(), method.getParameterTypes()); + isObjectMethod = true; + } catch (NoSuchMethodException exc) { + isObjectMethod = false; + } + + // We do not proxy methods of Object class. We call it on us. + if (isObjectMethod) { + try { + return method.invoke(this, args); + } catch (InvocationTargetException exc) { + throw exc.getTargetException(); + } + } + + long timestamp = System.currentTimeMillis(); + Object returnValue = null; + Throwable thrown = null; + + // We must know it before invocation. Simple reason: suppose the invoked method turns off logging. + boolean doWriteLog = isLoggingEnabled(); + + try { + boolean accessible = method.isAccessible(); + method.setAccessible(true); + returnValue = method.invoke(wrappedObject, args); + method.setAccessible(accessible); + } catch (InvocationTargetException exc) { + thrown = exc.getTargetException(); + } catch (IllegalAccessException | IllegalArgumentException exc) { + Log.log(LoggingProxyFactory.class, exc, "Error on proxy invocation"); + } finally { + if (doWriteLog) { + String report = constructReport( + timestamp, + wrappedObject.getClass().getSimpleName(), + method.getName(), + args, + thrown, + returnValue, + void.class.equals(method.getReturnType())); + + try { + writer.write(report); + writer.write(System.lineSeparator()); + writer.flush(); + } catch (IOException exc) { + Log.log(LoggingProxyFactory.class, exc, "Failed to write log report"); + } + } + } + + if (thrown != null) { + throw thrown; + } else { + return returnValue; + } + } + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/LoggingProxyFactoryJSON.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/LoggingProxyFactoryJSON.java new file mode 100644 index 000000000..f8b371361 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/LoggingProxyFactoryJSON.java @@ -0,0 +1,84 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support; + +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.json.JSONComplexObject; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.json.JSONField; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.json.JSONMaker; + +/** + * Extension which writes logs in JSON format using {@link ru.fizteh.fivt.students.fedorov_andrew + * .databaselibrary.json.JSONMaker} + */ +public class LoggingProxyFactoryJSON extends LoggingProxyFactoryBase { + + @Override + protected String constructReport(long timestamp, + String invokedClass, + String invokedMethod, + Object[] arguments, + Throwable thrown, + Object returnedValue, + boolean isVoid) { + Object report; + if (thrown != null) { + report = new LoggingReportWithThrown(timestamp, invokedClass, invokedMethod, arguments, thrown); + } else if (isVoid) { + report = new LoggingReport(timestamp, invokedClass, invokedMethod, arguments); + } else { + report = new LoggingReportWithReturnValue( + timestamp, invokedClass, invokedMethod, arguments, returnedValue); + } + return JSONMaker.makeJSON(report); + } + + @JSONComplexObject + private static class LoggingReport { + @JSONField + final long timestamp; + + @JSONField(name = "class") + final String invokeeClass; + + @JSONField(name = "method") + final String invokeeMethod; + + @JSONField + final Object[] arguments; + + public LoggingReport(long timestamp, String invokeeClass, String invokeeMethod, Object[] arguments) { + this.timestamp = timestamp; + this.invokeeClass = invokeeClass; + this.invokeeMethod = invokeeMethod; + this.arguments = arguments; + } + } + + @JSONComplexObject + private static class LoggingReportWithReturnValue extends LoggingReport { + @JSONField + final Object returnValue; + + public LoggingReportWithReturnValue(long timestamp, + String invokeeClass, + String invokeeMethod, + Object[] arguments, + Object returnValue) { + super(timestamp, invokeeClass, invokeeMethod, arguments); + this.returnValue = returnValue; + } + } + + @JSONComplexObject + private static class LoggingReportWithThrown extends LoggingReport { + @JSONField + final Throwable thrown; + + public LoggingReportWithThrown(long timestamp, + String invokeeClass, + String invokeeMethod, + Object[] arguments, + Throwable thrown) { + super(timestamp, invokeeClass, invokeeMethod, arguments); + this.thrown = thrown; + } + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/LoggingProxyFactoryXML.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/LoggingProxyFactoryXML.java new file mode 100644 index 000000000..fbe7a851d --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/LoggingProxyFactoryXML.java @@ -0,0 +1,82 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support; + +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.xml.XMLComplexObject; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.xml.XMLField; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.xml.XMLMaker; + +import java.util.Arrays; +import java.util.stream.Collectors; + +/** + * Extension which writes logs in XML format using {@link ru.fizteh.fivt.students.fedorov_andrew + * .databaselibrary.xml.XMLMaker}. + */ +public class LoggingProxyFactoryXML extends LoggingProxyFactoryBase { + + @Override + protected String constructReport(long timestamp, + String invokedClass, + String invokedMethod, + Object[] arguments, + Throwable thrown, + Object returnedValue, + boolean isVoid) { + Object report = + new LoggingReport(timestamp, invokedClass, invokedMethod, arguments, returnedValue, thrown); + return XMLMaker.makeXML(report, "invoke"); + } + + @XMLComplexObject(wrapper = true) + private static class Argument { + @XMLField + private final Object argument; + + public Argument(Object argument) { + this.argument = argument; + } + + public static Argument[] wrapObjects(Object... arguments) { + if (arguments == null) { + return null; + } + return Arrays.stream(arguments).map(Argument::new).collect(Collectors.toList()) + .toArray(new Argument[arguments.length]); + } + } + + @XMLComplexObject + private static class LoggingReport { + @XMLField(inline = true) + final long timestamp; + + @XMLField(name = "class", inline = true) + final String invokeeClass; + + @XMLField(name = "name", inline = true) + final String invokeeMethod; + + @XMLField(childName = "argument", nullPolicy = XMLField.NULLPOLICY_EMPTY_IF_NULL) + final Argument[] arguments; + + @XMLField(nullPolicy = XMLField.NULLPOLICY_IGNORE_IF_NULL) + final Throwable thrown; + + @XMLField(name = "return", nullPolicy = XMLField.NULLPOLICY_IGNORE_IF_NULL) + final Object returnValue; + + public LoggingReport(long timestamp, + String invokeeClass, + String invokeeMethod, + Object[] arguments, + Object returnValue, + Throwable thrown) { + this.timestamp = timestamp; + this.invokeeClass = invokeeClass; + this.invokeeMethod = invokeeMethod; + this.arguments = Argument.wrapObjects(arguments); + this.returnValue = returnValue; + this.thrown = thrown; + } + } + +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/Utility.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/Utility.java index aef753f42..ba39c9a8f 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/Utility.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/Utility.java @@ -1,9 +1,10 @@ package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.TerminalException; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Commands; import java.io.IOException; +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; import java.nio.file.DirectoryStream; import java.nio.file.FileVisitResult; import java.nio.file.FileVisitor; @@ -12,9 +13,12 @@ import java.nio.file.Paths; import java.nio.file.attribute.BasicFileAttributes; import java.text.ParseException; +import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -36,29 +40,6 @@ public static void checkAllTypesAreSupported(Collection> checkTypes, } } - /** - * Handles an occurred exception. - * @param cause - * occurred exception. If null, an {@link Exception} is constructed via {@link - * Exception#Exception(String)}. - * @param message - * message that can be reported to user and is written to log. - * @param reportToUser - * if true, message is printed to {@link System#err}. - */ - public static void handleError(String message, Throwable cause, boolean reportToUser) - throws TerminalException { - if (reportToUser) { - System.err.println(message == null ? cause.getMessage() : message); - } - Log.log(Commands.class, cause, message); - if (cause == null) { - throw new TerminalException(message); - } else { - throw new TerminalException(message, cause); - } - } - public static byte[] insertArray(byte[] source, int sourceOffset, int sourceSize, @@ -232,7 +213,7 @@ public static Map inverseMap(Map map) throws IllegalArgumentE * @param escapeSequence * Escape sequence. Quotes and this sequence occurrences will be prepended by escape sequence. * @return Endcoded string inside quotes. Returns null for null string. - * @see Utility#unquoteString(String, + * @see ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Utility#unquoteString(String, * String, String) */ public static String quoteString(String s, String quoteSequence, String escapeSequence) { @@ -301,6 +282,23 @@ public static int findClosingQuotes(String string, return -1; } + public static List getAllAnnotatedFields(Class searchableClass, + Class annoClass) { + List gatheredFields = new LinkedList<>(); + + while (searchableClass != Object.class) { + Arrays.stream(searchableClass.getDeclaredFields()).forEach( + (field) -> { + if (field.getAnnotation(annoClass) != null) { + gatheredFields.add(field); + } + }); + searchableClass = searchableClass.getSuperclass(); + } + + return gatheredFields; + } + /** * File visitor that deletes empty files and empty folders. * @author phoenix diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/ValidityController.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/ValidityController.java index 71953f070..6f9d55e51 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/ValidityController.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/ValidityController.java @@ -33,7 +33,7 @@ private void invalidate() { * 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 + * @throws IllegalStateException * if this object has been already invalidated. */ public UseLock use() throws IllegalStateException { @@ -49,7 +49,7 @@ public UseLock use() throws IllegalStateException { /** * Returns activated unique lock for the use of the object. After use object is invalidated. - * @throws java.lang.IllegalStateException + * @throws IllegalStateException * if this object has been already invalidated. */ public KillLock useAndKill() throws IllegalStateException { diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/Server.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/Server.java new file mode 100644 index 000000000..10d727ef4 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/Server.java @@ -0,0 +1,153 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet; + +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.TerminalException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.ShellState; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Log; + +import java.io.Closeable; +import java.io.IOException; +import java.io.PrintStream; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.function.Supplier; + +/** + * Server that listens to connections and creates communicator threads. + */ +public class Server implements Closeable { + private final PrintStream reportStream; + private final Set users; + private final Supplier shellStateSupplier; + private ServerSocket serverSocket; + private boolean deletingFromUsers = false; + + public Server(PrintStream reportStream, Supplier shellStateSupplier) { + this.reportStream = reportStream; + this.shellStateSupplier = shellStateSupplier; + users = new HashSet<>(); + } + + public PrintStream getReportStream() { + return reportStream; + } + + public ServerSocket getServerSocket() { + return serverSocket; + } + + /** + * Called by ServerCommunicator after it is closed. + */ + synchronized void onConnectionClosed(ServerCommunicator communicator) { + if (!deletingFromUsers) { + users.remove(communicator); + } + } + + public synchronized boolean isStarted() { + return serverSocket != null; + } + + public synchronized void startServer(int port) throws IOException, IllegalStateException { + if (serverSocket != null) { + throw new IllegalStateException("not started: already started"); + } + + try { + serverSocket = new ServerSocket(port); + } catch (IOException exc) { + throw new IOException("not started: " + exc.getMessage(), exc.getCause()); + } + + Thread serverThread = new Thread(this::run, this + ": server thread"); + serverThread.setDaemon(true); + serverThread.setPriority(Thread.MIN_PRIORITY); + serverThread.start(); + } + + public synchronized void stopServerIfStarted() throws IOException { + if (isStarted()) { + close(); + } + } + + public synchronized int stopServer() throws IOException, IllegalStateException { + checkStarted(); + int port = getServerSocket().getLocalPort(); + close(); + return port; + } + + public synchronized List listUsers() { + checkStarted(); + return new LinkedList<>(users); + } + + private void run() { + try { + while (!Thread.currentThread().isInterrupted()) { + ServerSocket serverSocketTemp; + + synchronized (this) { + serverSocketTemp = serverSocket; + } + + if (serverSocketTemp == null) { + return; + } + + Socket socket = serverSocketTemp.accept(); + ServerCommunicator communicator = new ServerCommunicator(socket, this, shellStateSupplier); + Thread communicatorThread = new Thread(communicator, "Communicator with " + communicator); + communicatorThread.setDaemon(true); + communicatorThread.start(); + + synchronized (this) { + users.add(communicator); + } + } + } catch (IOException exc) { + Log.log(Server.class, exc, "Error on listening to incoming connections"); + } catch (TerminalException exc) { + // Already handled. + } finally { + try { + close(); + } catch (IOException exc) { + Log.log(ServerState.class, exc, this + ": failed to close server socket"); + } + } + } + + private void checkStarted() { + if (serverSocket == null) { + throw new IllegalStateException("not started"); + } + } + + @Override + public synchronized void close() throws IOException { + if (serverSocket != null) { + deletingFromUsers = true; + for (ServerCommunicator communicator : users) { + try { + communicator.close(); + } catch (IOException exc) { + Log.log(Server.class, exc, "Error on closing socket: " + communicator); + } + } + users.clear(); + deletingFromUsers = false; + + try { + serverSocket.close(); + } finally { + serverSocket = null; + } + } + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/ServerCommands.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/ServerCommands.java new file mode 100644 index 000000000..cb20fb972 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/ServerCommands.java @@ -0,0 +1,143 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet; + +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.DatabaseIOException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.InvocationException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.NoActiveTableException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.TerminalException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.AbstractCommand; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Command; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.SimpleCommandContainer; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.SingleDatabaseShellState; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet.ServerState.User; + +import java.io.IOException; +import java.text.ParseException; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +/** + * Container for server commands: start, stop, listusers. + */ +public class ServerCommands extends SimpleCommandContainer { + public static final Command STOP = new AbstractCommand("", "stops server", 1) { + @Override + public void executeSafely(ServerState state, String[] args) throws + IllegalArgumentException, + NoActiveTableException, + IllegalStateException, + NullPointerException, + InvocationException, + ParseException, + IOException { + int port = state.stopServer(); + state.getOutputStream().println("stopped at " + port); + } + }; + public static final Command LISTUSERS = new AbstractCommand( + "", "prints list of ip addresses and ports of connected users", 1) { + @Override + public void executeSafely(ServerState state, String[] args) throws + IllegalArgumentException, + NoActiveTableException, + IllegalStateException, + NullPointerException, + InvocationException, + ParseException, + IOException { + List users = state.listUsers(); + + StringBuilder sb = new StringBuilder(); + for (User user : users) { + sb.append(user); + sb.append(System.lineSeparator()); + } + state.getOutputStream().print(sb.toString()); + } + }; + public static final Command EXIT = new AbstractCommand( + "", "stops server (if it is started) and closes the terminal", 1) { + @Override + public void execute(ServerState state, String[] args) throws TerminalException { + state.prepareToExit(0); + + // If all contracts are honoured, this line should not be reached. + throw new AssertionError("Exit request not thrown"); + } + + @Override + public void executeSafely(ServerState state, String[] args) throws + IllegalArgumentException, + NoActiveTableException, + IllegalStateException, + NullPointerException, + InvocationException, + ParseException, + IOException { + // Not used. + } + }; + public static final Command HELP = new AbstractCommand( + "", "prints out description of state commands", 1, Integer.MAX_VALUE) { + @Override + public void execute(ServerState state, String[] args) { + Map> commands = state.getCommands(); + + state.getOutputStream().println( + "You can start telnet database server ready for new connections!"); + + state.getOutputStream().println( + String.format( + "You can set database directory to work with using environment " + + "variable '%s'", SingleDatabaseShellState.DB_DIRECTORY_PROPERTY_NAME)); + + for (Entry> cmdEntry : commands.entrySet()) { + String cmdName = cmdEntry.getKey(); + Command command = cmdEntry.getValue(); + + state.getOutputStream().println(command.buildHelpLine(cmdName)); + } + } + + @Override + public void executeSafely(ServerState state, String[] args) throws DatabaseIOException { + // not used + } + }; + private static final int DEFAULT_PORT = 10001; + public static final Command START = new AbstractCommand( + "[port]", + "starts server at the specified port (or, if not specified, at " + DEFAULT_PORT + ")", + 1, + 2) { + @Override + public void executeSafely(ServerState state, String[] args) throws + IllegalArgumentException, + NoActiveTableException, + IllegalStateException, + InvocationException, + ParseException, + IOException { + int port; + if (args.length == 1) { + port = DEFAULT_PORT; + } else { + port = Integer.parseInt(args[1]); + } + + state.startServer(port); + state.getOutputStream().println("started at " + port); + } + }; + private static final ServerCommands INSTANCE = new ServerCommands(); + + /** + * Not for initializing. + */ + private ServerCommands() { + } + + public static ServerCommands obtainInstance() { + return INSTANCE; + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/ServerCommunicator.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/ServerCommunicator.java new file mode 100644 index 000000000..05cf32359 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/ServerCommunicator.java @@ -0,0 +1,85 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet; + +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.TerminalException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Shell; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.ShellState; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Log; + +import java.io.Closeable; +import java.io.IOException; +import java.io.PrintStream; +import java.net.Socket; +import java.util.function.Supplier; + +public class ServerCommunicator implements Runnable, Closeable { + private final Socket socket; + + private final Shell interpreter; + + private final PrintStream reportStream; + + private final Server server; + + public ServerCommunicator(Socket socket, Server server, Supplier shellStateSupplier) + throws IOException, TerminalException { + this.socket = socket; + this.server = server; + this.reportStream = server.getReportStream(); + this.interpreter = new Shell<>(shellStateSupplier.get(), socket.getOutputStream()); + } + + public Socket getSocket() { + return socket; + } + + @Override + public void run() { + try { + interpreter.run(socket.getInputStream()); + } catch (TerminalException exc) { + Log.log(ServerCommunicator.class, exc, "Interpreter run terminated"); + // Already handled. + } catch (IOException exc) { + reportStream.println(this + ": Error on obtaining socket input stream: " + exc.getMessage()); + } finally { + try { + close(); + } catch (IOException exc) { + Log.log( + ServerState.class, this + " failed to close connection after all commands execution"); + } + + } + } + + private void notifyConnectionClosed() { + server.onConnectionClosed(this); + } + + @Override + public synchronized void close() throws IOException { + if (!socket.isClosed()) { + socket.close(); + notifyConnectionClosed(); + } + } + + @Override + public int hashCode() { + return socket.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof ServerCommunicator) { + ServerCommunicator user = (ServerCommunicator) obj; + return socket.equals(user.socket); + } + return false; + } + + @Override + public String toString() { + return socket.toString(); + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/ServerState.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/ServerState.java new file mode 100644 index 000000000..5f4092c16 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/ServerState.java @@ -0,0 +1,110 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet; + +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.ExitRequest; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.BaseShellState; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Command; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Shell; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.ShellState; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Log; + +import java.io.IOException; +import java.net.InetAddress; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +public class ServerState extends BaseShellState { + private static final ServerCommands COMMANDS = ServerCommands.obtainInstance(); + private final Supplier clientShellStateSupplier; + private Server server; + + public ServerState(Supplier clientShellStateSupplier) { + this.clientShellStateSupplier = clientShellStateSupplier; + } + + public void startServer(int port) throws IOException, IllegalStateException { + checkInitialized(); + server.startServer(port); + } + + public int stopServer() throws IOException, NullPointerException { + checkInitialized(); + return server.stopServer(); + } + + public void stopServerIfStarted() throws IOException { + checkInitialized(); + server.stopServerIfStarted(); + } + + public int getServerPort() { + checkInitialized(); + return server.getServerSocket().getLocalPort(); + } + + public List listUsers() { + checkInitialized(); + return server.listUsers().stream().map( + (ServerCommunicator communicator) -> new User( + communicator.getSocket().getInetAddress(), communicator.getSocket().getPort())) + .collect(Collectors.toList()); + } + + @Override + public void cleanup() { + // Nothing to clean. + } + + @Override + public void persist() throws Exception { + // Nothing to persist. + } + + @Override + public void prepareToExit(int exitCode) throws ExitRequest { + try { + stopServerIfStarted(); + } catch (IOException exc) { + Log.log(ServerState.class, exc); + if (exitCode == 0) { + exitCode = -1; + } + } + throw new ExitRequest(exitCode); + } + + @Override + public void init(Shell host) throws Exception { + super.init(host); + server = new Server(host.getOutputStream(), clientShellStateSupplier); + } + + @Override + public Map> getCommands() { + return COMMANDS.getCommands(); + } + + public static class User { + private final InetAddress address; + private final int port; + + public User(InetAddress address, int port) { + this.address = address; + this.port = port; + } + + public InetAddress getAddress() { + return address; + } + + public int getPort() { + return port; + } + + @Override + public String toString() { + return address.getHostAddress() + ":" + port; + } + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/DatabaseShellTest.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/DatabaseShellTest.java index 9bd0fab1d..7c90c3c71 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/DatabaseShellTest.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/DatabaseShellTest.java @@ -5,9 +5,11 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.DatabaseIOException; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.TerminalException; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Shell; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.SingleDatabaseShellState; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.SingleFactoryDatabaseShellState; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test.support.MutatedSDSS; import java.io.FileOutputStream; @@ -27,6 +29,7 @@ */ @RunWith(JUnit4.class) public class DatabaseShellTest extends InterpreterTestBase { + @BeforeClass public static void globalPrepareDatabaseShellTest() { System.setProperty(SingleDatabaseShellState.DB_DIRECTORY_PROPERTY_NAME, DB_ROOT.toString()); @@ -39,7 +42,7 @@ private void createTableWithStringColumn(String table) throws TerminalException } @Test - public void testFailPersistOnExit() throws TerminalException { + public void testFailPersistOnExit() throws TerminalException, DatabaseIOException { MutatedSDSS state = new MutatedSDSS(0); interpreter = new Shell<>(state); @@ -76,7 +79,7 @@ public void testNotExistingCommand() throws TerminalException { @Override protected Shell constructInterpreter() throws TerminalException { - return new Shell<>(new SingleDatabaseShellState()); + return new Shell<>(new SingleFactoryDatabaseShellState()); } @After @@ -143,7 +146,7 @@ public void testCommit() throws TerminalException { } @Test - public void testCommitFail() throws TerminalException { + public void testCommitFail() throws TerminalException, DatabaseIOException { interpreter = new Shell<>(new MutatedSDSS(0)); runBatchExpectNonZero("create t1 (String)", "use t1", "put a [\"b\"]", "commit"); @@ -289,7 +292,7 @@ public void testShowUnexpectedOption() throws TerminalException { } @Test - public void testInitWithFarNotExistingDir() throws TerminalException { + public void testInitWithFarNotExistingDir() throws TerminalException, DatabaseIOException { System.setProperty( SingleDatabaseShellState.DB_DIRECTORY_PROPERTY_NAME, DB_ROOT.resolve("path1").resolve("path2").toString()); @@ -299,7 +302,7 @@ public void testInitWithFarNotExistingDir() throws TerminalException { "Database directory parent path does not exist or is not a " + "directory"); try { - interpreter = new Shell<>(new SingleDatabaseShellState()); + constructInterpreter(); } finally { System.setProperty( SingleDatabaseShellState.DB_DIRECTORY_PROPERTY_NAME, DB_ROOT.toString()); @@ -307,14 +310,14 @@ public void testInitWithFarNotExistingDir() throws TerminalException { } @Test - public void testInitWithNullDir() throws TerminalException { + public void testInitWithNullDir() throws TerminalException, DatabaseIOException { System.getProperties().remove(SingleDatabaseShellState.DB_DIRECTORY_PROPERTY_NAME); exception.expect(TerminalException.class); exception.expectMessage("Please mention database directory"); try { - interpreter = new Shell<>(new SingleDatabaseShellState()); + constructInterpreter(); } finally { System.setProperty( SingleDatabaseShellState.DB_DIRECTORY_PROPERTY_NAME, DB_ROOT.toString()); 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 1fd4b20fa..00548fde9 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/InterpreterTestBase.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/InterpreterTestBase.java @@ -6,6 +6,7 @@ import org.junit.BeforeClass; import org.junit.Rule; import org.junit.rules.ExpectedException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.DatabaseIOException; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.TerminalException; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Shell; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.ShellState; @@ -46,7 +47,7 @@ public static void globalCleanupInterpreterTestBase() { protected abstract Shell constructInterpreter() throws TerminalException; @Before - public void prepare() throws TerminalException { + public void prepare() throws TerminalException, DatabaseIOException { interpreter = constructInterpreter(); } @@ -113,12 +114,8 @@ int runBatch(String... commands) throws TerminalException { } } - try { - if (interpreter == null || !interpreter.isValid()) { - interpreter = constructInterpreter(); - } - } catch (TerminalException exc) { - throw new AssertionError(exc); + if (interpreter == null || !interpreter.isValid()) { + interpreter = constructInterpreter(); } return interpreter.run(commands); } diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/LoggingProxyFactoryJSONTest.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/LoggingProxyFactoryJSONTest.java new file mode 100644 index 000000000..79357f8f0 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/LoggingProxyFactoryJSONTest.java @@ -0,0 +1,137 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test; + +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import ru.fizteh.fivt.proxy.LoggingProxyFactory; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.json.JSONMaker; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.json.JSONParsedObject; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.json.JSONParser; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.LoggingProxyFactoryJSON; + +import java.text.ParseException; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +@RunWith(JUnit4.class) +public class LoggingProxyFactoryJSONTest extends LoggingProxyFactoryTestBase { + + /** + * Matcher that always returns true. + */ + private static final Matcher EXISTS_MATCHER = new BaseMatcher() { + @Override + public boolean matches(Object item) { + return true; + } + + @Override + public void describeTo(Description description) { + description.appendText("exists"); + } + }; + /** + * Requires presence of some sections and forbids situation when 'thrown' and 'returnValue" exist both. + */ + private static final Matcher MIN_REQUIREMENTS = allOf( + new LogPieceMatcher(TIMESTAMP, EXISTS_MATCHER), + new LogPieceMatcher(CLASS, EXISTS_MATCHER), + new LogPieceMatcher(METHOD, EXISTS_MATCHER), + new LogPieceMatcher(ARGUMENTS, EXISTS_MATCHER), + not( + allOf( + new LogPieceMatcher(THROWN, EXISTS_MATCHER), + new LogPieceMatcher(RETURN_VALUE, EXISTS_MATCHER)))); + + private static Matcher makeRequirements(Matcher... restrictions) { + return allOf(MIN_REQUIREMENTS, allOf(restrictions)); + } + + @Override + protected LoggingProxyFactory obtainFreshFactory() { + return new LoggingProxyFactoryJSON(); + } + + @Test + public void testDoNothingJSON() throws ParseException { + wrapped.doNothing(); + JSONParsedObject parsed = JSONParser.parseJSON(getOutput()); + assertThat(parsed, makeRequirements()); + } + + @Test + public void testBoolMethodJSON() throws ParseException { + wrapped.boolMethod(1, null); + JSONParsedObject parsedObject = JSONParser.parseJSON(getOutput()); + assertThat(parsedObject, makeRequirements()); + assertThat(parsedObject.getObject(ARGUMENTS).asArray(), equalTo(new Object[] {1L, null})); + assertThat(parsedObject, new LogPieceMatcher("returnValue", equalTo(Boolean.TRUE))); + } + + @Test + public void testThrowingMethodJSON() throws ParseException { + List> iterable = new LinkedList<>(); + iterable.add(Arrays.asList("1_1", "1_2", "1_3")); + iterable.add(Arrays.asList("2_1", "2_2")); + iterable.add(null); + + try { + wrapped.throwingMethod(iterable); + } catch (Exception exc) { + // Ignore it. + } + + JSONParsedObject parsedObject = JSONParser.parseJSON(getOutput()); + + assertThat(parsedObject, makeRequirements()); + assertEquals( + JSONMaker.makeJSON(new Object[] {iterable}), JSONMaker.makeJSON(parsedObject.get(ARGUMENTS))); + } + + @Test + public void testEqualsNotProxiedJSON() { + wrapped.equals(null); + assertEquals("", getOutput()); + } + + protected static class LogPieceMatcher extends BaseMatcher { + private final String fieldName; + private final Matcher valueMatcher; + + public LogPieceMatcher(String fieldName, Matcher valueMatcher) { + this.fieldName = fieldName; + this.valueMatcher = valueMatcher; + } + + @Override + public boolean matches(Object item) { + if (item instanceof JSONParsedObject) { + JSONParsedObject obj = (JSONParsedObject) item; + if (valueMatcher == null) { + return !obj.containsField(fieldName); + } else { + return obj.containsField(fieldName) && valueMatcher.matches(obj.get(fieldName)); + } + } + return false; + } + + @Override + public void describeTo(Description description) { + description.appendText("Field " + fieldName); + if (valueMatcher == null) { + description.appendText(" must not exist."); + } else { + description.appendText(" must match: "); + description.appendDescriptionOf(valueMatcher); + } + } + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/LoggingProxyFactoryTestBase.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/LoggingProxyFactoryTestBase.java new file mode 100644 index 000000000..098cd9b35 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/LoggingProxyFactoryTestBase.java @@ -0,0 +1,49 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test; + +import org.junit.Before; +import ru.fizteh.fivt.proxy.LoggingProxyFactory; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test.support.BAOSDuplicator; + +import java.io.OutputStreamWriter; +import java.io.Writer; + +public abstract class LoggingProxyFactoryTestBase { + protected static final String TIMESTAMP = "timestamp"; + protected static final String CLASS = "class"; + protected static final String METHOD = "method"; + protected static final String ARGUMENTS = "arguments"; + protected static final String THROWN = "thrown"; + protected static final String RETURN_VALUE = "returnValue"; + protected final BAOSDuplicator out = new BAOSDuplicator(System.out); + protected final Writer writer = new OutputStreamWriter(out); + protected LoggingProxyFactory factory; + protected TestFace wrapped; + + protected String getOutput() { + return out.toString(); + } + + @Before + public void prepare() { + out.reset(); + factory = obtainFreshFactory(); + wrapped = (TestFace) factory.wrap(writer, new TestFaceImpl(), TestFace.class); + } + + protected abstract LoggingProxyFactory obtainFreshFactory(); + + protected interface TestFace { + default void doNothing() { + } + + default boolean boolMethod(int a, Integer b) { + return true; + } + + default String throwingMethod(Iterable iterable) throws Exception { + throw new Exception(); + } + } + + protected static class TestFaceImpl implements TestFace {} +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/LoggingProxyFactoryXMLTest.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/LoggingProxyFactoryXMLTest.java new file mode 100644 index 000000000..b72b909a2 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/LoggingProxyFactoryXMLTest.java @@ -0,0 +1,83 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test; + +import org.junit.Test; +import ru.fizteh.fivt.proxy.LoggingProxyFactory; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.LoggingProxyFactoryXML; + +import javax.xml.stream.XMLEventReader; +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamException; +import java.io.StringReader; +import java.text.ParseException; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +public class LoggingProxyFactoryXMLTest extends LoggingProxyFactoryTestBase { + private static final String NEW_LINE = System.lineSeparator(); + + @Override + protected LoggingProxyFactory obtainFreshFactory() { + return new LoggingProxyFactoryXML(); + } + + @Test + public void testDoNothingJSON() throws ParseException, XMLStreamException { + wrapped.doNothing(); + XMLInputFactory xmlInputFactory = XMLInputFactory.newFactory(); + XMLEventReader reader = xmlInputFactory.createXMLEventReader( + new StringReader(getOutput())); + + reader.nextTag(); + + assertThat( + getOutput(), allOf( + startsWith("" + + NEW_LINE))); + } + + @Test + public void testBoolMethodJSON() throws ParseException { + wrapped.boolMethod(1, null); + assertThat( + getOutput(), allOf( + startsWith("1true" + NEW_LINE))); + } + + @Test + public void testThrowingMethodJSON() throws ParseException { + List> iterable = new LinkedList<>(); + iterable.add(Arrays.asList("1_1", "1_2", "1_3")); + iterable.add(Arrays.asList("2_1", "2_2")); + iterable.add(null); + + try { + wrapped.throwingMethod(iterable); + } catch (Exception exc) { + // Ignore it. + } + assertThat( + getOutput(), allOf( + startsWith( + "1_11_21_32_12_2java.lang" + + ".Exception" + NEW_LINE))); + } + + @Test + public void testEqualsNotProxiedJSON() { + wrapped.equals(null); + assertEquals("", getOutput()); + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/TableProviderTest.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/TableProviderTest.java index 0f80b9a8e..82e10fb62 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 @@ -64,9 +64,6 @@ public static void globalCleanup() throws Exception { @Before public void prepareProvider() throws Exception { - if (provider != null && provider instanceof AutoCloseable) { - ((AutoCloseable) provider).close(); - } provider = factory.create(DB_ROOT.toString()); } @@ -186,17 +183,6 @@ public void testDeserialize() throws IOException, ParseException { provider.deserialize(table, "3, 1, null"); } - @Test - public void testDeserialize1() throws IOException, ParseException { - Table table = createTable(Long.class, Byte.class, String.class); - exception.expect(ParseException.class); - exception.expectMessage( - wrongTypeMatcherAndAllOf( - containsString("Empty elements are not allowed in json"))); - - provider.deserialize(table, "[,,]"); - } - @Test public void testDeserialize2() throws IOException, ParseException { Table table = createTable(Long.class, Byte.class, String.class); @@ -297,6 +283,8 @@ public void testObtainTableWithInvalidSignatureFile() throws Exception { public void testObtainTableWithExtraFile() throws Exception { String tableName = "table"; + cleanupProvider(); + Files.createDirectories(DB_ROOT.resolve(tableName).resolve("1.dir")); Files.createFile(DB_ROOT.resolve(tableName).resolve("1.dir").resolve("file.txt")); @@ -323,6 +311,8 @@ public void testObtainTableWithMissingSignatureFile() throws Exception { public void testObtainTableWithBadIDOfPartFile() throws Exception { String tableName = "table"; + cleanupProvider(); + Files.createDirectories(DB_ROOT.resolve(tableName).resolve("1.dir")); Files.createFile(DB_ROOT.resolve(tableName).resolve("1.dir").resolve("10000.dat")); @@ -464,7 +454,9 @@ public void testDeserializeStringsWithCommas() throws IOException, ParseExceptio public void testObtainTableWithEmptySignatureFile() throws Exception { String tableName = "table"; - Files.createDirectory(DB_ROOT.resolve(tableName)); + cleanupProvider(); + + Files.createDirectories(DB_ROOT.resolve(tableName)); Files.createFile(DB_ROOT.resolve(tableName).resolve("signature.tsv")); prepareProvider(); 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 93e2c4824..48165ee6c 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/UtilityTest.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/UtilityTest.java @@ -140,7 +140,7 @@ public void testInverseMapWithTwoDuplicateValues() { exception.expectMessage("Source map contains at least two duplicate values"); Utility.inverseMap( - new ConvenientMap<>(new HashMap<>()).putNext(1, 2).putNext( + new ConvenientMap<>(new HashMap<>()).chainPut(1, 2).chainPut( 2, 2)); } diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/XMLTest.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/XMLTest.java new file mode 100644 index 000000000..957ecdadb --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/XMLTest.java @@ -0,0 +1,81 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.xml.XMLComplexObject; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.xml.XMLField; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.xml.XMLMaker; + +import static org.junit.Assert.*; + +@RunWith(JUnit4.class) +public class XMLTest { + @Test + public void testCyclicLinks() { + MyObject5 objA = new MyObject5(); + MyObject6 objB = new MyObject6(); + objB.a = objA; + objA.b = objB; + + assertEquals("cyclic", XMLMaker.makeXML(objA, "obj5")); + } + + @Test + public void testSimpleTag() { + assertEquals("1323", XMLMaker.makeXML(new MyObject1(), "obj")); + } + + @Test + public void testRenamedField() { + assertEquals("123", XMLMaker.makeXML(new MyObject2(), "obj")); + } + + @Test + public void testInlineAttribute() { + assertEquals("", XMLMaker.makeXML(new MyObject3(), "obj")); + } + + @Test + public void testArray() { + assertEquals( + "truefalseString", + XMLMaker.makeXML(new MyObject4(), "obj")); + } + + @XMLComplexObject + class MyObject1 { + @XMLField + private String field1 = "1323"; + } + + @XMLComplexObject + class MyObject2 { + @XMLField(name = "value") + private int field2 = 123; + } + + @XMLComplexObject + class MyObject3 { + @XMLField(inline = true) + private boolean tag = true; + } + + @XMLComplexObject + class MyObject4 { + @XMLField + private Object[] objects = new Object[] {"true", "false", "String"}; + } + + @XMLComplexObject + class MyObject5 { + @XMLField + MyObject6 b; + } + + @XMLComplexObject + class MyObject6 { + @XMLField + MyObject5 a; + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/AlternativeShellState.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/AlternativeShellState.java index 9bf24050a..074b7d0a0 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/AlternativeShellState.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/AlternativeShellState.java @@ -7,6 +7,7 @@ import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.ShellState; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.SimpleCommandContainer; +import java.io.PrintStream; import java.util.Arrays; import java.util.Map; @@ -66,12 +67,9 @@ public void setMakeExceptionOnInit(boolean makeExceptionOnInit) { this.makeExceptionOnInit = makeExceptionOnInit; } - public boolean isMakeRuntimeExceptionOnCleanup() { - return makeRuntimeExceptionOnCleanup; - } - - public void setMakeRuntimeExceptionOnCleanup(boolean makeRuntimeExceptionOnCleanup) { - this.makeRuntimeExceptionOnCleanup = makeRuntimeExceptionOnCleanup; + @Override + public PrintStream getOutputStream() { + return System.out; } @Override @@ -105,6 +103,14 @@ public void prepareToExit(int exitCode) throws ExitRequest { throw new ExitRequest(exitCode); } + public boolean isMakeRuntimeExceptionOnCleanup() { + return makeRuntimeExceptionOnCleanup; + } + + public void setMakeRuntimeExceptionOnCleanup(boolean makeRuntimeExceptionOnCleanup) { + this.makeRuntimeExceptionOnCleanup = makeRuntimeExceptionOnCleanup; + } + @Override public Map> getCommands() { return commandMap; diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/MutatedDatabase.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/MutatedDatabase.java index 2f3864a36..937412b65 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/MutatedDatabase.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/MutatedDatabase.java @@ -1,16 +1,16 @@ package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test.support; +import ru.fizteh.fivt.storage.structured.TableProvider; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.Database; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.DatabaseIOException; import java.io.IOException; -import java.nio.file.Path; public class MutatedDatabase extends Database { private int commitCallsLeft; - public MutatedDatabase(Path dbDirectory, int commitCallsLeft) throws DatabaseIOException { - super(dbDirectory); + public MutatedDatabase(TableProvider provider, String dbDirectory, int commitCallsLeft) { + super(provider, dbDirectory, System.out); this.commitCallsLeft = commitCallsLeft; } diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/MutatedSDSS.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/MutatedSDSS.java index c2575414c..acba8b52c 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/MutatedSDSS.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/MutatedSDSS.java @@ -1,11 +1,14 @@ package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test.support; +import ru.fizteh.fivt.storage.structured.TableProvider; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.DBTableProviderFactory; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.Database; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.DatabaseIOException; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Shell; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.SingleDatabaseShellState; -import java.nio.file.Paths; +import java.io.IOException; +import java.io.PrintStream; /** * Mutated SingleDatabaseShellState that limits calls to {@link ru.fizteh.fivt.students.fedorov_andrew @@ -13,22 +16,40 @@ * method. */ public class MutatedSDSS extends SingleDatabaseShellState { + private final DBTableProviderFactory factory; + private final TableProvider provider; + private final String databasePath; private int commitCallsLeft; - private MutatedDatabase mutatedActiveDatabase; - public MutatedSDSS(int commitCallsLeft) { + public MutatedSDSS(int commitCallsLeft) throws DatabaseIOException { if (commitCallsLeft < 0) { throw new IllegalArgumentException("commitCallsLeft must be positive or 0"); } this.commitCallsLeft = commitCallsLeft; + factory = new DBTableProviderFactory(); + databasePath = System.getProperty(DB_DIRECTORY_PROPERTY_NAME); + provider = factory.create(databasePath); + } + + @Override + public PrintStream getOutputStream() { + return System.out; + } + + @Override + public void init(Shell host) throws IllegalArgumentException, IOException { + this.mutatedActiveDatabase = new MutatedDatabase(provider, databasePath, commitCallsLeft); + } + + @Override + protected Database obtainNewActiveDatabase() throws Exception { + return null; } @Override - public void init(Shell host) - throws IllegalArgumentException, DatabaseIOException { - this.mutatedActiveDatabase = new MutatedDatabase( - Paths.get(System.getProperty(DB_DIRECTORY_PROPERTY_NAME)), commitCallsLeft); + public void cleanup() { + factory.close(); } @Override diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/xml/XMLComplexObject.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/xml/XMLComplexObject.java new file mode 100644 index 000000000..198120fbc --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/xml/XMLComplexObject.java @@ -0,0 +1,22 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.xml; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marker for objects that contain some fields that must be xml-serialized. + */ +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@Target(ElementType.TYPE) +public @interface XMLComplexObject { + /** + * If true, object's single field is written directly skipping this object's serialization. + */ + boolean wrapper() default false; +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/xml/XMLField.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/xml/XMLField.java new file mode 100644 index 000000000..32fa3da7c --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/xml/XMLField.java @@ -0,0 +1,45 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.xml; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marker for fields that must be xml-serialized. + */ +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface XMLField { + String DEFAULT_NAME = ""; + + int NULLPOLICY_EMPTY_IF_NULL = 0; + int NULLPOLICY_IGNORE_IF_NULL = 1; + int NULLPOLICY_FULL_IF_NULL = 2; + + /** + * Used for iterables, arrays to specify element tag alternative to 'value'.
    + * If empty, default tag is applied. + */ + String childName() default DEFAULT_NAME; + + /** + * Specifies behaviour if element that is going to be serialized is null.
    + * @see #NULLPOLICY_EMPTY_IF_NULL + * @see #NULLPOLICY_FULL_IF_NULL + * @see #NULLPOLICY_IGNORE_IF_NULL + */ + int nullPolicy() default NULLPOLICY_FULL_IF_NULL; + + /** + * Name of tag/attribute of this element (replaces declared field's name, if not empty). + */ + String name() default DEFAULT_NAME; + + /** + * If true, the value is written as attribute inside host tag.
    + * Otherwise the value is written inside inner node.
    + * If this element is set true for an object that cannot be inlined, exception is raised. + */ + boolean inline() default false; +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/xml/XMLMaker.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/xml/XMLMaker.java new file mode 100644 index 000000000..5a9a17b88 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/xml/XMLMaker.java @@ -0,0 +1,219 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.xml; + +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Utility; + +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; +import java.io.StringWriter; +import java.lang.reflect.Array; +import java.lang.reflect.Field; +import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +public class XMLMaker { + private static final String LIST_TAG = "list"; + private static final String LIST_ELEMENT = "value"; + private static final String CYCLIC_LINK = "cyclic"; + private static final String NULL_ELEMENT = "null"; + + /** + * Writes some xml. + * @param writer + * where to write. + * @param object + * what to serialize. Can be null. + * @param tagName + * name of the tag. Can be null. + * @param inline + * if this is an attribute or not. + * @param childName + * name of each child node (only for iterables/arrays). Can be null or empty. + * @param identityMap + * map to determine cyclic links and handle them in proper way. + * @throws XMLStreamException + * @throws IllegalAccessException + */ + private static void writeXML(XMLStreamWriter writer, + Object object, + String tagName, + boolean inline, + String childName, + IdentityHashMap identityMap) + throws XMLStreamException, IllegalAccessException { + ConditionVerifier inlineUseVerifier = () -> { + if (inline) { + throw new IllegalArgumentException("This object cannot be inlined: " + object); + } + }; + + if (tagName != null && !inline) { + writer.writeStartElement(tagName); + } + + if (object == null) { + writer.writeEmptyElement(NULL_ELEMENT); + } else { + Class objClass = object.getClass(); + + if (identityMap.containsKey(object)) { + writer.writeCharacters(CYCLIC_LINK); + } else { + identityMap.put(object, Boolean.TRUE); + + if (objClass.getAnnotation(XMLComplexObject.class) != null) { + inlineUseVerifier.verify(); + + // Convenience trick. + FieldXMLAppender xmlAppender = (field, overrideName) -> { + XMLField fieldAnno = field.getAnnotation(XMLField.class); + + String fieldName = fieldAnno.name(); + boolean accessible = field.isAccessible(); + field.setAccessible(true); + + if (overrideName != null && overrideName.isEmpty()) { + if (fieldName.isEmpty()) { + overrideName = field.getName(); + } else { + overrideName = fieldName; + } + } + + Object fieldValue = field.get(object); + if (fieldValue != null + || fieldAnno.nullPolicy() == XMLField.NULLPOLICY_FULL_IF_NULL) { + writeXML( + writer, + field.get(object), + overrideName, + fieldAnno.inline(), + fieldAnno.childName(), + identityMap); + } else if (fieldAnno.nullPolicy() == XMLField.NULLPOLICY_EMPTY_IF_NULL) { + writer.writeEmptyElement(overrideName); + } + field.setAccessible(accessible); + }; + + List annotatedFields = Utility.getAllAnnotatedFields(objClass, XMLField.class); + + if (annotatedFields.isEmpty()) { + throw new IllegalArgumentException( + "Class " + objClass.getSimpleName() + + " does not have any XML fields and cannot be serialized"); + } else if (objClass.getAnnotation(XMLComplexObject.class).wrapper()) { + if (annotatedFields.size() != 1) { + throw new IllegalArgumentException( + "Class " + objClass.getSimpleName() + + " has more then one XML fields, thus it cannot be annotated as " + + "'wrapper'"); + } + xmlAppender.appendInfo(annotatedFields.get(0), tagName); + } else { + // Attributes first. + for (Field field : annotatedFields) { + XMLField fieldAnno = field.getAnnotation(XMLField.class); + if (fieldAnno.inline()) { + xmlAppender.appendInfo(field); + } + } + + // Tags then. + for (Field field : annotatedFields) { + XMLField fieldAnno = field.getAnnotation(XMLField.class); + if (!fieldAnno.inline()) { + xmlAppender.appendInfo(field); + } + } + } + } else if (object instanceof Iterable || objClass.isArray()) { + inlineUseVerifier.verify(); + + Iterator iter = (object instanceof Iterable + ? ((Iterable) object).iterator() + : new Iterator() { + final int size = Array.getLength(object); + int index = 0; + + @Override + public boolean hasNext() { + return index < size; + } + + @Override + public Object next() { + if (!hasNext()) { + throw new NoSuchElementException( + "No more elements in the array"); + } + return Array.get(object, index++); + } + }); + + if (tagName == null) { + writer.writeStartElement(LIST_TAG); + } + while (iter.hasNext()) { + Object next = iter.next(); + writer.writeStartElement( + childName == null || childName.isEmpty() ? LIST_ELEMENT : childName); + writeXML(writer, next, null, false, null, identityMap); + writer.writeEndElement(); + } + if (tagName == null) { + writer.writeEndElement(); + } + } else { + String value = object.toString(); + if (inline) { + writer.writeAttribute(tagName, value); + } else { + writer.writeCharacters(value); + } + } + + identityMap.remove(object); + } + } + + if (tagName != null && !inline) { + writer.writeEndElement(); + } + } + + public static String makeXML(Object object, String tagName) { + try { + XMLOutputFactory factory = XMLOutputFactory.newFactory(); + StringWriter stringWriter = new StringWriter(); + XMLStreamWriter xmlWriter = factory.createXMLStreamWriter(stringWriter); + + writeXML(xmlWriter, object, tagName, false, null, new IdentityHashMap<>()); + + return stringWriter.toString(); + } catch (IllegalAccessException | XMLStreamException exc) { + throw new RuntimeException(exc); + } + } + + /** + * Convenience structure used in method {@link #writeXML(javax.xml.stream.XMLStreamWriter, Object, + * String, + * boolean, String, java.util.IdentityHashMap)}. + */ + @FunctionalInterface + private interface FieldXMLAppender { + void appendInfo(Field field, String overrideName) throws IllegalAccessException, XMLStreamException; + + default void appendInfo(Field field) throws IllegalAccessException, XMLStreamException { + appendInfo(field, ""); + } + } + + @FunctionalInterface + interface ConditionVerifier { + void verify() throws RuntimeException; + } +} From 1fe0e9b2dc0c7c1425c99f8df8c98e6a8f4ce7aa Mon Sep 17 00:00:00 2001 From: AndreosGhost Date: Wed, 17 Dec 2014 22:25:41 +0300 Subject: [PATCH 06/14] Travis --- .../databaselibrary/support/AccurateExceptionHandler.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/AccurateExceptionHandler.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/AccurateExceptionHandler.java index c2c9a8cf2..e602a3bc4 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/AccurateExceptionHandler.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/AccurateExceptionHandler.java @@ -1,5 +1,7 @@ package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.TerminalException; + /** * Interface for exception handlers that must intercept at least all checked exceptions. * @param @@ -9,5 +11,5 @@ */ @FunctionalInterface public interface AccurateExceptionHandler { - void handleException(Exception exc, T additionalData); + void handleException(Exception exc, T additionalData) throws TerminalException; } From e08fdb6120b9fd85808f2085083b6108bbfcb99e Mon Sep 17 00:00:00 2001 From: AndreosGhost Date: Sat, 20 Dec 2014 00:02:22 +0300 Subject: [PATCH 07/14] Fixed some bugs and implemented client API. Shell now supports both running server & client. --- .../db/AutoCloseableTableProviderFactory.java | 7 + .../databaselibrary/db/DBTableProvider.java | 4 +- .../db/DBTableProviderFactory.java | 7 +- .../databaselibrary/db/Database.java | 40 +-- .../databaselibrary/db/StoreableImpl.java | 3 +- .../db/StoreableTableImpl.java | 10 +- .../db/remote/IRemoteTable.java | 104 +++++++ .../db/remote/IRemoteTableProvider.java | 117 ++++++++ .../remote/IRemoteTableProviderFactory.java | 19 ++ .../db/remote/RemoteDatabaseStorage.java | 32 +++ .../db/remote/RemoteTableImpl.java | 73 +++++ .../RemoteTableProviderFactoryImpl.java | 110 ++++++++ .../db/remote/RemoteTableProviderImpl.java | 71 +++++ .../db/remote/RemoteTableProviderStub.java | 102 +++++++ .../db/remote/RemoteTableStub.java | 121 ++++++++ .../ExecutionNotPermittedException.java | 25 ++ .../exception/ExitRequest.java | 2 + .../exception/InvalidatedObjectException.java | 22 ++ .../databaselibrary/json/JSONParser.java | 17 +- .../shell/AbstractCommand.java | 76 +++-- .../databaselibrary/shell/BaseShellState.java | 5 + .../databaselibrary/shell/Command.java | 9 +- .../databaselibrary/shell/JoinedState.java | 194 +++++++++++++ .../databaselibrary/shell/Main.java | 27 +- .../databaselibrary/shell/Shell.java | 87 ++---- .../databaselibrary/shell/ShellState.java | 6 - .../shell/SimpleCommandContainer.java | 6 +- .../shell/SingleDBCommands.java | 260 ++++++++++++++++++ .../shell/SingleDatabaseShellState.java | 10 +- .../support/ValidityController.java | 12 +- .../telnet/ClientServerGeneralState.java | 122 ++++++++ .../telnet/client/ClientCommands.java | 116 ++++++++ .../telnet/client/ClientGeneralState.java | 119 ++++++++ .../telnet/client/ClientState.java | 80 ++++++ .../telnet/server/DBServerState.java | 172 ++++++++++++ .../databaselibrary/telnet/server/Server.java | 168 +++++++++++ .../telnet/server/ServerCommands.java | 141 ++++++++++ .../telnet/server/ServerCommunicator.java | 89 ++++++ .../test/ClientGeneralStateTest.java | 85 ++++++ .../test/DatabaseShellTest.java | 31 +-- .../databaselibrary/test/InterpreterTest.java | 9 - .../test/LoggingProxyFactoryXMLTest.java | 6 +- .../test/TableProviderTest.java | 4 +- .../databaselibrary/test/TableTest.java | 4 +- .../test/support/AlternativeShellState.java | 29 +- .../databaselibrary/xml/XMLMaker.java | 6 +- 46 files changed, 2555 insertions(+), 204 deletions(-) create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/AutoCloseableTableProviderFactory.java create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/IRemoteTable.java create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/IRemoteTableProvider.java create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/IRemoteTableProviderFactory.java create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteDatabaseStorage.java create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableImpl.java create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableProviderFactoryImpl.java create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableProviderImpl.java create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableProviderStub.java create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableStub.java create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/exception/ExecutionNotPermittedException.java create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/exception/InvalidatedObjectException.java create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/JoinedState.java create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/SingleDBCommands.java create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/ClientServerGeneralState.java create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/client/ClientCommands.java create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/client/ClientGeneralState.java create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/client/ClientState.java create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/server/DBServerState.java create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/server/Server.java create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/server/ServerCommands.java create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/server/ServerCommunicator.java create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/ClientGeneralStateTest.java diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/AutoCloseableTableProviderFactory.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/AutoCloseableTableProviderFactory.java new file mode 100644 index 000000000..a3f91d037 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/AutoCloseableTableProviderFactory.java @@ -0,0 +1,7 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db; + +import ru.fizteh.fivt.storage.structured.TableProviderFactory; + +import java.io.Closeable; + +public interface AutoCloseableTableProviderFactory extends TableProviderFactory, Closeable {} 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 cded1d74f..d29a04dab 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 @@ -316,7 +316,9 @@ public Storeable createFor(Table table, List values) if (table.getColumnsCount() != values.size()) { throw new IndexOutOfBoundsException( - "Wrong number of values given; expected: " + table.getColumnsCount() + ", actual: " + "Wrong number of values given; expected: " + + table.getColumnsCount() + + ", actual: " + values.size()); } 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 3874d21f8..6def964a7 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,7 +1,6 @@ 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.LoggingProxyFactoryJSON; @@ -20,7 +19,7 @@ import java.nio.file.Paths; import java.util.IdentityHashMap; -public final class DBTableProviderFactory implements TableProviderFactory, AutoCloseable { +public final class DBTableProviderFactory implements AutoCloseableTableProviderFactory { private static final LoggingProxyFactory LOGGING_PROXY_FACTORY = new LoggingProxyFactoryJSON(); private static final Writer LOG_WRITER; @@ -55,9 +54,7 @@ static T wrapImplementation(T implementation, Class interfaceClass) { public synchronized void close() { try (KillLock lock = validityController.useAndKill()) { providerClosedByMe = true; - for (AutoCloseableProvider provider : generatedProviders.keySet()) { - provider.close(); - } + generatedProviders.keySet().forEach(AutoCloseableProvider::close); generatedProviders.clear(); } finally { providerClosedByMe = false; 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 00aa23a7f..d253e90f3 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/Database.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/Database.java @@ -3,6 +3,7 @@ import ru.fizteh.fivt.storage.structured.Table; import ru.fizteh.fivt.storage.structured.TableProvider; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.DatabaseIOException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.InvalidatedObjectException; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.NoActiveTableException; import java.io.IOException; @@ -127,27 +128,32 @@ public void showTables() { * Name of table to use. */ public void useTable(String tableName) throws IOException, IllegalArgumentException { - if (activeTable != null) { - if (tableName.equals(activeTable.getName())) { - return; - } - - int uncommitted = activeTable.getNumberOfUncommittedChanges(); - if (uncommitted != 0) { - throw new DatabaseIOException(String.format("%d unsaved changes", uncommitted)); + try { + if (activeTable != null) { + if (tableName.equals(activeTable.getName())) { + return; + } + + int uncommitted = activeTable.getNumberOfUncommittedChanges(); + if (uncommitted != 0) { + throw new DatabaseIOException(String.format("%d unsaved changes", uncommitted)); + } } - } - Table oldActiveTable = activeTable; + Table oldActiveTable = activeTable; - try { - activeTable = provider.getTable(tableName); - if (activeTable == null) { - throw new IllegalArgumentException(tableName + " not exists"); + try { + activeTable = provider.getTable(tableName); + if (activeTable == null) { + throw new IllegalArgumentException(tableName + " not exists"); + } + } catch (Exception exc) { + activeTable = oldActiveTable; + throw exc; } - } catch (Exception exc) { - activeTable = oldActiveTable; - throw exc; + } catch (InvalidatedObjectException exc) { + activeTable = null; + useTable(tableName); } } } diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/StoreableImpl.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/StoreableImpl.java index 390975c7e..628184e29 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/StoreableImpl.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/StoreableImpl.java @@ -6,6 +6,7 @@ import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.json.JSONComplexObject; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.json.JSONField; +import java.io.Serializable; import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -16,7 +17,7 @@ * Not bound to any table. */ @JSONComplexObject(wrapper = true) -public final class StoreableImpl implements Storeable { +public final class StoreableImpl implements Storeable, Serializable { @JSONField private final Object[] values; 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 191b67e1b..58d3a7d11 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/StoreableTableImpl.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/StoreableTableImpl.java @@ -143,8 +143,11 @@ static AutoCloseableTable getTable(TableProvider provider, String msg = exc.getMessage(); String descriptionPart = msg.substring(msg.indexOf('('), msg.lastIndexOf(')')); throw new DatabaseIOException( - "wrong type (Invalid type description file for table " + store.getName() + ": " - + descriptionPart + ")"); + "wrong type (Invalid type description file for table " + + store.getName() + + ": " + + descriptionPart + + ")"); } else { throw new TableCorruptIOException( store.getName(), "Invalid type description file: " + exc.getMessage()); @@ -201,7 +204,8 @@ static void checkStoreableAppropriate(Table table, Storeable storeable) } // Optimization for checking column types. - if (!(storeable instanceof StoreableImpl && table instanceof StoreableTableImpl + 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++) { diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/IRemoteTable.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/IRemoteTable.java new file mode 100644 index 000000000..d34acde86 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/IRemoteTable.java @@ -0,0 +1,104 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.remote; + +import ru.fizteh.fivt.storage.structured.ColumnFormatException; +import ru.fizteh.fivt.storage.structured.Storeable; + +import java.io.IOException; +import java.rmi.Remote; +import java.rmi.RemoteException; +import java.util.List; + +interface IRemoteTable extends Remote { + /** + * Возвращает название таблицы или индекса. + * @return Название таблицы. + */ + String getName() throws RemoteException; + + /** + * Получает значение по указанному ключу. + * @param key + * Ключ для поиска значения. Не может быть null. + * Для индексов по не-строковым полям аргумент представляет собой сериализованное значение + * колонки. + * Его потребуется распарсить. + * @return Значение. Если не найдено, возвращает null. + * @throws IllegalArgumentException + * Если значение параметра key является null. + */ + Storeable get(String key) throws RemoteException; + + /** + * Устанавливает значение по указанному ключу. + * @param key + * Ключ для нового значения. Не может быть null. + * @param value + * Новое значение. Не может быть null. + * @return Значение, которое было записано по этому ключу ранее. Если ранее значения не было записано, + * возвращает null. + * @throws IllegalArgumentException + * Если значение параметров key или value является null. + * @throws ru.fizteh.fivt.storage.structured.ColumnFormatException + * - при попытке передать Storeable с колонками другого типа. + */ + Storeable put(String key, Storeable value) throws ColumnFormatException, RemoteException; + + /** + * Удаляет значение по указанному ключу. + * @param key + * Ключ для поиска значения. Не может быть null. + * @return Предыдущее значение. Если не найдено, возвращает null. + * @throws IllegalArgumentException + * Если значение параметра key является null. + */ + Storeable remove(String key) throws RemoteException; + + /** + * Возвращает количество ключей в таблице. Возвращает размер текущей версии, с учётом незафиксированных + * изменений. + * @return Количество ключей в таблице. + */ + int size() throws RemoteException; + + /** + * Выводит список ключей таблицы, с учётом незафиксированных изменений. + * @return Список ключей. + */ + List list() throws RemoteException; + + /** + * Выполняет фиксацию изменений. + * @return Число записанных изменений. + * @throws java.io.IOException + * если произошла ошибка ввода/вывода. Целостность таблицы не гарантируется. + */ + int commit() throws IOException; + + /** + * Выполняет откат изменений с момента последней фиксации. + * @return Число откаченных изменений. + */ + int rollback() throws RemoteException; + + /** + * Возвращает количество изменений, ожидающих фиксации. + * @return Количество изменений, ожидающих фиксации. + */ + int getNumberOfUncommittedChanges() throws RemoteException; + + /** + * Возвращает количество колонок в таблице. + * @return Количество колонок в таблице. + */ + int getColumnsCount() throws RemoteException; + + /** + * Возвращает тип значений в колонке. + * @param columnIndex + * Индекс колонки. Начинается с нуля. + * @return Класс, представляющий тип значения. + * @throws IndexOutOfBoundsException + * - неверный индекс колонки + */ + Class getColumnType(int columnIndex) throws IndexOutOfBoundsException, RemoteException; +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/IRemoteTableProvider.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/IRemoteTableProvider.java new file mode 100644 index 000000000..1a25e536c --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/IRemoteTableProvider.java @@ -0,0 +1,117 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.remote; + +import ru.fizteh.fivt.storage.structured.ColumnFormatException; +import ru.fizteh.fivt.storage.structured.Storeable; +import ru.fizteh.fivt.storage.structured.Table; + +import java.io.IOException; +import java.rmi.Remote; +import java.rmi.RemoteException; +import java.text.ParseException; +import java.util.List; + +interface IRemoteTableProvider extends Remote { + /** + * Возвращает таблицу с указанным названием. + * + * Последовательные вызовы метода с одинаковыми аргументами должны возвращать один и тот же объект + * таблицы, + * если он не был удален с помощью {@link #removeTable(String)}. + * @param name + * Название таблицы. + * @return Объект, представляющий таблицу. Если таблицы с указанным именем не существует, возвращает null. + * @throws IllegalArgumentException + * Если название таблицы null или имеет недопустимое значение. + */ + Table getTable(String name) throws RemoteException; + + /** + * Создаёт таблицу с указанным названием. + * Создает новую таблицу. Совершает необходимые дисковые операции. + * @param name + * Название таблицы. + * @param columnTypes + * Типы колонок таблицы. Не может быть пустой. + * @return Объект, представляющий таблицу. Если таблица с указанным именем существует, возвращает null. + * @throws IllegalArgumentException + * Если название таблицы null или имеет недопустимое значение. Если список типов + * колонок null или содержит недопустимые значения. + * @throws java.io.IOException + * При ошибках ввода/вывода. + */ + Table createTable(String name, List> columnTypes) throws IOException; + + /** + * Удаляет существующую таблицу с указанным названием. + * + * Объект удаленной таблицы, если был кем-то взят с помощью {@link #getTable(String)}, + * с этого момента должен бросать {@link IllegalStateException}. + * @param name + * Название таблицы. + * @throws IllegalArgumentException + * Если название таблицы null или имеет недопустимое значение. + * @throws IllegalStateException + * Если таблицы с указанным названием не существует. + * @throws java.io.IOException + * - при ошибках ввода/вывода. + */ + void removeTable(String name) throws IOException; + + /** + * Преобразовывает строку в объект {@link ru.fizteh.fivt.storage.structured.Storeable}, соответствующий + * структуре таблицы. + * @param table + * Таблица, которой должен принадлежать {@link ru.fizteh.fivt.storage.structured.Storeable}. + * @param value + * Строка, из которой нужно прочитать {@link ru.fizteh.fivt.storage.structured.Storeable}. + * @return Прочитанный {@link ru.fizteh.fivt.storage.structured.Storeable}. + * @throws java.text.ParseException + * - при каких-либо несоответстиях в прочитанных данных. + */ + Storeable deserialize(Table table, String value) throws RemoteException, ParseException; + + /** + * Преобразовывает объект {@link ru.fizteh.fivt.storage.structured.Storeable} в строку. + * @param table + * Таблица, которой должен принадлежать {@link ru.fizteh.fivt.storage.structured.Storeable}. + * @param value + * {@link ru.fizteh.fivt.storage.structured.Storeable}, который нужно записать. + * @return Строка с записанным значением. + * @throws ru.fizteh.fivt.storage.structured.ColumnFormatException + * При несоответствии типа в {@link ru.fizteh.fivt.storage.structured.Storeable} и типа колонки в + * таблице. + */ + String serialize(Table table, Storeable value) throws RemoteException, ColumnFormatException; + + /** + * Создает новый пустой {@link ru.fizteh.fivt.storage.structured.Storeable} для указанной таблицы. + * @param table + * Таблица, которой должен принадлежать {@link ru.fizteh.fivt.storage.structured.Storeable}. + * @return Пустой {@link ru.fizteh.fivt.storage.structured.Storeable}, нацеленный на использование с этой + * таблицей. + */ + Storeable createFor(Table table) throws RemoteException; + + /** + * Создает новый {@link ru.fizteh.fivt.storage.structured.Storeable} для указанной таблицы, подставляя + * туда переданные значения. + * @param table + * Таблица, которой должен принадлежать {@link ru.fizteh.fivt.storage.structured.Storeable}. + * @param values + * Список значений, которыми нужно проинициализировать поля Storeable. + * @return {@link ru.fizteh.fivt.storage.structured.Storeable}, проинициализированный переданными + * значениями. + * @throws ColumnFormatException + * При несоответствии типа переданного значения и колонки. + * @throws IndexOutOfBoundsException + * При несоответствии числа переданных значений и числа колонок. + */ + Storeable createFor(Table table, List values) + throws RemoteException, ColumnFormatException, IndexOutOfBoundsException; + + /** + * Возвращает имена существующих таблиц, которые могут быть получены с помощью {@link #getTable(String)}. + * @return Имена существующих таблиц. + */ + List getTableNames() throws RemoteException; +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/IRemoteTableProviderFactory.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/IRemoteTableProviderFactory.java new file mode 100644 index 000000000..0f3380fbd --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/IRemoteTableProviderFactory.java @@ -0,0 +1,19 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.remote; + +import ru.fizteh.fivt.storage.structured.RemoteTableProvider; +import ru.fizteh.fivt.storage.structured.TableProvider; + +import java.io.Closeable; +import java.io.IOException; +import java.rmi.Remote; +import java.rmi.registry.Registry; + +public interface IRemoteTableProviderFactory extends Remote, Closeable { + RemoteTableProvider obtainRemoteProvider() throws IOException; + + default TableProvider establishStorage(String localDatabaseRoot) throws IOException { + return establishStorage(localDatabaseRoot, Registry.REGISTRY_PORT); + } + + TableProvider establishStorage(String localDatabaseRoot, int port) throws IOException; +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteDatabaseStorage.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteDatabaseStorage.java new file mode 100644 index 000000000..2f727cd99 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteDatabaseStorage.java @@ -0,0 +1,32 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.remote; + +import ru.fizteh.fivt.storage.structured.RemoteTableProvider; +import ru.fizteh.fivt.storage.structured.RemoteTableProviderFactory; + +import java.io.IOException; +import java.rmi.NotBoundException; +import java.rmi.registry.LocateRegistry; +import java.rmi.registry.Registry; + +/** + * Class used to connect to a remote database storage and obtain providers. + */ +public class RemoteDatabaseStorage implements RemoteTableProviderFactory { + + public RemoteDatabaseStorage() { + + } + + @Override + public RemoteTableProvider connect(String hostname, int port) throws IOException { + Registry registry = LocateRegistry.getRegistry(hostname, port); + try { + IRemoteTableProviderFactory factory = (IRemoteTableProviderFactory) registry + .lookup(RemoteTableProviderFactoryImpl.FACTORY_NAME); + return factory.obtainRemoteProvider(); + } catch (NotBoundException exc) { + // This cannot happen. + throw new RuntimeException(exc); + } + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableImpl.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableImpl.java new file mode 100644 index 000000000..03f046a49 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableImpl.java @@ -0,0 +1,73 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.remote; + +import ru.fizteh.fivt.storage.structured.ColumnFormatException; +import ru.fizteh.fivt.storage.structured.Storeable; +import ru.fizteh.fivt.storage.structured.Table; + +import java.io.IOException; +import java.rmi.RemoteException; +import java.rmi.server.UnicastRemoteObject; +import java.util.List; + +class RemoteTableImpl extends UnicastRemoteObject implements IRemoteTable { + private final transient Table table; + + public RemoteTableImpl(Table table) throws RemoteException { + this.table = table; + } + + @Override + public String getName() throws RemoteException { + return table.getName(); + } + + @Override + public Storeable get(String key) throws RemoteException, IllegalStateException { + return table.get(key); + } + + @Override + public Storeable put(String key, Storeable value) throws ColumnFormatException, RemoteException { + return table.put(key, value); + } + + @Override + public Storeable remove(String key) throws RemoteException { + return table.remove(key); + } + + @Override + public int size() throws RemoteException { + return table.size(); + } + + @Override + public List list() throws RemoteException { + return table.list(); + } + + @Override + public int commit() throws IOException { + return table.commit(); + } + + @Override + public int rollback() throws RemoteException { + return table.rollback(); + } + + @Override + public int getNumberOfUncommittedChanges() throws RemoteException { + return table.getNumberOfUncommittedChanges(); + } + + @Override + public int getColumnsCount() throws RemoteException { + return table.getColumnsCount(); + } + + @Override + public Class getColumnType(int columnIndex) throws RemoteException, IndexOutOfBoundsException { + return table.getColumnType(columnIndex); + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableProviderFactoryImpl.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableProviderFactoryImpl.java new file mode 100644 index 000000000..cfbed8d99 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableProviderFactoryImpl.java @@ -0,0 +1,110 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.remote; + +import ru.fizteh.fivt.storage.structured.RemoteTableProvider; +import ru.fizteh.fivt.storage.structured.TableProvider; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.AutoCloseableTableProviderFactory; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.DBTableProviderFactory; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Log; + +import java.io.IOException; +import java.rmi.NotBoundException; +import java.rmi.RemoteException; +import java.rmi.registry.LocateRegistry; +import java.rmi.registry.Registry; +import java.rmi.server.UnicastRemoteObject; + +/** + * My remote factory. + */ +public class RemoteTableProviderFactoryImpl extends UnicastRemoteObject + implements IRemoteTableProviderFactory { + + static final String FACTORY_NAME = "/Database"; + + private Registry registry; + + private AutoCloseableTableProviderFactory factory; + + private TableProvider provider; + + private RemoteTableProvider remoteProvider; + + public RemoteTableProviderFactoryImpl() throws RemoteException { + } + + public TableProvider getProvider() { + return provider; + } + + private boolean isBound() { + return registry != null; + } + + private void requireBound() throws IllegalStateException { + if (!isBound()) { + throw new IllegalStateException("Factory is not established yet."); + } + } + + @Override + public void close() throws IOException { + if (!isBound()) { + return; + } + try { + registry.unbind(FACTORY_NAME); + factory.close(); + } catch (NotBoundException exc) { + throw new IllegalStateException("The factory has not been established"); + } finally { + factory = null; + provider = null; + registry = null; + } + } + + @Override + public RemoteTableProvider obtainRemoteProvider() throws IOException { + return remoteProvider; + } + + @Override + public TableProvider establishStorage(String localDatabaseRoot, int port) throws IOException { + if (isBound()) { + throw new IllegalStateException("Factory already established"); + } + + try { + factory = new DBTableProviderFactory(); + provider = factory.create(localDatabaseRoot); + + try { + registry = LocateRegistry.createRegistry(port); + } catch (RemoteException exc) { + registry = LocateRegistry.getRegistry(port); + } + registry.rebind(FACTORY_NAME, this); + + remoteProvider = new RemoteTableProviderStub(new RemoteTableProviderImpl(provider)); + + return provider; + } catch (Exception exc) { + Log.log(RemoteTableProviderFactoryImpl.class, exc, "Got exception on establishment"); + try { + if (factory != null) { + factory.close(); + } + factory = null; + provider = null; + registry = null; + } catch (Exception ignored) { + Log.log( + RemoteTableProviderFactoryImpl.class, + ignored, + "Failed to cleanup after getting exception on establishment"); + } finally { + throw exc; + } + } + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableProviderImpl.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableProviderImpl.java new file mode 100644 index 000000000..b5cc44297 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableProviderImpl.java @@ -0,0 +1,71 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.remote; + +import ru.fizteh.fivt.storage.structured.ColumnFormatException; +import ru.fizteh.fivt.storage.structured.Storeable; +import ru.fizteh.fivt.storage.structured.Table; +import ru.fizteh.fivt.storage.structured.TableProvider; + +import java.io.IOException; +import java.rmi.RemoteException; +import java.rmi.server.UnicastRemoteObject; +import java.text.ParseException; +import java.util.List; + +class RemoteTableProviderImpl extends UnicastRemoteObject implements IRemoteTableProvider { + private final TableProvider provider; + + public RemoteTableProviderImpl(TableProvider provider) throws RemoteException { + this.provider = provider; + } + + private Table wrapIntoRemote(Table table) throws RemoteException { + return table == null ? null : new RemoteTableStub(new RemoteTableImpl(table)); + } + + @Override + public Table getTable(String name) { + try { + Table table = provider.getTable(name); + return wrapIntoRemote(table); + } catch (RemoteException exc) { + throw new RuntimeException(exc.getMessage(), exc.getCause()); + } + } + + @Override + public Table createTable(String name, List> columnTypes) throws IOException { + Table table = provider.createTable(name, columnTypes); + return wrapIntoRemote(table); + } + + @Override + public void removeTable(String name) throws IOException { + provider.removeTable(name); + } + + @Override + public Storeable deserialize(Table table, String value) throws ParseException { + return provider.deserialize(table, value); + } + + @Override + public String serialize(Table table, Storeable value) throws ColumnFormatException { + return provider.serialize(table, value); + } + + @Override + public Storeable createFor(Table table) { + return provider.createFor(table); + } + + @Override + public Storeable createFor(Table table, List values) + throws ColumnFormatException, IndexOutOfBoundsException { + return provider.createFor(table, values); + } + + @Override + public List getTableNames() { + return provider.getTableNames(); + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableProviderStub.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableProviderStub.java new file mode 100644 index 000000000..e78fa3de8 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableProviderStub.java @@ -0,0 +1,102 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.remote; + +import ru.fizteh.fivt.storage.structured.ColumnFormatException; +import ru.fizteh.fivt.storage.structured.RemoteTableProvider; +import ru.fizteh.fivt.storage.structured.Storeable; +import ru.fizteh.fivt.storage.structured.Table; + +import java.io.IOException; +import java.io.Serializable; +import java.rmi.RemoteException; +import java.text.ParseException; +import java.util.List; + +/** + * Stub of remote table provider that is transferred from server to client.
    + * I created this stub because task API is not friendly and there are no RemoteExceptions. + */ +class RemoteTableProviderStub implements RemoteTableProvider, Serializable { + private final IRemoteTableProvider provider; + + public RemoteTableProviderStub(IRemoteTableProvider provider) { + this.provider = provider; + } + + @Override + public void close() throws IOException { + // I can't influence rmi connections -> do nothing. + } + + @Override + public Table getTable(String name) { + try { + return provider.getTable(name); + } catch (RemoteException exc) { + throw new RuntimeException(exc); + } + } + + @Override + public Table createTable(String name, List> columnTypes) throws IOException { + try { + return provider.createTable(name, columnTypes); + } catch (RemoteException exc) { + throw new RuntimeException(exc); + } + } + + @Override + public void removeTable(String name) throws IOException { + try { + provider.removeTable(name); + } catch (RemoteException exc) { + throw new RuntimeException(exc); + } + } + + @Override + public Storeable deserialize(Table table, String value) throws ParseException { + try { + return provider.deserialize(table, value); + } catch (RemoteException exc) { + throw new RuntimeException(exc); + } + } + + @Override + public String serialize(Table table, Storeable value) throws ColumnFormatException { + try { + return provider.serialize(table, value); + } catch (RemoteException exc) { + throw new RuntimeException(exc); + } + } + + @Override + public Storeable createFor(Table table) { + try { + return provider.createFor(table); + } catch (RemoteException exc) { + throw new RuntimeException(exc); + } + } + + @Override + public Storeable createFor(Table table, List values) + throws ColumnFormatException, IndexOutOfBoundsException { + try { + return provider.createFor(table, values); + } catch (RemoteException exc) { + throw new RuntimeException(exc); + } + } + + @Override + public List getTableNames() { + try { + return provider.getTableNames(); + } catch (RemoteException exc) { + throw new RuntimeException(exc); + } + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableStub.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableStub.java new file mode 100644 index 000000000..cec9dab5b --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableStub.java @@ -0,0 +1,121 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.remote; + +import ru.fizteh.fivt.storage.structured.ColumnFormatException; +import ru.fizteh.fivt.storage.structured.Storeable; +import ru.fizteh.fivt.storage.structured.Table; + +import java.io.IOException; +import java.io.Serializable; +import java.rmi.RemoteException; +import java.util.List; + +/** + * Stub of remote table that is transferred from server to client.
    + * I created this stub because task API is not friendly and there are no RemoteExceptions. + */ +class RemoteTableStub implements Table, Serializable { + private final IRemoteTable table; + + public RemoteTableStub(RemoteTableImpl table) { + this.table = table; + } + + @Override + public Storeable put(String key, Storeable value) throws ColumnFormatException { + try { + return table.put(key, value); + } catch (RemoteException exc) { + throw new RuntimeException(exc); + } + } + + @Override + public Storeable remove(String key) { + try { + return table.remove(key); + } catch (RemoteException exc) { + throw new RuntimeException(exc); + } + } + + @Override + public int size() { + try { + return table.size(); + } catch (RemoteException exc) { + throw new RuntimeException(exc); + } + } + + @Override + public List list() { + try { + return table.list(); + } catch (RemoteException exc) { + throw new RuntimeException(exc); + } + } + + @Override + public int commit() throws IOException { + try { + return table.commit(); + } catch (RemoteException exc) { + throw new RuntimeException(exc); + } + } + + @Override + public int rollback() { + try { + return table.rollback(); + } catch (RemoteException exc) { + throw new RuntimeException(exc); + } + } + + @Override + public int getNumberOfUncommittedChanges() { + try { + return table.getNumberOfUncommittedChanges(); + } catch (RemoteException exc) { + throw new RuntimeException(exc); + } + } + + @Override + public int getColumnsCount() { + try { + return table.getColumnsCount(); + } catch (RemoteException exc) { + throw new RuntimeException(exc); + } + } + + @Override + public Class getColumnType(int columnIndex) throws IndexOutOfBoundsException { + try { + return table.getColumnType(columnIndex); + } catch (RemoteException exc) { + throw new RuntimeException(exc); + } + } + + @Override + public String getName() { + try { + return table.getName(); + } catch (RemoteException exc) { + throw new RuntimeException(exc); + } + } + + @Override + public Storeable get(String key) { + try { + return table.get(key); + } catch (RemoteException exc) { + throw new RuntimeException(exc); + } + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/exception/ExecutionNotPermittedException.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/exception/ExecutionNotPermittedException.java new file mode 100644 index 000000000..5bf583b75 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/exception/ExecutionNotPermittedException.java @@ -0,0 +1,25 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception; + +public class ExecutionNotPermittedException extends Exception { + public ExecutionNotPermittedException(String message, + Throwable cause, + boolean enableSuppression, + boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } + + public ExecutionNotPermittedException() { + } + + public ExecutionNotPermittedException(String message) { + super(message); + } + + public ExecutionNotPermittedException(String message, Throwable cause) { + super(message, cause); + } + + public ExecutionNotPermittedException(Throwable cause) { + super(cause); + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/exception/ExitRequest.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/exception/ExitRequest.java index 7c24621d2..753c926fd 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/exception/ExitRequest.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/exception/ExitRequest.java @@ -4,6 +4,8 @@ * This exception describes exit request. Used in interpreter. */ public class ExitRequest extends RuntimeException { + public static final int NORMAL_TERMINATION_CODE = 0; + private final int code; public ExitRequest(int code) { diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/exception/InvalidatedObjectException.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/exception/InvalidatedObjectException.java new file mode 100644 index 000000000..b8cf6bb54 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/exception/InvalidatedObjectException.java @@ -0,0 +1,22 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception; + +/** + * This exception occurs when object can be no longer used because of {@link + * ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.ValidityController} control. + */ +public class InvalidatedObjectException extends IllegalStateException { + public InvalidatedObjectException() { + } + + public InvalidatedObjectException(String s) { + super(s); + } + + public InvalidatedObjectException(String message, Throwable cause) { + super(message, cause); + } + + public InvalidatedObjectException(Throwable cause) { + super(cause); + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/json/JSONParser.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/json/JSONParser.java index 1b4fe9ea3..42019e675 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/json/JSONParser.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/json/JSONParser.java @@ -230,8 +230,13 @@ public static JSONParsedObject parseJSON(final String json) throws ParseExceptio 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 " + "Quotes are expected in positions " + + nameOpeningQuotes.index + + ", " + + nameClosingQuotes.index + + ", but found " + + nameOpeningQuotes.type + + " and " + nameClosingQuotes.type, nameOpeningQuotes.index); } @@ -303,8 +308,12 @@ public static JSONParsedObject parseJSON(final String json) throws ParseExceptio 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); + "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; 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 1f7eebfef..3ab15723f 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/AbstractCommand.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/AbstractCommand.java @@ -1,14 +1,17 @@ package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.ExecutionNotPermittedException; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.InvocationException; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.NoActiveTableException; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.TerminalException; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.WrongArgsNumberException; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.AccurateExceptionHandler; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Log; import java.io.IOException; import java.io.PrintStream; import java.text.ParseException; +import java.util.Objects; import static ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Utility.*; @@ -17,14 +20,30 @@ * @author phoenix */ public abstract class AbstractCommand> implements Command { - private static final Class[] EXECUTE_SAFELY_THROWN_EXCEPTIONS = - obtainExceptionsThrownByExecuteSafely(); + private static final Class[] EXECUTE_SAFELY_THROWN_EXCEPTIONS; + + static { + Class[] exceptions = null; + + try { + exceptions = AbstractCommand.class.getDeclaredMethod( + "executeSafely", ShellState.class, String[].class).getExceptionTypes(); + } catch (Exception exc) { + Log.log(AbstractCommand.class, exc, "Failed to obtain exceptions thrown by executeSafely"); + throw new RuntimeException( + "Failed to obtain exceptions thrown by executeSafely: " + exc.getMessage(), + exc.getCause()); + } finally { + EXECUTE_SAFELY_THROWN_EXCEPTIONS = exceptions; + } + } + /** * Used for unsafe calls. Catches and handles all exceptions thrown by {@link * AbstractCommand#executeSafely * (SingleDatabaseShellState, String[]) } and {@link IllegalArgumentException }. */ - static final AccurateExceptionHandler DATABASE_ERROR_HANDLER = + public static final AccurateExceptionHandler DATABASE_ERROR_HANDLER = (Exception exc, PrintStream ps) -> { boolean found = false; Class actualType = exc.getClass(); @@ -47,6 +66,8 @@ public abstract class AbstractCommand> implement throw new RuntimeException("Unexpected exception", exc); } }; + + private final String name; private final String info; private final String invocationArgs; private final int minimalArgsCount; @@ -58,27 +79,30 @@ public abstract class AbstractCommand> implement * @param info * Short description of command. */ - public AbstractCommand(String invocationArgs, String info, int minimalArgsCount, int maximalArgsCount) { + public AbstractCommand(String name, + String invocationArgs, + String info, + int minimalArgsCount, + int maximalArgsCount) { + Objects.requireNonNull(name, "Name must not be null"); + + this.name = name; this.info = info; this.invocationArgs = invocationArgs; this.minimalArgsCount = minimalArgsCount; this.maximalArgsCount = maximalArgsCount; } - public AbstractCommand(String invocationArgs, String info, int expectedArgsCount) { - this(invocationArgs, info, expectedArgsCount, expectedArgsCount); + public AbstractCommand(String name, String invocationArgs, String info, int expectedArgsCount) { + this(name, invocationArgs, info, expectedArgsCount, expectedArgsCount); } - private static Class[] obtainExceptionsThrownByExecuteSafely() { - Class[] exceptions; + public int getMinimalArgsCount() { + return minimalArgsCount; + } - try { - exceptions = AbstractCommand.class.getMethod("executeSafely", ShellState.class, String[].class) - .getExceptionTypes(); - } catch (Exception exc) { - throw new RuntimeException("Failed to obtain exceptions thrown by executeSafely"); - } - return exceptions; + public int getMaximalArgsCount() { + return maximalArgsCount; } /** @@ -98,6 +122,11 @@ public void execute(final State state, final String[] args) throws TerminalExcep }, DATABASE_ERROR_HANDLER, state.getOutputStream()); } + @Override + public String getName() { + return name; + } + @Override public String getInfo() { return info; @@ -108,14 +137,15 @@ public String getInvocation() { return invocationArgs; } - public abstract void executeSafely(State state, String[] args) throws - IllegalArgumentException, - NoActiveTableException, - IllegalStateException, - NullPointerException, - InvocationException, - ParseException, - IOException; + protected abstract void executeSafely(State state, String[] args) throws + IllegalArgumentException, + NoActiveTableException, + IllegalStateException, + NullPointerException, + InvocationException, + ParseException, + IOException, + ExecutionNotPermittedException; void checkArgsNumber(String[] args, int minimal, int maximal) throws WrongArgsNumberException { if (args.length < minimal || args.length > maximal) { diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/BaseShellState.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/BaseShellState.java index bd615a3ee..47ac1b77d 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/BaseShellState.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/BaseShellState.java @@ -36,4 +36,9 @@ public void init(Shell host) throws Exception { this.host = host; } + @Override + protected void finalize() throws Throwable { + super.finalize(); + cleanup(); + } } diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/Command.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/Command.java index 80680ff76..4d64137ea 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/Command.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/Command.java @@ -9,6 +9,11 @@ public interface Command> { void execute(State state, String[] args) throws TerminalException; + /** + * Name this command can be invoked by. + */ + String getName(); + /** * Information text for the command. */ @@ -19,8 +24,8 @@ public interface Command> { */ String getInvocation(); - default String buildHelpLine(String commandName) { + default String buildHelpLine() { return String.format( - "\t%s%s\t%s", commandName, getInvocation() == null ? "" : (' ' + getInvocation()), getInfo()); + "\t%s%s\t%s", getName(), getInvocation() == null ? "" : (' ' + getInvocation()), getInfo()); } } diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/JoinedState.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/JoinedState.java new file mode 100644 index 000000000..a987b260a --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/JoinedState.java @@ -0,0 +1,194 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell; + +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.ExitRequest; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.TerminalException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.AccurateExceptionHandler; + +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public abstract class JoinedState> extends BaseShellState { + private Map> allCommands; + private int statesCount; + + private AccurateExceptionHandler exceptionHandler; + + public JoinedState(AccurateExceptionHandler exceptionHandler, + Map>... commandsMaps) { + this(exceptionHandler); + setAllCommands(commandsMaps); + } + + public JoinedState(AccurateExceptionHandler exceptionHandler) { + setExceptionHandler(exceptionHandler); + } + + public JoinedState(Map>... commandsMaps) { + setAllCommands(commandsMaps); + } + + public JoinedState() { + + } + + public AccurateExceptionHandler getExceptionHandler() { + return exceptionHandler; + } + + protected void setExceptionHandler(AccurateExceptionHandler exceptionHandler) { + this.exceptionHandler = exceptionHandler; + } + + protected final void setAllCommands(Map>... + commandsMaps) { + this.statesCount = commandsMaps.length; + Map> allCommandsMap = new HashMap<>(); + + int id = 0; + for (Map> commands : commandsMaps) { + int stateID = id; + commands.forEach( + (name, command) -> { + if (allCommandsMap.containsKey(name)) { + ((CommandWrapper) allCommandsMap.get(name)).addCommandAndState(stateID, command); + } else { + allCommandsMap.put(name, new CommandWrapper(stateID, command)); + } + }); + id++; + } + + allCommands = Collections.unmodifiableMap(allCommandsMap); + } + + /** + * Calls {@link ShellState#cleanup()} on each registered state if it is not null.
    + * States are obtained by {@link #obtainState(int)}. + */ + @Override + public void cleanup() { + for (int stateID = 0; stateID < statesCount; stateID++) { + ShellState state = obtainState(stateID); + if (state != null) { + state.cleanup(); + } + } + } + + /** + * Safely calls {@link ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell + * .ShellState#prepareToExit(int)} + * on each registered state if it is not null.
    + * States are obtained by {@link #obtainState(int)}.
    + * The resulting exit code depends on states' implementations. + */ + @Override + public void prepareToExit(int exitCode) throws ExitRequest { + int code = exitCode; + for (int stateID = 0; stateID < statesCount; stateID++) { + ShellState state = obtainState(stateID); + if (state != null) { + try { + state.prepareToExit(exitCode); + throw new AssertionError( + "prepareToExit() contract not honoured by " + state.getClass()); + } catch (ExitRequest exitRequest) { + code = exitRequest.getCode(); + } + } + } + + throw new ExitRequest(code); + } + + @Override + public Map> getCommands() { + return allCommands; + } + + protected boolean areNamesEqual(Command commandA, Command commandB) { + return Objects.equals(commandA.getName(), commandB.getName()); + } + + /** + * This method is called when there are several commands with the same name from different states. You + * decide how to resolve this conflict. + * @throws Exception + */ + protected void onExecuteConflict(List stateIDs, List commands, String[] args) + throws Exception { + throw new UnsupportedOperationException("Execute conflicts are not resolved"); + } + + protected void executeNormally(int stateID, Command command, String[] args) throws Exception { + command.execute(obtainState(stateID), args); + } + + protected abstract ShellState obtainState(int stateID); + + /** + * This method is called when one of commands is requested to invoke. You decide whether it happens.
    + * Default implementation of this method executes any given command. + * @param stateID + * id of host state for the command. + * @param command + * command that is requested to invoke + * @param args + * arguments given to the command. + */ + protected void onExecuteRequested(int stateID, Command command, String[] args) throws Exception { + executeNormally(stateID, command, args); + } + + class CommandWrapper implements Command { + private List states = new LinkedList<>(); + private List commands = new LinkedList<>(); + + public CommandWrapper(int stateID, Command wrappedCommand) { + states.add(stateID); + commands.add(wrappedCommand); + } + + public void addCommandAndState(int state, Command command) { + states.add(state); + commands.add(command); + } + + @Override + public void execute(ShellState state, String[] args) throws TerminalException { + try { + if (states.size() == 1) { + onExecuteRequested(states.get(0), commands.get(0), args); + } else { + onExecuteConflict( + Collections.unmodifiableList(states), + Collections.unmodifiableList(commands), + args); + } + } catch (TerminalException handledException) { + throw handledException; + } catch (Exception exc) { + exceptionHandler.handleException(exc, null); + } + } + + @Override + public String getName() { + return commands.get(0).getName(); + } + + @Override + public String getInfo() { + throw new UnsupportedOperationException("You cannot operate with multi command directly."); + } + + @Override + public String getInvocation() { + throw new UnsupportedOperationException("You cannot operate with multi command directly."); + } + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/Main.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/Main.java index cc01d4aad..2d52e4109 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/Main.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/Main.java @@ -1,8 +1,12 @@ package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell; +import ru.fizteh.fivt.storage.structured.RemoteTableProvider; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.Database; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.remote.RemoteDatabaseStorage; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.TerminalException; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet.ServerState; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet.ClientServerGeneralState; + +import java.rmi.registry.Registry; class Main { //java -Dfizteh.db.dir=/home/phoenix/test/DB ru.fizteh.fivt.students.fedorov_andrew.databaselibrary @@ -11,15 +15,17 @@ class Main { private static final String PATH_PROPERTY = "fizteh.db.dir"; public static void main(String[] args) { - try (DatabaseFactory factory = new DatabaseFactory()) { - Shell shell = new Shell<>( - new ServerState( - () -> new SingleDatabaseShellState() { - @Override - protected Database obtainNewActiveDatabase() throws Exception { - return factory.obtainDatabase(getOutputStream()); - } - })); + try { + Shell shell = new Shell<>( + new ClientServerGeneralState() { + @Override + protected Database obtainNewActiveDatabase() throws Exception { + RemoteDatabaseStorage storage = new RemoteDatabaseStorage(); + RemoteTableProvider provider = + storage.connect("localhost", Registry.REGISTRY_PORT); + return new Database(provider, "", getOutputStream()); + } + }); int exitCode; if (args.length == 0) { exitCode = shell.run(System.in); @@ -32,6 +38,7 @@ protected Database obtainNewActiveDatabase() throws Exception { // Already handled. System.exit(1); } catch (Exception exc) { + // exc.printStackTrace(); System.out.println(exc.getMessage()); System.exit(1); } diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/Shell.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/Shell.java index 6ad096588..93480908e 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 @@ -6,16 +6,17 @@ import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Utility; import java.io.BufferedReader; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.PrintStream; +import java.io.Reader; import java.text.ParseException; import java.util.Arrays; import java.util.LinkedList; import java.util.List; -import java.util.Map; /** * Class that represents a terminal which can execute some commands that work with some data. @@ -40,15 +41,14 @@ public class Shell> { private final ShellStateImpl shellState; private final PrintStream outputStream; private boolean valid = true; - /** - * Available commands for invocation. - */ - private Map> commandMap; + /** * If the user is entering commands or it is package mode. */ private boolean interactive; + private Reader inputStreamReader; + public Shell(ShellStateImpl shellState, OutputStream outputStream) throws TerminalException { this.shellState = shellState; this.outputStream = outputStream instanceof PrintStream @@ -78,7 +78,7 @@ public static void handleError(String message, if (reportToUser) { errorStream.println(message == null ? cause.getMessage() : message); } - Log.log(Commands.class, cause, message); + Log.log(SingleDBCommands.class, cause, message); if (cause == null) { throw new TerminalException(message); } else { @@ -152,7 +152,7 @@ private void execute(String[] args) throws TerminalException, ExitRequest { Log.log(Shell.class, "Invocation request: " + Arrays.toString(args)); - Command command = commandMap.get(args[0]); + Command command = shellState.getCommands().get(args[0]); if (command == null) { handleError(args[0] + ": command is missing", null, true, getOutputStream()); } else { @@ -178,8 +178,6 @@ private void init() throws TerminalException { } catch (Exception exc) { handleError(exc.getMessage(), exc, true, getOutputStream()); } - - commandMap = shellState.getCommands(); } public boolean isInteractive() { @@ -192,14 +190,20 @@ private void checkValid() throws IllegalStateException { } } + public Reader getInputStreamReader() { + if (inputStreamReader == null) { + throw new IllegalStateException("No reader now"); + } + return inputStreamReader; + } + /** * Execute commands from input stream. Commands are awaited till the-end-of-stream. */ - public int run(InputStream stream) throws TerminalException { + private int run(InputStream stream, boolean interactive) throws TerminalException { checkValid(); valid = false; - - interactive = true; + this.interactive = interactive; if (stream == null) { throw new IllegalArgumentException("Input stream must not be null"); @@ -209,8 +213,11 @@ public int run(InputStream stream) throws TerminalException { try (BufferedReader reader = new BufferedReader( new InputStreamReader(stream), READ_BUFFER_SIZE)) { + inputStreamReader = reader; while (true) { - outputStream.print(shellState.getGreetingString()); + if (interactive) { + outputStream.print(shellState.getGreetingString()); + } String str = reader.readLine(); // End of stream. @@ -225,6 +232,9 @@ public int run(InputStream stream) throws TerminalException { } } catch (TerminalException exc) { // Exception is already handled. + if (!interactive) { + shellState.prepareToExit(1); + } } } } catch (IOException | ParseException exc) { @@ -235,9 +245,10 @@ public int run(InputStream stream) throws TerminalException { exitRequested = true; return request.getCode(); } finally { + inputStreamReader = null; if (!exitRequested) { try { - persistSafelyAndPrepareToExit(); + shellState.prepareToExit(0); } catch (ExitRequest request) { return request.getCode(); } @@ -248,6 +259,10 @@ public int run(InputStream stream) throws TerminalException { throw new AssertionError("No exit request performed"); } + public int run(InputStream inputStream) throws TerminalException { + return run(inputStream, true); + } + /** * Execute commands from command line arguments. Note that command line arguments are first * concatenated into a single line then split and parsed. @@ -258,49 +273,7 @@ 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; - - try { - List commands = splitCommandsString(String.join(" ", args)); - - for (String[] command : commands) { - execute(command); - } - } catch (TerminalException exc) { - // Exception already handled. - shellState.prepareToExit(1); - } catch (ParseException exc) { - handleError( - "Cannot parse command arguments: " + exc.getMessage(), exc, true, getOutputStream()); - } - persistSafelyAndPrepareToExit(); - } catch (ExitRequest request) { - return request.getCode(); - } - - // If all contracts are honoured, this line is unreachable. - throw new AssertionError("No exit request performed"); - } - - /** - * Persists shell state. If fails, calls {@link ru.fizteh.fivt.students.fedorov_andrew - * .databaselibrary.shell.ShellState#prepareToExit(int)} - * with non zero exit code. - */ - private void persistSafelyAndPrepareToExit() throws ExitRequest { - try { - shellState.persist(); - shellState.prepareToExit(0); - } catch (ExitRequest request) { - throw request; - } catch (Exception exc) { - Log.log(Shell.class, exc, "Failed to persist shell state"); - shellState.prepareToExit(1); - } + return run(new ByteArrayInputStream(String.join(" ", args).getBytes()), false); } public boolean isValid() { diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/ShellState.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/ShellState.java index 410ab39f9..cb6d62542 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/ShellState.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/ShellState.java @@ -32,12 +32,6 @@ public interface ShellState> extends CommandContainer */ void init(Shell host) throws Exception; - /** - * Persist object's state somehow. - * @throws Exception - */ - void persist() throws Exception; - /** * Safely exit with cleanup. * @throws ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.ExitRequest diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/SimpleCommandContainer.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/SimpleCommandContainer.java index e93649fd5..56beb9e1f 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/SimpleCommandContainer.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/SimpleCommandContainer.java @@ -1,7 +1,6 @@ package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Log; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Utility; import java.lang.reflect.Field; import java.util.HashMap; @@ -31,7 +30,10 @@ public Map> getCommands() { for (Field field : fields) { try { Command command = (Command) field.get(null); - String commandName = Utility.simplifyFieldName(field.getName()); + String commandName = command.getName(); + if (commandsMap.containsKey(commandName)) { + throw new IllegalStateException("Duplicate command name: " + commandName); + } commandsMap.put(commandName, command); Log.log("Registered command with name " + commandName); } catch (IllegalAccessException | ClassCastException exc) { diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/SingleDBCommands.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/SingleDBCommands.java new file mode 100644 index 000000000..6b7b99ed1 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/SingleDBCommands.java @@ -0,0 +1,260 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell; + +import ru.fizteh.fivt.storage.structured.Storeable; +import ru.fizteh.fivt.storage.structured.Table; +import ru.fizteh.fivt.storage.structured.TableProvider; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.StoreableTableImpl; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.DatabaseIOException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.InvocationException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.NoActiveTableException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.TerminalException; + +import java.io.IOException; +import java.text.ParseException; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +public class SingleDBCommands extends SimpleCommandContainer { + public static final Command COMMIT = + new AbstractCommand( + "commit", null, "saves all changes made from the last commit", 1) { + @Override + public void executeSafely(SingleDatabaseShellState state, String[] args) + throws IOException, IllegalArgumentException { + int changes = state.getActiveDatabase().commit(); + state.getOutputStream().println(changes); + } + }; + public static final Command ROLLBACK = + new AbstractCommand( + "rollback", null, "cancels all changes made from the last commit", 1) { + + @Override + public void executeSafely(SingleDatabaseShellState state, String[] args) + throws DatabaseIOException, IllegalArgumentException { + int changes = state.getActiveDatabase().rollback(); + state.getOutputStream().println(changes); + } + }; + public static final Command SIZE = + new AbstractCommand( + "size", null, "prints count of stored keys in current table", 1) { + @Override + public void executeSafely(SingleDatabaseShellState state, String[] args) + throws DatabaseIOException, IllegalArgumentException, NoActiveTableException { + int size = state.getActiveDatabase().getActiveTable().size(); + state.getOutputStream().println(size); + } + }; + public static final Command CREATE = + new AbstractCommand( + "create", + " (type0 type1 ... typeN)", + "creates a new table with the given name and column types (must be specified inside " + + "round " + + "brackets); type can be one of the following: int, long, byte, float, double, " + + "boolean, String;", + 3, + Integer.MAX_VALUE) { + @Override + public void executeSafely(SingleDatabaseShellState state, String[] args) + throws IOException, InvocationException { + if (!args[2].startsWith("(") || !args[args.length - 1].endsWith(")")) { + throw new InvocationException( + this, + args[0], + "Round brackets must exist and contain types list inside them"); + } + + // Joining strings. + String typesString = String.join(" ", Arrays.asList(args).subList(2, args.length)); + + // Removing brackets. + typesString = typesString.substring(1, typesString.length() - 1).trim(); + + List> columnTypes = StoreableTableImpl.parseColumnTypes(typesString); + String tableName = args[1]; + + boolean created = state.getActiveDatabase().createTable(tableName, columnTypes); + if (created) { + state.getOutputStream().println("created"); + } else { + throw new DatabaseIOException(tableName + " exists"); + } + } + }; + public static final Command DROP = + new AbstractCommand( + "drop", "", "deletes table with the given name from file system", 2) { + @Override + public void executeSafely(SingleDatabaseShellState state, final String[] args) + throws IOException { + state.getActiveDatabase().dropTable(args[1]); + state.getOutputStream().println("dropped"); + } + }; + public static final Command EXIT = + new AbstractCommand( + "exit", null, "saves all data to file system and stops interpretation", 1) { + @Override + public void execute(SingleDatabaseShellState state, String[] args) throws TerminalException { + state.prepareToExit(0); + + // If all contracts are honoured, this line should not be reached. + throw new AssertionError("Exit request not thrown"); + } + + @Override + public void executeSafely(SingleDatabaseShellState state, String[] args) + throws DatabaseIOException, IllegalArgumentException { + // Not used. + } + }; + public static final Command GET = + new AbstractCommand("get", "", "obtains value by the key", 2) { + @Override + public void executeSafely(SingleDatabaseShellState state, final String[] args) + throws NoActiveTableException { + String key = args[1]; + + Table table = state.getActiveTable(); + TableProvider provider = state.getProvider(); + + Storeable value = table.get(key); + + if (value == null) { + throw new IllegalArgumentException("not found"); + } else { + state.getOutputStream().println("found"); + state.getOutputStream().println(provider.serialize(table, value)); + } + } + }; + public static final Command HELP = + new AbstractCommand( + "help", null, "prints out description of state commands", 1, Integer.MAX_VALUE) { + @Override + public void execute(SingleDatabaseShellState state, String[] args) { + Map> commands = state.getCommands(); + + state.getOutputStream().println( + "DatabaseLibrary is an utility that lets you work with a simple database"); + + state.getOutputStream().println( + String.format( + "You can set database directory to work with using environment " + + "variable '%s'", SingleDatabaseShellState.DB_DIRECTORY_PROPERTY_NAME)); + + commands.values() + .forEach((command) -> state.getOutputStream().println(command.buildHelpLine())); + } + + @Override + public void executeSafely(SingleDatabaseShellState state, String[] args) + throws DatabaseIOException { + // not used + } + }; + public static final Command LIST = + new AbstractCommand( + "list", null, "prints all keys stored in the map", 1) { + @Override + public void executeSafely(SingleDatabaseShellState state, String[] args) + throws NoActiveTableException { + List keySet = state.getActiveTable().list(); + StringBuilder sb = new StringBuilder(); + + boolean comma = false; + + for (String key : keySet) { + sb.append(comma ? ", " : "").append(key); + comma = true; + } + + state.getOutputStream().println(sb); + } + }; + public static final Command PUT = new AbstractCommand( + "put", + " [ {boolean|number|string|null}... ]", + "assigns new storeable to the key", + 3, + Integer.MAX_VALUE) { + @Override + public void executeSafely(SingleDatabaseShellState state, String[] args) + throws NoActiveTableException, ParseException { + String key = args[1]; + + Table table = state.getActiveTable(); + + String valueStr = String.join(" ", Arrays.asList(args).subList(2, args.length)); + Storeable value = state.getProvider().deserialize(table, valueStr); + + Storeable oldValue = state.getActiveTable().put(key, value); + + if (oldValue == null) { + state.getOutputStream().println("new"); + } else { + String oldValueStr = state.getProvider().serialize(table, oldValue); + state.getOutputStream().println("overwrite"); + state.getOutputStream().println("old " + oldValueStr); + } + } + }; + public static final Command REMOVE = + new AbstractCommand("remove", "", "removes value by the key", 2) { + @Override + public void executeSafely(SingleDatabaseShellState state, String[] args) + throws DatabaseIOException, NoActiveTableException { + String key = args[1]; + + Storeable oldValue = state.getActiveTable().remove(key); + + if (oldValue == null) { + throw new DatabaseIOException("not found"); + } else { + state.getOutputStream().println("removed"); + } + } + }; + public static final Command SHOW = + new AbstractCommand( + "show", "tables", "prints info on all tables assigned to the working database", 2) { + @Override + public void executeSafely(SingleDatabaseShellState state, String[] args) + throws IllegalArgumentException { + switch (args[1]) { + case "tables": { + state.getActiveDatabase().showTables(); + break; + } + default: { + throw new IllegalArgumentException("show: unexpected option: " + args[1]); + } + } + } + }; + public static final Command USE = new AbstractCommand( + "use", + "", + "saves all changes made to the current table (if present) and makes table" + + " with the given name the current one", + 2) { + @Override + public void executeSafely(final SingleDatabaseShellState state, final String[] args) + throws IOException { + state.getActiveDatabase().useTable(args[1]); + state.getOutputStream().println("using " + args[1]); + } + }; + private static final SingleDBCommands INSTANCE = new SingleDBCommands(); + + private SingleDBCommands() { + + } + + public static SingleDBCommands getInstance() { + return INSTANCE; + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/SingleDatabaseShellState.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/SingleDatabaseShellState.java index 14126e4ee..352a1eb04 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 @@ -7,7 +7,6 @@ import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.NoActiveTableException; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Log; -import java.io.IOException; import java.io.PrintStream; import java.util.Map; @@ -23,7 +22,7 @@ public abstract class SingleDatabaseShellState extends BaseShellState { + private static final int CLIENT_STATE_ID = 0; + private static final int SERVER_STATE_ID = 1; + + private ClientGeneralState clientState; + private DBServerState serverState; + + public ClientServerGeneralState() throws TerminalException { + ClientServerGeneralState genState = this; + clientState = new ClientGeneralState() { + @Override + protected Database obtainNewActiveDatabase() throws Exception { + return genState.obtainNewActiveDatabase(); + } + }; + serverState = new DBServerState(); + + new Shell<>(clientState); + new Shell<>(serverState); + + setAllCommands(clientState.getCommands(), serverState.getCommands()); + setExceptionHandler( + (exception, noData) -> AbstractCommand.DATABASE_ERROR_HANDLER + .handleException(exception, getOutputStream())); + } + + @Override + protected void onExecuteConflict(List stateIDs, List commands, String[] args) + throws Exception { + Command serverCommand = null; + Command clientCommand = null; + + for (int i = 0; i < stateIDs.size(); i++) { + switch (stateIDs.get(i)) { + case CLIENT_STATE_ID: { + clientCommand = commands.get(i); + break; + + } + case SERVER_STATE_ID: { + serverCommand = commands.get(i); + break; + } + default: { + throw new IllegalArgumentException("Illegal state ID: " + stateIDs.get(i)); + } + } + } + + boolean serverActive = serverState.isStarted(); + boolean clientActive = clientState.isConnected(); + + // Conflict between server and client EXIT or HELP command. + if (serverActive && !clientActive) { + // Server variant + onExecuteRequested(SERVER_STATE_ID, serverCommand, args); + } else if (clientActive && !serverActive) { + // Client variant + onExecuteRequested(CLIENT_STATE_ID, clientCommand, args); + } else if (areNamesEqual(serverCommand, ServerCommands.HELP) && areNamesEqual( + clientCommand, ClientCommands.HELP)) { + executeNormally(SERVER_STATE_ID, serverCommand, args); + executeNormally(CLIENT_STATE_ID, clientCommand, args); + } else if (areNamesEqual(serverCommand, ServerCommands.EXIT) && areNamesEqual( + clientCommand, ClientCommands.EXIT)) { + prepareToExit(0); + } else { + throw new UnsupportedOperationException( + "Cannot resolve command conflict: " + serverCommand.getName() + " and " + clientCommand + .getName()); + } + } + + @Override + protected ShellState obtainState(int stateID) { + switch (stateID) { + case CLIENT_STATE_ID: + return clientState; + case SERVER_STATE_ID: + return serverState; + default: + throw new IllegalArgumentException("Illegal state ID: " + stateID); + } + } + + @Override + protected void onExecuteRequested(int stateID, Command command, String[] args) throws Exception { + if (stateID == CLIENT_STATE_ID) { + if (serverState.isStarted()) { + throw new ExecutionNotPermittedException("You cannot execute this command in server mode"); + } else { + executeNormally(stateID, command, args); + } + } else if (stateID == SERVER_STATE_ID) { + if (clientState.isConnected()) { + throw new ExecutionNotPermittedException( + "You cannot execute this command when connected as client"); + } else { + executeNormally(stateID, command, args); + } + } + } + + protected abstract Database obtainNewActiveDatabase() throws Exception; +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/client/ClientCommands.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/client/ClientCommands.java new file mode 100644 index 000000000..9b9567b57 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/client/ClientCommands.java @@ -0,0 +1,116 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet.client; + +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.DatabaseIOException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.InvocationException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.NoActiveTableException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.TerminalException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.AbstractCommand; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Command; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.SimpleCommandContainer; + +import java.io.IOException; +import java.net.InetAddress; +import java.text.ParseException; +import java.util.Map; + +public final class ClientCommands extends SimpleCommandContainer { + public static final Command CONNECT = new AbstractCommand( + "connect", " ", "connects to a database storage server", 3) { + @Override + public void executeSafely(ClientState state, String[] args) throws + IllegalArgumentException, + NoActiveTableException, + IllegalStateException, + NullPointerException, + InvocationException, + ParseException, + IOException { + int port; + + try { + port = Integer.parseInt(args[2]); + } catch (NumberFormatException exc) { + throw new NumberFormatException("not connected: " + exc.getMessage()); + } + state.connect(args[1], port); + state.getOutputStream().println("connected"); + } + }; + public static final Command DISCONNECT = + new AbstractCommand("disconnect", "", "disconnects from database storage", 1) { + @Override + public void executeSafely(ClientState state, String[] args) throws + IllegalArgumentException, + NoActiveTableException, + IllegalStateException, + NullPointerException, + InvocationException, + ParseException, + IOException { + state.disconnect(); + state.getOutputStream().println("disconnected"); + } + }; + public static final Command WHEREAMI = + new AbstractCommand("whereami", "", "prints host and port you are connected to", 1) { + @Override + public void executeSafely(ClientState state, String[] args) throws + IllegalArgumentException, + NoActiveTableException, + IllegalStateException, + NullPointerException, + InvocationException, + ParseException, + IOException { + InetAddress address = state.getHost(); + int port = state.getPort(); + + if (address.isSiteLocalAddress()) { + state.getOutputStream().println("local " + port); + } else { + state.getOutputStream().println("remote " + address.getHostAddress() + ":" + port); + } + } + }; + public static final Command HELP = new AbstractCommand( + "help", null, "prints out description of state commands", 1, Integer.MAX_VALUE) { + @Override + public void execute(ClientState state, String[] args) { + Map> commands = state.getCommands(); + + state.getOutputStream().println( + "You can connect to a database storage server"); + + commands.values().forEach((command) -> state.getOutputStream().println(command.buildHelpLine())); + } + + @Override + public void executeSafely(ClientState state, String[] args) throws DatabaseIOException { + // not used + } + }; + public static final Command EXIT = new AbstractCommand( + "exit", null, "saves all data to file system and stops interpretation", 1) { + @Override + public void execute(ClientState state, String[] args) throws TerminalException { + state.prepareToExit(0); + + // If all contracts are honoured, this line should not be reached. + throw new AssertionError("Exit request not thrown"); + } + + @Override + public void executeSafely(ClientState state, String[] args) + throws DatabaseIOException, IllegalArgumentException { + // Not used. + } + }; + private static final ClientCommands INSTANCE = new ClientCommands(); + + private ClientCommands() { + } + + public static ClientCommands getInstance() { + return INSTANCE; + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/client/ClientGeneralState.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/client/ClientGeneralState.java new file mode 100644 index 000000000..1a7e09d9e --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/client/ClientGeneralState.java @@ -0,0 +1,119 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet.client; + +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.Database; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.ExecutionNotPermittedException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.AbstractCommand; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Command; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.JoinedState; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Shell; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.ShellState; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.SingleDBCommands; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.SingleDatabaseShellState; + +import java.util.List; + +public abstract class ClientGeneralState extends JoinedState { + private static final int DB_STATE_ID = 0; + private static final int CLIENT_STATE_ID = 1; + + private final ClientState clientState = new ClientState(); + private SingleDatabaseShellState databaseState; + + public ClientGeneralState() { + super(SingleDBCommands.getInstance().getCommands(), ClientCommands.getInstance().getCommands()); + } + + public boolean isConnected() { + return clientState.isConnected(); + } + + protected abstract Database obtainNewActiveDatabase() throws Exception; + + @Override + public void init(Shell host) throws Exception { + super.init(host); + clientState.init(host); + setExceptionHandler( + (exception, noData) -> AbstractCommand.DATABASE_ERROR_HANDLER + .handleException(exception, getOutputStream())); + } + + @Override + protected void onExecuteConflict(List stateIDs, List commands, String[] args) + throws Exception { + Command clientCommand = null; + Command databaseCommand = null; + + for (int i = 0; i < stateIDs.size(); i++) { + switch (stateIDs.get(i)) { + case CLIENT_STATE_ID: { + clientCommand = commands.get(i); + break; + } + case DB_STATE_ID: { + databaseCommand = commands.get(i); + break; + } + default: { + throw new IllegalArgumentException("Illegal state ID: " + stateIDs.get(i)); + } + } + } + + if (areNamesEqual(clientCommand, ClientCommands.HELP) && areNamesEqual( + databaseCommand, SingleDBCommands.HELP)) { + onExecuteRequested(CLIENT_STATE_ID, clientCommand, args); + if (clientState.isConnected()) { + onExecuteRequested(DB_STATE_ID, databaseCommand, args); + } + } else { + throw new UnsupportedOperationException( + "Cannot resolve command conflict: " + clientCommand.getName()); + } + } + + @Override + protected ShellState obtainState(int stateID) { + if (stateID == CLIENT_STATE_ID) { + return clientState; + } else if (stateID == DB_STATE_ID) { + return databaseState; + } else { + throw new IllegalArgumentException("Illegal state id: " + stateID); + } + } + + @Override + protected void onExecuteRequested(int stateID, Command command, String[] args) throws Exception { + if (stateID == CLIENT_STATE_ID) { + executeNormally(stateID, command, args); + + if (areNamesEqual(ClientCommands.CONNECT, command)) { + ClientGeneralState generalState = this; + + databaseState = new SingleDatabaseShellState() { + + @Override + protected Database obtainNewActiveDatabase() throws Exception { + return generalState.obtainNewActiveDatabase(); + } + }; + try { + new Shell<>(databaseState); + } catch (Exception exc) { + throw new RuntimeException( + "Failed to build joined state: " + exc.getMessage(), exc.getCause()); + } + } else if (areNamesEqual(ClientCommands.DISCONNECT, command)) { + databaseState.cleanup(); + databaseState = null; + } + } else if (stateID == DB_STATE_ID) { + if (!clientState.isConnected()) { + throw new ExecutionNotPermittedException("You should connect to a database storage at first"); + } else { + executeNormally(stateID, command, args); + } + } + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/client/ClientState.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/client/ClientState.java new file mode 100644 index 000000000..f3073a9f6 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/client/ClientState.java @@ -0,0 +1,80 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet.client; + +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.ExitRequest; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.BaseShellState; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Command; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Log; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; +import java.util.Map; + +public class ClientState extends BaseShellState { + private Socket connection; + + public void connect(String host, int port) throws IOException { + checkInitialized(); + if (connection == null) { + try { + connection = new Socket(host, port); + } catch (IOException exc) { + throw new IOException("not connected: " + exc.getMessage(), exc.getCause()); + } + } else { + throw new IllegalStateException("not connected: already connected"); + } + } + + public InetAddress getHost() { + requireConnected(); + return connection.getInetAddress(); + } + + public int getPort() { + requireConnected(); + return connection.getPort(); + } + + public void disconnect() throws IllegalStateException, IOException { + requireConnected(); + try { + connection.close(); + } finally { + connection = null; + } + } + + private void requireConnected() { + if (!isConnected()) { + throw new IllegalStateException("not connected"); + } + } + + public boolean isConnected() { + return connection != null; + } + + @Override + public void cleanup() { + try { + if (isConnected()) { + disconnect(); + } + } catch (IOException exc) { + Log.log(ClientState.class, exc, "Exception occurred on cleanup"); + } + } + + @Override + public void prepareToExit(int exitCode) throws ExitRequest { + Log.log(ClientState.class, "Preparing to exit with code:" + exitCode); + cleanup(); + throw new ExitRequest(exitCode); + } + + @Override + public Map> getCommands() { + return ClientCommands.getInstance().getCommands(); + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/server/DBServerState.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/server/DBServerState.java new file mode 100644 index 000000000..9aed67faa --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/server/DBServerState.java @@ -0,0 +1,172 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet.server; + +import ru.fizteh.fivt.storage.structured.RemoteTableProvider; +import ru.fizteh.fivt.storage.structured.RemoteTableProviderFactory; +import ru.fizteh.fivt.storage.structured.TableProvider; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.Database; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.remote.IRemoteTableProviderFactory; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.remote.RemoteDatabaseStorage; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.remote.RemoteTableProviderFactoryImpl; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.ExitRequest; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.BaseShellState; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Command; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Shell; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.ShellState; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.SingleDatabaseShellState; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Log; + +import java.io.IOException; +import java.net.InetAddress; +import java.rmi.registry.Registry; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +public class DBServerState extends BaseShellState { + private static final ServerCommands COMMANDS = ServerCommands.obtainInstance(); + private static final String DATABASE_ROOT_PROPERTY = "fizteh.db.dir"; + private Supplier clientShellStateSupplier; + private Server server; + private IRemoteTableProviderFactory factory; + private TableProvider provider; + private String databaseRoot; + + public DBServerState() { + RemoteTableProviderFactory storage = new RemoteDatabaseStorage(); + DBServerState serverState = this; + this.clientShellStateSupplier = () -> new SingleDatabaseShellState() { + @Override + protected Database obtainNewActiveDatabase() throws Exception { + RemoteTableProvider provider = storage.connect("127.0.0.1", Registry.REGISTRY_PORT); + return new Database( + provider, serverState.getDatabaseRoot(), getOutputStream()); + } + }; + } + + public String getDatabaseRoot() { + return databaseRoot; + } + + public TableProvider getProvider() { + return provider; + } + + public void startServer(int port) throws IOException, IllegalStateException { + checkInitialized(); + if (server.isStarted()) { + throw new IllegalStateException("not started: already started"); + } + + try { + databaseRoot = System.getProperty(DATABASE_ROOT_PROPERTY); + factory = new RemoteTableProviderFactoryImpl(); + provider = factory.establishStorage(databaseRoot); + + server.setShellStateSupplier(clientShellStateSupplier); + server.startServer(port); + } catch (Exception exc) { + try { + if (provider != null) { + factory.close(); + } + } catch (Exception ignored) { + Log.log(DBServerState.class, ignored, "Failed to close factory after server install failure"); + } finally { + throw exc; + } + } + } + + public boolean isStarted() { + return server.isStarted(); + } + + private void closeFactory() throws IOException { + factory.close(); + factory = null; + provider = null; + } + + public int stopServer() throws IOException { + checkInitialized(); + int port = server.stopServer(); + closeFactory(); + return port; + } + + public void stopServerIfStarted() throws IOException { + checkInitialized(); + server.stopServerIfStarted(); + if (factory != null) { + closeFactory(); + } + } + + public List listUsers() { + checkInitialized(); + return server.listUsers().stream().map( + (ServerCommunicator communicator) -> new User( + communicator.getSocket().getInetAddress(), communicator.getSocket().getPort())) + .collect(Collectors.toList()); + } + + @Override + public void cleanup() { + // Nothing to clean. + } + + @Override + public void prepareToExit(int exitCode) throws ExitRequest { + try { + stopServerIfStarted(); + } catch (Exception exc) { + Log.log(DBServerState.class, exc); + if (exitCode == 0) { + exitCode = -1; + } + } + throw new ExitRequest(exitCode); + } + + @Override + public void init(Shell host) throws Exception { + super.init(host); + server = new Server(host.getOutputStream()); + } + + @Override + protected void finalize() throws Throwable { + super.finalize(); + stopServerIfStarted(); + } + + @Override + public Map> getCommands() { + return COMMANDS.getCommands(); + } + + public static class User { + private final InetAddress address; + private final int port; + + public User(InetAddress address, int port) { + this.address = address; + this.port = port; + } + + public InetAddress getAddress() { + return address; + } + + public int getPort() { + return port; + } + + @Override + public String toString() { + return address.getHostAddress() + ":" + port; + } + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/server/Server.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/server/Server.java new file mode 100644 index 000000000..cf738fa84 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/server/Server.java @@ -0,0 +1,168 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet.server; + +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.TerminalException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.ShellState; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Log; + +import java.io.Closeable; +import java.io.IOException; +import java.io.PrintStream; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.function.Supplier; + +/** + * Server that listens to connections and creates communicator threads. + */ +public class Server implements Closeable { + private final PrintStream reportStream; + private final Set users; + private Supplier shellStateSupplier; + private ServerSocket serverSocket; + private boolean deletingFromUsers = false; + + public Server(PrintStream reportStream) { + this.reportStream = reportStream; + users = new HashSet<>(); + } + + public synchronized Supplier getShellStateSupplier() { + return shellStateSupplier; + } + + public synchronized void setShellStateSupplier(Supplier shellStateSupplier) { + if (isStarted()) { + throw new IllegalStateException("Cannot set state supplier: already started"); + } + this.shellStateSupplier = shellStateSupplier; + } + + public PrintStream getReportStream() { + return reportStream; + } + + public synchronized ServerSocket getServerSocket() { + return serverSocket; + } + + /** + * Called by ServerCommunicator after it is closed. + */ + synchronized void onConnectionClosed(ServerCommunicator communicator) { + Log.log(Server.class, "Disconnected: " + communicator); + if (!deletingFromUsers) { + users.remove(communicator); + } + } + + public synchronized boolean isStarted() { + return serverSocket != null; + } + + public synchronized void startServer(int port) throws IOException, IllegalStateException { + if (isStarted()) { + throw new IllegalStateException("not started: already started"); + } + + if (shellStateSupplier == null) { + throw new IllegalStateException("not started: state supplier not defined"); + } + + try { + serverSocket = new ServerSocket(port); + } catch (IOException exc) { + throw new IOException("not started: " + exc.getMessage(), exc.getCause()); + } + + Thread serverThread = new Thread(this::run, this + ": server thread"); + serverThread.setDaemon(true); + serverThread.setPriority(Thread.MIN_PRIORITY); + serverThread.start(); + } + + public synchronized void stopServerIfStarted() throws IOException { + if (isStarted()) { + close(); + } + } + + public synchronized int stopServer() throws IOException, IllegalStateException { + requireStarted(); + int port = getServerSocket().getLocalPort(); + close(); + return port; + } + + public synchronized List listUsers() { + requireStarted(); + return new LinkedList<>(users); + } + + private void run() { + try { + while (!Thread.currentThread().isInterrupted()) { + ServerSocket serverSocketTemp; + + synchronized (this) { + serverSocketTemp = serverSocket; + } + + if (serverSocketTemp == null) { + return; + } + + Socket socket = serverSocketTemp.accept(); + ServerCommunicator communicator = new ServerCommunicator(socket, this, shellStateSupplier); + Thread communicatorThread = new Thread(communicator, "Communicator with " + communicator); + communicatorThread.setDaemon(true); + communicatorThread.start(); + + synchronized (this) { + users.add(communicator); + } + } + } catch (IOException exc) { + Log.log(Server.class, exc, "Error on listening to incoming connections"); + } catch (TerminalException exc) { + // Already handled. + } finally { + try { + close(); + } catch (IOException exc) { + Log.log(DBServerState.class, exc, this + ": failed to close server socket"); + } + } + } + + private void requireStarted() throws IllegalStateException { + if (!isStarted()) { + throw new IllegalStateException("not started"); + } + } + + @Override + public synchronized void close() throws IOException { + if (serverSocket != null) { + deletingFromUsers = true; + for (ServerCommunicator communicator : users) { + try { + communicator.close(); + } catch (IOException exc) { + Log.log(Server.class, exc, "Error on closing socket: " + communicator); + } + } + users.clear(); + deletingFromUsers = false; + + try { + serverSocket.close(); + } finally { + serverSocket = null; + } + } + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/server/ServerCommands.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/server/ServerCommands.java new file mode 100644 index 000000000..8ee445268 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/server/ServerCommands.java @@ -0,0 +1,141 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet.server; + +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.DatabaseIOException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.InvocationException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.NoActiveTableException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.TerminalException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.AbstractCommand; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Command; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.SimpleCommandContainer; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.SingleDatabaseShellState; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet.server.DBServerState.User; + +import java.io.IOException; +import java.text.ParseException; +import java.util.List; +import java.util.Map; + +/** + * Container for server commands: start, stop, listusers. + */ +public class ServerCommands extends SimpleCommandContainer { + public static final Command STOP = + new AbstractCommand("stop", "", "stops server", 1) { + @Override + public void executeSafely(DBServerState state, String[] args) throws + IllegalArgumentException, + NoActiveTableException, + IllegalStateException, + NullPointerException, + InvocationException, + ParseException, + IOException { + int port = state.stopServer(); + state.getOutputStream().println("stopped at " + port); + } + }; + public static final Command LISTUSERS = new AbstractCommand( + "listusers", "", "prints list of ip addresses and ports of connected users", 1) { + @Override + public void executeSafely(DBServerState state, String[] args) throws + IllegalArgumentException, + NoActiveTableException, + IllegalStateException, + NullPointerException, + InvocationException, + ParseException, + IOException { + List users = state.listUsers(); + + StringBuilder sb = new StringBuilder(); + for (User user : users) { + sb.append(user); + sb.append(System.lineSeparator()); + } + state.getOutputStream().print(sb.toString()); + } + }; + public static final Command EXIT = new AbstractCommand( + "exit", "", "stops server (if it is started) and closes the terminal", 1) { + @Override + public void execute(DBServerState state, String[] args) throws TerminalException { + state.prepareToExit(0); + + // If all contracts are honoured, this line should not be reached. + throw new AssertionError("Exit request not thrown"); + } + + @Override + public void executeSafely(DBServerState state, String[] args) throws + IllegalArgumentException, + NoActiveTableException, + IllegalStateException, + NullPointerException, + InvocationException, + ParseException, + IOException { + // Not used. + } + }; + public static final Command HELP = new AbstractCommand( + "help", "", "prints out description of state commands", 1, Integer.MAX_VALUE) { + @Override + public void execute(DBServerState state, String[] args) { + Map> commands = state.getCommands(); + + state.getOutputStream().println( + "You can start telnet database server ready for new connections!"); + + state.getOutputStream().println( + String.format( + "You can set database directory to work with using environment " + + "variable '%s'", SingleDatabaseShellState.DB_DIRECTORY_PROPERTY_NAME)); + + for (Command command : commands.values()) { + state.getOutputStream().println(command.buildHelpLine()); + } + } + + @Override + public void executeSafely(DBServerState state, String[] args) throws DatabaseIOException { + // not used + } + }; + private static final int DEFAULT_PORT = 10001; + public static final Command START = new AbstractCommand( + "start", + "[port]", + "starts server at the specified port (or, if not specified, at " + DEFAULT_PORT + ")", + 1, + 2) { + @Override + public void executeSafely(DBServerState state, String[] args) throws + IllegalArgumentException, + NoActiveTableException, + IllegalStateException, + InvocationException, + ParseException, + IOException { + int port; + if (args.length == 1) { + port = DEFAULT_PORT; + } else { + port = Integer.parseInt(args[1]); + } + + state.startServer(port); + state.getOutputStream().println("started at " + port); + } + }; + private static final ServerCommands INSTANCE = new ServerCommands(); + + /** + * Not for initializing. + */ + private ServerCommands() { + } + + public static ServerCommands obtainInstance() { + return INSTANCE; + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/server/ServerCommunicator.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/server/ServerCommunicator.java new file mode 100644 index 000000000..e457ad097 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/server/ServerCommunicator.java @@ -0,0 +1,89 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet.server; + +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.TerminalException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Shell; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.ShellState; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Log; + +import java.io.Closeable; +import java.io.IOException; +import java.io.PrintStream; +import java.net.Socket; +import java.util.function.Supplier; + +public class ServerCommunicator implements Runnable, Closeable { + private final Socket socket; + + private final Shell interpreter; + + private final PrintStream reportStream; + + private final Server server; + + private boolean closed = false; + + public ServerCommunicator(Socket socket, Server server, Supplier shellStateSupplier) + throws IOException, TerminalException { + this.socket = socket; + this.server = server; + this.reportStream = server.getReportStream(); + this.interpreter = new Shell<>(shellStateSupplier.get(), socket.getOutputStream()); + } + + public Socket getSocket() { + return socket; + } + + @Override + public void run() { + try { + interpreter.run(socket.getInputStream()); + } catch (TerminalException exc) { + Log.log(ServerCommunicator.class, exc, "Interpreter run terminated"); + // Already handled. + } catch (IOException exc) { + reportStream.println(this + ": Error on obtaining socket input stream: " + exc.getMessage()); + } finally { + try { + close(); + } catch (IOException exc) { + Log.log( + DBServerState.class, + this + " failed to close connection after all commands execution"); + } + + } + } + + private void notifyConnectionClosed() { + server.onConnectionClosed(this); + } + + @Override + public synchronized void close() throws IOException { + if (!closed) { + closed = true; + socket.close(); + notifyConnectionClosed(); + } + } + + @Override + public int hashCode() { + return socket.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof ServerCommunicator) { + ServerCommunicator user = (ServerCommunicator) obj; + return socket.equals(user.socket); + } + return false; + } + + @Override + public String toString() { + return socket.toString(); + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/ClientGeneralStateTest.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/ClientGeneralStateTest.java new file mode 100644 index 000000000..43e4f9568 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/ClientGeneralStateTest.java @@ -0,0 +1,85 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import ru.fizteh.fivt.storage.structured.RemoteTableProvider; +import ru.fizteh.fivt.storage.structured.RemoteTableProviderFactory; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.Database; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.remote.RemoteDatabaseStorage; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.TerminalException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Shell; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet.client.ClientGeneralState; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet.server.DBServerState; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test.support.RegexMatcher; + +import java.io.IOException; +import java.io.PrintStream; +import java.rmi.registry.Registry; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +@RunWith(JUnit4.class) +public class ClientGeneralStateTest extends InterpreterTestBase { + private DBServerState serverState; + private ClientGeneralState clientState; + + @Override + protected Shell constructInterpreter() throws TerminalException { + this.clientState = new ClientGeneralState() { + @Override + protected Database obtainNewActiveDatabase() throws Exception { + RemoteTableProviderFactory factory = new RemoteDatabaseStorage(); + RemoteTableProvider provider = factory.connect("localhost", Registry.REGISTRY_PORT); + return new Database(provider, "", getOutputStream()); + } + + @Override + public PrintStream getOutputStream() { + return System.out; + } + }; + return new Shell<>(clientState); + } + + @Before + public void prepareRemoteAPITest() throws Exception { + serverState = new DBServerState(); + new Shell(serverState); + serverState.startServer(10001); + } + + @After + public void cleanupRemoteAPITest() throws IOException { + serverState.stopServer(); + } + + @Test + public void testConnectToNotExistentServer() throws IOException, TerminalException { + runBatchExpectNonZero("connect 127.0.0.1 10500"); + assertThat(getOutput(), startsWith("not connected")); + } + + @Test + public void testConnectAndCallNotExistentCommand() throws IOException, TerminalException { + runInteractiveExpectZero("connect 127.0.0.1 10001", "not_exists_yeah?", "disconnect"); + assertThat( + getOutput(), new RegexMatcher( + makeTerminalExpectedRegex( + "\\Q" + clientState.getGreetingString() + "\\E", + "connected", + "\\Qnot_exists_yeah?: command is missing\\E", + "disconnected"))); + } + + @Test + public void testCallShowTablesWithoutConnect() throws IOException, TerminalException { + runBatchExpectNonZero("show tables"); + assertEquals( + getOutput(), makeTerminalExpectedMessage( + "You should connect to a database storage at first")); + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/DatabaseShellTest.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/DatabaseShellTest.java index 7c90c3c71..90a04963c 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 @@ -41,21 +41,13 @@ private void createTableWithStringColumn(String table) throws TerminalException "Creating table", makeTerminalExpectedMessage("created"), getOutput()); } - @Test - public void testFailPersistOnExit() throws TerminalException, DatabaseIOException { - MutatedSDSS state = new MutatedSDSS(0); - interpreter = new Shell<>(state); - - runBatchExpectNonZero("exit"); - assertEquals( - "Failing on persist() call", - makeTerminalExpectedMessage("Fail on commit [test mode]"), - getOutput()); - } - @Test public void testPutCompositeStoreable() throws TerminalException { - runBatchExpectZero("create t1 (String int boolean)", "use t1", "put key [ \"hello\", 2, null ]"); + runBatchExpectZero( + "create t1 (String int boolean)", + "use t1", + "put key [ \"hello\", 2, null ]", + "commit"); assertThat("Output after put", getOutput(), containsString(makeTerminalExpectedMessage("new"))); @@ -98,7 +90,8 @@ public void testUseWithUncommittedChanges() throws TerminalException { createTableWithStringColumn(tableA); String command = String.format( - "use " + tableA + "use " + + tableA + "; put a [\"b\"]; put b [\"c\"]; put c [\"d\"]; remove b; put a [\"bbb\"]; use %s", tableB); String expectedReply = String.format( "using tableA%nnew%nnew%nnew%nremoved%noverwrite%nold [\"b\"]%n2 unsaved changes%n"); @@ -229,7 +222,7 @@ public void testInteractiveMode1() throws TerminalException { runBatchExpectZero( "use " + table, - "put a [\"b\"]; put b [\"c\"]; put c [\"d\"]; put d [\"e\"]; put e [\"a\"];", + "put a [\"b\"]; put b [\"c\"]; put c [\"d\"]; put d [\"e\"]; put e [\"a\"]; commit", "exit"); runInteractiveExpectZero( "use " + table, "show tables", "use " + fakeTable + "; list", "use " + table + "; list"); @@ -276,7 +269,8 @@ public void testSize() throws TerminalException { createTableWithStringColumn(table); runBatchExpectZero( "use " + table, - "put a [\"b\"]; put c [\"d\"]; put d [\"e\"]; remove c; put d [\"dd\"]; put k [\"a\"]"); + "put a [\"b\"]; put c [\"d\"]; put d [\"e\"]; remove c; put d [\"dd\"]; put k [\"a\"]; " + + "commit"); runBatchExpectZero("use " + table, "size"); assertEquals(makeTerminalExpectedMessage("using " + table, "3"), getOutput()); } @@ -444,7 +438,7 @@ public void testRunInteractiveWithUnparseableStream() throws TerminalException { @Test public void testRunWithUnparseableStream() throws TerminalException { exception.expect(TerminalException.class); - exception.expectMessage(containsString("Cannot parse command arguments")); + exception.expectMessage(containsString("Error in input stream: Cannot find closing quotes")); runBatchExpectNonZero("do smth \""); // Unclosed quotes. } @@ -452,7 +446,8 @@ public void testRunWithUnparseableStream() throws TerminalException { @Test public void testCreateTableWithInvalidName1() throws TerminalException { runBatchExpectNonZero( - "create " + Paths.get("..", DB_ROOT.getFileName().toString(), "table").toString() + "create " + + Paths.get("..", DB_ROOT.getFileName().toString(), "table").toString() + " (String)"); assertEquals( "Illegal table name error must be raised", diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/InterpreterTest.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/InterpreterTest.java index 3fd4c30b5..4a82c303c 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/InterpreterTest.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/InterpreterTest.java @@ -41,15 +41,6 @@ public void testInteractiveContinueWorkAfterError() throws TerminalException { assertTrue("Interactive: should be okay after error", getOutput().matches(regex)); } - @Test - public void testErrorInPersistOnExit() throws TerminalException { - state.setMakeExceptionOnPersist(true); - - runBatchExpectNonZero(); - - assertEquals("Expecting empty output", getOutput(), makeTerminalExpectedMessage()); - } - @Test public void testErrorOnInit() { state.setMakeExceptionOnInit(true); diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/LoggingProxyFactoryXMLTest.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/LoggingProxyFactoryXMLTest.java index b72b909a2..91b5ad8cf 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/LoggingProxyFactoryXMLTest.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/LoggingProxyFactoryXMLTest.java @@ -48,7 +48,8 @@ public void testBoolMethodJSON() throws ParseException { startsWith("1true" + NEW_LINE))); + + "/>true" + + NEW_LINE))); } @Test @@ -72,7 +73,8 @@ public void testThrowingMethodJSON() throws ParseException { + ">1_11_21_32_12_2java.lang" - + ".Exception" + NEW_LINE))); + + ".Exception" + + NEW_LINE))); } @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 82e10fb62..b432c6d14 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 @@ -490,7 +490,9 @@ public void testPutOneStoreableToAnotherTable2() throws IOException { exception.expectMessage( wrongTypeMatcherAndAllOf( containsString( - "Column #0 expected to have type " + INT_TYPE + ", but actual type is " + "Column #0 expected to have type " + + INT_TYPE + + ", but actual type is " + LONG_TYPE))); tableA.put("key", storeableB); diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/TableTest.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/TableTest.java index f6324c118..3ad250d87 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 @@ -294,7 +294,9 @@ public void testManyConcurrentCommits() throws Exception { Consumer trashFiller = (actionsCount) -> { try { System.err.println( - Thread.currentThread().getName() + ": doing " + actionsCount + Thread.currentThread().getName() + + ": doing " + + actionsCount + " actions"); for (int j = 0; j < actionsCount; j++) { put( diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/AlternativeShellState.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/AlternativeShellState.java index 074b7d0a0..fd796a6fb 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/AlternativeShellState.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/AlternativeShellState.java @@ -43,22 +43,12 @@ public void execute(AlternativeShellState state, String[] args) throws TerminalE private final Map> commandMap; - private boolean makeExceptionOnPersist; private boolean makeExceptionOnInit; - private boolean makeRuntimeExceptionOnCleanup; public AlternativeShellState() { commandMap = super.getCommands(); } - public boolean isMakeExceptionOnPersist() { - return makeExceptionOnPersist; - } - - public void setMakeExceptionOnPersist(boolean makeExceptionOnPersist) { - this.makeExceptionOnPersist = makeExceptionOnPersist; - } - public boolean isMakeExceptionOnInit() { return makeExceptionOnInit; } @@ -74,9 +64,7 @@ public PrintStream getOutputStream() { @Override public void cleanup() { - if (makeRuntimeExceptionOnCleanup) { - throw new SpontaniousRuntimeException(); - } + } @Override @@ -91,26 +79,11 @@ public void init(Shell host) throws Exception { } } - @Override - public void persist() throws Exception { - if (makeExceptionOnPersist) { - throw new SpontaniousException(); - } - } - @Override public void prepareToExit(int exitCode) throws ExitRequest { throw new ExitRequest(exitCode); } - public boolean isMakeRuntimeExceptionOnCleanup() { - return makeRuntimeExceptionOnCleanup; - } - - public void setMakeRuntimeExceptionOnCleanup(boolean makeRuntimeExceptionOnCleanup) { - this.makeRuntimeExceptionOnCleanup = makeRuntimeExceptionOnCleanup; - } - @Override public Map> getCommands() { return commandMap; diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/xml/XMLMaker.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/xml/XMLMaker.java index 5a9a17b88..a69e86056 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/xml/XMLMaker.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/xml/XMLMaker.java @@ -102,12 +102,14 @@ private static void writeXML(XMLStreamWriter writer, if (annotatedFields.isEmpty()) { throw new IllegalArgumentException( - "Class " + objClass.getSimpleName() + "Class " + + objClass.getSimpleName() + " does not have any XML fields and cannot be serialized"); } else if (objClass.getAnnotation(XMLComplexObject.class).wrapper()) { if (annotatedFields.size() != 1) { throw new IllegalArgumentException( - "Class " + objClass.getSimpleName() + "Class " + + objClass.getSimpleName() + " has more then one XML fields, thus it cannot be annotated as " + "'wrapper'"); } From a037663193af41233c1ac595c5c0bf2a62dcc6be Mon Sep 17 00:00:00 2001 From: AndreosGhost Date: Sat, 20 Dec 2014 00:10:09 +0300 Subject: [PATCH 08/14] Remembered to delete files which names had changed --- .../db/DBTableProviderFactory.java | 4 +- .../databaselibrary/shell/Commands.java | 270 ------------------ .../support/LoggingProxyFactoryImpl.java | 194 ------------- .../databaselibrary/telnet/Server.java | 153 ---------- .../telnet/ServerCommands.java | 143 ---------- .../telnet/ServerCommunicator.java | 85 ------ .../databaselibrary/telnet/ServerState.java | 110 ------- .../test/LoggingProxyFactoryTest.java | 179 ------------ 8 files changed, 2 insertions(+), 1136 deletions(-) delete mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/Commands.java delete mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/LoggingProxyFactoryImpl.java delete mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/Server.java delete mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/ServerCommands.java delete mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/ServerCommunicator.java delete mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/ServerState.java delete mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/LoggingProxyFactoryTest.java 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 6def964a7..e83f4815b 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/DBTableProviderFactory.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/DBTableProviderFactory.java @@ -3,7 +3,7 @@ import ru.fizteh.fivt.proxy.LoggingProxyFactory; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.DatabaseIOException; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Log; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.LoggingProxyFactoryJSON; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.LoggingProxyFactoryXML; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Utility; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.ValidityController; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.ValidityController.KillLock; @@ -20,7 +20,7 @@ import java.util.IdentityHashMap; public final class DBTableProviderFactory implements AutoCloseableTableProviderFactory { - private static final LoggingProxyFactory LOGGING_PROXY_FACTORY = new LoggingProxyFactoryJSON(); + private static final LoggingProxyFactory LOGGING_PROXY_FACTORY = new LoggingProxyFactoryXML(); private static final Writer LOG_WRITER; static { diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/Commands.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/Commands.java deleted file mode 100644 index 013228069..000000000 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/Commands.java +++ /dev/null @@ -1,270 +0,0 @@ -package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell; - -import ru.fizteh.fivt.storage.structured.Storeable; -import ru.fizteh.fivt.storage.structured.Table; -import ru.fizteh.fivt.storage.structured.TableProvider; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.StoreableTableImpl; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.DatabaseIOException; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.InvocationException; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.NoActiveTableException; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.TerminalException; - -import java.io.IOException; -import java.text.ParseException; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; - -public class Commands extends SimpleCommandContainer { - public static final Command COMMIT = - new AbstractCommand( - null, "saves all changes made from the last commit", 1) { - @Override - public void executeSafely(SingleDatabaseShellState state, String[] args) - throws IOException, IllegalArgumentException { - int changes = state.getActiveDatabase().commit(); - state.getOutputStream().println(changes); - } - }; - public static final Command ROLLBACK = - new AbstractCommand( - null, "cancels all changes made from the last commit", 1) { - - @Override - public void executeSafely(SingleDatabaseShellState state, String[] args) - throws DatabaseIOException, IllegalArgumentException { - int changes = state.getActiveDatabase().rollback(); - state.getOutputStream().println(changes); - } - }; - public static final Command SIZE = - new AbstractCommand( - null, "prints count of stored keys in current table", 1) { - @Override - public void executeSafely(SingleDatabaseShellState state, String[] args) - throws DatabaseIOException, IllegalArgumentException, NoActiveTableException { - int size = state.getActiveDatabase().getActiveTable().size(); - state.getOutputStream().println(size); - } - }; - public static final Command CREATE = - new AbstractCommand( - " (type0 type1 ... typeN)", - "creates a new table with the given name and column types (must be specified inside " - + "round " - + "brackets); type can be one of the following: int, long, byte, float, double, " - + "boolean, String;", - 3, - Integer.MAX_VALUE) { - @Override - public void executeSafely(SingleDatabaseShellState state, String[] args) - throws IOException, InvocationException { - if (!args[2].startsWith("(") || !args[args.length - 1].endsWith(")")) { - throw new InvocationException( - this, - args[0], - "Round brackets must exist and contain types list inside them"); - } - - // Joining strings. - String typesString = String.join(" ", Arrays.asList(args).subList(2, args.length)); - - // Removing brackets. - typesString = typesString.substring(1, typesString.length() - 1).trim(); - - List> columnTypes = StoreableTableImpl.parseColumnTypes(typesString); - String tableName = args[1]; - - boolean created = state.getActiveDatabase().createTable(tableName, columnTypes); - if (created) { - state.getOutputStream().println("created"); - } else { - throw new DatabaseIOException(tableName + " exists"); - } - } - }; - public static final Command DROP = - new AbstractCommand( - "", "deletes table with the given name from file system", 2) { - @Override - public void executeSafely(SingleDatabaseShellState state, final String[] args) - throws IOException { - state.getActiveDatabase().dropTable(args[1]); - state.getOutputStream().println("dropped"); - } - }; - public static final Command EXIT = - new AbstractCommand( - null, "saves all data to file system and stops interpretation", 1) { - @Override - public void execute(SingleDatabaseShellState state, String[] args) throws TerminalException { - int exitCode = 0; - - try { - state.persist(); - } catch (Exception exc) { - exitCode = 1; - DATABASE_ERROR_HANDLER.handleException(exc, state.getOutputStream()); - } finally { - state.prepareToExit(exitCode); - } - - // If all contracts are honoured, this line should not be reached. - throw new AssertionError("Exit request not thrown"); - } - - @Override - public void executeSafely(SingleDatabaseShellState state, String[] args) - throws DatabaseIOException, IllegalArgumentException { - // Not used. - } - }; - public static final Command GET = - new AbstractCommand("", "obtains value by the key", 2) { - @Override - public void executeSafely(SingleDatabaseShellState state, final String[] args) - throws NoActiveTableException { - String key = args[1]; - - Table table = state.getActiveTable(); - TableProvider provider = state.getProvider(); - - Storeable value = table.get(key); - - if (value == null) { - throw new IllegalArgumentException("not found"); - } else { - state.getOutputStream().println("found"); - state.getOutputStream().println(provider.serialize(table, value)); - } - } - }; - public static final Command HELP = - new AbstractCommand( - null, "prints out description of state commands", 1, Integer.MAX_VALUE) { - @Override - public void execute(SingleDatabaseShellState state, String[] args) { - Map> commands = state.getCommands(); - - state.getOutputStream().println( - "DatabaseLibrary is an utility that lets you work with a simple database"); - - state.getOutputStream().println( - String.format( - "You can set database directory to work with using environment " - + "variable '%s'", SingleDatabaseShellState.DB_DIRECTORY_PROPERTY_NAME)); - - for (Entry> cmdEntry : commands.entrySet()) { - String cmdName = cmdEntry.getKey(); - Command command = cmdEntry.getValue(); - - state.getOutputStream().println(command.buildHelpLine(cmdName)); - } - } - - @Override - public void executeSafely(SingleDatabaseShellState state, String[] args) - throws DatabaseIOException { - // not used - } - }; - public static final Command LIST = - new AbstractCommand(null, "prints all keys stored in the map", 1) { - @Override - public void executeSafely(SingleDatabaseShellState state, String[] args) - throws NoActiveTableException { - List keySet = state.getActiveTable().list(); - StringBuilder sb = new StringBuilder(); - - boolean comma = false; - - for (String key : keySet) { - sb.append(comma ? ", " : "").append(key); - comma = true; - } - - state.getOutputStream().println(sb); - } - }; - public static final Command PUT = new AbstractCommand( - " [ {boolean|number|string|null}... ]", - "assigns new storeable to the key", - 3, - Integer.MAX_VALUE) { - @Override - public void executeSafely(SingleDatabaseShellState state, String[] args) - throws NoActiveTableException, ParseException { - String key = args[1]; - - Table table = state.getActiveTable(); - - String valueStr = String.join(" ", Arrays.asList(args).subList(2, args.length)); - Storeable value = state.getProvider().deserialize(table, valueStr); - - Storeable oldValue = state.getActiveTable().put(key, value); - - if (oldValue == null) { - state.getOutputStream().println("new"); - } else { - String oldValueStr = state.getProvider().serialize(table, oldValue); - state.getOutputStream().println("overwrite"); - state.getOutputStream().println("old " + oldValueStr); - } - } - }; - public static final Command REMOVE = - new AbstractCommand("", "removes value by the key", 2) { - @Override - public void executeSafely(SingleDatabaseShellState state, String[] args) - throws DatabaseIOException, NoActiveTableException { - String key = args[1]; - - Storeable oldValue = state.getActiveTable().remove(key); - - if (oldValue == null) { - throw new DatabaseIOException("not found"); - } else { - state.getOutputStream().println("removed"); - } - } - }; - public static final Command SHOW = - new AbstractCommand( - "tables", "prints info on all tables assigned to the working database", 2) { - @Override - public void executeSafely(SingleDatabaseShellState state, String[] args) - throws IllegalArgumentException { - switch (args[1]) { - case "tables": { - state.getActiveDatabase().showTables(); - break; - } - default: { - throw new IllegalArgumentException("show: unexpected option: " + args[1]); - } - } - } - }; - public static final Command USE = new AbstractCommand( - "", - "saves all changes made to the current table (if present) and makes table" - + " with the given name the current one", - 2) { - @Override - public void executeSafely(final SingleDatabaseShellState state, final String[] args) - throws IOException { - state.getActiveDatabase().useTable(args[1]); - state.getOutputStream().println("using " + args[1]); - } - }; - private static final Commands INSTANCE = new Commands(); - - private Commands() { - - } - - public static Commands obtainInstance() { - return INSTANCE; - } -} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/LoggingProxyFactoryImpl.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/LoggingProxyFactoryImpl.java deleted file mode 100644 index d3fb1ebdf..000000000 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/LoggingProxyFactoryImpl.java +++ /dev/null @@ -1,194 +0,0 @@ -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/telnet/Server.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/Server.java deleted file mode 100644 index 10d727ef4..000000000 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/Server.java +++ /dev/null @@ -1,153 +0,0 @@ -package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet; - -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.TerminalException; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.ShellState; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Log; - -import java.io.Closeable; -import java.io.IOException; -import java.io.PrintStream; -import java.net.ServerSocket; -import java.net.Socket; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Set; -import java.util.function.Supplier; - -/** - * Server that listens to connections and creates communicator threads. - */ -public class Server implements Closeable { - private final PrintStream reportStream; - private final Set users; - private final Supplier shellStateSupplier; - private ServerSocket serverSocket; - private boolean deletingFromUsers = false; - - public Server(PrintStream reportStream, Supplier shellStateSupplier) { - this.reportStream = reportStream; - this.shellStateSupplier = shellStateSupplier; - users = new HashSet<>(); - } - - public PrintStream getReportStream() { - return reportStream; - } - - public ServerSocket getServerSocket() { - return serverSocket; - } - - /** - * Called by ServerCommunicator after it is closed. - */ - synchronized void onConnectionClosed(ServerCommunicator communicator) { - if (!deletingFromUsers) { - users.remove(communicator); - } - } - - public synchronized boolean isStarted() { - return serverSocket != null; - } - - public synchronized void startServer(int port) throws IOException, IllegalStateException { - if (serverSocket != null) { - throw new IllegalStateException("not started: already started"); - } - - try { - serverSocket = new ServerSocket(port); - } catch (IOException exc) { - throw new IOException("not started: " + exc.getMessage(), exc.getCause()); - } - - Thread serverThread = new Thread(this::run, this + ": server thread"); - serverThread.setDaemon(true); - serverThread.setPriority(Thread.MIN_PRIORITY); - serverThread.start(); - } - - public synchronized void stopServerIfStarted() throws IOException { - if (isStarted()) { - close(); - } - } - - public synchronized int stopServer() throws IOException, IllegalStateException { - checkStarted(); - int port = getServerSocket().getLocalPort(); - close(); - return port; - } - - public synchronized List listUsers() { - checkStarted(); - return new LinkedList<>(users); - } - - private void run() { - try { - while (!Thread.currentThread().isInterrupted()) { - ServerSocket serverSocketTemp; - - synchronized (this) { - serverSocketTemp = serverSocket; - } - - if (serverSocketTemp == null) { - return; - } - - Socket socket = serverSocketTemp.accept(); - ServerCommunicator communicator = new ServerCommunicator(socket, this, shellStateSupplier); - Thread communicatorThread = new Thread(communicator, "Communicator with " + communicator); - communicatorThread.setDaemon(true); - communicatorThread.start(); - - synchronized (this) { - users.add(communicator); - } - } - } catch (IOException exc) { - Log.log(Server.class, exc, "Error on listening to incoming connections"); - } catch (TerminalException exc) { - // Already handled. - } finally { - try { - close(); - } catch (IOException exc) { - Log.log(ServerState.class, exc, this + ": failed to close server socket"); - } - } - } - - private void checkStarted() { - if (serverSocket == null) { - throw new IllegalStateException("not started"); - } - } - - @Override - public synchronized void close() throws IOException { - if (serverSocket != null) { - deletingFromUsers = true; - for (ServerCommunicator communicator : users) { - try { - communicator.close(); - } catch (IOException exc) { - Log.log(Server.class, exc, "Error on closing socket: " + communicator); - } - } - users.clear(); - deletingFromUsers = false; - - try { - serverSocket.close(); - } finally { - serverSocket = null; - } - } - } -} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/ServerCommands.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/ServerCommands.java deleted file mode 100644 index cb20fb972..000000000 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/ServerCommands.java +++ /dev/null @@ -1,143 +0,0 @@ -package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet; - -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.DatabaseIOException; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.InvocationException; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.NoActiveTableException; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.TerminalException; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.AbstractCommand; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Command; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.SimpleCommandContainer; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.SingleDatabaseShellState; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet.ServerState.User; - -import java.io.IOException; -import java.text.ParseException; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; - -/** - * Container for server commands: start, stop, listusers. - */ -public class ServerCommands extends SimpleCommandContainer { - public static final Command STOP = new AbstractCommand("", "stops server", 1) { - @Override - public void executeSafely(ServerState state, String[] args) throws - IllegalArgumentException, - NoActiveTableException, - IllegalStateException, - NullPointerException, - InvocationException, - ParseException, - IOException { - int port = state.stopServer(); - state.getOutputStream().println("stopped at " + port); - } - }; - public static final Command LISTUSERS = new AbstractCommand( - "", "prints list of ip addresses and ports of connected users", 1) { - @Override - public void executeSafely(ServerState state, String[] args) throws - IllegalArgumentException, - NoActiveTableException, - IllegalStateException, - NullPointerException, - InvocationException, - ParseException, - IOException { - List users = state.listUsers(); - - StringBuilder sb = new StringBuilder(); - for (User user : users) { - sb.append(user); - sb.append(System.lineSeparator()); - } - state.getOutputStream().print(sb.toString()); - } - }; - public static final Command EXIT = new AbstractCommand( - "", "stops server (if it is started) and closes the terminal", 1) { - @Override - public void execute(ServerState state, String[] args) throws TerminalException { - state.prepareToExit(0); - - // If all contracts are honoured, this line should not be reached. - throw new AssertionError("Exit request not thrown"); - } - - @Override - public void executeSafely(ServerState state, String[] args) throws - IllegalArgumentException, - NoActiveTableException, - IllegalStateException, - NullPointerException, - InvocationException, - ParseException, - IOException { - // Not used. - } - }; - public static final Command HELP = new AbstractCommand( - "", "prints out description of state commands", 1, Integer.MAX_VALUE) { - @Override - public void execute(ServerState state, String[] args) { - Map> commands = state.getCommands(); - - state.getOutputStream().println( - "You can start telnet database server ready for new connections!"); - - state.getOutputStream().println( - String.format( - "You can set database directory to work with using environment " - + "variable '%s'", SingleDatabaseShellState.DB_DIRECTORY_PROPERTY_NAME)); - - for (Entry> cmdEntry : commands.entrySet()) { - String cmdName = cmdEntry.getKey(); - Command command = cmdEntry.getValue(); - - state.getOutputStream().println(command.buildHelpLine(cmdName)); - } - } - - @Override - public void executeSafely(ServerState state, String[] args) throws DatabaseIOException { - // not used - } - }; - private static final int DEFAULT_PORT = 10001; - public static final Command START = new AbstractCommand( - "[port]", - "starts server at the specified port (or, if not specified, at " + DEFAULT_PORT + ")", - 1, - 2) { - @Override - public void executeSafely(ServerState state, String[] args) throws - IllegalArgumentException, - NoActiveTableException, - IllegalStateException, - InvocationException, - ParseException, - IOException { - int port; - if (args.length == 1) { - port = DEFAULT_PORT; - } else { - port = Integer.parseInt(args[1]); - } - - state.startServer(port); - state.getOutputStream().println("started at " + port); - } - }; - private static final ServerCommands INSTANCE = new ServerCommands(); - - /** - * Not for initializing. - */ - private ServerCommands() { - } - - public static ServerCommands obtainInstance() { - return INSTANCE; - } -} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/ServerCommunicator.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/ServerCommunicator.java deleted file mode 100644 index 05cf32359..000000000 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/ServerCommunicator.java +++ /dev/null @@ -1,85 +0,0 @@ -package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet; - -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.TerminalException; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Shell; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.ShellState; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Log; - -import java.io.Closeable; -import java.io.IOException; -import java.io.PrintStream; -import java.net.Socket; -import java.util.function.Supplier; - -public class ServerCommunicator implements Runnable, Closeable { - private final Socket socket; - - private final Shell interpreter; - - private final PrintStream reportStream; - - private final Server server; - - public ServerCommunicator(Socket socket, Server server, Supplier shellStateSupplier) - throws IOException, TerminalException { - this.socket = socket; - this.server = server; - this.reportStream = server.getReportStream(); - this.interpreter = new Shell<>(shellStateSupplier.get(), socket.getOutputStream()); - } - - public Socket getSocket() { - return socket; - } - - @Override - public void run() { - try { - interpreter.run(socket.getInputStream()); - } catch (TerminalException exc) { - Log.log(ServerCommunicator.class, exc, "Interpreter run terminated"); - // Already handled. - } catch (IOException exc) { - reportStream.println(this + ": Error on obtaining socket input stream: " + exc.getMessage()); - } finally { - try { - close(); - } catch (IOException exc) { - Log.log( - ServerState.class, this + " failed to close connection after all commands execution"); - } - - } - } - - private void notifyConnectionClosed() { - server.onConnectionClosed(this); - } - - @Override - public synchronized void close() throws IOException { - if (!socket.isClosed()) { - socket.close(); - notifyConnectionClosed(); - } - } - - @Override - public int hashCode() { - return socket.hashCode(); - } - - @Override - public boolean equals(Object obj) { - if (obj instanceof ServerCommunicator) { - ServerCommunicator user = (ServerCommunicator) obj; - return socket.equals(user.socket); - } - return false; - } - - @Override - public String toString() { - return socket.toString(); - } -} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/ServerState.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/ServerState.java deleted file mode 100644 index 5f4092c16..000000000 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/ServerState.java +++ /dev/null @@ -1,110 +0,0 @@ -package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet; - -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.ExitRequest; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.BaseShellState; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Command; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Shell; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.ShellState; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Log; - -import java.io.IOException; -import java.net.InetAddress; -import java.util.List; -import java.util.Map; -import java.util.function.Supplier; -import java.util.stream.Collectors; - -public class ServerState extends BaseShellState { - private static final ServerCommands COMMANDS = ServerCommands.obtainInstance(); - private final Supplier clientShellStateSupplier; - private Server server; - - public ServerState(Supplier clientShellStateSupplier) { - this.clientShellStateSupplier = clientShellStateSupplier; - } - - public void startServer(int port) throws IOException, IllegalStateException { - checkInitialized(); - server.startServer(port); - } - - public int stopServer() throws IOException, NullPointerException { - checkInitialized(); - return server.stopServer(); - } - - public void stopServerIfStarted() throws IOException { - checkInitialized(); - server.stopServerIfStarted(); - } - - public int getServerPort() { - checkInitialized(); - return server.getServerSocket().getLocalPort(); - } - - public List listUsers() { - checkInitialized(); - return server.listUsers().stream().map( - (ServerCommunicator communicator) -> new User( - communicator.getSocket().getInetAddress(), communicator.getSocket().getPort())) - .collect(Collectors.toList()); - } - - @Override - public void cleanup() { - // Nothing to clean. - } - - @Override - public void persist() throws Exception { - // Nothing to persist. - } - - @Override - public void prepareToExit(int exitCode) throws ExitRequest { - try { - stopServerIfStarted(); - } catch (IOException exc) { - Log.log(ServerState.class, exc); - if (exitCode == 0) { - exitCode = -1; - } - } - throw new ExitRequest(exitCode); - } - - @Override - public void init(Shell host) throws Exception { - super.init(host); - server = new Server(host.getOutputStream(), clientShellStateSupplier); - } - - @Override - public Map> getCommands() { - return COMMANDS.getCommands(); - } - - public static class User { - private final InetAddress address; - private final int port; - - public User(InetAddress address, int port) { - this.address = address; - this.port = port; - } - - public InetAddress getAddress() { - return address; - } - - public int getPort() { - return port; - } - - @Override - public String toString() { - return address.getHostAddress() + ":" + port; - } - } -} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/LoggingProxyFactoryTest.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/LoggingProxyFactoryTest.java deleted file mode 100644 index b79c9a9eb..000000000 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/LoggingProxyFactoryTest.java +++ /dev/null @@ -1,179 +0,0 @@ -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); - } - } - } -} From 66bed7075e527e115defb31bb44a206f51d9b897 Mon Sep 17 00:00:00 2001 From: AndreosGhost Date: Sun, 21 Dec 2014 11:57:25 +0300 Subject: [PATCH 09/14] Implemented remote's proper close(). --- .../databaselibrary/db/DBTableProvider.java | 2 +- .../db/remote/IRemoteTableProvider.java | 4 +- .../remote/IRemoteTableProviderFactory.java | 5 +- .../db/remote/RemoteDatabaseStorage.java | 52 +++- .../db/remote/RemoteTableImpl.java | 6 +- .../RemoteTableProviderFactoryImpl.java | 36 ++- .../db/remote/RemoteTableProviderImpl.java | 29 ++- .../db/remote/RemoteTableProviderStub.java | 225 +++++++++++++++--- .../db/remote/RemoteTableStub.java | 177 +++++++++++--- .../exception/UnexpectedRemoteException.java | 9 + .../shell/AbstractCommand.java | 4 +- .../databaselibrary/shell/JoinedState.java | 95 ++++++-- .../shell/SingleDBCommands.java | 2 +- .../shell/SingleDatabaseShellState.java | 2 +- .../support/ValidityController.java | 39 +-- .../telnet/client/ClientCommands.java | 97 ++++---- .../telnet/client/ClientGeneralState.java | 8 +- .../telnet/client/DBClientState.java | 80 +++++++ .../telnet/server/DBServerState.java | 26 +- .../test/DatabaseShellTest.java | 5 +- .../test/RemoteDataStorageTest.java | 139 +++++++++++ .../test/TableProviderTest.java | 5 +- 22 files changed, 827 insertions(+), 220 deletions(-) create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/exception/UnexpectedRemoteException.java create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/client/DBClientState.java create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/RemoteDataStorageTest.java 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 d29a04dab..580d23192 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 @@ -371,7 +371,7 @@ private void loadTable(Path tablePath) { * @throws DatabaseIOException */ private void loadMissingTables() throws DatabaseIOException { - loadTables(tables::containsKey); + loadTables((name) -> !tables.containsKey(name)); } private void reloadAllTables() throws DatabaseIOException { diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/IRemoteTableProvider.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/IRemoteTableProvider.java index 1a25e536c..5bede51c7 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/IRemoteTableProvider.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/IRemoteTableProvider.java @@ -23,7 +23,7 @@ interface IRemoteTableProvider extends Remote { * @throws IllegalArgumentException * Если название таблицы null или имеет недопустимое значение. */ - Table getTable(String name) throws RemoteException; + RemoteTableStub getTable(String name) throws RemoteException; /** * Создаёт таблицу с указанным названием. @@ -39,7 +39,7 @@ interface IRemoteTableProvider extends Remote { * @throws java.io.IOException * При ошибках ввода/вывода. */ - Table createTable(String name, List> columnTypes) throws IOException; + RemoteTableStub createTable(String name, List> columnTypes) throws IOException; /** * Удаляет существующую таблицу с указанным названием. diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/IRemoteTableProviderFactory.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/IRemoteTableProviderFactory.java index 0f3380fbd..2ab933c82 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/IRemoteTableProviderFactory.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/IRemoteTableProviderFactory.java @@ -1,6 +1,5 @@ package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.remote; -import ru.fizteh.fivt.storage.structured.RemoteTableProvider; import ru.fizteh.fivt.storage.structured.TableProvider; import java.io.Closeable; @@ -8,8 +7,8 @@ import java.rmi.Remote; import java.rmi.registry.Registry; -public interface IRemoteTableProviderFactory extends Remote, Closeable { - RemoteTableProvider obtainRemoteProvider() throws IOException; +interface IRemoteTableProviderFactory extends Remote, Closeable { + RemoteTableProviderStub obtainRemoteProvider() throws IOException; default TableProvider establishStorage(String localDatabaseRoot) throws IOException { return establishStorage(localDatabaseRoot, Registry.REGISTRY_PORT); diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteDatabaseStorage.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteDatabaseStorage.java index 2f727cd99..d5fbe4dc1 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteDatabaseStorage.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteDatabaseStorage.java @@ -4,17 +4,67 @@ import ru.fizteh.fivt.storage.structured.RemoteTableProviderFactory; import java.io.IOException; +import java.net.InetAddress; +import java.net.UnknownHostException; import java.rmi.NotBoundException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; +import java.util.HashMap; +import java.util.Map; /** * Class used to connect to a remote database storage and obtain providers. */ public class RemoteDatabaseStorage implements RemoteTableProviderFactory { + // There is no read-write sync, because obtaining many providers seem to be not so popular as + // obtaining many tables. + + private final Map providerStubs = new HashMap<>(); + + private boolean providerStubClosedByMe = false; public RemoteDatabaseStorage() { + } + + private String makeLocation(String hostname, int port) throws UnknownHostException { + return InetAddress.getByName(hostname).getHostAddress() + ":" + port; + } + + void onProviderStubClosed(String locationInStorage) { + synchronized (providerStubs) { + if (!providerStubClosedByMe) { + providerStubs.remove(locationInStorage); + } + } + } + @Override + protected void finalize() throws Throwable { + super.finalize(); + synchronized (providerStubs) { + providerStubClosedByMe = true; + providerStubs.values().forEach(RemoteTableProviderStub::close); + providerStubs.clear(); + } + } + + private RemoteTableProviderStub tryReplaceProviderStub(String location, + RemoteTableProviderStub providerStub) { + if (providerStub == null) { + return null; + } + + synchronized (providerStubs) { + RemoteTableProviderStub oldStub = providerStubs.get(location); + + if (oldStub == null) { + providerStubs.put(location, providerStub); + providerStub.bindToStorage(this, location); + return providerStub; + } else { + return oldStub; + } + } } @Override @@ -23,7 +73,7 @@ public RemoteTableProvider connect(String hostname, int port) throws IOException try { IRemoteTableProviderFactory factory = (IRemoteTableProviderFactory) registry .lookup(RemoteTableProviderFactoryImpl.FACTORY_NAME); - return factory.obtainRemoteProvider(); + return tryReplaceProviderStub(makeLocation(hostname, port), factory.obtainRemoteProvider()); } catch (NotBoundException exc) { // This cannot happen. throw new RuntimeException(exc); diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableImpl.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableImpl.java index 03f046a49..d58675997 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableImpl.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableImpl.java @@ -9,8 +9,12 @@ import java.rmi.server.UnicastRemoteObject; import java.util.List; +/** + * Remote table object that is stored on server and available through RMI mechanism. + */ class RemoteTableImpl extends UnicastRemoteObject implements IRemoteTable { - private final transient Table table; + + private final Table table; public RemoteTableImpl(Table table) throws RemoteException { this.table = table; diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableProviderFactoryImpl.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableProviderFactoryImpl.java index cfbed8d99..ebaecb7e1 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableProviderFactoryImpl.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableProviderFactoryImpl.java @@ -1,6 +1,5 @@ package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.remote; -import ru.fizteh.fivt.storage.structured.RemoteTableProvider; import ru.fizteh.fivt.storage.structured.TableProvider; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.AutoCloseableTableProviderFactory; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.DBTableProviderFactory; @@ -25,14 +24,20 @@ public class RemoteTableProviderFactoryImpl extends UnicastRemoteObject private AutoCloseableTableProviderFactory factory; + /** + * Server-local table provider. + */ private TableProvider provider; - private RemoteTableProvider remoteProvider; + /** + * Server-local singleton implementation of remote table provider that is wrapped in stub. + */ + private IRemoteTableProvider remoteProvider; public RemoteTableProviderFactoryImpl() throws RemoteException { } - public TableProvider getProvider() { + public synchronized TableProvider getProvider() { return provider; } @@ -47,7 +52,7 @@ private void requireBound() throws IllegalStateException { } @Override - public void close() throws IOException { + public synchronized void close() throws IOException { if (!isBound()) { return; } @@ -59,17 +64,20 @@ public void close() throws IOException { } finally { factory = null; provider = null; + remoteProvider = null; registry = null; } } @Override - public RemoteTableProvider obtainRemoteProvider() throws IOException { - return remoteProvider; + public synchronized RemoteTableProviderStub obtainRemoteProvider() throws IOException { + requireBound(); + return new RemoteTableProviderStub(remoteProvider); } @Override - public TableProvider establishStorage(String localDatabaseRoot, int port) throws IOException { + public synchronized TableProvider establishStorage(String localDatabaseRoot, int port) + throws IOException { if (isBound()) { throw new IllegalStateException("Factory already established"); } @@ -79,13 +87,15 @@ public TableProvider establishStorage(String localDatabaseRoot, int port) throws provider = factory.create(localDatabaseRoot); try { - registry = LocateRegistry.createRegistry(port); - } catch (RemoteException exc) { registry = LocateRegistry.getRegistry(port); + registry.rebind(FACTORY_NAME, this); + + } catch (RemoteException exc) { + registry = LocateRegistry.createRegistry(port); + registry.rebind(FACTORY_NAME, this); } - registry.rebind(FACTORY_NAME, this); - remoteProvider = new RemoteTableProviderStub(new RemoteTableProviderImpl(provider)); + remoteProvider = new RemoteTableProviderImpl(provider); return provider; } catch (Exception exc) { @@ -97,14 +107,14 @@ public TableProvider establishStorage(String localDatabaseRoot, int port) throws factory = null; provider = null; registry = null; + remoteProvider = null; } catch (Exception ignored) { Log.log( RemoteTableProviderFactoryImpl.class, ignored, "Failed to cleanup after getting exception on establishment"); - } finally { - throw exc; } + throw exc; } } } diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableProviderImpl.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableProviderImpl.java index b5cc44297..42cdf7501 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableProviderImpl.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableProviderImpl.java @@ -4,6 +4,7 @@ import ru.fizteh.fivt.storage.structured.Storeable; import ru.fizteh.fivt.storage.structured.Table; import ru.fizteh.fivt.storage.structured.TableProvider; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.UnexpectedRemoteException; import java.io.IOException; import java.rmi.RemoteException; @@ -11,31 +12,35 @@ import java.text.ParseException; import java.util.List; +/** + * Remote table provider that is created at server (once) and can be exploited by clients. + */ class RemoteTableProviderImpl extends UnicastRemoteObject implements IRemoteTableProvider { + // Even if we keep the match (pure table -> table stub), the same stubs sent to the client will be + // deserealized as different instances. So, we do not keep this match. + private final TableProvider provider; public RemoteTableProviderImpl(TableProvider provider) throws RemoteException { this.provider = provider; } - private Table wrapIntoRemote(Table table) throws RemoteException { - return table == null ? null : new RemoteTableStub(new RemoteTableImpl(table)); - } - - @Override - public Table getTable(String name) { + private RemoteTableStub wrapInStub(Table table) throws UnexpectedRemoteException { try { - Table table = provider.getTable(name); - return wrapIntoRemote(table); + return new RemoteTableStub(new RemoteTableImpl(table), (table == null ? null : table.getName())); } catch (RemoteException exc) { - throw new RuntimeException(exc.getMessage(), exc.getCause()); + throw new UnexpectedRemoteException(exc); } } @Override - public Table createTable(String name, List> columnTypes) throws IOException { - Table table = provider.createTable(name, columnTypes); - return wrapIntoRemote(table); + public RemoteTableStub getTable(String name) { + return wrapInStub(provider.getTable(name)); + } + + @Override + public RemoteTableStub createTable(String name, List> columnTypes) throws IOException { + return wrapInStub(provider.createTable(name, columnTypes)); } @Override diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableProviderStub.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableProviderStub.java index e78fa3de8..9b975bbc6 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableProviderStub.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableProviderStub.java @@ -4,99 +4,260 @@ import ru.fizteh.fivt.storage.structured.RemoteTableProvider; import ru.fizteh.fivt.storage.structured.Storeable; import ru.fizteh.fivt.storage.structured.Table; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.InvalidatedObjectException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.UnexpectedRemoteException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.ValidityController; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.ValidityController.KillLock; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.ValidityController.UseLock; import java.io.IOException; import java.io.Serializable; import java.rmi.RemoteException; import java.text.ParseException; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; /** - * Stub of remote table provider that is transferred from server to client.
    - * I created this stub because task API is not friendly and there are no RemoteExceptions. + * Stub of remote table provider that is exploited on the client side. */ -class RemoteTableProviderStub implements RemoteTableProvider, Serializable { - private final IRemoteTableProvider provider; +final class RemoteTableProviderStub implements RemoteTableProvider, Serializable { + /** + * Wrapped remote table provider. + */ + private final IRemoteTableProvider remoteProvider; + /** + * Read-write sync on tableStubs. Because getTable() is very popular :) + */ + private final ReadWriteLock tableStubsLock = new ReentrantReadWriteLock(true); + /** + * For synchronization on client side. + */ + private transient ValidityController validityController; + /** + * Local stubs associated with this provider. + */ + private transient Map tableStubs; - public RemoteTableProviderStub(IRemoteTableProvider provider) { - this.provider = provider; + private transient boolean tableStubClosedByMe; + + /** + * Local storage stub that manages instances of {@link ru.fizteh.fivt.students.fedorov_andrew + * .databaselibrary.db.remote.RemoteTableProviderStub}. + */ + private transient RemoteDatabaseStorage storage; + + /** + * Location on storage that identifies this provider stub. + */ + private transient String locationInStorage; + + public RemoteTableProviderStub(IRemoteTableProvider remoteProvider) { + this.remoteProvider = remoteProvider; + } + + public void bindToStorage(RemoteDatabaseStorage storage, String locationInStorage) { + this.storage = storage; + this.locationInStorage = locationInStorage; + + // Initializing transient fields. + validityController = new ValidityController(); + tableStubs = new HashMap<>(); + tableStubClosedByMe = false; + } + + private RemoteTableStub tryReplaceTableStub(String name, RemoteTableStub tableStub) { + if (tableStub == null) { + return null; + } + + Lock lock = tableStubsLock.readLock(); + lock.lock(); + try { + RemoteTableStub oldStub = tableStubs.get(name); + if (oldStub == null) { + lock.unlock(); + lock = tableStubsLock.writeLock(); + lock.lock(); + oldStub = tableStubs.get(name); + if (oldStub == null) { + tableStubs.put(name, tableStub); + tableStub.bindToProviderStub(this); + return tableStub; + } else { + return oldStub; + } + } else { + return oldStub; + } + } finally { + lock.unlock(); + } + } + + private void removeTableStub(String tableName) { + tableStubsLock.writeLock().lock(); + try { + RemoteTableStub tableStub = tableStubs.get(tableName); + if (tableStub != null) { + tableStub.close(); + } + } finally { + tableStubsLock.writeLock().unlock(); + } + } + + public void onTableStubClosed(String tableName) { + tableStubsLock.writeLock().lock(); + try { + if (!tableStubClosedByMe) { + tableStubs.remove(tableName); + } + } finally { + tableStubsLock.writeLock().unlock(); + } } @Override - public void close() throws IOException { - // I can't influence rmi connections -> do nothing. + public void close() { + try (KillLock lock = validityController.useAndKill()) { + tableStubsLock.writeLock().lock(); + try { + tableStubClosedByMe = true; + tableStubs.values().forEach(RemoteTableStub::close); + storage.onProviderStubClosed(locationInStorage); + } finally { + tableStubsLock.writeLock().unlock(); + } + } + } + + @Override + protected void finalize() throws Throwable { + super.finalize(); + close(); + } + + private void forceClose(UseLock activeUseLock) { + try (KillLock killLock = activeUseLock.obtainKillLockInstead()) { + close(); + } } @Override public Table getTable(String name) { - try { - return provider.getTable(name); + try (UseLock lock = validityController.use()) { + try { + return tryReplaceTableStub(name, remoteProvider.getTable(name)); + } catch (InvalidatedObjectException exc) { + forceClose(lock); + throw exc; + } } catch (RemoteException exc) { - throw new RuntimeException(exc); + throw new UnexpectedRemoteException(exc); } } @Override public Table createTable(String name, List> columnTypes) throws IOException { - try { - return provider.createTable(name, columnTypes); + try (UseLock lock = validityController.use()) { + try { + return tryReplaceTableStub(name, remoteProvider.createTable(name, columnTypes)); + } catch (InvalidatedObjectException exc) { + forceClose(lock); + throw exc; + } } catch (RemoteException exc) { - throw new RuntimeException(exc); + throw new UnexpectedRemoteException(exc); } } @Override public void removeTable(String name) throws IOException { - try { - provider.removeTable(name); + try (UseLock lock = validityController.use()) { + try { + removeTableStub(name); + remoteProvider.removeTable(name); + } catch (InvalidatedObjectException exc) { + forceClose(lock); + throw exc; + } } catch (RemoteException exc) { - throw new RuntimeException(exc); + throw new UnexpectedRemoteException(exc); } } @Override public Storeable deserialize(Table table, String value) throws ParseException { - try { - return provider.deserialize(table, value); + try (UseLock lock = validityController.use()) { + try { + return remoteProvider.deserialize(table, value); + } catch (InvalidatedObjectException exc) { + forceClose(lock); + throw exc; + } } catch (RemoteException exc) { - throw new RuntimeException(exc); + throw new UnexpectedRemoteException(exc); } } @Override public String serialize(Table table, Storeable value) throws ColumnFormatException { - try { - return provider.serialize(table, value); + try (UseLock lock = validityController.use()) { + try { + return remoteProvider.serialize(table, value); + } catch (InvalidatedObjectException exc) { + forceClose(lock); + throw exc; + } } catch (RemoteException exc) { - throw new RuntimeException(exc); + throw new UnexpectedRemoteException(exc); } } @Override public Storeable createFor(Table table) { - try { - return provider.createFor(table); + try (UseLock lock = validityController.use()) { + try { + return remoteProvider.createFor(table); + } catch (InvalidatedObjectException exc) { + forceClose(lock); + throw exc; + } } catch (RemoteException exc) { - throw new RuntimeException(exc); + throw new UnexpectedRemoteException(exc); } } @Override public Storeable createFor(Table table, List values) throws ColumnFormatException, IndexOutOfBoundsException { - try { - return provider.createFor(table, values); + try (UseLock lock = validityController.use()) { + try { + return remoteProvider.createFor(table, values); + } catch (InvalidatedObjectException exc) { + forceClose(lock); + throw exc; + } } catch (RemoteException exc) { - throw new RuntimeException(exc); + throw new UnexpectedRemoteException(exc); } } @Override public List getTableNames() { - try { - return provider.getTableNames(); + try (UseLock lock = validityController.use()) { + try { + return remoteProvider.getTableNames(); + } catch (InvalidatedObjectException exc) { + forceClose(lock); + throw exc; + } } catch (RemoteException exc) { - throw new RuntimeException(exc); + throw new UnexpectedRemoteException(exc); } } } diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableStub.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableStub.java index cec9dab5b..4fea3964b 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableStub.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableStub.java @@ -3,7 +3,13 @@ import ru.fizteh.fivt.storage.structured.ColumnFormatException; import ru.fizteh.fivt.storage.structured.Storeable; import ru.fizteh.fivt.storage.structured.Table; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.InvalidatedObjectException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.UnexpectedRemoteException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.ValidityController; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.ValidityController.KillLock; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.ValidityController.UseLock; +import java.io.Closeable; import java.io.IOException; import java.io.Serializable; import java.rmi.RemoteException; @@ -13,109 +19,206 @@ * Stub of remote table that is transferred from server to client.
    * I created this stub because task API is not friendly and there are no RemoteExceptions. */ -class RemoteTableStub implements Table, Serializable { - private final IRemoteTable table; +final class RemoteTableStub implements Table, Serializable, Closeable { + /** + * Table name is cached for correct close handling at client's local. + */ + private final String tableName; - public RemoteTableStub(RemoteTableImpl table) { - this.table = table; + private final IRemoteTable remoteTable; + + /** + * Local variable at the client side. + */ + private transient RemoteTableProviderStub providerStub; + + /** + * For synchronization on client side. + */ + private transient ValidityController validityController; + + public RemoteTableStub(RemoteTableImpl remoteTable, String tableName) { + this.remoteTable = remoteTable; + this.tableName = tableName; + } + + public void bindToProviderStub(RemoteTableProviderStub providerStub) { + this.providerStub = providerStub; + + // Initializing transient fields. + validityController = new ValidityController(); + } + + @Override + public void close() { + try (KillLock lock = validityController.useAndKill()) { + providerStub.onTableStubClosed(tableName); + } + } + + @Override + protected void finalize() throws Throwable { + super.finalize(); + close(); + } + + private void forceClose(UseLock activeUseLock) { + try (KillLock killLock = activeUseLock.obtainKillLockInstead()) { + close(); + } } @Override public Storeable put(String key, Storeable value) throws ColumnFormatException { - try { - return table.put(key, value); + try (UseLock lock = validityController.use()) { + try { + return remoteTable.put(key, value); + } catch (InvalidatedObjectException exc) { + forceClose(lock); + throw exc; + } } catch (RemoteException exc) { - throw new RuntimeException(exc); + throw new UnexpectedRemoteException(exc); } } @Override public Storeable remove(String key) { - try { - return table.remove(key); + try (UseLock lock = validityController.use()) { + try { + return remoteTable.remove(key); + } catch (InvalidatedObjectException exc) { + forceClose(lock); + throw exc; + } } catch (RemoteException exc) { - throw new RuntimeException(exc); + throw new UnexpectedRemoteException(exc); } } @Override public int size() { - try { - return table.size(); + try (UseLock lock = validityController.use()) { + try { + return remoteTable.size(); + } catch (InvalidatedObjectException exc) { + forceClose(lock); + throw exc; + } } catch (RemoteException exc) { - throw new RuntimeException(exc); + throw new UnexpectedRemoteException(exc); } } @Override public List list() { - try { - return table.list(); + try (UseLock lock = validityController.use()) { + try { + return remoteTable.list(); + } catch (InvalidatedObjectException exc) { + forceClose(lock); + throw exc; + } } catch (RemoteException exc) { - throw new RuntimeException(exc); + throw new UnexpectedRemoteException(exc); } } @Override public int commit() throws IOException { - try { - return table.commit(); + try (UseLock lock = validityController.use()) { + try { + return remoteTable.commit(); + } catch (InvalidatedObjectException exc) { + forceClose(lock); + throw exc; + } } catch (RemoteException exc) { - throw new RuntimeException(exc); + throw new UnexpectedRemoteException(exc); } } @Override public int rollback() { - try { - return table.rollback(); + try (UseLock lock = validityController.use()) { + try { + return remoteTable.rollback(); + } catch (InvalidatedObjectException exc) { + forceClose(lock); + throw exc; + } } catch (RemoteException exc) { - throw new RuntimeException(exc); + throw new UnexpectedRemoteException(exc); } } @Override public int getNumberOfUncommittedChanges() { - try { - return table.getNumberOfUncommittedChanges(); + try (UseLock lock = validityController.use()) { + try { + return remoteTable.getNumberOfUncommittedChanges(); + } catch (InvalidatedObjectException exc) { + forceClose(lock); + throw exc; + } } catch (RemoteException exc) { - throw new RuntimeException(exc); + throw new UnexpectedRemoteException(exc); } } @Override public int getColumnsCount() { - try { - return table.getColumnsCount(); + try (UseLock lock = validityController.use()) { + try { + return remoteTable.getColumnsCount(); + } catch (InvalidatedObjectException exc) { + forceClose(lock); + throw exc; + } } catch (RemoteException exc) { - throw new RuntimeException(exc); + throw new UnexpectedRemoteException(exc); } } @Override public Class getColumnType(int columnIndex) throws IndexOutOfBoundsException { - try { - return table.getColumnType(columnIndex); + try (UseLock lock = validityController.use()) { + try { + return remoteTable.getColumnType(columnIndex); + } catch (InvalidatedObjectException exc) { + forceClose(lock); + throw exc; + } } catch (RemoteException exc) { - throw new RuntimeException(exc); + throw new UnexpectedRemoteException(exc); } } @Override public String getName() { - try { - return table.getName(); + try (UseLock lock = validityController.use()) { + try { + return remoteTable.getName(); + } catch (InvalidatedObjectException exc) { + forceClose(lock); + throw exc; + } } catch (RemoteException exc) { - throw new RuntimeException(exc); + throw new UnexpectedRemoteException(exc); } } @Override public Storeable get(String key) { - try { - return table.get(key); + try (UseLock lock = validityController.use()) { + try { + return remoteTable.get(key); + } catch (InvalidatedObjectException exc) { + forceClose(lock); + throw exc; + } } catch (RemoteException exc) { - throw new RuntimeException(exc); + throw new UnexpectedRemoteException(exc); } } } diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/exception/UnexpectedRemoteException.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/exception/UnexpectedRemoteException.java new file mode 100644 index 000000000..cecfcccba --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/exception/UnexpectedRemoteException.java @@ -0,0 +1,9 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception; + +import java.rmi.RemoteException; + +public class UnexpectedRemoteException extends RuntimeException { + public UnexpectedRemoteException(RemoteException exc) { + super(exc.getMessage(), exc.getCause()); + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/AbstractCommand.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/AbstractCommand.java index 3ab15723f..b23c18442 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 @@ -4,6 +4,7 @@ import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.InvocationException; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.NoActiveTableException; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.TerminalException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.UnexpectedRemoteException; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.WrongArgsNumberException; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.AccurateExceptionHandler; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Log; @@ -145,7 +146,8 @@ protected abstract void executeSafely(State state, String[] args) throws InvocationException, ParseException, IOException, - ExecutionNotPermittedException; + ExecutionNotPermittedException, + UnexpectedRemoteException; void checkArgsNumber(String[] args, int minimal, int maximal) throws WrongArgsNumberException { if (args.length < minimal || args.length > maximal) { diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/JoinedState.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/JoinedState.java index a987b260a..bb4b5b3cc 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/JoinedState.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/JoinedState.java @@ -11,46 +11,59 @@ import java.util.Map; import java.util.Objects; +/** + * State that encapsulates several states that can be used. You need to create shell interpreter only for the + * joined state. You can decide when and how to react to command invocation requests to any state.
    + * If a command during execution throws {@link ru.fizteh.fivt.students.fedorov_andrew.databaselibrary + * .exception.ExitRequest} it is not handled by this default implementation. + * @param + * extending state for which interpreter is going to be created. + */ public abstract class JoinedState> extends BaseShellState { + /** + * Wrapped commands + */ private Map> allCommands; private int statesCount; private AccurateExceptionHandler exceptionHandler; - public JoinedState(AccurateExceptionHandler exceptionHandler, - Map>... commandsMaps) { - this(exceptionHandler); - setAllCommands(commandsMaps); - } - - public JoinedState(AccurateExceptionHandler exceptionHandler) { - setExceptionHandler(exceptionHandler); - } - - public JoinedState(Map>... commandsMaps) { - setAllCommands(commandsMaps); - } - - public JoinedState() { - + /** + * Pure state with no commands and exception handler. + */ + protected JoinedState() { } + /** + * Obtains currently used exception handler. + */ public AccurateExceptionHandler getExceptionHandler() { return exceptionHandler; } + /** + * Sets the exception handler that is used to handle errors during command executions. + * @param exceptionHandler + * exception handler with no extra data. + */ protected void setExceptionHandler(AccurateExceptionHandler exceptionHandler) { this.exceptionHandler = exceptionHandler; } + /** + * Sets all acceptable commands that can be invoked. + * @param commandsMaps + * array of command maps. Each command map is associated with state id (that is equal to the + * order + * number in this parameters sequence) which is used further to distinguish commands. + */ protected final void setAllCommands(Map>... commandsMaps) { - this.statesCount = commandsMaps.length; Map> allCommandsMap = new HashMap<>(); - int id = 0; + int idCounter = 0; for (Map> commands : commandsMaps) { - int stateID = id; + int stateID = idCounter; commands.forEach( (name, command) -> { if (allCommandsMap.containsKey(name)) { @@ -59,10 +72,11 @@ protected final void setAllCommands(Map + * Exceptions thrown by these methods are handled with local exception handler.
    + * As soon as all possible state commands wrapped by one multicommand have the same name, {@link + * Command#getName()} returns that name, but both {@link Command#getInfo()} and {@link + * Command#getInvocation()} throw {@link UnsupportedOperationException}. + * @see #setExceptionHandler(AccurateExceptionHandler) + * @see #getExceptionHandler() + * @see #onExecuteRequested(int, Command, String[]) + * @see #onExecuteConflict(List, List, String[]) + */ @Override public Map> getCommands() { return allCommands; } + /** + * Convenience method. + * Equivalent to {@code Objects.equals(commandA.getName(), commandB.getName())}. + */ protected boolean areNamesEqual(Command commandA, Command commandB) { return Objects.equals(commandA.getName(), commandB.getName()); } /** * This method is called when there are several commands with the same name from different states. You - * decide how to resolve this conflict. + * decide how to resolve this conflict.
    + * The default implementation throws {@link java.lang.UnsupportedOperationException}.
    + * In this method you can also call {@link #onExecuteRequested(int, Command, String[])} after you have + * chosen for which state to call the command. * @throws Exception */ protected void onExecuteConflict(List stateIDs, List commands, String[] args) @@ -124,10 +158,18 @@ protected void onExecuteConflict(List stateIDs, List commands, throw new UnsupportedOperationException("Execute conflicts are not resolved"); } + /** + * Runs normal execution of the given state command. The default implementation is {@code + * command.execute(obtainState(stateID), args)}. + * @throws java.lang.Exception + */ protected void executeNormally(int stateID, Command command, String[] args) throws Exception { command.execute(obtainState(stateID), args); } + /** + * Obtains existing or new state for the given state id. + */ protected abstract ShellState obtainState(int stateID); /** @@ -144,7 +186,12 @@ protected void onExecuteRequested(int stateID, Command command, String[] args) t executeNormally(stateID, command, args); } - class CommandWrapper implements Command { + /** + * Command for {@link ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.JoinedState} that + * wraps + * at least one command from a state. + */ + class CommandWrapper implements Command { private List states = new LinkedList<>(); private List commands = new LinkedList<>(); @@ -183,12 +230,12 @@ public String getName() { @Override public String getInfo() { - throw new UnsupportedOperationException("You cannot operate with multi command directly."); + throw new UnsupportedOperationException("You cannot operate with command wrapper directly."); } @Override public String getInvocation() { - throw new UnsupportedOperationException("You cannot operate with multi command directly."); + throw new UnsupportedOperationException("You cannot operate with command wrapper directly."); } } } diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/SingleDBCommands.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/SingleDBCommands.java index 6b7b99ed1..c98c586cd 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/SingleDBCommands.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/SingleDBCommands.java @@ -96,7 +96,7 @@ public void executeSafely(SingleDatabaseShellState state, final String[] args) }; public static final Command EXIT = new AbstractCommand( - "exit", null, "saves all data to file system and stops interpretation", 1) { + "exit", null, "rollbacks all changes and stops interpretation", 1) { @Override public void execute(SingleDatabaseShellState state, String[] args) throws TerminalException { state.prepareToExit(0); 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 352a1eb04..3ec155d8b 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 @@ -60,7 +60,7 @@ public void init(Shell host) throws Exception { @Override public void cleanup() { - + getActiveDatabase().rollback(); } @Override 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 index aa68e5b1b..6d71bbbf1 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/ValidityController.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/ValidityController.java @@ -14,20 +14,7 @@ public final class ValidityController { private void checkValid() { if (!valid) { - throw new InvalidatedObjectException("This object has been invalidated: "); - } - } - - /** - * Invalidates - */ - private void invalidate() { - validityLock.writeLock().lock(); - try { - checkValid(); - valid = false; - } finally { - validityLock.writeLock().unlock(); + throw new InvalidatedObjectException("Object has been invalidated"); } } @@ -65,17 +52,33 @@ public KillLock useAndKill() throws InvalidatedObjectException { } } - public class UseLock implements AutoCloseable { + public interface ValidityLock extends AutoCloseable { + @Override + void close(); + } + + public class UseLock implements ValidityLock { + private boolean allowMultipleCloseAttempts = false; + @Override public void close() { - validityLock.readLock().unlock(); + if (!allowMultipleCloseAttempts) { + validityLock.readLock().unlock(); + } + } + + public KillLock obtainKillLockInstead() { + close(); + allowMultipleCloseAttempts = true; + // Further close() calls (e.g., from try-with-resources) will be ignored for UseLock. + return useAndKill(); } } - public class KillLock implements AutoCloseable { + public class KillLock implements ValidityLock { @Override public void close() { - invalidate(); + valid = false; validityLock.writeLock().unlock(); } } diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/client/ClientCommands.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/client/ClientCommands.java index 9b9567b57..e0ec7ee97 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/client/ClientCommands.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/client/ClientCommands.java @@ -13,18 +13,18 @@ import java.text.ParseException; import java.util.Map; -public final class ClientCommands extends SimpleCommandContainer { - public static final Command CONNECT = new AbstractCommand( +public final class ClientCommands extends SimpleCommandContainer { + public static final Command CONNECT = new AbstractCommand( "connect", " ", "connects to a database storage server", 3) { @Override - public void executeSafely(ClientState state, String[] args) throws - IllegalArgumentException, - NoActiveTableException, - IllegalStateException, - NullPointerException, - InvocationException, - ParseException, - IOException { + public void executeSafely(DBClientState state, String[] args) throws + IllegalArgumentException, + NoActiveTableException, + IllegalStateException, + NullPointerException, + InvocationException, + ParseException, + IOException { int port; try { @@ -36,47 +36,50 @@ public void executeSafely(ClientState state, String[] args) throws state.getOutputStream().println("connected"); } }; - public static final Command DISCONNECT = - new AbstractCommand("disconnect", "", "disconnects from database storage", 1) { + public static final Command DISCONNECT = + new AbstractCommand("disconnect", "", "disconnects from database storage", 1) { @Override - public void executeSafely(ClientState state, String[] args) throws - IllegalArgumentException, - NoActiveTableException, - IllegalStateException, - NullPointerException, - InvocationException, - ParseException, - IOException { + public void executeSafely(DBClientState state, String[] args) throws + IllegalArgumentException, + NoActiveTableException, + IllegalStateException, + NullPointerException, + InvocationException, + ParseException, + IOException { state.disconnect(); state.getOutputStream().println("disconnected"); } }; - public static final Command WHEREAMI = - new AbstractCommand("whereami", "", "prints host and port you are connected to", 1) { - @Override - public void executeSafely(ClientState state, String[] args) throws - IllegalArgumentException, - NoActiveTableException, - IllegalStateException, - NullPointerException, - InvocationException, - ParseException, - IOException { - InetAddress address = state.getHost(); - int port = state.getPort(); + public static final Command WHEREAMI = new AbstractCommand( + "whereami", + "", + "prints host and port you are connected to", + 1) { + @Override + public void executeSafely(DBClientState state, String[] args) throws + IllegalArgumentException, + NoActiveTableException, + IllegalStateException, + NullPointerException, + InvocationException, + ParseException, + IOException { + InetAddress address = state.getHost(); + int port = state.getPort(); - if (address.isSiteLocalAddress()) { - state.getOutputStream().println("local " + port); - } else { - state.getOutputStream().println("remote " + address.getHostAddress() + ":" + port); - } - } - }; - public static final Command HELP = new AbstractCommand( + if (address.isLoopbackAddress()) { + state.getOutputStream().println("local " + port); + } else { + state.getOutputStream().println("remote " + address.getHostAddress() + ":" + port); + } + } + }; + public static final Command HELP = new AbstractCommand( "help", null, "prints out description of state commands", 1, Integer.MAX_VALUE) { @Override - public void execute(ClientState state, String[] args) { - Map> commands = state.getCommands(); + public void execute(DBClientState state, String[] args) { + Map> commands = state.getCommands(); state.getOutputStream().println( "You can connect to a database storage server"); @@ -85,14 +88,14 @@ public void execute(ClientState state, String[] args) { } @Override - public void executeSafely(ClientState state, String[] args) throws DatabaseIOException { + public void executeSafely(DBClientState state, String[] args) throws DatabaseIOException { // not used } }; - public static final Command EXIT = new AbstractCommand( + public static final Command EXIT = new AbstractCommand( "exit", null, "saves all data to file system and stops interpretation", 1) { @Override - public void execute(ClientState state, String[] args) throws TerminalException { + public void execute(DBClientState state, String[] args) throws TerminalException { state.prepareToExit(0); // If all contracts are honoured, this line should not be reached. @@ -100,7 +103,7 @@ public void execute(ClientState state, String[] args) throws TerminalException { } @Override - public void executeSafely(ClientState state, String[] args) + public void executeSafely(DBClientState state, String[] args) throws DatabaseIOException, IllegalArgumentException { // Not used. } diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/client/ClientGeneralState.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/client/ClientGeneralState.java index 1a7e09d9e..5cc90f008 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/client/ClientGeneralState.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/client/ClientGeneralState.java @@ -16,11 +16,12 @@ public abstract class ClientGeneralState extends JoinedState private static final int DB_STATE_ID = 0; private static final int CLIENT_STATE_ID = 1; - private final ClientState clientState = new ClientState(); + private final DBClientState clientState = new DBClientState(); private SingleDatabaseShellState databaseState; public ClientGeneralState() { - super(SingleDBCommands.getInstance().getCommands(), ClientCommands.getInstance().getCommands()); + setAllCommands( + SingleDBCommands.getInstance().getCommands(), ClientCommands.getInstance().getCommands()); } public boolean isConnected() { @@ -66,6 +67,9 @@ protected void onExecuteConflict(List stateIDs, List commands, if (clientState.isConnected()) { onExecuteRequested(DB_STATE_ID, databaseCommand, args); } + } else if (areNamesEqual(clientCommand, ClientCommands.EXIT) && areNamesEqual( + databaseCommand, SingleDBCommands.EXIT)) { + prepareToExit(0); } else { throw new UnsupportedOperationException( "Cannot resolve command conflict: " + clientCommand.getName()); diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/client/DBClientState.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/client/DBClientState.java new file mode 100644 index 000000000..bf959c89d --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/client/DBClientState.java @@ -0,0 +1,80 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet.client; + +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.ExitRequest; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.BaseShellState; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Command; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Log; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; +import java.util.Map; + +public class DBClientState extends BaseShellState { + private Socket connection; + + public void connect(String host, int port) throws IOException { + checkInitialized(); + if (connection == null) { + try { + connection = new Socket(host, port); + } catch (IOException exc) { + throw new IOException("not connected: " + exc.getMessage(), exc.getCause()); + } + } else { + throw new IllegalStateException("not connected: already connected"); + } + } + + public InetAddress getHost() { + requireConnected(); + return connection.getInetAddress(); + } + + public int getPort() { + requireConnected(); + return connection.getPort(); + } + + public void disconnect() throws IllegalStateException, IOException { + requireConnected(); + try { + connection.close(); + } finally { + connection = null; + } + } + + private void requireConnected() { + if (!isConnected()) { + throw new IllegalStateException("not connected"); + } + } + + public boolean isConnected() { + return connection != null; + } + + @Override + public void cleanup() { + try { + if (isConnected()) { + disconnect(); + } + } catch (IOException exc) { + Log.log(DBClientState.class, exc, "Exception occurred on cleanup"); + } + } + + @Override + public void prepareToExit(int exitCode) throws ExitRequest { + Log.log(DBClientState.class, "Preparing to exit with code:" + exitCode); + cleanup(); + throw new ExitRequest(exitCode); + } + + @Override + public Map> getCommands() { + return ClientCommands.getInstance().getCommands(); + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/server/DBServerState.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/server/DBServerState.java index 9aed67faa..d03eb9a3f 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/server/DBServerState.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/server/DBServerState.java @@ -4,7 +4,6 @@ import ru.fizteh.fivt.storage.structured.RemoteTableProviderFactory; import ru.fizteh.fivt.storage.structured.TableProvider; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.Database; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.remote.IRemoteTableProviderFactory; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.remote.RemoteDatabaseStorage; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.remote.RemoteTableProviderFactoryImpl; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.ExitRequest; @@ -28,7 +27,7 @@ public class DBServerState extends BaseShellState { private static final String DATABASE_ROOT_PROPERTY = "fizteh.db.dir"; private Supplier clientShellStateSupplier; private Server server; - private IRemoteTableProviderFactory factory; + private RemoteTableProviderFactoryImpl factory; private TableProvider provider; private String databaseRoot; @@ -73,9 +72,8 @@ public void startServer(int port) throws IOException, IllegalStateException { } } catch (Exception ignored) { Log.log(DBServerState.class, ignored, "Failed to close factory after server install failure"); - } finally { - throw exc; } + throw exc; } } @@ -114,19 +112,17 @@ public List listUsers() { @Override public void cleanup() { - // Nothing to clean. - } - - @Override - public void prepareToExit(int exitCode) throws ExitRequest { try { stopServerIfStarted(); } catch (Exception exc) { Log.log(DBServerState.class, exc); - if (exitCode == 0) { - exitCode = -1; - } } + } + + @Override + public void prepareToExit(int exitCode) throws ExitRequest { + Log.log("Preparing to stop server with exit code: " + exitCode); + cleanup(); throw new ExitRequest(exitCode); } @@ -136,12 +132,6 @@ public void init(Shell host) throws Exception { server = new Server(host.getOutputStream()); } - @Override - protected void finalize() throws Throwable { - super.finalize(); - stopServerIfStarted(); - } - @Override public Map> getCommands() { return COMMANDS.getCommands(); 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 90a04963c..ae72a567a 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 @@ -44,10 +44,7 @@ private void createTableWithStringColumn(String table) throws TerminalException @Test public void testPutCompositeStoreable() throws TerminalException { runBatchExpectZero( - "create t1 (String int boolean)", - "use t1", - "put key [ \"hello\", 2, null ]", - "commit"); + "create t1 (String int boolean)", "use t1", "put key [ \"hello\", 2, null ]", "commit"); assertThat("Output after put", getOutput(), containsString(makeTerminalExpectedMessage("new"))); diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/RemoteDataStorageTest.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/RemoteDataStorageTest.java new file mode 100644 index 000000000..bfcc2528f --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/RemoteDataStorageTest.java @@ -0,0 +1,139 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import ru.fizteh.fivt.storage.structured.RemoteTableProvider; +import ru.fizteh.fivt.storage.structured.Table; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.remote.RemoteDatabaseStorage; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.remote.RemoteTableProviderFactoryImpl; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.InvalidatedObjectException; + +import java.io.IOException; +import java.rmi.registry.Registry; +import java.util.Arrays; + +import static org.junit.Assert.*; + +@RunWith(JUnit4.class) +public class RemoteDataStorageTest extends TestBase { + @Rule + public ExpectedException exception = ExpectedException.none(); + private RemoteTableProviderFactoryImpl factory; + + @Before + public void prepare() throws Exception { + factory = new RemoteTableProviderFactoryImpl(); + factory.establishStorage(DB_ROOT.toString()); + } + + @After + public void cleanup() throws Exception { + factory.close(); + cleanDBRoot(); + } + + @Test + public void testProviderStubsAreSame() throws IOException { + RemoteDatabaseStorage storage = new RemoteDatabaseStorage(); + RemoteTableProvider providerA = storage.connect("localhost", Registry.REGISTRY_PORT); + RemoteTableProvider providerB = storage.connect("127.0.0.1", Registry.REGISTRY_PORT); + assertSame(providerA, providerB); + } + + @Test + public void testProviderStubsClosedProperly() throws IOException { + RemoteDatabaseStorage storage = new RemoteDatabaseStorage(); + RemoteTableProvider providerA = storage.connect("localhost", Registry.REGISTRY_PORT); + providerA.close(); + RemoteTableProvider providerB = storage.connect("localhost", Registry.REGISTRY_PORT); + + assertNotSame(providerA, providerB); + + // Check it is actual. + providerB.getTableNames(); + + exception.expect(InvalidatedObjectException.class); + + // It was closed = invalidated. + providerA.getTableNames(); + } + + @Test + public void testTableStubsAreSame() throws Exception { + RemoteDatabaseStorage storage = new RemoteDatabaseStorage(); + RemoteTableProvider provider = storage.connect("localhost", Registry.REGISTRY_PORT); + + String tableName = "table"; + + Table remoteTableA = provider.createTable(tableName, Arrays.asList(String.class)); + Table remoteTableB = provider.getTable(tableName); + Table remoteTableC = provider.getTable(tableName); + + assertSame(remoteTableA, remoteTableB); + assertSame(remoteTableB, remoteTableC); + } + + @Test + public void testServerTableCloseInvalidatesRemoteTable() throws Exception { + RemoteDatabaseStorage storage = new RemoteDatabaseStorage(); + RemoteTableProvider provider = storage.connect("localhost", Registry.REGISTRY_PORT); + + String tableName = "table"; + + Table remoteTable = provider.createTable(tableName, Arrays.asList(String.class)); + + Table serverTable = factory.getProvider().getTable(tableName); + ((AutoCloseable) serverTable).close(); + + exception.expect(InvalidatedObjectException.class); + remoteTable.getName(); + } + + @Test + public void testServerFactoryCloseInvalidatesRemoteTable() throws Exception { + RemoteDatabaseStorage storage = new RemoteDatabaseStorage(); + RemoteTableProvider provider = storage.connect("localhost", Registry.REGISTRY_PORT); + + String tableName = "table"; + + Table remoteTable = provider.createTable(tableName, Arrays.asList(String.class)); + + factory.close(); + + exception.expect(InvalidatedObjectException.class); + remoteTable.getName(); + } + + @Test + public void testRemoteTableClosesAfterTableIsInvalidated() throws Exception { + RemoteDatabaseStorage storage = new RemoteDatabaseStorage(); + RemoteTableProvider remoteProvider = storage.connect("localhost", Registry.REGISTRY_PORT); + + String tableName = "table"; + + Table remoteTable = remoteProvider.createTable(tableName, Arrays.asList(String.class)); + + Table serverTable = factory.getProvider().getTable(tableName); + ((AutoCloseable) serverTable).close(); + + try { + remoteTable.list(); + } catch (InvalidatedObjectException exc) { + // Ignore it. + } + + // Now client's remoteTable must have been completely closed. + Table remoteTable2 = remoteProvider.getTable(tableName); + + // Checking availability. + remoteTable2.getName(); + + exception.expect(InvalidatedObjectException.class); + remoteTable.getName(); + } +} 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 b432c6d14..5f61d412f 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 @@ -18,6 +18,7 @@ import ru.fizteh.fivt.storage.structured.TableProviderFactory; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.DBTableProviderFactory; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.StringTableImpl; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.InvalidatedObjectException; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test.support.TestUtils; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test.support.parallel.ControllableAgent; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test.support.parallel.ControllableRunnable; @@ -389,8 +390,8 @@ public void testRemoveTableExistent() throws IOException { Table table = provider.createTable(name, DEFAULT_COLUMN_TYPES); provider.removeTable(name); - exception.expect(IllegalStateException.class); - exception.expectMessage("This object has been invalidated"); + exception.expect(InvalidatedObjectException.class); + exception.expectMessage("Object has been invalidated"); // Even calling to this simple method can cause IllegalStateException if the table has been removed. table.getName(); From 2e542d49eadaa398ef760e0030217aa06d47ec3c Mon Sep 17 00:00:00 2001 From: AndreosGhost Date: Sun, 21 Dec 2014 12:01:08 +0300 Subject: [PATCH 10/14] Deleted old version of renamed file. --- .../telnet/client/ClientState.java | 80 ------------------- 1 file changed, 80 deletions(-) delete mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/client/ClientState.java diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/client/ClientState.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/client/ClientState.java deleted file mode 100644 index f3073a9f6..000000000 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/client/ClientState.java +++ /dev/null @@ -1,80 +0,0 @@ -package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet.client; - -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.ExitRequest; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.BaseShellState; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Command; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Log; - -import java.io.IOException; -import java.net.InetAddress; -import java.net.Socket; -import java.util.Map; - -public class ClientState extends BaseShellState { - private Socket connection; - - public void connect(String host, int port) throws IOException { - checkInitialized(); - if (connection == null) { - try { - connection = new Socket(host, port); - } catch (IOException exc) { - throw new IOException("not connected: " + exc.getMessage(), exc.getCause()); - } - } else { - throw new IllegalStateException("not connected: already connected"); - } - } - - public InetAddress getHost() { - requireConnected(); - return connection.getInetAddress(); - } - - public int getPort() { - requireConnected(); - return connection.getPort(); - } - - public void disconnect() throws IllegalStateException, IOException { - requireConnected(); - try { - connection.close(); - } finally { - connection = null; - } - } - - private void requireConnected() { - if (!isConnected()) { - throw new IllegalStateException("not connected"); - } - } - - public boolean isConnected() { - return connection != null; - } - - @Override - public void cleanup() { - try { - if (isConnected()) { - disconnect(); - } - } catch (IOException exc) { - Log.log(ClientState.class, exc, "Exception occurred on cleanup"); - } - } - - @Override - public void prepareToExit(int exitCode) throws ExitRequest { - Log.log(ClientState.class, "Preparing to exit with code:" + exitCode); - cleanup(); - throw new ExitRequest(exitCode); - } - - @Override - public Map> getCommands() { - return ClientCommands.getInstance().getCommands(); - } -} From 361f5e91f0777fba00f00e80af5a8d9b8e19a012 Mon Sep 17 00:00:00 2001 From: AndreosGhost Date: Sun, 21 Dec 2014 15:15:23 +0300 Subject: [PATCH 11/14] Fix problem with table stubs given back to server in provider.deserialize() methods --- .../db/remote/RemoteTableImpl.java | 4 +++ .../db/remote/RemoteTableProviderImpl.java | 2 +- .../db/remote/RemoteTableProviderStub.java | 17 +++++---- .../db/remote/RemoteTableStub.java | 26 +++++++++----- .../shell/DatabaseFactory.java | 4 +-- .../databaselibrary/shell/Main.java | 4 ++- .../databaselibrary/shell/Shell.java | 25 +++++-------- .../support/ValidityController.java | 3 +- .../telnet/ClientServerGeneralState.java | 4 +-- .../telnet/client/ClientCommands.java | 5 +-- .../telnet/server/DBServerState.java | 6 ++-- .../test/ClientGeneralStateTest.java | 17 ++++++++- .../test/DatabaseShellTest.java | 16 --------- .../databaselibrary/test/InterpreterTest.java | 7 ++++ .../test/RemoteDataStorageTest.java | 36 +++++++++++++++++++ 15 files changed, 112 insertions(+), 64 deletions(-) diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableImpl.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableImpl.java index d58675997..d8d2e63c9 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableImpl.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableImpl.java @@ -20,6 +20,10 @@ public RemoteTableImpl(Table table) throws RemoteException { this.table = table; } + public Table getPureTable() { + return table; + } + @Override public String getName() throws RemoteException { return table.getName(); diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableProviderImpl.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableProviderImpl.java index 42cdf7501..8c8e5375c 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableProviderImpl.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableProviderImpl.java @@ -27,7 +27,7 @@ public RemoteTableProviderImpl(TableProvider provider) throws RemoteException { private RemoteTableStub wrapInStub(Table table) throws UnexpectedRemoteException { try { - return new RemoteTableStub(new RemoteTableImpl(table), (table == null ? null : table.getName())); + return table == null ? null : new RemoteTableStub(new RemoteTableImpl(table), table.getName()); } catch (RemoteException exc) { throw new UnexpectedRemoteException(exc); } diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableProviderStub.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableProviderStub.java index 9b975bbc6..3e5809e7d 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableProviderStub.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableProviderStub.java @@ -36,7 +36,7 @@ final class RemoteTableProviderStub implements RemoteTableProvider, Serializable /** * For synchronization on client side. */ - private transient ValidityController validityController; + private final ValidityController validityController = new ValidityController(); /** * Local stubs associated with this provider. */ @@ -64,7 +64,6 @@ public void bindToStorage(RemoteDatabaseStorage storage, String locationInStorag this.locationInStorage = locationInStorage; // Initializing transient fields. - validityController = new ValidityController(); tableStubs = new HashMap<>(); tableStubClosedByMe = false; } @@ -77,21 +76,21 @@ private RemoteTableStub tryReplaceTableStub(String name, RemoteTableStub tableSt Lock lock = tableStubsLock.readLock(); lock.lock(); try { - RemoteTableStub oldStub = tableStubs.get(name); - if (oldStub == null) { + if (!tableStubs.containsKey(name)) { lock.unlock(); lock = tableStubsLock.writeLock(); lock.lock(); - oldStub = tableStubs.get(name); - if (oldStub == null) { + if (!tableStubs.containsKey(name)) { tableStubs.put(name, tableStub); - tableStub.bindToProviderStub(this); + if (tableStub != null) { + tableStub.bindToProviderStub(this); + } return tableStub; } else { - return oldStub; + return tableStubs.get(name); } } else { - return oldStub; + return tableStubs.get(name); } } finally { lock.unlock(); diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableStub.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableStub.java index 4fea3964b..4b3458808 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableStub.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableStub.java @@ -26,27 +26,37 @@ final class RemoteTableStub implements Table, Serializable, Closeable { private final String tableName; private final IRemoteTable remoteTable; - /** - * Local variable at the client side. + * For synchronization on client side. */ - private transient RemoteTableProviderStub providerStub; - + private final ValidityController validityController = new ValidityController(); /** - * For synchronization on client side. + * Local variable at the client side. */ - private transient ValidityController validityController; + private transient RemoteTableProviderStub providerStub; public RemoteTableStub(RemoteTableImpl remoteTable, String tableName) { this.remoteTable = remoteTable; this.tableName = tableName; + init(); // Called at server side. } + public IRemoteTable getRemoteTable() { + return remoteTable; + } + + private void init() { + // Initializing transient fields. + // validityController = new ValidityController(); + } + + /** + * Called at client side when we get the stub. + */ public void bindToProviderStub(RemoteTableProviderStub providerStub) { this.providerStub = providerStub; - // Initializing transient fields. - validityController = new ValidityController(); + init(); } @Override diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/DatabaseFactory.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/DatabaseFactory.java index c9594f78c..10a1a0165 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/DatabaseFactory.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/DatabaseFactory.java @@ -12,9 +12,9 @@ public final class DatabaseFactory implements Closeable { private final TableProvider provider; private final String databasePath; - public DatabaseFactory() throws Exception { + public DatabaseFactory(String databasePath) throws Exception { factory = new DBTableProviderFactory(); - databasePath = System.getProperty(SingleDatabaseShellState.DB_DIRECTORY_PROPERTY_NAME); + this.databasePath = databasePath; provider = factory.create(databasePath); } 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 2d52e4109..b5a7d06fd 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 @@ -17,7 +17,9 @@ class Main { public static void main(String[] args) { try { Shell shell = new Shell<>( - new ClientServerGeneralState() { + new ClientServerGeneralState( + System.getProperty( + SingleDatabaseShellState.DB_DIRECTORY_PROPERTY_NAME)) { @Override protected Database obtainNewActiveDatabase() throws Exception { RemoteDatabaseStorage storage = new RemoteDatabaseStorage(); 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 93480908e..5b82bf802 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/Shell.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/Shell.java @@ -12,7 +12,6 @@ import java.io.InputStreamReader; import java.io.OutputStream; import java.io.PrintStream; -import java.io.Reader; import java.text.ParseException; import java.util.Arrays; import java.util.LinkedList; @@ -47,8 +46,6 @@ public class Shell> { */ private boolean interactive; - private Reader inputStreamReader; - public Shell(ShellStateImpl shellState, OutputStream outputStream) throws TerminalException { this.shellState = shellState; this.outputStream = outputStream instanceof PrintStream @@ -190,13 +187,6 @@ private void checkValid() throws IllegalStateException { } } - public Reader getInputStreamReader() { - if (inputStreamReader == null) { - throw new IllegalStateException("No reader now"); - } - return inputStreamReader; - } - /** * Execute commands from input stream. Commands are awaited till the-end-of-stream. */ @@ -213,7 +203,6 @@ private int run(InputStream stream, boolean interactive) throws TerminalExceptio try (BufferedReader reader = new BufferedReader( new InputStreamReader(stream), READ_BUFFER_SIZE)) { - inputStreamReader = reader; while (true) { if (interactive) { outputStream.print(shellState.getGreetingString()); @@ -225,19 +214,24 @@ private int run(InputStream stream, boolean interactive) throws TerminalExceptio break; } - List commands = splitCommandsString(str); try { - for (String[] command : commands) { - execute(command); + try { + List commands = splitCommandsString(str); + for (String[] command : commands) { + execute(command); + } + } catch (ParseException exc) { + handleError("Failed to parse: " + exc.getMessage(), exc, true, getOutputStream()); } } catch (TerminalException exc) { // Exception is already handled. if (!interactive) { + exitRequested = true; shellState.prepareToExit(1); } } } - } catch (IOException | ParseException exc) { + } catch (IOException exc) { exitRequested = true; handleError("Error in input stream: " + exc.getMessage(), exc, true, getOutputStream()); // No need to cleanup - work has not been started. @@ -245,7 +239,6 @@ private int run(InputStream stream, boolean interactive) throws TerminalExceptio exitRequested = true; return request.getCode(); } finally { - inputStreamReader = null; if (!exitRequested) { try { shellState.prepareToExit(0); 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 index 6d71bbbf1..b827f06f0 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/ValidityController.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/ValidityController.java @@ -2,13 +2,14 @@ import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.InvalidatedObjectException; +import java.io.Serializable; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; /** * Convenience class to control one's validity. */ -public final class ValidityController { +public final class ValidityController implements Serializable { private final ReadWriteLock validityLock = new ReentrantReadWriteLock(true); private boolean valid = true; diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/ClientServerGeneralState.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/ClientServerGeneralState.java index d8bb1b5d5..fc2df25db 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/ClientServerGeneralState.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/ClientServerGeneralState.java @@ -22,7 +22,7 @@ public abstract class ClientServerGeneralState extends JoinedState(clientState); new Shell<>(serverState); diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/client/ClientCommands.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/client/ClientCommands.java index e0ec7ee97..88d3f3652 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/client/ClientCommands.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/client/ClientCommands.java @@ -52,10 +52,7 @@ public void executeSafely(DBClientState state, String[] args) throws } }; public static final Command WHEREAMI = new AbstractCommand( - "whereami", - "", - "prints host and port you are connected to", - 1) { + "whereami", "", "prints host and port you are connected to", 1) { @Override public void executeSafely(DBClientState state, String[] args) throws IllegalArgumentException, diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/server/DBServerState.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/server/DBServerState.java index d03eb9a3f..448c6b86e 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/server/DBServerState.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/server/DBServerState.java @@ -25,13 +25,14 @@ public class DBServerState extends BaseShellState { private static final ServerCommands COMMANDS = ServerCommands.obtainInstance(); private static final String DATABASE_ROOT_PROPERTY = "fizteh.db.dir"; + private final String databaseRoot; private Supplier clientShellStateSupplier; private Server server; private RemoteTableProviderFactoryImpl factory; private TableProvider provider; - private String databaseRoot; - public DBServerState() { + public DBServerState(String databaseRoot) { + this.databaseRoot = databaseRoot; RemoteTableProviderFactory storage = new RemoteDatabaseStorage(); DBServerState serverState = this; this.clientShellStateSupplier = () -> new SingleDatabaseShellState() { @@ -59,7 +60,6 @@ public void startServer(int port) throws IOException, IllegalStateException { } try { - databaseRoot = System.getProperty(DATABASE_ROOT_PROPERTY); factory = new RemoteTableProviderFactoryImpl(); provider = factory.establishStorage(databaseRoot); diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/ClientGeneralStateTest.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/ClientGeneralStateTest.java index 43e4f9568..92da7926c 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/ClientGeneralStateTest.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/ClientGeneralStateTest.java @@ -47,7 +47,7 @@ public PrintStream getOutputStream() { @Before public void prepareRemoteAPITest() throws Exception { - serverState = new DBServerState(); + serverState = new DBServerState(DB_ROOT.toString()); new Shell(serverState); serverState.startServer(10001); } @@ -57,6 +57,21 @@ public void cleanupRemoteAPITest() throws IOException { serverState.stopServer(); } + @Test + public void testCreateTableAndPutSmth() throws IOException, TerminalException { + runBatchExpectZero( + "connect localhost 10001", + "create t1 (String)", + "use t1", + "put a [\"b\"]", + "commit"); + assertEquals( + makeTerminalExpectedMessage("connected", "created", "using t1", "new", "1"), + getOutput()); + runBatchExpectZero("connect localhost 10001", "use t1", "get a"); + assertEquals(makeTerminalExpectedMessage("connected", "using t1", "found", "[\"b\"]"), getOutput()); + } + @Test public void testConnectToNotExistentServer() throws IOException, TerminalException { runBatchExpectNonZero("connect 127.0.0.1 10500"); 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 ae72a567a..f5ce3b786 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/DatabaseShellTest.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/DatabaseShellTest.java @@ -424,22 +424,6 @@ public void testCreateTableWithInvalidName() throws TerminalException { getOutput()); } - @Test - public void testRunInteractiveWithUnparseableStream() throws TerminalException { - exception.expect(TerminalException.class); - exception.expectMessage(containsString("Error in input stream")); - - runInteractiveExpectNonZero("do smth \""); - } - - @Test - public void testRunWithUnparseableStream() throws TerminalException { - exception.expect(TerminalException.class); - exception.expectMessage(containsString("Error in input stream: Cannot find closing quotes")); - - runBatchExpectNonZero("do smth \""); // Unclosed quotes. - } - @Test public void testCreateTableWithInvalidName1() throws TerminalException { runBatchExpectNonZero( diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/InterpreterTest.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/InterpreterTest.java index 4a82c303c..18d74392c 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/InterpreterTest.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/InterpreterTest.java @@ -20,6 +20,13 @@ protected Shell constructInterpreter() throws TerminalExc return new Shell<>(state); } + @Test + public void testUnclosedQuotes() throws TerminalException { + runBatchExpectNonZero("echo [\"quotes']"); + + assertEquals(makeTerminalExpectedMessage("Failed to parse: Cannot find closing quotes"), getOutput()); + } + @Test public void testUnexpectedMethodError() throws TerminalException { runBatchExpectNonZero(AlternativeShellState.THROW_RUNTIME.getName()); diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/RemoteDataStorageTest.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/RemoteDataStorageTest.java index bfcc2528f..958f90c58 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/RemoteDataStorageTest.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/RemoteDataStorageTest.java @@ -8,6 +8,7 @@ import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import ru.fizteh.fivt.storage.structured.RemoteTableProvider; +import ru.fizteh.fivt.storage.structured.Storeable; import ru.fizteh.fivt.storage.structured.Table; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.remote.RemoteDatabaseStorage; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.remote.RemoteTableProviderFactoryImpl; @@ -136,4 +137,39 @@ public void testRemoteTableClosesAfterTableIsInvalidated() throws Exception { exception.expect(InvalidatedObjectException.class); remoteTable.getName(); } + + @Test + public void testTwoCreatesOfTheSameTable() throws IOException { + RemoteDatabaseStorage storage = new RemoteDatabaseStorage(); + RemoteTableProvider providerA = storage.connect("localhost", Registry.REGISTRY_PORT); + RemoteTableProvider providerB = storage.connect("localhost", Registry.REGISTRY_PORT); + + String tableName = "table"; + + Table remoteTableA = providerA.createTable(tableName, Arrays.asList(String.class)); + Table remoteTableB = providerB.createTable(tableName, Arrays.asList(String.class)); + + assertNull(remoteTableB); + } + + @Test + public void testPutValue() throws IOException { + RemoteDatabaseStorage storage = new RemoteDatabaseStorage(); + RemoteTableProvider providerA = storage.connect("localhost", Registry.REGISTRY_PORT); + RemoteTableProvider providerB = storage.connect("localhost", Registry.REGISTRY_PORT); + + String tableName = "table"; + String key = "key"; + String value = "value"; + + providerA.createTable(tableName, Arrays.asList(String.class)); + + Table tableFromA = providerA.getTable(tableName); + Table tableFromB = providerB.getTable(tableName); + + tableFromA.put(key, providerA.createFor(tableFromA, Arrays.asList(value))); + Storeable storeable = tableFromB.get(key); + + assertEquals(value, storeable.getStringAt(0)); + } } From a6c0cff12f8da3d9c8de3eb1ba24ea550fc37f14 Mon Sep 17 00:00:00 2001 From: AndreosGhost Date: Sun, 21 Dec 2014 15:52:56 +0300 Subject: [PATCH 12/14] More tests --- .../db/remote/RemoteTableStub.java | 8 --- .../test/ClientGeneralStateTest.java | 15 ++-- .../databaselibrary/test/DBServerTest.java | 69 +++++++++++++++++++ .../test/RemoteDataStorageTest.java | 19 +++++ 4 files changed, 96 insertions(+), 15 deletions(-) create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/DBServerTest.java diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableStub.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableStub.java index 4b3458808..21e80b3d1 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableStub.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableStub.java @@ -38,25 +38,17 @@ final class RemoteTableStub implements Table, Serializable, Closeable { public RemoteTableStub(RemoteTableImpl remoteTable, String tableName) { this.remoteTable = remoteTable; this.tableName = tableName; - init(); // Called at server side. } public IRemoteTable getRemoteTable() { return remoteTable; } - private void init() { - // Initializing transient fields. - // validityController = new ValidityController(); - } - /** * Called at client side when we get the stub. */ public void bindToProviderStub(RemoteTableProviderStub providerStub) { this.providerStub = providerStub; - - init(); } @Override diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/ClientGeneralStateTest.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/ClientGeneralStateTest.java index 92da7926c..ee1883b44 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/ClientGeneralStateTest.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/ClientGeneralStateTest.java @@ -57,17 +57,18 @@ public void cleanupRemoteAPITest() throws IOException { serverState.stopServer(); } + @Test + public void testWhereAmI() throws IOException, TerminalException { + runBatchExpectZero("connect localhost 10001", "whereami"); + assertEquals(makeTerminalExpectedMessage("connected", "local 10001"), getOutput()); + } + @Test public void testCreateTableAndPutSmth() throws IOException, TerminalException { runBatchExpectZero( - "connect localhost 10001", - "create t1 (String)", - "use t1", - "put a [\"b\"]", - "commit"); + "connect localhost 10001", "create t1 (String)", "use t1", "put a [\"b\"]", "commit"); assertEquals( - makeTerminalExpectedMessage("connected", "created", "using t1", "new", "1"), - getOutput()); + makeTerminalExpectedMessage("connected", "created", "using t1", "new", "1"), getOutput()); runBatchExpectZero("connect localhost 10001", "use t1", "get a"); assertEquals(makeTerminalExpectedMessage("connected", "using t1", "found", "[\"b\"]"), getOutput()); } diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/DBServerTest.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/DBServerTest.java new file mode 100644 index 000000000..a74b171c8 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/DBServerTest.java @@ -0,0 +1,69 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Shell; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet.server.DBServerState; + +import java.io.IOException; + +import static org.junit.Assert.*; + +@RunWith(JUnit4.class) +public class DBServerTest extends TestBase { + @Rule + public ExpectedException exception = ExpectedException.none(); + private DBServerState serverState; + + @Before + public void prepareRemoteAPITest() throws Exception { + serverState = new DBServerState(DB_ROOT.toString()); + new Shell(serverState); + } + + @After + public void cleanupRemoteAPITest() throws IOException { + serverState.stopServerIfStarted(); + } + + @Test + public void testDoubleStart() throws IOException { + serverState.startServer(10001); + exception.expect(IllegalStateException.class); + exception.expectMessage("not started: already started"); + serverState.startServer(10002); + } + + @Test + public void testDisconnectNotConnected() throws IOException { + exception.expect(IllegalStateException.class); + exception.expectMessage("not started"); + serverState.stopServer(); + } + + @Test + public void testStartStop() throws IOException { + serverState.startServer(10001); + assertTrue(serverState.isStarted()); + serverState.stopServer(); + assertFalse(serverState.isStarted()); + } + + @Test + public void testStopIfConnected() throws IOException { + serverState.stopServerIfStarted(); + } + + @Test + public void testStopIfConnected2() throws IOException { + serverState.startServer(10001); + assertTrue(serverState.isStarted()); + serverState.stopServerIfStarted(); + assertFalse(serverState.isStarted()); + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/RemoteDataStorageTest.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/RemoteDataStorageTest.java index 958f90c58..fc1cca962 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/RemoteDataStorageTest.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/RemoteDataStorageTest.java @@ -171,5 +171,24 @@ public void testPutValue() throws IOException { Storeable storeable = tableFromB.get(key); assertEquals(value, storeable.getStringAt(0)); + + tableFromB.remove(key); + assertNull(tableFromA.get(key)); + assertNull(tableFromB.get(key)); + } + + @Test + public void testRemoveTable() throws IOException { + RemoteDatabaseStorage storage = new RemoteDatabaseStorage(); + RemoteTableProvider provider = storage.connect("localhost", Registry.REGISTRY_PORT); + + String tableName = "table"; + + Table table = provider.createTable(tableName, Arrays.asList(String.class)); + + provider.removeTable(tableName); + + exception.expect(InvalidatedObjectException.class); + table.list(); } } From d3daaec497c2874702bfe5c42c68883b5e103a3e Mon Sep 17 00:00:00 2001 From: AndreosGhost Date: Tue, 23 Dec 2014 00:10:40 +0300 Subject: [PATCH 13/14] Implemented Servlet --- .../db/AutoCloseableProvider.java | 9 + .../db/AutoCloseableTableProviderFactory.java | 6 +- .../databaselibrary/db/DBTableProvider.java | 150 +++++------ .../databaselibrary/db/Database.java | 9 +- .../databaselibrary/db/ProviderWrap.java | 249 ++++++++++++++++++ .../databaselibrary/db/StringTableImpl.java | 2 +- .../db/remote/IRemoteTableProvider.java | 2 +- .../remote/IRemoteTableProviderFactory.java | 8 +- .../RemoteTableProviderFactoryImpl.java | 50 ++-- .../db/remote/RemoteTableStub.java | 9 +- .../databaselibrary/json/JSONMaker.java | 4 +- .../json/JSONParsedObject.java | 8 +- .../parallel/ControllableAgent.java | 2 +- .../parallel/ControllableRunnable.java | 2 +- .../parallel/ControllableRunner.java | 2 +- .../parallel/ExceptionFreeRunnable.java | 2 +- .../databaselibrary/servlet/HttpDBServer.java | 234 ++++++++++++++++ .../servlet/HttpDBServerCommands.java | 94 +++++++ .../servlet/HttpDBServerState.java | 48 ++++ .../servlet/transactions/Transaction.java | 196 ++++++++++++++ .../servlet/transactions/TransactionPool.java | 195 ++++++++++++++ .../shell/AbstractCommand.java | 86 +++--- .../shell/CommandContainer.java | 2 +- .../shell/DatabaseFactory.java | 35 --- .../databaselibrary/shell/JoinedState.java | 35 ++- .../databaselibrary/shell/Main.java | 31 +-- .../databaselibrary/shell/ShellState.java | 10 +- .../shell/SingleDatabaseShellState.java | 15 +- .../databaselibrary/support/Utility.java | 2 +- .../telnet/ClientServerGeneralState.java | 122 --------- .../telnet/DBShellGeneralState.java | 152 +++++++++++ .../telnet/client/ClientCommands.java | 7 +- .../telnet/client/ClientGeneralState.java | 8 +- .../telnet/client/DBClientState.java | 39 +-- .../databaselibrary/telnet/server/Server.java | 2 +- .../telnet/server/ServerCommands.java | 88 +++---- .../telnet/server/ServerCommunicator.java | 2 +- ...verState.java => TelnetDBServerState.java} | 40 ++- .../test/ClientGeneralStateTest.java | 27 +- .../test/ControllableRunnerTest.java | 6 +- .../databaselibrary/test/DBServerTest.java | 28 +- .../test/InterpreterTestBase.java | 64 ++--- .../test/RemoteDataStorageTest.java | 22 +- .../test/TableProviderTest.java | 6 +- .../databaselibrary/test/TableTest.java | 6 +- .../test/TransactionPoolTest.java | 78 ++++++ .../test/support/AlternativeShellState.java | 5 - .../databaselibrary/xml/XMLMaker.java | 2 +- 48 files changed, 1655 insertions(+), 546 deletions(-) create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/ProviderWrap.java rename src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/{test/support => }/parallel/ControllableAgent.java (83%) rename src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/{test/support => }/parallel/ControllableRunnable.java (94%) rename src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/{test/support => }/parallel/ControllableRunner.java (98%) rename src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/{test/support => }/parallel/ExceptionFreeRunnable.java (82%) create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/servlet/HttpDBServer.java create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/servlet/HttpDBServerCommands.java create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/servlet/HttpDBServerState.java create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/servlet/transactions/Transaction.java create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/servlet/transactions/TransactionPool.java delete mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/DatabaseFactory.java delete mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/ClientServerGeneralState.java create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/DBShellGeneralState.java rename src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/server/{DBServerState.java => TelnetDBServerState.java} (79%) create mode 100644 src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/TransactionPoolTest.java 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 index 887456bf9..aa712a32b 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/AutoCloseableProvider.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/AutoCloseableProvider.java @@ -2,7 +2,16 @@ import ru.fizteh.fivt.storage.structured.TableProvider; +import java.io.IOException; +import java.util.List; + public interface AutoCloseableProvider extends TableProvider, AutoCloseable { @Override void close(); + + @Override + AutoCloseableTable getTable(String name); + + @Override + AutoCloseableTable createTable(String name, List> columnTypes) throws IOException; } diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/AutoCloseableTableProviderFactory.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/AutoCloseableTableProviderFactory.java index a3f91d037..a6df2037c 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/AutoCloseableTableProviderFactory.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/AutoCloseableTableProviderFactory.java @@ -3,5 +3,9 @@ import ru.fizteh.fivt.storage.structured.TableProviderFactory; import java.io.Closeable; +import java.io.IOException; -public interface AutoCloseableTableProviderFactory extends TableProviderFactory, Closeable {} +public interface AutoCloseableTableProviderFactory extends TableProviderFactory, Closeable { + @Override + AutoCloseableProvider create(String path) throws IOException; +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/DBTableProvider.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/DBTableProvider.java index 580d23192..ce8b41343 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/DBTableProvider.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/DBTableProvider.java @@ -101,81 +101,6 @@ public Path getDatabaseRoot() { } } - @Override - public AutoCloseableTable getTable(String name) throws IllegalArgumentException { - try (UseLock useLock = validityController.use()) { - Utility.checkTableNameIsCorrect(name); - - Lock lock = persistenceLock.readLock(); - lock.lock(); - try { - if (!tables.containsKey(name)) { - // Read table from FS. Must get a better lock. - lock.unlock(); - lock = persistenceLock.writeLock(); - lock.lock(); - if (!tables.containsKey(name)) { - try { - loadMissingTables(); - } catch (DatabaseIOException exc) { - throw new IllegalArgumentException(exc.getMessage(), exc); - } - } - } - - if (tables.containsKey(name)) { - AutoCloseableTable table = tables.get(name); - if (table == null) { - // Table is corrupt. - DatabaseIOException corruptionReason = corruptTables.get(name); - throw new IllegalArgumentException( - corruptionReason.getMessage(), corruptionReason); - } - - // Table is normal. - return table; - } else { - // Table not exists. - return null; - } - } finally { - lock.unlock(); - } - } - } - - @Override - public AutoCloseableTable createTable(String name, List> columnTypes) - throws IllegalArgumentException, DatabaseIOException { - try (UseLock useLock = validityController.use()) { - Utility.checkTableNameIsCorrect(name); - - if (columnTypes == null) { - throw new IllegalArgumentException("Column types list must not be null"); - } - if (columnTypes.isEmpty()) { - throw new IllegalArgumentException("Column types list must not be empty"); - } - Utility.checkAllTypesAreSupported(columnTypes, SUPPORTED_TYPES); - - Path tablePath = databaseRoot.resolve(name); - - persistenceLock.writeLock().lock(); - try { - if (tables.containsKey(name) && tables.get(name) != null) { - return null; - } - - AutoCloseableTable newTable = - StoreableTableImpl.createTable(this, this::onTableClosed, tablePath, columnTypes); - tables.put(name, newTable); - return newTable; - } finally { - persistenceLock.writeLock().unlock(); - } - } - } - @Override public void removeTable(String name) throws IllegalArgumentException, IllegalStateException, DatabaseIOException { @@ -457,4 +382,79 @@ public void close() { tableClosedByMe = false; } } + + @Override + public AutoCloseableTable getTable(String name) throws IllegalArgumentException { + try (UseLock useLock = validityController.use()) { + Utility.checkTableNameIsCorrect(name); + + Lock lock = persistenceLock.readLock(); + lock.lock(); + try { + if (!tables.containsKey(name)) { + // Read table from FS. Must get a better lock. + lock.unlock(); + lock = persistenceLock.writeLock(); + lock.lock(); + if (!tables.containsKey(name)) { + try { + loadMissingTables(); + } catch (DatabaseIOException exc) { + throw new IllegalArgumentException(exc.getMessage(), exc); + } + } + } + + if (tables.containsKey(name)) { + AutoCloseableTable table = tables.get(name); + if (table == null) { + // Table is corrupt. + DatabaseIOException corruptionReason = corruptTables.get(name); + throw new IllegalArgumentException( + corruptionReason.getMessage(), corruptionReason); + } + + // Table is normal. + return table; + } else { + // Table not exists. + return null; + } + } finally { + lock.unlock(); + } + } + } + + @Override + public AutoCloseableTable createTable(String name, List> columnTypes) + throws IllegalArgumentException, DatabaseIOException { + try (UseLock useLock = validityController.use()) { + Utility.checkTableNameIsCorrect(name); + + if (columnTypes == null) { + throw new IllegalArgumentException("Column types list must not be null"); + } + if (columnTypes.isEmpty()) { + throw new IllegalArgumentException("Column types list must not be empty"); + } + Utility.checkAllTypesAreSupported(columnTypes, SUPPORTED_TYPES); + + Path tablePath = databaseRoot.resolve(name); + + persistenceLock.writeLock().lock(); + try { + if (tables.containsKey(name) && tables.get(name) != null) { + return null; + } + + AutoCloseableTable newTable = + StoreableTableImpl.createTable(this, this::onTableClosed, tablePath, columnTypes); + tables.put(name, newTable); + return newTable; + } finally { + persistenceLock.writeLock().unlock(); + } + } + } } diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/Database.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/Database.java index d253e90f3..7ad52bd22 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/Database.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/Database.java @@ -61,7 +61,14 @@ public boolean createTable(String tableName, List> columnTypes) * Name of table to drop. */ public void dropTable(String tableName) throws IllegalArgumentException, IOException { - String activeTableName = activeTable == null ? null : activeTable.getName(); + String activeTableName = null; + if (activeTable != null) { + try { + activeTableName = activeTable.getName(); + } catch (InvalidatedObjectException exc) { + // Ignore it. + } + } provider.removeTable(tableName); diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/ProviderWrap.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/ProviderWrap.java new file mode 100644 index 000000000..93b3ea3f6 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/ProviderWrap.java @@ -0,0 +1,249 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db; + +import ru.fizteh.fivt.storage.structured.ColumnFormatException; +import ru.fizteh.fivt.storage.structured.Storeable; +import ru.fizteh.fivt.storage.structured.Table; +import ru.fizteh.fivt.storage.structured.TableProvider; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.InvalidatedObjectException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.ValidityController; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.ValidityController.KillLock; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.ValidityController.UseLock; + +import java.io.IOException; +import java.text.ParseException; +import java.util.List; + +/** + * Wraps the given table provider.
    + * {@link ProviderWrap#close()} method just invalidates the wrap. Original provider is not affected.
    + * Also returned tables are wraps that can be closed without affecting original tables. + */ +public class ProviderWrap implements AutoCloseableProvider { + private final TableProvider provider; + + private final ValidityController providerVC = new ValidityController(); + + public ProviderWrap(TableProvider provider) { + this.provider = provider; + } + + @Override + public void removeTable(String name) throws IOException { + try (UseLock lock = providerVC.use()) { + provider.removeTable(name); + } + } + + @Override + public Storeable deserialize(Table table, String value) throws ParseException { + try (UseLock lock = providerVC.use()) { + return provider.deserialize(table, value); + } + } + + @Override + public String serialize(Table table, Storeable value) throws ColumnFormatException { + try (UseLock lock = providerVC.use()) { + return provider.serialize(table, value); + } + } + + @Override + public Storeable createFor(Table table) { + try (UseLock lock = providerVC.use()) { + return provider.createFor(table); + } + } + + @Override + public Storeable createFor(Table table, List values) + throws ColumnFormatException, IndexOutOfBoundsException { + try (UseLock lock = providerVC.use()) { + return provider.createFor(table, values); + } + } + + @Override + public List getTableNames() { + try (UseLock lock = providerVC.use()) { + return provider.getTableNames(); + } + } + + @Override + public void close() { + try (KillLock lock = providerVC.useAndKill()) { + // Killing. + } + } + + @Override + public AutoCloseableTable getTable(String name) { + try (UseLock lock = providerVC.use()) { + Table table = provider.getTable(name); + return table == null ? null : new TableWrap(table); + } + } + + @Override + public AutoCloseableTable createTable(String name, List> columnTypes) throws IOException { + try (UseLock lock = providerVC.use()) { + Table table = provider.createTable(name, columnTypes); + return table == null ? null : new TableWrap(table); + } + } + + class TableWrap implements AutoCloseableTable { + private final Table table; + private final ValidityController tableVC = new ValidityController(); + + public TableWrap(Table table) { + this.table = table; + } + + private void forceClose(UseLock tableLock) { + try (KillLock lock = tableLock.obtainKillLockInstead()) { + close(); + } + } + + @Override + public void close() { + try (KillLock tableLock = tableVC.useAndKill()) { + // Killing. + } + } + + @Override + public Storeable put(String key, Storeable value) throws ColumnFormatException { + try (UseLock tableLock = tableVC.use()) { + try (UseLock providerLock = providerVC.use()) { + return table.put(key, value); + } catch (InvalidatedObjectException exc) { + forceClose(tableLock); + throw exc; + } + } + } + + @Override + public Storeable remove(String key) { + try (UseLock tableLock = tableVC.use()) { + try (UseLock providerLock = providerVC.use()) { + return table.remove(key); + } catch (InvalidatedObjectException exc) { + forceClose(tableLock); + throw exc; + } + } + } + + @Override + public int size() { + try (UseLock tableLock = tableVC.use()) { + try (UseLock providerLock = providerVC.use()) { + return table.size(); + } catch (InvalidatedObjectException exc) { + forceClose(tableLock); + throw exc; + } + } + } + + @Override + public List list() { + try (UseLock tableLock = tableVC.use()) { + try (UseLock providerLock = providerVC.use()) { + return table.list(); + } catch (InvalidatedObjectException exc) { + forceClose(tableLock); + throw exc; + } + } + } + + @Override + public int commit() throws IOException { + try (UseLock tableLock = tableVC.use()) { + try (UseLock providerLock = providerVC.use()) { + return table.commit(); + } catch (InvalidatedObjectException exc) { + forceClose(tableLock); + throw exc; + } + } + } + + @Override + public int rollback() { + try (UseLock tableLock = tableVC.use()) { + try (UseLock providerLock = providerVC.use()) { + return table.rollback(); + } catch (InvalidatedObjectException exc) { + forceClose(tableLock); + throw exc; + } + } + } + + @Override + public int getNumberOfUncommittedChanges() { + try (UseLock tableLock = tableVC.use()) { + try (UseLock providerLock = providerVC.use()) { + return table.getNumberOfUncommittedChanges(); + } catch (InvalidatedObjectException exc) { + forceClose(tableLock); + throw exc; + } + } + } + + @Override + public int getColumnsCount() { + try (UseLock tableLock = tableVC.use()) { + try (UseLock providerLock = providerVC.use()) { + return table.getColumnsCount(); + } catch (InvalidatedObjectException exc) { + forceClose(tableLock); + throw exc; + } + } + } + + @Override + public Class getColumnType(int columnIndex) throws IndexOutOfBoundsException { + try (UseLock tableLock = tableVC.use()) { + try (UseLock providerLock = providerVC.use()) { + return table.getColumnType(columnIndex); + } catch (InvalidatedObjectException exc) { + forceClose(tableLock); + throw exc; + } + } + } + + @Override + public String getName() { + try (UseLock tableLock = tableVC.use()) { + try (UseLock providerLock = providerVC.use()) { + return table.getName(); + } catch (InvalidatedObjectException exc) { + forceClose(tableLock); + throw exc; + } + } + } + + @Override + public Storeable get(String key) { + try (UseLock tableLock = tableVC.use()) { + try (UseLock providerLock = providerVC.use()) { + return table.get(key); + } catch (InvalidatedObjectException exc) { + forceClose(tableLock); + throw exc; + } + } + } + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/StringTableImpl.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/StringTableImpl.java index fd4f4d97b..e6db148e7 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/StringTableImpl.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/StringTableImpl.java @@ -361,7 +361,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/remote/IRemoteTableProvider.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/IRemoteTableProvider.java index 5bede51c7..7b202e273 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/IRemoteTableProvider.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/IRemoteTableProvider.java @@ -101,7 +101,7 @@ interface IRemoteTableProvider extends Remote { * Список значений, которыми нужно проинициализировать поля Storeable. * @return {@link ru.fizteh.fivt.storage.structured.Storeable}, проинициализированный переданными * значениями. - * @throws ColumnFormatException + * @throws ru.fizteh.fivt.storage.structured.ColumnFormatException * При несоответствии типа переданного значения и колонки. * @throws IndexOutOfBoundsException * При несоответствии числа переданных значений и числа колонок. diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/IRemoteTableProviderFactory.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/IRemoteTableProviderFactory.java index 2ab933c82..c931dd8a4 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/IRemoteTableProviderFactory.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/IRemoteTableProviderFactory.java @@ -1,7 +1,5 @@ package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.remote; -import ru.fizteh.fivt.storage.structured.TableProvider; - import java.io.Closeable; import java.io.IOException; import java.rmi.Remote; @@ -10,9 +8,9 @@ interface IRemoteTableProviderFactory extends Remote, Closeable { RemoteTableProviderStub obtainRemoteProvider() throws IOException; - default TableProvider establishStorage(String localDatabaseRoot) throws IOException { - return establishStorage(localDatabaseRoot, Registry.REGISTRY_PORT); + default void establishStorage(String localDatabaseRoot) throws IOException { + establishStorage(localDatabaseRoot, Registry.REGISTRY_PORT); } - TableProvider establishStorage(String localDatabaseRoot, int port) throws IOException; + void establishStorage(String localDatabaseRoot, int port) throws IOException; } diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableProviderFactoryImpl.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableProviderFactoryImpl.java index ebaecb7e1..599852be9 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableProviderFactoryImpl.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableProviderFactoryImpl.java @@ -1,8 +1,8 @@ package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.remote; import ru.fizteh.fivt.storage.structured.TableProvider; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.AutoCloseableTableProviderFactory; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.DBTableProviderFactory; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.AutoCloseableProvider; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.ProviderWrap; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Log; import java.io.IOException; @@ -19,26 +19,22 @@ public class RemoteTableProviderFactoryImpl extends UnicastRemoteObject implements IRemoteTableProviderFactory { static final String FACTORY_NAME = "/Database"; - + /** + * Local provider that is final for all time. + */ + private final TableProvider localProvider; private Registry registry; - - private AutoCloseableTableProviderFactory factory; - /** - * Server-local table provider. + * Provider wrap that can be closed. */ - private TableProvider provider; - + private AutoCloseableProvider providerWrap; /** - * Server-local singleton implementation of remote table provider that is wrapped in stub. + * Server-local singleton implementation of remote table providerWrap that is wrapped in stub. */ private IRemoteTableProvider remoteProvider; - public RemoteTableProviderFactoryImpl() throws RemoteException { - } - - public synchronized TableProvider getProvider() { - return provider; + public RemoteTableProviderFactoryImpl(TableProvider provider) throws RemoteException { + this.localProvider = provider; } private boolean isBound() { @@ -57,13 +53,12 @@ public synchronized void close() throws IOException { return; } try { + providerWrap.close(); registry.unbind(FACTORY_NAME); - factory.close(); } catch (NotBoundException exc) { throw new IllegalStateException("The factory has not been established"); } finally { - factory = null; - provider = null; + providerWrap = null; remoteProvider = null; registry = null; } @@ -76,16 +71,12 @@ public synchronized RemoteTableProviderStub obtainRemoteProvider() throws IOExce } @Override - public synchronized TableProvider establishStorage(String localDatabaseRoot, int port) - throws IOException { + public synchronized void establishStorage(String localDatabaseRoot, int port) throws IOException { if (isBound()) { throw new IllegalStateException("Factory already established"); } try { - factory = new DBTableProviderFactory(); - provider = factory.create(localDatabaseRoot); - try { registry = LocateRegistry.getRegistry(port); registry.rebind(FACTORY_NAME, this); @@ -95,17 +86,13 @@ public synchronized TableProvider establishStorage(String localDatabaseRoot, int registry.rebind(FACTORY_NAME, this); } - remoteProvider = new RemoteTableProviderImpl(provider); - - return provider; + providerWrap = new ProviderWrap(localProvider); + remoteProvider = new RemoteTableProviderImpl(providerWrap); } catch (Exception exc) { Log.log(RemoteTableProviderFactoryImpl.class, exc, "Got exception on establishment"); try { - if (factory != null) { - factory.close(); - } - factory = null; - provider = null; + providerWrap.close(); + providerWrap = null; registry = null; remoteProvider = null; } catch (Exception ignored) { @@ -117,4 +104,5 @@ public synchronized TableProvider establishStorage(String localDatabaseRoot, int throw exc; } } + } diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableStub.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableStub.java index 21e80b3d1..9b5360c8e 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableStub.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/db/remote/RemoteTableStub.java @@ -20,6 +20,9 @@ * I created this stub because task API is not friendly and there are no RemoteExceptions. */ final class RemoteTableStub implements Table, Serializable, Closeable { + // Frankly speaking there is no need to perform forceClose() method. It it done to decrease count of + // remote requests. + /** * Table name is cached for correct close handling at client's local. */ @@ -27,7 +30,7 @@ final class RemoteTableStub implements Table, Serializable, Closeable { private final IRemoteTable remoteTable; /** - * For synchronization on client side. + * For validity control on client side. */ private final ValidityController validityController = new ValidityController(); /** @@ -40,10 +43,6 @@ public RemoteTableStub(RemoteTableImpl remoteTable, String tableName) { this.tableName = tableName; } - public IRemoteTable getRemoteTable() { - return remoteTable; - } - /** * Called at client side when we get the stub. */ 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 index 1c15b2f8c..c7f220bfb 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/json/JSONMaker.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/json/JSONMaker.java @@ -16,7 +16,7 @@ * This class helps to construct a JSON string from any object using special annotations. * @author Phoenix * @see JSONComplexObject - * @see ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.json.JSONField + * @see JSONField */ public final class JSONMaker { private JSONMaker() { } @@ -169,7 +169,7 @@ private static void appendJSONString(final StringBuilder sb, * @see Object#toString() * @see Number * @see JSONComplexObject - * @see ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.json.JSONField + * @see JSONField */ public static String makeJSON(Object object) throws RuntimeException { try { 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 index ef8325608..3d9d6b389 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/json/JSONParsedObject.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/json/JSONParsedObject.java @@ -12,7 +12,7 @@ *
  • {@link Double}
  • *
  • {@link Boolean}
  • *
  • {@link String}
  • - *
  • {@link JSONParsedObject}
  • + *
  • {@link ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.json.JSONParsedObject}
  • * */ public interface JSONParsedObject { @@ -41,7 +41,7 @@ public interface JSONParsedObject { * @param name * name of the field * @return null, {@link Integer}, {@link String}, {@link Double}, {@link Boolean} or {@link - * ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.json.JSONParsedObject}. + * JSONParsedObject}. * @throws UnsupportedOperationException */ Object get(String name) throws UnsupportedOperationException; @@ -51,7 +51,7 @@ public interface JSONParsedObject { * @param index * index of the element * @return null, {@link Integer}, {@link String}, {@link Double}, {@link Boolean} or {@link - * ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.json.JSONParsedObject}. + * JSONParsedObject}. * @throws UnsupportedOperationException */ Object get(int index) throws UnsupportedOperationException, ClassCastException; @@ -113,7 +113,7 @@ default JSONParsedObject getObject(int index) throws UnsupportedOperationExcepti * @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 - * ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.json.JSONParsedObject}. + * JSONParsedObject}. */ Object deepGet(Object... namePieces); diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/parallel/ControllableAgent.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/parallel/ControllableAgent.java similarity index 83% rename from src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/parallel/ControllableAgent.java rename to src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/parallel/ControllableAgent.java index 0cd93277c..b31d70799 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/parallel/ControllableAgent.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/parallel/ControllableAgent.java @@ -1,4 +1,4 @@ -package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test.support.parallel; +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.parallel; /** * Interface that lets the controllable runnable to notify all waiting threads that the pause has come and 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/parallel/ControllableRunnable.java similarity index 94% rename from src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/parallel/ControllableRunnable.java rename to src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/parallel/ControllableRunnable.java index 3bf903c4e..2af52b2c4 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/parallel/ControllableRunnable.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/parallel/ControllableRunnable.java @@ -1,4 +1,4 @@ -package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test.support.parallel; +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.parallel; /** * Base class for runnables served by this runner. 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/parallel/ControllableRunner.java similarity index 98% rename from src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/parallel/ControllableRunner.java rename to src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/parallel/ControllableRunner.java index d71b1ba8a..663797e0d 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/parallel/ControllableRunner.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/parallel/ControllableRunner.java @@ -1,4 +1,4 @@ -package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test.support.parallel; +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.parallel; /** * Runnable that consumes some collections of runnables that must be executed sequentially and executes them 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/parallel/ExceptionFreeRunnable.java similarity index 82% rename from src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/parallel/ExceptionFreeRunnable.java rename to src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/parallel/ExceptionFreeRunnable.java index 4204d625c..f5a9c7c53 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/parallel/ExceptionFreeRunnable.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/parallel/ExceptionFreeRunnable.java @@ -1,4 +1,4 @@ -package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test.support.parallel; +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.parallel; /** * Interface for runnable that can throw any exceptions. diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/servlet/HttpDBServer.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/servlet/HttpDBServer.java new file mode 100644 index 000000000..ab640d1a4 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/servlet/HttpDBServer.java @@ -0,0 +1,234 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.servlet; + +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import ru.fizteh.fivt.storage.structured.Storeable; +import ru.fizteh.fivt.storage.structured.Table; +import ru.fizteh.fivt.storage.structured.TableProvider; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.servlet.transactions.Transaction; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.servlet.transactions.TransactionPool; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.net.InetSocketAddress; + +public class HttpDBServer { + public static final String PARAM_TABLE = "table"; + public static final String PARAM_TRANSACTION_ID = "tid"; + public static final String FIELD_DIFF = "diff"; + public static final String PARAM_KEY = "key"; + public static final String PARAM_VALUE = "value"; + private static final int TRANSACTION_ID_UPPER_BOUND = 100000; + /** + * Default value: 10 minutes. + */ + private static final long TRANSACTION_TIME_TO_LIVE = 10 * 60 * 1000L; + private final TableProvider localProvider; + private Server httpServer; + private volatile TransactionPool transactionPool; + + public HttpDBServer(TableProvider localProvider) { + this.localProvider = localProvider; + } + + public void startHttpServer(String host, int port) throws Exception { + if (isStarted()) { + throw new IllegalStateException("HttpServer is already initialized"); + } + + transactionPool = new TransactionPool(TRANSACTION_ID_UPPER_BOUND, TRANSACTION_TIME_TO_LIVE); + + httpServer = new Server(new InetSocketAddress(host, port)); + ServletContextHandler handler = new ServletContextHandler(ServletContextHandler.NO_SESSIONS); + + // Adding servlets. + handler.addServlet(new ServletHolder(new BeginServlet()), "/begin"); + handler.addServlet(new ServletHolder(new CommitServlet()), "/commit"); + handler.addServlet(new ServletHolder(new RollbackServlet()), "/rollback"); + handler.addServlet(new ServletHolder(new GetServlet()), "/get"); + handler.addServlet(new ServletHolder(new PutServlet()), "/put"); + handler.addServlet(new ServletHolder(new SizeServlet()), "/size"); + + httpServer.setHandler(handler); + + try { + httpServer.start(); + } catch (Exception exc) { + transactionPool.closePool(); + transactionPool = null; + httpServer = null; + throw exc; + } + } + + public void stopHttpServerIfStarted() throws Exception { + if (isStarted()) { + stopHttpServer(); + } + } + + public void stopHttpServer() throws Exception { + if (!isStarted()) { + throw new IllegalStateException("HttpServer not initialized"); + } + + try { + httpServer.stop(); + } finally { + transactionPool.closePool(); + httpServer = null; + transactionPool = null; + } + } + + public boolean isStarted() { + return httpServer != null; + } + + class BadRequestException extends Exception { + public BadRequestException(String message) { + super(message); + } + } + + abstract class BaseDBServlet extends HttpServlet { + protected abstract void serveGet(HttpServletRequest request, HttpServletResponse response) + throws Exception; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + try { + serveGet(req, resp); + } catch (BadRequestException exc) { + resp.reset(); + resp.setStatus(HttpServletResponse.SC_BAD_REQUEST); + resp.getWriter().println(exc.getMessage()); + } catch (Exception exc) { + resp.reset(); + resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + resp.getWriter().println(exc.getMessage()); + } + } + } + + class SizeServlet extends BaseDBServlet { + @Override + protected void serveGet(HttpServletRequest request, HttpServletResponse response) throws Exception { + int transactionID = Integer.parseInt(request.getParameter(PARAM_TRANSACTION_ID)); + Transaction
    transaction = transactionPool.obtainTransaction(transactionID); + try { + int size = transaction.getExtraData().size(); + response.getWriter().print(size + ""); + } finally { + transactionPool.releaseTransaction(transaction); + } + } + } + + class PutServlet extends BaseDBServlet { + @Override + protected void serveGet(HttpServletRequest request, HttpServletResponse response) throws Exception { + int transactionID = Integer.parseInt(request.getParameter(PARAM_TRANSACTION_ID)); + Transaction
    transaction = transactionPool.obtainTransaction(transactionID); + try { + String key = request.getParameter(PARAM_KEY); + String serializedNewValue = request.getParameter(PARAM_VALUE); + + // Deserializing new value. + Storeable storeableNewValue = + localProvider.deserialize(transaction.getExtraData(), serializedNewValue); + + // Putting new value instead of old. + Storeable storeableOldValue = transaction.getExtraData().put(key, storeableNewValue); + + // This key did not exist before. + if (storeableOldValue == null) { + throw new BadRequestException("Key not found: " + key); + } + // Now we must serialize old value. + String serializedOldValue = + localProvider.serialize(transaction.getExtraData(), storeableOldValue); + response.getWriter().print(serializedOldValue); + } finally { + transactionPool.releaseTransaction(transaction); + } + } + } + + class GetServlet extends BaseDBServlet { + @Override + protected void serveGet(HttpServletRequest request, HttpServletResponse response) throws Exception { + int transactionID = Integer.parseInt(request.getParameter(PARAM_TRANSACTION_ID)); + Transaction
    transaction = transactionPool.obtainTransaction(transactionID); + try { + String key = request.getParameter(PARAM_KEY); + Storeable storeableValue = transaction.getExtraData().get(key); + // Not found + if (storeableValue == null) { + throw new BadRequestException("Key not found: " + key); + } + // Now we must serialize it. + String serializedValue = localProvider.serialize(transaction.getExtraData(), storeableValue); + response.getWriter().print(serializedValue); + } finally { + transactionPool.releaseTransaction(transaction); + } + } + } + + class CommitServlet extends BaseDBServlet { + @Override + protected void serveGet(HttpServletRequest request, HttpServletResponse response) throws Exception { + int transactionID = Integer.parseInt(request.getParameter(PARAM_TRANSACTION_ID)); + Transaction
    transaction = transactionPool.obtainTransaction(transactionID); + try { + int diff = transaction.getExtraData().commit(); + response.getWriter().print(String.format("%s=%d", FIELD_DIFF, diff)); + } finally { + transactionPool.killTransaction(transaction); + } + } + } + + class RollbackServlet extends BaseDBServlet { + @Override + protected void serveGet(HttpServletRequest request, HttpServletResponse response) throws Exception { + int transactionID = Integer.parseInt(request.getParameter(PARAM_TRANSACTION_ID)); + Transaction
    transaction = transactionPool.obtainTransaction(transactionID); + try { + int diff = transaction.getExtraData().rollback(); + response.getWriter().print(String.format("%s=%d", FIELD_DIFF, diff)); + } finally { + transactionPool.killTransaction(transaction); + } + } + } + + class BeginServlet extends BaseDBServlet { + @Override + protected void serveGet(HttpServletRequest request, HttpServletResponse response) throws Exception { + String tableName = request.getParameter(PARAM_TABLE); + + // exception can occur here + Table table = localProvider.getTable(tableName); + if (table == null) { + throw new IllegalArgumentException("Table " + tableName + " not exists"); + } + + Transaction
    transaction = transactionPool.newTransaction(); + try { + transaction.setExtraData(table); + int transactionID = transaction.getTransactionID(); + response.getWriter().print(String.format("%s=%05d", PARAM_TRANSACTION_ID, transactionID)); + } finally { + transactionPool.releaseTransaction(transaction); + } + } + + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/servlet/HttpDBServerCommands.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/servlet/HttpDBServerCommands.java new file mode 100644 index 000000000..5b45e76ed --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/servlet/HttpDBServerCommands.java @@ -0,0 +1,94 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.servlet; + +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.DatabaseIOException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.TerminalException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.AbstractCommand; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Command; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.SimpleCommandContainer; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.SingleDatabaseShellState; + +import java.util.Map; + +/** + * Container for server commands: start, stop. + */ +public class HttpDBServerCommands extends SimpleCommandContainer { + public static final Command STOPHTTP = + new AbstractCommand("stophttp", "", "stops http server", 1) { + @Override + public void executeSafely(HttpDBServerState state, String[] args) throws Exception { + int port = state.stopHttpServer(); + state.getOutputStream().println("stopped at " + port); + } + }; + public static final Command EXIT = new AbstractCommand( + "exit", "", "stops http server (if it is started) and closes the terminal", 1) { + @Override + public void execute(HttpDBServerState state, String[] args) throws TerminalException { + state.prepareToExit(0); + + // If all contracts are honoured, this line should not be reached. + throw new AssertionError("Exit request not thrown"); + } + + @Override + public void executeSafely(HttpDBServerState state, String[] args) throws Exception { + // Not used. + } + }; + public static final Command HELP = new AbstractCommand( + "help", "", "prints out description of state commands", 1, Integer.MAX_VALUE) { + @Override + public void execute(HttpDBServerState state, String[] args) { + Map> commands = state.getCommands(); + + state.getOutputStream().println( + "You can start http database server ready for new connections!"); + + state.getOutputStream().println( + String.format( + "You can set database directory to work with using environment " + + "variable '%s'", SingleDatabaseShellState.DB_DIRECTORY_PROPERTY_NAME)); + + for (Command command : commands.values()) { + state.getOutputStream().println(command.buildHelpLine()); + } + } + + @Override + public void executeSafely(HttpDBServerState state, String[] args) throws DatabaseIOException { + // not used + } + }; + private static final int DEFAULT_PORT = 8080; + public static final Command STARTHTTP = new AbstractCommand( + "starthttp", + "[port]", + "starts http server at the specified port (or, if not specified, at " + DEFAULT_PORT + ")", + 1, + 2) { + @Override + public void executeSafely(HttpDBServerState state, String[] args) throws Exception { + int port; + if (args.length == 1) { + port = DEFAULT_PORT; + } else { + port = Integer.parseInt(args[1]); + } + + state.startHttpServer(port); + state.getOutputStream().println("started at " + port); + } + }; + private static final HttpDBServerCommands INSTANCE = new HttpDBServerCommands(); + + /** + * Not for initializing. + */ + private HttpDBServerCommands() { + } + + public static HttpDBServerCommands obtainInstance() { + return INSTANCE; + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/servlet/HttpDBServerState.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/servlet/HttpDBServerState.java new file mode 100644 index 000000000..6e9c3535b --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/servlet/HttpDBServerState.java @@ -0,0 +1,48 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.servlet; + +import ru.fizteh.fivt.storage.structured.TableProvider; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.BaseShellState; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Command; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Log; + +import java.util.Map; + +public class HttpDBServerState extends BaseShellState { + private final HttpDBServer server; + + private int port = -1; + + public HttpDBServerState(TableProvider localProvider) { + server = new HttpDBServer(localProvider); + } + + public void startHttpServer(int port) throws Exception { + server.startHttpServer("localhost", port); + this.port = port; + } + + public int stopHttpServer() throws Exception { + server.stopHttpServer(); + int oldPort = port; + port = -1; + return oldPort; + } + + public boolean isStarted() { + return server.isStarted(); + } + + @Override + public void cleanup() { + try { + server.stopHttpServerIfStarted(); + } catch (Exception exc) { + Log.log(HttpDBServerState.class, exc, "Failed to cleanup"); + } + } + + @Override + public Map> getCommands() { + return HttpDBServerCommands.obtainInstance().getCommands(); + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/servlet/transactions/Transaction.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/servlet/transactions/Transaction.java new file mode 100644 index 000000000..301abd73e --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/servlet/transactions/Transaction.java @@ -0,0 +1,196 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.servlet.transactions; + +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.parallel.ControllableAgent; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.parallel.ControllableRunner; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.parallel.ExceptionFreeRunnable; + +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock; + +/** + * Transaction object that executes some action within the same thread which sleep between actions.
    + * Can be used only by one thread at a time.
    + * You can reuse this object many times. + */ +public class Transaction { + /** + * ID of this transaction. + */ + private final int transactionID; + /** + * Action performer. Run by hostRunner. Sleeps between action performing. + */ + private final ActionPerformer performer = new ActionPerformer(); + /** + * Lock for thread-safe use. + */ + private final WriteLock useLock = new ReentrantReadWriteLock(true).writeLock(); + /** + * Host runner to run the performer. + */ + private final ControllableRunner hostRunner = new ControllableRunner(); + /** + * Action to perform next. Nullable. + */ + private Action action; + /** + * Result of performed action. Can be null. + */ + private Object result; + /** + * If true, execution must be stopped. + */ + private State state = State.NotStarted; + /** + * Error occurred during action performing. Null, if all is ok. + */ + private Exception error; + /** + * Timestamp when this transaction was last accessed. {@link TransactionPool} decides when to kill this + * transaction looking at this timestamp. + */ + private long lastAccessTime; + private T extraData; + + public Transaction(int transactionID) { + this.transactionID = transactionID; + } + + public int getTransactionID() { + return transactionID; + } + + public T getExtraData() { + useLock.lock(); + try { + return extraData; + } finally { + useLock.unlock(); + } + } + + public void setExtraData(T extraData) { + useLock.lock(); + try { + this.extraData = extraData; + } finally { + useLock.unlock(); + } + } + + /** + * This method is called by TransactionPool to decide whether to kill this transaction or not. + * @return + */ + long getLastAccessTime() { + useLock.lock(); + try { + return lastAccessTime; + } finally { + useLock.unlock(); + } + } + + /** + * This method is called by TransactionPool when this transaction is created.
    + * @throws Exception + */ + void init() throws Exception { + lastAccessTime = System.currentTimeMillis(); + if (state != State.NotStarted) { + throw new IllegalStateException("Already initialized"); + } + state = State.Active; + hostRunner.createAndAssign(performer); + new Thread(hostRunner, "Transaction " + transactionID).start(); + + hostRunner.waitUntilPause(); + } + + /** + * This method is called by TransactionPool when this transaction must be killed.
    + * Lock is expected to be obtained outside this method. + * @throws Exception + */ + void destroy() throws Exception { + if (state == State.Active) { + state = State.Stopped; + hostRunner.waitUntilEndOfWork(); + } else { + state = State.Stopped; + } + } + + void obtainWriteLock() { + useLock.lock(); + } + + void releaseWriteLock() { + useLock.unlock(); + } + + @Override + protected void finalize() throws Throwable { + super.finalize(); + destroy(); + } + + public T executeAction(Action action) throws Exception { + useLock.lock(); + try { + lastAccessTime = System.currentTimeMillis(); + if (state != State.Active) { + throw new IllegalStateException("You can execute actions only in active state"); + } + + this.action = action; + + hostRunner.continueWork(); + hostRunner.waitUntilPause(); + + if (error != null) { + throw error; + } + return (T) result; + } finally { + useLock.unlock(); + } + } + + enum State { + NotStarted, + Stopped, + Active + } + + public interface Action { + T perform() throws Exception; + } + + class ActionPerformer implements ExceptionFreeRunnable { + @Override + public void runWithFreedom(ControllableAgent agent) throws Exception, AssertionError { + while (true) { + agent.notifyAndWait(); + + if (state == State.Stopped) { + return; + } + + if (action == null) { + throw new IllegalStateException("Cannot run without action"); + } + + // Exception can occur on this step. + try { + error = null; + result = action.perform(); + } catch (Exception exc) { + error = exc; + } finally { + action = null; + } + } + } + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/servlet/transactions/TransactionPool.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/servlet/transactions/TransactionPool.java new file mode 100644 index 000000000..568691969 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/servlet/transactions/TransactionPool.java @@ -0,0 +1,195 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.servlet.transactions; + +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.InvalidatedObjectException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Log; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.ValidityController; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.ValidityController.KillLock; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.ValidityController.UseLock; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.Predicate; + +/** + * Thread-safe transaction pool. + */ +public class TransactionPool { + private static final int RANDOM_ID_ATTEMPTS = 10; + private final int idUpperBound; + private final Timer timer; + + private final ValidityController validityController = new ValidityController(); + + /** + * Mapping between transaction IDs and transactions. + */ + private final Map> transactionMap = new HashMap<>(); + + /** + * For operating with transactionMap. + */ + private final ReadWriteLock lock = new ReentrantReadWriteLock(true); + + /** + * Time to live for each transaction. If at least this time passes after a transaction was last accessed, + * it must be killed. + */ + private final long transactionTimeToLive; + + public TransactionPool(int idUpperBound, long transactionTimeToLive) { + this.idUpperBound = idUpperBound; + this.transactionTimeToLive = transactionTimeToLive; + timer = new Timer(); + timer.schedule(new PeriodicCleanup(), transactionTimeToLive, transactionTimeToLive / 2); + } + + public void closePool() { + try (KillLock killLock = validityController.useAndKill()) { + timer.cancel(); + cleanup((transaction) -> true); + } + } + + @Override + protected void finalize() throws Throwable { + super.finalize(); + try { + closePool(); + } catch (InvalidatedObjectException exc) { + // Ignore it. + } + } + + /** + * Perform pool cleanup. + * @param filter + * if true, transaction must be killed. Otherwise it is not effected. + */ + private void cleanup(Predicate> filter) { + try (UseLock useLock = validityController.use()) { + lock.writeLock().lock(); + try { + Iterator>> transactionsIter = + transactionMap.entrySet().iterator(); + while (transactionsIter.hasNext()) { + Transaction transaction = transactionsIter.next().getValue(); + transaction.obtainWriteLock(); + try { + if (filter.test(transaction)) { + try { + transactionsIter.remove(); + transaction.destroy(); + } catch (Exception exc) { + Log.log( + TransactionPool.class, + exc, + "Error while killing transaction: " + transaction.getTransactionID()); + } + } + } finally { + transaction.releaseWriteLock(); + } + } + } finally { + lock.writeLock().unlock(); + } + } + } + + public void killTransaction(Transaction transaction) throws Exception { + try (UseLock useLock = validityController.use()) { + lock.writeLock().lock(); + try { + if (transactionMap.get(transaction.getTransactionID()) != transaction) { + throw new IllegalArgumentException("Transaction not from this pool"); + } + transactionMap.remove(transaction.getTransactionID()); + try { + transaction.destroy(); + } finally { + releaseTransaction(transaction); + } + } finally { + lock.writeLock().unlock(); + } + } + } + + public void releaseTransaction(Transaction transaction) { + try (UseLock useLock = validityController.use()) { + transaction.releaseWriteLock(); + } + } + + public Transaction obtainTransaction(int transactionID) throws IllegalArgumentException { + try (UseLock useLock = validityController.use()) { + lock.readLock().lock(); + try { + Transaction transaction = transactionMap.get(transactionID); + if (transaction == null) { + throw new IllegalArgumentException("Transaction not found: " + transactionID); + } + + transaction.obtainWriteLock(); + return transaction; + } finally { + lock.readLock().unlock(); + } + } + } + + public Transaction newTransaction() throws Exception { + try (UseLock useLock = validityController.use()) { + ThreadLocalRandom random = ThreadLocalRandom.current(); + lock.writeLock().lock(); + try { + if (idUpperBound == transactionMap.size()) { + throw new IllegalStateException("No more free transaction IDs"); + } + int transactionID = random.nextInt(idUpperBound); + int times = 1; + while (transactionMap.containsKey(transactionID) && times < RANDOM_ID_ATTEMPTS) { + transactionID = random.nextInt(idUpperBound); + times++; + } + if (transactionMap.containsKey(transactionID)) { + for (transactionID = 0; transactionID < idUpperBound; transactionID++) { + if (!transactionMap.containsKey(transactionID)) { + break; + } + } + } + + Transaction transaction = new Transaction<>(transactionID); + transaction.obtainWriteLock(); + + transaction.init(); + transactionMap.put(transactionID, transaction); + return transaction; + } finally { + lock.writeLock().unlock(); + } + } + } + + class PeriodicCleanup extends TimerTask { + @Override + public void run() { + try { + long currentTime = System.currentTimeMillis(); + cleanup( + (transaction) -> currentTime - transaction.getLastAccessTime() + >= transactionTimeToLive); + } catch (Exception exc) { + Log.log(PeriodicCleanup.class, exc, "Failed to perform periodic cleanup"); + } + } + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/AbstractCommand.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/AbstractCommand.java index b23c18442..925912f8e 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/AbstractCommand.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/AbstractCommand.java @@ -7,7 +7,6 @@ import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.UnexpectedRemoteException; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.WrongArgsNumberException; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.AccurateExceptionHandler; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Log; import java.io.IOException; import java.io.PrintStream; @@ -21,50 +20,44 @@ * @author phoenix */ public abstract class AbstractCommand> implements Command { - private static final Class[] EXECUTE_SAFELY_THROWN_EXCEPTIONS; - - static { - Class[] exceptions = null; - - try { - exceptions = AbstractCommand.class.getDeclaredMethod( - "executeSafely", ShellState.class, String[].class).getExceptionTypes(); - } catch (Exception exc) { - Log.log(AbstractCommand.class, exc, "Failed to obtain exceptions thrown by executeSafely"); - throw new RuntimeException( - "Failed to obtain exceptions thrown by executeSafely: " + exc.getMessage(), - exc.getCause()); - } finally { - EXECUTE_SAFELY_THROWN_EXCEPTIONS = exceptions; - } - } /** - * Used for unsafe calls. Catches and handles all exceptions thrown by {@link - * AbstractCommand#executeSafely - * (SingleDatabaseShellState, String[]) } and {@link IllegalArgumentException }. + * Used for unsafe calls. Redirects handling of all exceptions to Shell. */ - public static final AccurateExceptionHandler DATABASE_ERROR_HANDLER = - (Exception exc, PrintStream ps) -> { - boolean found = false; - Class actualType = exc.getClass(); - - for (Class expectedType : EXECUTE_SAFELY_THROWN_EXCEPTIONS) { - try { - actualType.asSubclass(expectedType); - found = true; - break; - } catch (ClassCastException cce) { - // Ignore it. + public static final AccurateExceptionHandler DEFAULT_EXCEPTION_HANDLER = + new AccurateExceptionHandler() { + final Class[] handledExceptions = new Class[] {IllegalArgumentException.class, + NoActiveTableException.class, + IllegalStateException.class, + NullPointerException.class, + InvocationException.class, + ParseException.class, + IOException.class, + UnexpectedRemoteException.class, + ExecutionNotPermittedException.class}; + + @Override + public void handleException(Exception exc, PrintStream ps) throws TerminalException { + Class actualType = exc.getClass(); + boolean found = false; + + for (Class expectedType : handledExceptions) { + try { + actualType.asSubclass(expectedType); + found = true; + break; + } catch (ClassCastException cce) { + // Ignore it. + } } - } - if (found) { - Shell.handleError(exc.getMessage(), exc, true, ps); - } else if (exc instanceof RuntimeException) { - throw (RuntimeException) exc; - } else { - throw new RuntimeException("Unexpected exception", exc); + if (found) { + Shell.handleError(exc.getMessage(), exc, true, ps); + } else if (exc instanceof RuntimeException) { + throw (RuntimeException) exc; + } else { + throw new RuntimeException("Unexpected exception: " + exc.toString(), exc); + } } }; @@ -120,7 +113,7 @@ public void execute(final State state, final String[] args) throws TerminalExcep () -> { checkArgsNumber(args, minimalArgsCount, maximalArgsCount); executeSafely(state, args); - }, DATABASE_ERROR_HANDLER, state.getOutputStream()); + }, DEFAULT_EXCEPTION_HANDLER, state.getOutputStream()); } @Override @@ -138,16 +131,7 @@ public String getInvocation() { return invocationArgs; } - protected abstract void executeSafely(State state, String[] args) throws - IllegalArgumentException, - NoActiveTableException, - IllegalStateException, - NullPointerException, - InvocationException, - ParseException, - IOException, - ExecutionNotPermittedException, - UnexpectedRemoteException; + protected abstract void executeSafely(State state, String[] args) throws Exception; void checkArgsNumber(String[] args, int minimal, int maximal) throws WrongArgsNumberException { if (args.length < minimal || args.length > maximal) { 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/DatabaseFactory.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/DatabaseFactory.java deleted file mode 100644 index 10a1a0165..000000000 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/DatabaseFactory.java +++ /dev/null @@ -1,35 +0,0 @@ -package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell; - -import ru.fizteh.fivt.storage.structured.TableProvider; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.DBTableProviderFactory; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.Database; - -import java.io.Closeable; -import java.io.PrintStream; - -public final class DatabaseFactory implements Closeable { - private final DBTableProviderFactory factory; - private final TableProvider provider; - private final String databasePath; - - public DatabaseFactory(String databasePath) throws Exception { - factory = new DBTableProviderFactory(); - this.databasePath = databasePath; - provider = factory.create(databasePath); - } - - public Database obtainDatabase(PrintStream outputStream) { - return new Database(provider, databasePath, outputStream); - } - - @Override - public void close() { - factory.close(); - } - - @Override - protected void finalize() throws Throwable { - super.finalize(); - close(); - } -} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/JoinedState.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/JoinedState.java index bb4b5b3cc..5dae1beaf 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/JoinedState.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/JoinedState.java @@ -80,7 +80,9 @@ protected final void setAllCommands(Map + * Calls {@link ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.ShellState#cleanup()} on + * each + * registered state if it is not null.
    * States are obtained by {@link #obtainState(int)}. */ @Override @@ -120,17 +122,24 @@ public void prepareToExit(int exitCode) throws ExitRequest { } /** - * Returns mapping between command names and command wrappers that on {@link Command#execute(ShellState, - * String[])} calls one of two methods: {@link #onExecuteConflict(List, List, - * String[])} or {@link #onExecuteRequested(int, Command, String[])}.
    + * Returns mapping between command names and command wrappers that on {@link + * ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Command#execute(ru.fizteh.fivt.students + * .fedorov_andrew.databaselibrary.shell.ShellState, + * String[])} calls one of two methods: {@link #onExecuteConflict(java.util.List, java.util.List, + * String[])} or {@link #onExecuteRequested(int, ru.fizteh.fivt.students.fedorov_andrew.databaselibrary + * .shell.Command, + * String[])}.
    * Exceptions thrown by these methods are handled with local exception handler.
    * As soon as all possible state commands wrapped by one multicommand have the same name, {@link - * Command#getName()} returns that name, but both {@link Command#getInfo()} and {@link - * Command#getInvocation()} throw {@link UnsupportedOperationException}. + * ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Command#getName()} returns that name, but + * both {@link ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Command#getInfo()} and {@link + * ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Command#getInvocation()} throw {@link + * UnsupportedOperationException}. * @see #setExceptionHandler(AccurateExceptionHandler) * @see #getExceptionHandler() - * @see #onExecuteRequested(int, Command, String[]) - * @see #onExecuteConflict(List, List, String[]) + * @see #onExecuteRequested(int, ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Command, + * String[]) + * @see #onExecuteConflict(java.util.List, java.util.List, String[]) */ @Override public Map> getCommands() { @@ -148,8 +157,10 @@ protected boolean areNamesEqual(Command commandA, Command commandB) { /** * This method is called when there are several commands with the same name from different states. You * decide how to resolve this conflict.
    - * The default implementation throws {@link java.lang.UnsupportedOperationException}.
    - * In this method you can also call {@link #onExecuteRequested(int, Command, String[])} after you have + * The default implementation throws {@link UnsupportedOperationException}.
    + * In this method you can also call {@link #onExecuteRequested(int, ru.fizteh.fivt.students + * .fedorov_andrew.databaselibrary.shell.Command, + * String[])} after you have * chosen for which state to call the command. * @throws Exception */ @@ -161,7 +172,7 @@ protected void onExecuteConflict(List stateIDs, List commands, /** * Runs normal execution of the given state command. The default implementation is {@code * command.execute(obtainState(stateID), args)}. - * @throws java.lang.Exception + * @throws Exception */ protected void executeNormally(int stateID, Command command, String[] args) throws Exception { command.execute(obtainState(stateID), args); @@ -187,7 +198,7 @@ protected void onExecuteRequested(int stateID, Command command, String[] args) t } /** - * Command for {@link ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.JoinedState} that + * Command for {@link JoinedState} that * wraps * at least one command from a state. */ 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 b5a7d06fd..b3f70aba5 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/Main.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/Main.java @@ -1,12 +1,11 @@ package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell; -import ru.fizteh.fivt.storage.structured.RemoteTableProvider; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.Database; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.remote.RemoteDatabaseStorage; +import ru.fizteh.fivt.storage.structured.TableProvider; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.AutoCloseableTableProviderFactory; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.DBTableProviderFactory; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.TerminalException; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet.ClientServerGeneralState; - -import java.rmi.registry.Registry; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Log; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet.DBShellGeneralState; class Main { //java -Dfizteh.db.dir=/home/phoenix/test/DB ru.fizteh.fivt.students.fedorov_andrew.databaselibrary @@ -15,19 +14,11 @@ class Main { private static final String PATH_PROPERTY = "fizteh.db.dir"; public static void main(String[] args) { - try { - Shell shell = new Shell<>( - new ClientServerGeneralState( - System.getProperty( - SingleDatabaseShellState.DB_DIRECTORY_PROPERTY_NAME)) { - @Override - protected Database obtainNewActiveDatabase() throws Exception { - RemoteDatabaseStorage storage = new RemoteDatabaseStorage(); - RemoteTableProvider provider = - storage.connect("localhost", Registry.REGISTRY_PORT); - return new Database(provider, "", getOutputStream()); - } - }); + try (AutoCloseableTableProviderFactory factory = new DBTableProviderFactory()) { + String databaseRoot = System.getProperty(SingleDatabaseShellState.DB_DIRECTORY_PROPERTY_NAME); + TableProvider provider = factory.create(databaseRoot); + + Shell shell = new Shell<>(new DBShellGeneralState(provider, databaseRoot)); int exitCode; if (args.length == 0) { exitCode = shell.run(System.in); @@ -40,7 +31,7 @@ protected Database obtainNewActiveDatabase() throws Exception { // Already handled. System.exit(1); } catch (Exception exc) { - // exc.printStackTrace(); + Log.log(Main.class, exc); System.out.println(exc.getMessage()); System.exit(1); } diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/ShellState.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/ShellState.java index cb6d62542..fbf448be0 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/ShellState.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/ShellState.java @@ -1,6 +1,7 @@ package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.ExitRequest; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Log; import java.io.PrintStream; @@ -33,10 +34,15 @@ public interface ShellState> extends CommandContainer void init(Shell host) throws Exception; /** - * Safely exit with cleanup. + * Safely exit with cleanup. Default implementation calls {@link #cleanup()} and throws {@link + * ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.ExitRequest} with the given code. * @throws ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.ExitRequest * you must throw this exception to indicate that you really want to exit. Do no call * {@link System#exit(int)} instead of it. */ - void prepareToExit(int exitCode) throws ExitRequest; + default void prepareToExit(int exitCode) throws ExitRequest { + Log.log(getClass(), "Preparing to exit with code " + exitCode); + cleanup(); + throw new ExitRequest(exitCode); + } } diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/SingleDatabaseShellState.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/SingleDatabaseShellState.java index 3ec155d8b..3d2d48132 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/SingleDatabaseShellState.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/shell/SingleDatabaseShellState.java @@ -3,9 +3,7 @@ import ru.fizteh.fivt.storage.structured.Table; import ru.fizteh.fivt.storage.structured.TableProvider; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.Database; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.ExitRequest; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.NoActiveTableException; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Log; import java.io.PrintStream; import java.util.Map; @@ -60,15 +58,10 @@ public void init(Shell host) throws Exception { @Override public void cleanup() { - getActiveDatabase().rollback(); - } - - @Override - public void prepareToExit(int exitCode) throws ExitRequest { - Log.log(SingleDatabaseShellState.class, "Preparing to exit with code " + exitCode); - cleanup(); - Log.close(); - throw new ExitRequest(exitCode); + Database activeDatabase = getActiveDatabase(); + if (activeDatabase != null) { + activeDatabase.rollback(); + } } /** 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 ba39c9a8f..e29a263d0 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/Utility.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/support/Utility.java @@ -213,7 +213,7 @@ public static Map inverseMap(Map map) throws IllegalArgumentE * @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) { diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/ClientServerGeneralState.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/ClientServerGeneralState.java deleted file mode 100644 index fc2df25db..000000000 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/ClientServerGeneralState.java +++ /dev/null @@ -1,122 +0,0 @@ -package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet; - -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.Database; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.ExecutionNotPermittedException; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.TerminalException; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.AbstractCommand; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Command; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.JoinedState; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Shell; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.ShellState; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet.client.ClientCommands; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet.client.ClientGeneralState; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet.server.DBServerState; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet.server.ServerCommands; - -import java.util.List; - -public abstract class ClientServerGeneralState extends JoinedState { - private static final int CLIENT_STATE_ID = 0; - private static final int SERVER_STATE_ID = 1; - - private ClientGeneralState clientState; - private DBServerState serverState; - - public ClientServerGeneralState(String databaseRoot) throws TerminalException { - ClientServerGeneralState genState = this; - clientState = new ClientGeneralState() { - @Override - protected Database obtainNewActiveDatabase() throws Exception { - return genState.obtainNewActiveDatabase(); - } - }; - serverState = new DBServerState(databaseRoot); - - new Shell<>(clientState); - new Shell<>(serverState); - - setAllCommands(clientState.getCommands(), serverState.getCommands()); - setExceptionHandler( - (exception, noData) -> AbstractCommand.DATABASE_ERROR_HANDLER - .handleException(exception, getOutputStream())); - } - - @Override - protected void onExecuteConflict(List stateIDs, List commands, String[] args) - throws Exception { - Command serverCommand = null; - Command clientCommand = null; - - for (int i = 0; i < stateIDs.size(); i++) { - switch (stateIDs.get(i)) { - case CLIENT_STATE_ID: { - clientCommand = commands.get(i); - break; - - } - case SERVER_STATE_ID: { - serverCommand = commands.get(i); - break; - } - default: { - throw new IllegalArgumentException("Illegal state ID: " + stateIDs.get(i)); - } - } - } - - boolean serverActive = serverState.isStarted(); - boolean clientActive = clientState.isConnected(); - - // Conflict between server and client EXIT or HELP command. - if (serverActive && !clientActive) { - // Server variant - onExecuteRequested(SERVER_STATE_ID, serverCommand, args); - } else if (clientActive && !serverActive) { - // Client variant - onExecuteRequested(CLIENT_STATE_ID, clientCommand, args); - } else if (areNamesEqual(serverCommand, ServerCommands.HELP) && areNamesEqual( - clientCommand, ClientCommands.HELP)) { - executeNormally(SERVER_STATE_ID, serverCommand, args); - executeNormally(CLIENT_STATE_ID, clientCommand, args); - } else if (areNamesEqual(serverCommand, ServerCommands.EXIT) && areNamesEqual( - clientCommand, ClientCommands.EXIT)) { - prepareToExit(0); - } else { - throw new UnsupportedOperationException( - "Cannot resolve command conflict: " + serverCommand.getName() + " and " + clientCommand - .getName()); - } - } - - @Override - protected ShellState obtainState(int stateID) { - switch (stateID) { - case CLIENT_STATE_ID: - return clientState; - case SERVER_STATE_ID: - return serverState; - default: - throw new IllegalArgumentException("Illegal state ID: " + stateID); - } - } - - @Override - protected void onExecuteRequested(int stateID, Command command, String[] args) throws Exception { - if (stateID == CLIENT_STATE_ID) { - if (serverState.isStarted()) { - throw new ExecutionNotPermittedException("You cannot execute this command in server mode"); - } else { - executeNormally(stateID, command, args); - } - } else if (stateID == SERVER_STATE_ID) { - if (clientState.isConnected()) { - throw new ExecutionNotPermittedException( - "You cannot execute this command when connected as client"); - } else { - executeNormally(stateID, command, args); - } - } - } - - protected abstract Database obtainNewActiveDatabase() throws Exception; -} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/DBShellGeneralState.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/DBShellGeneralState.java new file mode 100644 index 000000000..393e0188e --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/DBShellGeneralState.java @@ -0,0 +1,152 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet; + +import ru.fizteh.fivt.storage.structured.TableProvider; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.ExecutionNotPermittedException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.TerminalException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.servlet.HttpDBServerCommands; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.servlet.HttpDBServerState; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.AbstractCommand; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Command; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.JoinedState; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Shell; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.ShellState; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet.client.ClientCommands; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet.client.ClientGeneralState; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet.server.ServerCommands; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet.server.TelnetDBServerState; + +import java.util.List; + +public class DBShellGeneralState extends JoinedState { + private static final int CLIENT_STATE_ID = 0; + private static final int TELNET_SERVER_STATE_ID = 1; + private static final int HTTP_SERVER_STATE_ID = 2; + + private ClientGeneralState clientState; + private TelnetDBServerState telnetServerState; + private HttpDBServerState httpServerState; + + public DBShellGeneralState(TableProvider provider, String databaseRoot) throws TerminalException { + //TODO: we need remote provider here. + + clientState = new ClientGeneralState(); + telnetServerState = new TelnetDBServerState(provider, databaseRoot); + httpServerState = new HttpDBServerState(provider); + + // Fake initialization. + new Shell<>(clientState); + new Shell<>(telnetServerState); + new Shell<>(httpServerState); + + setAllCommands( + clientState.getCommands(), telnetServerState.getCommands(), httpServerState.getCommands()); + setExceptionHandler( + (exception, noData) -> AbstractCommand.DEFAULT_EXCEPTION_HANDLER + .handleException(exception, getOutputStream())); + } + + @Override + protected void onExecuteConflict(List stateIDs, List commands, String[] args) + throws Exception { + Command telnetServerCommand = null; + Command clientCommand = null; + Command httpServerCommand = null; + + for (int i = 0; i < stateIDs.size(); i++) { + switch (stateIDs.get(i)) { + case CLIENT_STATE_ID: { + clientCommand = commands.get(i); + break; + + } + case TELNET_SERVER_STATE_ID: { + telnetServerCommand = commands.get(i); + break; + } + case HTTP_SERVER_STATE_ID: { + httpServerCommand = commands.get(i); + break; + } + default: { + throw new IllegalArgumentException("Illegal state ID: " + stateIDs.get(i)); + } + } + } + + boolean telnetServerActive = telnetServerState.isStarted(); + boolean clientActive = clientState.isConnected(); + boolean httpServerActive = httpServerState.isStarted(); + + boolean nobodyActive = !telnetServerActive && !clientActive && !httpServerActive; + + boolean helpCommand = areNamesEqual(telnetServerCommand, ServerCommands.HELP) && areNamesEqual( + clientCommand, ClientCommands.HELP) && areNamesEqual( + httpServerCommand, HttpDBServerCommands.HELP); + boolean exitCommand = areNamesEqual(telnetServerCommand, ServerCommands.EXIT) && areNamesEqual( + clientCommand, ClientCommands.EXIT) && areNamesEqual( + httpServerCommand, HttpDBServerCommands.EXIT); + + // Conflict between server and client EXIT or HELP command. + // if (telnetServerActive || httpServerActive) { + // if (telnetServerActive) { + // // Telnet Server variant + // onExecuteRequested(TELNET_SERVER_STATE_ID, telnetServerCommand, args); + // } + // if (httpServerActive) { + // onExecuteRequested(HTTP_SERVER_STATE_ID, httpServerCommand, args); + // } + // } else if (clientActive) { + // // Client variant + // onExecuteRequested(CLIENT_STATE_ID, clientCommand, args); + // } else + if (helpCommand) { + if (clientActive || nobodyActive) { + executeNormally(CLIENT_STATE_ID, clientCommand, args); + } + if (!clientActive || nobodyActive) { + executeNormally(HTTP_SERVER_STATE_ID, httpServerCommand, args); + executeNormally(TELNET_SERVER_STATE_ID, telnetServerCommand, args); + } + } else if (exitCommand) { + prepareToExit(0); + } else { + throw new UnsupportedOperationException( + "Cannot resolve command conflict: " + + telnetServerCommand.getName() + + " and " + + clientCommand.getName()); + } + } + + @Override + protected ShellState obtainState(int stateID) { + switch (stateID) { + case CLIENT_STATE_ID: + return clientState; + case TELNET_SERVER_STATE_ID: + return telnetServerState; + case HTTP_SERVER_STATE_ID: + return httpServerState; + default: + throw new IllegalArgumentException("Illegal state ID: " + stateID); + } + } + + @Override + protected void onExecuteRequested(int stateID, Command command, String[] args) throws Exception { + if (stateID == CLIENT_STATE_ID) { + if (telnetServerState.isStarted() || httpServerState.isStarted()) { + throw new ExecutionNotPermittedException("You cannot execute this command in server mode"); + } else { + executeNormally(stateID, command, args); + } + } else { + if (clientState.isConnected()) { + throw new ExecutionNotPermittedException( + "You cannot execute this command when connected as client"); + } else { + executeNormally(stateID, command, args); + } + } + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/client/ClientCommands.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/client/ClientCommands.java index 88d3f3652..bf31bb26b 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/client/ClientCommands.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/client/ClientCommands.java @@ -62,13 +62,14 @@ public void executeSafely(DBClientState state, String[] args) throws InvocationException, ParseException, IOException { - InetAddress address = state.getHost(); + String address = state.getHost(); int port = state.getPort(); - if (address.isLoopbackAddress()) { + InetAddress inetAddress = InetAddress.getByName(address); + if (inetAddress.isLoopbackAddress()) { state.getOutputStream().println("local " + port); } else { - state.getOutputStream().println("remote " + address.getHostAddress() + ":" + port); + state.getOutputStream().println("remote " + inetAddress.getHostAddress() + ":" + port); } } }; diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/client/ClientGeneralState.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/client/ClientGeneralState.java index 5cc90f008..8f58bb37f 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/client/ClientGeneralState.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/client/ClientGeneralState.java @@ -12,7 +12,7 @@ import java.util.List; -public abstract class ClientGeneralState extends JoinedState { +public class ClientGeneralState extends JoinedState { private static final int DB_STATE_ID = 0; private static final int CLIENT_STATE_ID = 1; @@ -28,14 +28,16 @@ public boolean isConnected() { return clientState.isConnected(); } - protected abstract Database obtainNewActiveDatabase() throws Exception; + protected Database obtainNewActiveDatabase() throws Exception { + return new Database(clientState.getRemoteProvider(), clientState.getHost(), getOutputStream()); + } @Override public void init(Shell host) throws Exception { super.init(host); clientState.init(host); setExceptionHandler( - (exception, noData) -> AbstractCommand.DATABASE_ERROR_HANDLER + (exception, noData) -> AbstractCommand.DEFAULT_EXCEPTION_HANDLER .handleException(exception, getOutputStream())); } diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/client/DBClientState.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/client/DBClientState.java index bf959c89d..3c416d1ad 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/client/DBClientState.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/client/DBClientState.java @@ -1,23 +1,27 @@ package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet.client; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.ExitRequest; +import ru.fizteh.fivt.storage.structured.RemoteTableProvider; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.remote.RemoteDatabaseStorage; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.BaseShellState; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Command; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.support.Log; import java.io.IOException; -import java.net.InetAddress; -import java.net.Socket; import java.util.Map; public class DBClientState extends BaseShellState { - private Socket connection; + private final RemoteDatabaseStorage storage = new RemoteDatabaseStorage(); + private RemoteTableProvider remoteProvider; + private String host; + private int port = -1; public void connect(String host, int port) throws IOException { checkInitialized(); - if (connection == null) { + if (remoteProvider == null) { try { - connection = new Socket(host, port); + remoteProvider = storage.connect(host, port); + this.host = host; + this.port = port; } catch (IOException exc) { throw new IOException("not connected: " + exc.getMessage(), exc.getCause()); } @@ -26,22 +30,26 @@ public void connect(String host, int port) throws IOException { } } - public InetAddress getHost() { + public RemoteTableProvider getRemoteProvider() { + return remoteProvider; + } + + public String getHost() { requireConnected(); - return connection.getInetAddress(); + return host; } public int getPort() { requireConnected(); - return connection.getPort(); + return port; } public void disconnect() throws IllegalStateException, IOException { requireConnected(); try { - connection.close(); + remoteProvider.close(); } finally { - connection = null; + remoteProvider = null; } } @@ -52,7 +60,7 @@ private void requireConnected() { } public boolean isConnected() { - return connection != null; + return remoteProvider != null; } @Override @@ -66,13 +74,6 @@ public void cleanup() { } } - @Override - public void prepareToExit(int exitCode) throws ExitRequest { - Log.log(DBClientState.class, "Preparing to exit with code:" + exitCode); - cleanup(); - throw new ExitRequest(exitCode); - } - @Override public Map> getCommands() { return ClientCommands.getInstance().getCommands(); diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/server/Server.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/server/Server.java index cf738fa84..628a92764 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/server/Server.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/server/Server.java @@ -133,7 +133,7 @@ private void run() { try { close(); } catch (IOException exc) { - Log.log(DBServerState.class, exc, this + ": failed to close server socket"); + Log.log(TelnetDBServerState.class, exc, this + ": failed to close server socket"); } } } diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/server/ServerCommands.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/server/ServerCommands.java index 8ee445268..5d37eeb56 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/server/ServerCommands.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/server/ServerCommands.java @@ -8,7 +8,7 @@ import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Command; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.SimpleCommandContainer; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.SingleDatabaseShellState; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet.server.DBServerState.User; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet.server.TelnetDBServerState.User; import java.io.IOException; import java.text.ParseException; @@ -18,33 +18,33 @@ /** * Container for server commands: start, stop, listusers. */ -public class ServerCommands extends SimpleCommandContainer { - public static final Command STOP = - new AbstractCommand("stop", "", "stops server", 1) { +public class ServerCommands extends SimpleCommandContainer { + public static final Command STOP = + new AbstractCommand("stop", "", "stops server", 1) { @Override - public void executeSafely(DBServerState state, String[] args) throws - IllegalArgumentException, - NoActiveTableException, - IllegalStateException, - NullPointerException, - InvocationException, - ParseException, - IOException { + public void executeSafely(TelnetDBServerState state, String[] args) throws + IllegalArgumentException, + NoActiveTableException, + IllegalStateException, + NullPointerException, + InvocationException, + ParseException, + IOException { int port = state.stopServer(); state.getOutputStream().println("stopped at " + port); } }; - public static final Command LISTUSERS = new AbstractCommand( + public static final Command LISTUSERS = new AbstractCommand( "listusers", "", "prints list of ip addresses and ports of connected users", 1) { @Override - public void executeSafely(DBServerState state, String[] args) throws - IllegalArgumentException, - NoActiveTableException, - IllegalStateException, - NullPointerException, - InvocationException, - ParseException, - IOException { + public void executeSafely(TelnetDBServerState state, String[] args) throws + IllegalArgumentException, + NoActiveTableException, + IllegalStateException, + NullPointerException, + InvocationException, + ParseException, + IOException { List users = state.listUsers(); StringBuilder sb = new StringBuilder(); @@ -55,10 +55,10 @@ public void executeSafely(DBServerState state, String[] args) throws state.getOutputStream().print(sb.toString()); } }; - public static final Command EXIT = new AbstractCommand( + public static final Command EXIT = new AbstractCommand( "exit", "", "stops server (if it is started) and closes the terminal", 1) { @Override - public void execute(DBServerState state, String[] args) throws TerminalException { + public void execute(TelnetDBServerState state, String[] args) throws TerminalException { state.prepareToExit(0); // If all contracts are honoured, this line should not be reached. @@ -66,22 +66,22 @@ public void execute(DBServerState state, String[] args) throws TerminalException } @Override - public void executeSafely(DBServerState state, String[] args) throws - IllegalArgumentException, - NoActiveTableException, - IllegalStateException, - NullPointerException, - InvocationException, - ParseException, - IOException { + public void executeSafely(TelnetDBServerState state, String[] args) throws + IllegalArgumentException, + NoActiveTableException, + IllegalStateException, + NullPointerException, + InvocationException, + ParseException, + IOException { // Not used. } }; - public static final Command HELP = new AbstractCommand( + public static final Command HELP = new AbstractCommand( "help", "", "prints out description of state commands", 1, Integer.MAX_VALUE) { @Override - public void execute(DBServerState state, String[] args) { - Map> commands = state.getCommands(); + public void execute(TelnetDBServerState state, String[] args) { + Map> commands = state.getCommands(); state.getOutputStream().println( "You can start telnet database server ready for new connections!"); @@ -91,31 +91,31 @@ public void execute(DBServerState state, String[] args) { "You can set database directory to work with using environment " + "variable '%s'", SingleDatabaseShellState.DB_DIRECTORY_PROPERTY_NAME)); - for (Command command : commands.values()) { + for (Command command : commands.values()) { state.getOutputStream().println(command.buildHelpLine()); } } @Override - public void executeSafely(DBServerState state, String[] args) throws DatabaseIOException { + public void executeSafely(TelnetDBServerState state, String[] args) throws DatabaseIOException { // not used } }; private static final int DEFAULT_PORT = 10001; - public static final Command START = new AbstractCommand( + public static final Command START = new AbstractCommand( "start", "[port]", "starts server at the specified port (or, if not specified, at " + DEFAULT_PORT + ")", 1, 2) { @Override - public void executeSafely(DBServerState state, String[] args) throws - IllegalArgumentException, - NoActiveTableException, - IllegalStateException, - InvocationException, - ParseException, - IOException { + public void executeSafely(TelnetDBServerState state, String[] args) throws + IllegalArgumentException, + NoActiveTableException, + IllegalStateException, + InvocationException, + ParseException, + IOException { int port; if (args.length == 1) { port = DEFAULT_PORT; diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/server/ServerCommunicator.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/server/ServerCommunicator.java index e457ad097..ec409ef08 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/server/ServerCommunicator.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/server/ServerCommunicator.java @@ -48,7 +48,7 @@ public void run() { close(); } catch (IOException exc) { Log.log( - DBServerState.class, + TelnetDBServerState.class, this + " failed to close connection after all commands execution"); } diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/server/DBServerState.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/server/TelnetDBServerState.java similarity index 79% rename from src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/server/DBServerState.java rename to src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/server/TelnetDBServerState.java index 448c6b86e..c4c0484d5 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/server/DBServerState.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/telnet/server/TelnetDBServerState.java @@ -6,7 +6,6 @@ import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.Database; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.remote.RemoteDatabaseStorage; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.remote.RemoteTableProviderFactoryImpl; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.ExitRequest; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.BaseShellState; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Command; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Shell; @@ -22,19 +21,19 @@ import java.util.function.Supplier; import java.util.stream.Collectors; -public class DBServerState extends BaseShellState { +public class TelnetDBServerState extends BaseShellState { private static final ServerCommands COMMANDS = ServerCommands.obtainInstance(); - private static final String DATABASE_ROOT_PROPERTY = "fizteh.db.dir"; private final String databaseRoot; + private final TableProvider localProvider; private Supplier clientShellStateSupplier; private Server server; private RemoteTableProviderFactoryImpl factory; - private TableProvider provider; - public DBServerState(String databaseRoot) { + public TelnetDBServerState(TableProvider localProvider, String databaseRoot) { + this.localProvider = localProvider; this.databaseRoot = databaseRoot; RemoteTableProviderFactory storage = new RemoteDatabaseStorage(); - DBServerState serverState = this; + TelnetDBServerState serverState = this; this.clientShellStateSupplier = () -> new SingleDatabaseShellState() { @Override protected Database obtainNewActiveDatabase() throws Exception { @@ -49,10 +48,6 @@ public String getDatabaseRoot() { return databaseRoot; } - public TableProvider getProvider() { - return provider; - } - public void startServer(int port) throws IOException, IllegalStateException { checkInitialized(); if (server.isStarted()) { @@ -60,18 +55,21 @@ public void startServer(int port) throws IOException, IllegalStateException { } try { - factory = new RemoteTableProviderFactoryImpl(); - provider = factory.establishStorage(databaseRoot); + factory = new RemoteTableProviderFactoryImpl(localProvider); + factory.establishStorage(databaseRoot); server.setShellStateSupplier(clientShellStateSupplier); server.startServer(port); } catch (Exception exc) { try { - if (provider != null) { + if (factory != null) { factory.close(); } } catch (Exception ignored) { - Log.log(DBServerState.class, ignored, "Failed to close factory after server install failure"); + Log.log( + TelnetDBServerState.class, + ignored, + "Failed to close factory after server install failure"); } throw exc; } @@ -84,7 +82,6 @@ public boolean isStarted() { private void closeFactory() throws IOException { factory.close(); factory = null; - provider = null; } public int stopServer() throws IOException { @@ -115,25 +112,18 @@ public void cleanup() { try { stopServerIfStarted(); } catch (Exception exc) { - Log.log(DBServerState.class, exc); + Log.log(TelnetDBServerState.class, exc); } } @Override - public void prepareToExit(int exitCode) throws ExitRequest { - Log.log("Preparing to stop server with exit code: " + exitCode); - cleanup(); - throw new ExitRequest(exitCode); - } - - @Override - public void init(Shell host) throws Exception { + public void init(Shell host) throws Exception { super.init(host); server = new Server(host.getOutputStream()); } @Override - public Map> getCommands() { + public Map> getCommands() { return COMMANDS.getCommands(); } diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/ClientGeneralStateTest.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/ClientGeneralStateTest.java index ee1883b44..6e8cdc8d6 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/ClientGeneralStateTest.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/ClientGeneralStateTest.java @@ -7,12 +7,15 @@ import org.junit.runners.JUnit4; import ru.fizteh.fivt.storage.structured.RemoteTableProvider; import ru.fizteh.fivt.storage.structured.RemoteTableProviderFactory; +import ru.fizteh.fivt.storage.structured.TableProvider; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.AutoCloseableTableProviderFactory; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.DBTableProviderFactory; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.Database; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.remote.RemoteDatabaseStorage; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.TerminalException; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Shell; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet.client.ClientGeneralState; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet.server.DBServerState; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet.server.TelnetDBServerState; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test.support.RegexMatcher; import java.io.IOException; @@ -24,9 +27,11 @@ @RunWith(JUnit4.class) public class ClientGeneralStateTest extends InterpreterTestBase { - private DBServerState serverState; + private TelnetDBServerState serverState; private ClientGeneralState clientState; + private AutoCloseableTableProviderFactory factory; + @Override protected Shell constructInterpreter() throws TerminalException { this.clientState = new ClientGeneralState() { @@ -47,29 +52,33 @@ public PrintStream getOutputStream() { @Before public void prepareRemoteAPITest() throws Exception { - serverState = new DBServerState(DB_ROOT.toString()); + factory = new DBTableProviderFactory(); + TableProvider provider = factory.create(DB_ROOT.toString()); + + serverState = new TelnetDBServerState(provider, DB_ROOT.toString()); new Shell(serverState); serverState.startServer(10001); } @After public void cleanupRemoteAPITest() throws IOException { - serverState.stopServer(); + serverState.stopServerIfStarted(); + factory.close(); } @Test public void testWhereAmI() throws IOException, TerminalException { - runBatchExpectZero("connect localhost 10001", "whereami"); - assertEquals(makeTerminalExpectedMessage("connected", "local 10001"), getOutput()); + runBatchExpectZero("connect localhost 1099", "whereami"); + assertEquals(makeTerminalExpectedMessage("connected", "local 1099"), getOutput()); } @Test public void testCreateTableAndPutSmth() throws IOException, TerminalException { runBatchExpectZero( - "connect localhost 10001", "create t1 (String)", "use t1", "put a [\"b\"]", "commit"); + "connect localhost 1099", "create t1 (String)", "use t1", "put a [\"b\"]", "commit"); assertEquals( makeTerminalExpectedMessage("connected", "created", "using t1", "new", "1"), getOutput()); - runBatchExpectZero("connect localhost 10001", "use t1", "get a"); + runBatchExpectZero("connect localhost 1099", "use t1", "get a"); assertEquals(makeTerminalExpectedMessage("connected", "using t1", "found", "[\"b\"]"), getOutput()); } @@ -81,7 +90,7 @@ public void testConnectToNotExistentServer() throws IOException, TerminalExcepti @Test public void testConnectAndCallNotExistentCommand() throws IOException, TerminalException { - runInteractiveExpectZero("connect 127.0.0.1 10001", "not_exists_yeah?", "disconnect"); + runInteractiveExpectZero("connect 127.0.0.1 1099", "not_exists_yeah?", "disconnect"); assertThat( getOutput(), new RegexMatcher( makeTerminalExpectedRegex( 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 af5f8b71e..facf329ec 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 @@ -3,9 +3,9 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test.support.parallel.ControllableAgent; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test.support.parallel.ControllableRunnable; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test.support.parallel.ControllableRunner; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.parallel.ControllableAgent; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.parallel.ControllableRunnable; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.parallel.ControllableRunner; import static org.junit.Assert.*; diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/DBServerTest.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/DBServerTest.java index a74b171c8..9cb2cbd9b 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/DBServerTest.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/DBServerTest.java @@ -7,10 +7,16 @@ import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; +import ru.fizteh.fivt.storage.structured.TableProvider; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.AutoCloseableTableProviderFactory; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.DBTableProviderFactory; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.shell.Shell; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet.server.DBServerState; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.telnet.server.TelnetDBServerState; +import java.io.BufferedReader; import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; import static org.junit.Assert.*; @@ -18,17 +24,33 @@ public class DBServerTest extends TestBase { @Rule public ExpectedException exception = ExpectedException.none(); - private DBServerState serverState; + private TelnetDBServerState serverState; + + private AutoCloseableTableProviderFactory factory; @Before public void prepareRemoteAPITest() throws Exception { - serverState = new DBServerState(DB_ROOT.toString()); + factory = new DBTableProviderFactory(); + TableProvider provider = factory.create(DB_ROOT.toString()); + + serverState = new TelnetDBServerState(provider, DB_ROOT.toString()); new Shell(serverState); } @After public void cleanupRemoteAPITest() throws IOException { serverState.stopServerIfStarted(); + factory.close(); + } + + private String collectFromInputStream(InputStream inputStream) throws IOException { + StringBuilder sb = new StringBuilder(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) { + while (reader.ready()) { + sb.append(reader.readLine()).append(System.lineSeparator()); + } + } + return sb.toString(); } @Test 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 00548fde9..47553d1bf 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/InterpreterTestBase.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/InterpreterTestBase.java @@ -44,23 +44,6 @@ public static void globalCleanupInterpreterTestBase() { DuplicatedIOTestBase.globalCleanupDuplicatedIOTestBase(); } - protected abstract Shell constructInterpreter() throws TerminalException; - - @Before - public void prepare() throws TerminalException, DatabaseIOException { - interpreter = constructInterpreter(); - } - - /** - * Removes all files under {@link #DB_ROOT}. - * @throws java.io.IOException - */ - @After - public void cleanup() throws IOException { - interpreter = null; - IO_DUPLICATOR.cleanup(); - } - /** * Constructs a multiline regular expression that expected output must match.
    * Recommended to be used to test interpreter mode. @@ -79,7 +62,7 @@ public void cleanup() throws IOException { * @return Regex for full interpreter answer. * @see java.util.regex.Pattern */ - String makeTerminalExpectedRegex(String greetingRegex, String... reports) { + public static String makeTerminalExpectedRegex(String greetingRegex, String... reports) { StringBuilder sb = new StringBuilder(String.format("(?m)^%s", greetingRegex)); for (String s : reports) { sb.append(String.format("%s$%n^%s", s, greetingRegex)); @@ -87,6 +70,37 @@ String makeTerminalExpectedRegex(String greetingRegex, String... reports) { return sb.toString(); } + /** + * Constructs a multiline message that expected output must be equal to.
    + * Recommended to be used to test batch mode.
    + * Each report is considered to be a separate line. Lines are separated using {@link + * System#lineSeparator()}. + */ + public static String makeTerminalExpectedMessage(String... reports) { + StringBuilder sb = new StringBuilder(); + for (String s : reports) { + sb.append(String.format("%s%n", s)); + } + return sb.toString(); + } + + protected abstract Shell constructInterpreter() throws TerminalException; + + @Before + public void prepare() throws TerminalException, DatabaseIOException { + interpreter = constructInterpreter(); + } + + /** + * Removes all files under {@link #DB_ROOT}. + * @throws java.io.IOException + */ + @After + public void cleanup() throws IOException { + interpreter = null; + IO_DUPLICATOR.cleanup(); + } + /** * Obtains everything that was output by the interpreter.
    */ @@ -164,18 +178,4 @@ void runBatchExpectZero(String... commands) throws TerminalException { void runBatchExpectNonZero(String... commands) throws TerminalException { assertNotEquals("Non-zero exit status expected", 0, runBatch(commands)); } - - /** - * Constructs a multiline message that expected output must be equal to.
    - * Recommended to be used to test batch mode.
    - * Each report is considered to be a separate line. Lines are separated using {@link - * System#lineSeparator()}. - */ - String makeTerminalExpectedMessage(String... reports) { - StringBuilder sb = new StringBuilder(); - for (String s : reports) { - sb.append(String.format("%s%n", s)); - } - return sb.toString(); - } } diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/RemoteDataStorageTest.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/RemoteDataStorageTest.java index fc1cca962..b5754e782 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/RemoteDataStorageTest.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/RemoteDataStorageTest.java @@ -10,6 +10,9 @@ import ru.fizteh.fivt.storage.structured.RemoteTableProvider; import ru.fizteh.fivt.storage.structured.Storeable; import ru.fizteh.fivt.storage.structured.Table; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.AutoCloseableProvider; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.AutoCloseableTableProviderFactory; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.DBTableProviderFactory; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.remote.RemoteDatabaseStorage; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.remote.RemoteTableProviderFactoryImpl; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.InvalidatedObjectException; @@ -24,16 +27,23 @@ public class RemoteDataStorageTest extends TestBase { @Rule public ExpectedException exception = ExpectedException.none(); - private RemoteTableProviderFactoryImpl factory; + private RemoteTableProviderFactoryImpl remoteFactory; + + private AutoCloseableTableProviderFactory factory; + private AutoCloseableProvider localProvider; @Before public void prepare() throws Exception { - factory = new RemoteTableProviderFactoryImpl(); - factory.establishStorage(DB_ROOT.toString()); + factory = new DBTableProviderFactory(); + localProvider = factory.create(DB_ROOT.toString()); + + remoteFactory = new RemoteTableProviderFactoryImpl(localProvider); + remoteFactory.establishStorage(DB_ROOT.toString()); } @After public void cleanup() throws Exception { + remoteFactory.close(); factory.close(); cleanDBRoot(); } @@ -88,7 +98,7 @@ public void testServerTableCloseInvalidatesRemoteTable() throws Exception { Table remoteTable = provider.createTable(tableName, Arrays.asList(String.class)); - Table serverTable = factory.getProvider().getTable(tableName); + Table serverTable = localProvider.getTable(tableName); ((AutoCloseable) serverTable).close(); exception.expect(InvalidatedObjectException.class); @@ -104,7 +114,7 @@ public void testServerFactoryCloseInvalidatesRemoteTable() throws Exception { Table remoteTable = provider.createTable(tableName, Arrays.asList(String.class)); - factory.close(); + remoteFactory.close(); exception.expect(InvalidatedObjectException.class); remoteTable.getName(); @@ -119,7 +129,7 @@ public void testRemoteTableClosesAfterTableIsInvalidated() throws Exception { Table remoteTable = remoteProvider.createTable(tableName, Arrays.asList(String.class)); - Table serverTable = factory.getProvider().getTable(tableName); + Table serverTable = localProvider.getTable(tableName); ((AutoCloseable) serverTable).close(); try { 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 5f61d412f..1a6e7a72f 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/TableProviderTest.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/TableProviderTest.java @@ -19,10 +19,10 @@ import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.DBTableProviderFactory; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.StringTableImpl; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.exception.InvalidatedObjectException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.parallel.ControllableAgent; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.parallel.ControllableRunnable; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.parallel.ControllableRunner; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test.support.TestUtils; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test.support.parallel.ControllableAgent; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test.support.parallel.ControllableRunnable; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test.support.parallel.ControllableRunner; import java.io.IOException; import java.io.PrintWriter; 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 3ad250d87..4338a93d3 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/TableTest.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/TableTest.java @@ -13,10 +13,10 @@ import ru.fizteh.fivt.storage.structured.TableProvider; import ru.fizteh.fivt.storage.structured.TableProviderFactory; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.db.StoreableTableImpl; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.parallel.ControllableAgent; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.parallel.ControllableRunnable; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.parallel.ControllableRunner; import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test.support.TestUtils; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test.support.parallel.ControllableAgent; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test.support.parallel.ControllableRunnable; -import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test.support.parallel.ControllableRunner; import java.io.IOException; import java.io.PrintWriter; diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/TransactionPoolTest.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/TransactionPoolTest.java new file mode 100644 index 000000000..4ad6342b0 --- /dev/null +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/TransactionPoolTest.java @@ -0,0 +1,78 @@ +package ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.test; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.servlet.transactions.Transaction; +import ru.fizteh.fivt.students.fedorov_andrew.databaselibrary.servlet.transactions.TransactionPool; + +import static org.junit.Assert.*; + +public class TransactionPoolTest { + @Rule + public ExpectedException exception = ExpectedException.none(); + + @Test + public void testTooManyTransactions() throws Exception { + int count = 10; + TransactionPool pool = new TransactionPool(count, 100000L); + + Transaction[] transactions = new Transaction[count]; + + for (int i = 0; i < 10; i++) { + Transaction transaction = pool.newTransaction(); + int id = transaction.getTransactionID(); + assertNull("Duplicate id", transactions[id]); + transactions[id] = transaction; + pool.releaseTransaction(transaction); + } + + exception.expect(IllegalStateException.class); + exception.expectMessage("No more free transaction IDs"); + pool.newTransaction(); + } + + @Test + public void testRemoveTransactionAndPerformAction() throws Exception { + TransactionPool pool = new TransactionPool(100500, 100000L); + Transaction tr = pool.newTransaction(); + pool.killTransaction(tr); + + exception.expect(IllegalStateException.class); + exception.expectMessage("You can execute actions only in active state"); + + tr.executeAction(null); + } + + @Test + public void testTransactionDiesAfterSomeTime() throws Exception { + TransactionPool pool = new TransactionPool(100, 100L); + Transaction tr = pool.newTransaction(); + pool.releaseTransaction(tr); + + Thread.sleep(500L); + + exception.expect(IllegalStateException.class); + exception.expectMessage("You can execute actions only in active state"); + tr.executeAction(null); + } + + @Test + public void testTransactionDiesAfterSomeTime1() throws Exception { + TransactionPool pool = new TransactionPool(100, 100L); + Transaction tr = pool.newTransaction(); + int id = tr.getTransactionID(); + pool.releaseTransaction(tr); + + Thread.sleep(500L); + + exception.expect(IllegalArgumentException.class); + exception.expectMessage("Transaction not found: " + id); + tr = pool.obtainTransaction(id); + try { + tr.executeAction(null); + } finally { + pool.releaseTransaction(tr); + } + } +} diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/AlternativeShellState.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/AlternativeShellState.java index fd796a6fb..b80371993 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/AlternativeShellState.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/test/support/AlternativeShellState.java @@ -79,11 +79,6 @@ public void init(Shell host) throws Exception { } } - @Override - public void prepareToExit(int exitCode) throws ExitRequest { - throw new ExitRequest(exitCode); - } - @Override public Map> getCommands() { return commandMap; diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/xml/XMLMaker.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/xml/XMLMaker.java index a69e86056..585d8f5d5 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/xml/XMLMaker.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/xml/XMLMaker.java @@ -33,7 +33,7 @@ public class XMLMaker { * name of each child node (only for iterables/arrays). Can be null or empty. * @param identityMap * map to determine cyclic links and handle them in proper way. - * @throws XMLStreamException + * @throws javax.xml.stream.XMLStreamException * @throws IllegalAccessException */ private static void writeXML(XMLStreamWriter writer, From 12b5b4a8f0d26f291fe715869bf6ba6db921bf93 Mon Sep 17 00:00:00 2001 From: AndreosGhost Date: Tue, 23 Dec 2014 13:20:14 +0300 Subject: [PATCH 14/14] Fixed HTTP server --- .../databaselibrary/servlet/HttpDBServer.java | 98 ++++++++++++------- .../servlet/transactions/Transaction.java | 16 +-- 2 files changed, 66 insertions(+), 48 deletions(-) diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/servlet/HttpDBServer.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/servlet/HttpDBServer.java index ab640d1a4..ed2c91458 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/servlet/HttpDBServer.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/servlet/HttpDBServer.java @@ -122,8 +122,12 @@ protected void serveGet(HttpServletRequest request, HttpServletResponse response int transactionID = Integer.parseInt(request.getParameter(PARAM_TRANSACTION_ID)); Transaction
    transaction = transactionPool.obtainTransaction(transactionID); try { - int size = transaction.getExtraData().size(); - response.getWriter().print(size + ""); + transaction.executeAction( + () -> { + int size = transaction.getExtraData().size(); + response.getWriter().print(size + ""); + return null; + }); } finally { transactionPool.releaseTransaction(transaction); } @@ -136,24 +140,30 @@ protected void serveGet(HttpServletRequest request, HttpServletResponse response int transactionID = Integer.parseInt(request.getParameter(PARAM_TRANSACTION_ID)); Transaction
    transaction = transactionPool.obtainTransaction(transactionID); try { - String key = request.getParameter(PARAM_KEY); - String serializedNewValue = request.getParameter(PARAM_VALUE); - - // Deserializing new value. - Storeable storeableNewValue = - localProvider.deserialize(transaction.getExtraData(), serializedNewValue); - - // Putting new value instead of old. - Storeable storeableOldValue = transaction.getExtraData().put(key, storeableNewValue); - - // This key did not exist before. - if (storeableOldValue == null) { - throw new BadRequestException("Key not found: " + key); - } - // Now we must serialize old value. - String serializedOldValue = - localProvider.serialize(transaction.getExtraData(), storeableOldValue); - response.getWriter().print(serializedOldValue); + transaction.executeAction( + () -> { + String key = request.getParameter(PARAM_KEY); + String serializedNewValue = request.getParameter(PARAM_VALUE); + + // Deserializing new value. + Storeable storeableNewValue = + localProvider.deserialize(transaction.getExtraData(), serializedNewValue); + + // Putting new value instead of old. + Storeable storeableOldValue = + transaction.getExtraData().put(key, storeableNewValue); + + // This key did not exist before. + if (storeableOldValue == null) { + throw new BadRequestException("Key not found: " + key); + } + // Now we must serialize old value. + String serializedOldValue = + localProvider.serialize(transaction.getExtraData(), storeableOldValue); + response.getWriter().print(serializedOldValue); + return null; + }); + } finally { transactionPool.releaseTransaction(transaction); } @@ -166,15 +176,20 @@ protected void serveGet(HttpServletRequest request, HttpServletResponse response int transactionID = Integer.parseInt(request.getParameter(PARAM_TRANSACTION_ID)); Transaction
    transaction = transactionPool.obtainTransaction(transactionID); try { - String key = request.getParameter(PARAM_KEY); - Storeable storeableValue = transaction.getExtraData().get(key); - // Not found - if (storeableValue == null) { - throw new BadRequestException("Key not found: " + key); - } - // Now we must serialize it. - String serializedValue = localProvider.serialize(transaction.getExtraData(), storeableValue); - response.getWriter().print(serializedValue); + transaction.executeAction( + () -> { + String key = request.getParameter(PARAM_KEY); + Storeable storeableValue = transaction.getExtraData().get(key); + // Not found + if (storeableValue == null) { + throw new BadRequestException("Key not found: " + key); + } + // Now we must serialize it. + String serializedValue = + localProvider.serialize(transaction.getExtraData(), storeableValue); + response.getWriter().print(serializedValue); + return null; + }); } finally { transactionPool.releaseTransaction(transaction); } @@ -187,8 +202,12 @@ protected void serveGet(HttpServletRequest request, HttpServletResponse response int transactionID = Integer.parseInt(request.getParameter(PARAM_TRANSACTION_ID)); Transaction
    transaction = transactionPool.obtainTransaction(transactionID); try { - int diff = transaction.getExtraData().commit(); - response.getWriter().print(String.format("%s=%d", FIELD_DIFF, diff)); + transaction.executeAction( + () -> { + int diff = transaction.getExtraData().commit(); + response.getWriter().print(String.format("%s=%d", FIELD_DIFF, diff)); + return null; + }); } finally { transactionPool.killTransaction(transaction); } @@ -201,8 +220,12 @@ protected void serveGet(HttpServletRequest request, HttpServletResponse response int transactionID = Integer.parseInt(request.getParameter(PARAM_TRANSACTION_ID)); Transaction
    transaction = transactionPool.obtainTransaction(transactionID); try { - int diff = transaction.getExtraData().rollback(); - response.getWriter().print(String.format("%s=%d", FIELD_DIFF, diff)); + transaction.executeAction( + () -> { + int diff = transaction.getExtraData().rollback(); + response.getWriter().print(String.format("%s=%d", FIELD_DIFF, diff)); + return null; + }); } finally { transactionPool.killTransaction(transaction); } @@ -223,8 +246,13 @@ protected void serveGet(HttpServletRequest request, HttpServletResponse response Transaction
    transaction = transactionPool.newTransaction(); try { transaction.setExtraData(table); - int transactionID = transaction.getTransactionID(); - response.getWriter().print(String.format("%s=%05d", PARAM_TRANSACTION_ID, transactionID)); + transaction.executeAction( + () -> { + int transactionID = transaction.getTransactionID(); + response.getWriter() + .print(String.format("%s=%05d", PARAM_TRANSACTION_ID, transactionID)); + return null; + }); } finally { transactionPool.releaseTransaction(transaction); } diff --git a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/servlet/transactions/Transaction.java b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/servlet/transactions/Transaction.java index 301abd73e..da2cab815 100644 --- a/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/servlet/transactions/Transaction.java +++ b/src/ru/fizteh/fivt/students/fedorov_andrew/databaselibrary/servlet/transactions/Transaction.java @@ -50,7 +50,7 @@ public class Transaction { * transaction looking at this timestamp. */ private long lastAccessTime; - private T extraData; + private volatile T extraData; public Transaction(int transactionID) { this.transactionID = transactionID; @@ -61,21 +61,11 @@ public int getTransactionID() { } public T getExtraData() { - useLock.lock(); - try { - return extraData; - } finally { - useLock.unlock(); - } + return extraData; } public void setExtraData(T extraData) { - useLock.lock(); - try { - this.extraData = extraData; - } finally { - useLock.unlock(); - } + this.extraData = extraData; } /**