diff --git a/libcobj/app/src/main/java/jp/osscons/opensourcecobol/libcobj/file/CobolIndexedFile.java b/libcobj/app/src/main/java/jp/osscons/opensourcecobol/libcobj/file/CobolIndexedFile.java index 582f6d92..b29df769 100644 --- a/libcobj/app/src/main/java/jp/osscons/opensourcecobol/libcobj/file/CobolIndexedFile.java +++ b/libcobj/app/src/main/java/jp/osscons/opensourcecobol/libcobj/file/CobolIndexedFile.java @@ -18,10 +18,6 @@ */ package jp.osscons.opensourcecobol.libcobj.file; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; @@ -61,6 +57,9 @@ public class CobolIndexedFile extends CobolFile { /** TODO: 準備中 */ public static final int COB_NE = 6; + private static String storedProcessUuid = null; + private static String storedProcessId = null; + /** * TODO: 準備中 * @@ -144,6 +143,24 @@ public CobolIndexedFile( fileVersion); } + private static String getProcessUuid() { + if (CobolIndexedFile.storedProcessUuid == null) { + CobolIndexedFile.storedProcessUuid = java.util.UUID.randomUUID().toString(); + } + return CobolIndexedFile.storedProcessUuid; + } + + private static String getProcessId() { + if (CobolIndexedFile.storedProcessId == null) { + CobolIndexedFile.storedProcessId = + String.valueOf( + java.lang.management.ManagementFactory.getRuntimeMXBean() + .getName() + .split("@")[0]); + } + return CobolIndexedFile.storedProcessId; + } + private static String getIndexName(int index) { return String.format("index%d", index); } @@ -193,33 +210,71 @@ private byte[] DBT_SET(AbstractCobolField field) { @Override public int open_(String filename, int mode, int sharing) { IndexedFile p = new IndexedFile(); + this.filei = p; - SQLiteConfig config = new SQLiteConfig(); - config.setReadOnly(mode == COB_OPEN_INPUT); + // If the file does not exist and the mode is COB_OPEN_INPUT, return ENOENT + boolean fileExists = new java.io.File(filename).exists(); + if (mode == COB_OPEN_INPUT && !fileExists) { + return ENOENT; + } - if (mode == COB_OPEN_OUTPUT) { - Path path = Paths.get(filename); + // Establish a connection to the database + int getConnectionStatus = this.getConnection(filename); + + // If the connection could not be established, return the error code + if (getConnectionStatus != COB_STATUS_00_SUCCESS) { + return getConnectionStatus; + } + + try { + // Acquire a file lock + boolean succeedToFileLock = this.acquireFileLock(filename, mode, fileExists); + if (succeedToFileLock) { + if (mode == COB_OPEN_OUTPUT) { + this.deleteAllTablesExceptForFileLockTable(); + } + this.createAllTablesIfNotExists(); + if (mode == COB_OPEN_OUTPUT) { + this.writeMetaData(p); + } + p.connection.commit(); + this.setInitialParameters(filename); + return COB_STATUS_00_SUCCESS; + } else { + try { + p.connection.close(); + } catch (SQLException closeEx) { + return COB_STATUS_30_PERMANENT_ERROR; + } + return COB_STATUS_61_FILE_SHARING; + } + } catch (SQLException e) { try { - Files.deleteIfExists(path); - } catch (IOException e) { + p.connection.close(); + } catch (SQLException closeEx) { return COB_STATUS_30_PERMANENT_ERROR; } + return COB_STATUS_30_PERMANENT_ERROR; } + } - boolean fileExists = new java.io.File(filename).exists(); - - if (mode == COB_OPEN_INPUT && !fileExists) { - return ENOENT; - } + private int getConnection(String filename) { + IndexedFile p = this.filei; + // Establishes a connection to the SQLite database using the provided filename. + SQLiteConfig config = new SQLiteConfig(); + config.setReadOnly(false); p.connection = null; try { p.connection = DriverManager.getConnection("jdbc:sqlite:" + filename, config.toProperties()); + p.connection.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE); p.connection.setAutoCommit(false); // Check if the file is accessible try (Statement st = p.connection.createStatement()) { + // Wait for finishing other processes' transactions up to 5 seconds + st.execute("PRAGMA busy_timeout = 5000"); st.execute("select 1"); } p.connection.commit(); @@ -233,67 +288,165 @@ public int open_(String filename, int mode, int sharing) { } catch (Exception e) { return COB_STATUS_30_PERMANENT_ERROR; } + return COB_STATUS_00_SUCCESS; + } - p.filenamelen = filename.length(); - p.last_dupno = new int[this.nkeys]; - p.rewrite_sec_key = new int[this.nkeys]; + private String getOpenModeString(int mode) { + switch (mode) { + case COB_OPEN_INPUT: + return "INPUT"; + case COB_OPEN_OUTPUT: + return "OUTPUT"; + case COB_OPEN_I_O: + return "I-O"; + case COB_OPEN_EXTEND: + return "EXTEND"; + default: + return null; + } + } - int maxsize = 0; - for (int i = 0; i < this.nkeys; ++i) { - if (this.keys[i].getField().getSize() > maxsize) { - maxsize = this.keys[i].getField().getSize(); + private boolean acquireFileLock(String filename, int mode, boolean fileExists) + throws SQLException { + if (!checkFileIsLocked(filename, mode, fileExists)) { + return false; // File is already locked + } + + IndexedFile p = this.filei; + + // Insert a new lock record into the file_lock table + String openMode = this.getOpenModeString(mode); + if (openMode == null) { + return false; // Invalid open mode + } + String insertSql = + "insert into file_lock (locked_by, process_id, locked_at, open_mode) values (?, ?," + + " datetime('now'), ?)"; + String processUuid = this.getProcessUuid(); + String processId = this.getProcessId(); + + try (PreparedStatement statement = p.connection.prepareStatement(insertSql)) { + statement.setString(1, processUuid); + statement.setString(2, processId); + statement.setString(3, openMode); + int insertedRecordsCount = statement.executeUpdate(); + if (insertedRecordsCount != 1) { + p.connection.rollback(); + return false; } + p.connection.commit(); + return true; + } catch (SQLException e) { + p.connection.rollback(); + return false; } + } - if (mode == COB_OPEN_OUTPUT - || (!fileExists && (mode == COB_OPEN_EXTEND || mode == COB_OPEN_I_O))) { - try { - for (int i = 0; i < this.nkeys; ++i) { - String tableName = getTableName(i); - Statement statement = p.connection.createStatement(); - if (i == 0) { + private boolean checkFileIsLocked(String filename, int mode, boolean fileExists) + throws SQLException { + IndexedFile p = this.filei; + try (Statement statement = p.connection.createStatement()) { + if (mode == COB_OPEN_OUTPUT + || (!fileExists && (mode == COB_OPEN_EXTEND || mode == COB_OPEN_I_O))) { + String query = + "select exists(select 1 from sqlite_master where type = 'table' and name =" + + " 'file_lock')"; + ResultSet rs = statement.executeQuery(query); + // If the file_lock table does not exist, create it and return true + if (!rs.next() || rs.getInt(1) == 0) { + statement.execute( + "CREATE TABLE if not exists file_lock (locked_by text primary key," + + " process_id text, locked_at timestamp, open_mode text CONSTRAINT" + + " check_open_mode CHECK (open_mode IN ('INPUT', 'OUTPUT', 'I-O'," + + " 'EXTEND')))"); + return true; + } + } + + String query; + if (mode == COB_OPEN_OUTPUT) { + query = "select exists(select 1 from file_lock)"; + } else { + query = "select exists(select 1 from file_lock where open_mode = 'OUTPUT')"; + } + ResultSet rs = statement.executeQuery(query); + // If the file is already locked, return false + if (rs.next() && rs.getInt(1) == 1) { + p.connection.rollback(); + return false; + } + return true; + } catch (SQLException e) { + p.connection.rollback(); + return false; + } + } + + private void deleteAllTablesExceptForFileLockTable() throws SQLException { + IndexedFile p = this.filei; + try (Statement statement = p.connection.createStatement()) { + for (int i = 0; i < this.nkeys; ++i) { + statement.execute("drop table if exists " + getTableName(i)); + } + statement.execute("drop table if exists metadata_string_int"); + statement.execute("drop table if exists metadata_key"); + } + } + + private void createAllTablesIfNotExists() throws SQLException { + IndexedFile p = this.filei; + try (Statement statement = p.connection.createStatement()) { + for (int i = 0; i < this.nkeys; ++i) { + String tableName = getTableName(i); + if (i == 0) { + statement.execute( + String.format( + "create table if not exists %s (key blob not null primary key," + + " value blob not null)", + tableName)); + } else { + if (this.keys[i].getFlag() == 0) { statement.execute( String.format( - "create table %s (key blob not null primary key, value blob" - + " not null)", - tableName)); + "create table if not exists %s (key blob not null primary" + + " key, value blob not null, constraint %s foreign key" + + " (value) references %s (key))", + tableName, getConstraintName(i), getTableName(0))); } else { - if (this.keys[i].getFlag() == 0) { - statement.execute( - String.format( - "create table %s (key blob not null primary key, value" - + " blob not null, constraint %s foreign key" - + " (value) references %s (key))", - tableName, getConstraintName(i), getTableName(0))); - } else { - statement.execute( - String.format( - "create table %s (key blob not null, value blob not" - + " null, dupNo integer not null, constraint %s" - + " foreign key (value) references %s (key))", - tableName, getConstraintName(i), getTableName(0))); - } statement.execute( String.format( - "create index %s on %s(value)", - getSubIndexName(i), tableName)); + "create table if not exists %s (key blob not null, value" + + " blob not null, dupNo integer not null, constraint" + + " %s foreign key (value) references %s (key))", + tableName, getConstraintName(i), getTableName(0))); } statement.execute( String.format( - "create index %s on %s(key)", getIndexName(i), tableName)); - statement.close(); + "create index if not exists %s on %s(value)", + getSubIndexName(i), tableName)); } - this.writeMetaData(p); - if (this.commitOnModification) { - p.connection.commit(); - } - } catch (SQLException e) { - return COB_STATUS_30_PERMANENT_ERROR; + statement.execute( + String.format( + "create index if not exists %s on %s(key)", + getIndexName(i), tableName)); + } + } + } + + private void setInitialParameters(String filename) { + IndexedFile p = this.filei; + p.filenamelen = filename.length(); + p.last_dupno = new int[this.nkeys]; + p.rewrite_sec_key = new int[this.nkeys]; + + int maxsize = 0; + for (int i = 0; i < this.nkeys; ++i) { + if (this.keys[i].getField().getSize() > maxsize) { + maxsize = this.keys[i].getField().getSize(); } } p.temp_key = new CobolDataStorage(maxsize + 4); - this.filei = p; p.key_index = 0; p.last_key = null; @@ -307,7 +460,6 @@ public int open_(String filename, int mode, int sharing) { this.callStart = false; this.fetchKeyIndex = -1; - return 0; } // Write a metadata to the database @@ -315,11 +467,11 @@ private void writeMetaData(IndexedFile p) throws SQLException { Statement statement = p.connection.createStatement(); // Create a table to store metadata statement.execute( - "create table metadata_string_int (key text not null primary key, value integer not" - + " null)"); + "create table if not exists metadata_string_int (key text not null primary key," + + " value integer not null)"); statement.execute( - "create table metadata_key (idx integer not null primary key, offset integer not" - + " null, size integer not null, duplicate boolean)"); + "create table if not exists metadata_key (idx integer not null primary key, offset" + + " integer not null, size integer not null, duplicate boolean)"); statement.close(); // Store the size of a record @@ -353,6 +505,16 @@ public int close_(int opt) { this.closeCursor(); try { + try (Statement statement = p.connection.createStatement()) { + // Close the file lock + String deleteSql = "delete from file_lock where locked_by = ? and process_id = ?"; + try (PreparedStatement deleteStatement = p.connection.prepareStatement(deleteSql)) { + deleteStatement.setString(1, this.getProcessUuid()); + deleteStatement.setString(2, this.getProcessId()); + deleteStatement.executeUpdate(); + } + } + p.connection.commit(); p.connection.close(); } catch (SQLException e) { return COB_STATUS_30_PERMANENT_ERROR; diff --git a/tests/run.src/miscellaneous.at b/tests/run.src/miscellaneous.at index 4a91dc2d..a9ee29ae 100644 --- a/tests/run.src/miscellaneous.at +++ b/tests/run.src/miscellaneous.at @@ -2135,3 +2135,272 @@ AT_CHECK([java prog], [0], [30 ]) AT_CLEANUP + +AT_SETUP([Open an invalid formatted indexed file]) + +AT_CHECK([echo invalid-data > invalid-formatted-file]) + +AT_DATA([prog.cbl], +[ + IDENTIFICATION DIVISION. + PROGRAM-ID. prog. + + ENVIRONMENT DIVISION. + INPUT-OUTPUT SECTION. + FILE-CONTROL. + SELECT F ASSIGN TO "invalid-formatted-file" + ORGANIZATION IS INDEXED + ACCESS MODE IS DYNAMIC + RECORD KEY IS REC-KEY + FILE STATUS IS FILE-STATUS. + + DATA DIVISION. + FILE SECTION. + FD f. + 01 REC. + 05 REC-KEY PIC X(5). + + WORKING-STORAGE SECTION. + 01 FILE-STATUS PIC XX. + PROCEDURE DIVISION. + MAIN-PROCEDURE. + + OPEN INPUT f. + DISPLAY FILE-STATUS. + CLOSE f. +]) + +AT_CHECK([${COMPILE} prog.cbl]) +AT_CHECK([java prog], [0], +[30 +]) +AT_CLEANUP + +AT_SETUP([File locking of indexed files]) +AT_DATA([make_indexed_file.cbl],[ + identification division. + program-id. make_indexed_file@@id@@. + + environment division. + input-output section. + + file-control. + select indexed-file + assign to external "indexed@@id@@.dat" + organization is indexed + record key is rec-key + file status is indexed-file-status. + + data division. + file section. + fd indexed-file. + 01 dup-record. + 05 rec-key pic x(5). + 05 rec-value pic x(5). + working-storage section. + 01 indexed-file-status pic 99. + procedure division. + main-procedure. + + open output indexed-file. + close indexed-file. +]) + +AT_DATA([prog1.cbl], [ + identification division. + program-id. prog1@@id@@. + + environment division. + input-output section. + + file-control. + select indexed-file + assign to "indexed@@id@@.dat" + organization is indexed + record key is rec-key + file status is indexed-file-status. + + select shared-file1 + assign to "shared1@@id@@.dat" + organization is sequential + file status is shared-file1-status. + + select shared-file2 + assign to "shared2@@id@@.dat" + organization is sequential + file status is shared-file2-status. + + data division. + file section. + fd indexed-file. + 01 dup-record. + 05 rec-key pic x(5). + 05 rec-value pic x(5). + fd shared-file1. + 01 shared-record1 pic x(10). + fd shared-file2. + 01 shared-record2 pic x(10). + working-storage section. + 01 indexed-file-status pic 99. + 01 shared-file1-status pic 99. + 01 shared-file2-status pic 99. + procedure division. + main-procedure. + + * open the indexed file before the other process does. + open @@open_mode@@ indexed-file. + + * Notify the other process that + * this process opened the indexed file. + open output shared-file1. + close shared-file1. + + * Wait for the other process to finish. + call "C$SLEEP" using 1. + perform forever + open input shared-file2 + if shared-file2-status = 0 + exit perform + end-if + close shared-file2 + call "C$SLEEP" using 1 + end-perform. + close shared-file2. + + * Close the indexed file after the other process has finished. + close indexed-file. +]) + +AT_DATA([prog2.cbl], [ + identification division. + program-id. prog2@@id@@. + + environment division. + input-output section. + + file-control. + select indexed-file + assign to "indexed@@id@@.dat" + organization is indexed + record key is rec-key + file status is indexed-file-status. + + select shared-file1 + assign to "shared1@@id@@.dat" + organization is sequential + file status is shared-file1-status. + + select shared-file2 + assign to "shared2@@id@@.dat" + organization is sequential + file status is shared-file2-status. + + data division. + file section. + fd indexed-file. + 01 dup-record. + 05 rec-key pic x(5). + 05 rec-value pic x(5). + fd shared-file1. + 01 shared-record1 pic x(10). + fd shared-file2. + 01 shared-record2 pic x(10). + working-storage section. + 01 indexed-file-status pic 99. + 01 shared-file1-status pic 99. + 01 shared-file2-status pic 99. + procedure division. + main-procedure. + + * Wait for the other process to open the indexed file. + call "C$SLEEP" using 1. + perform forever + open input shared-file1 + if shared-file1-status = 0 + close shared-file1 + exit perform + end-if + close shared-file1 + call "C$SLEEP" using 1 + end-perform. + + * Open the indexed file that another process has already opened. + open @@open_mode@@ indexed-file. + display indexed-file-status. + close indexed-file. + + * Notify the other process that + * this process finished. + open output shared-file2. + close shared-file2. +]) + +AT_DATA([run.sh], [ +#!/bin/bash + +function run_test() { + SEQ_NUMBER=$1 + OPEN_MODE_1=$2 + OPEN_MODE_2=$3 + EXPECTED_EXIT_CODE=$4 + + rm -f *.dat + + TEST_ID="$(echo "${OPEN_MODE_1}_${OPEN_MODE_2}" | sed 's/-/_/g')" + + PROGRAM_NAME_1="prog1${TEST_ID}" + PROGRAM_NAME_2="prog2${TEST_ID}" + + cat "prog1.cbl" | + sed "s/@@id@@/${TEST_ID}/g" | + sed "s/@@open_mode@@/${OPEN_MODE_1}/g" \ + > ${PROGRAM_NAME_1}.cbl + + cat "prog2.cbl" | + sed "s/@@id@@/${TEST_ID}/g" | + sed "s/@@open_mode@@/${OPEN_MODE_2}/g" \ + > ${PROGRAM_NAME_2}.cbl + + cat "make_indexed_file.cbl" | + sed "s/@@id@@/${TEST_ID}/g" \ + > make_indexed_file${TEST_ID}.cbl + + cobj ${PROGRAM_NAME_1}.cbl ${PROGRAM_NAME_2}.cbl make_indexed_file${TEST_ID}.cbl + + java make_indexed_file${TEST_ID} + + java $PROGRAM_NAME_1 & + PID1=$! + + java $PROGRAM_NAME_2 > ${TEST_ID}.log & + PID2=$! + + wait $PID1 $PID2 + if test "$(cat ${TEST_ID}.log)" != "${EXPECTED_EXIT_CODE}"; then + echo "<${SEQ_NUMBER}> Test ${TEST_ID} failed. Expected exit code: ${EXPECTED_EXIT_CODE}, got: $(cat ${TEST_ID}.log)" \ + exit 1 + fi +} + +################################### + +run_test 00 INPUT INPUT 00 +run_test 01 INPUT OUTPUT 61 +run_test 02 INPUT I-O 00 +run_test 03 INPUT EXTEND 00 +run_test 04 OUTPUT INPUT 61 +run_test 05 OUTPUT OUTPUT 61 +run_test 06 OUTPUT I-O 61 +run_test 07 OUTPUT EXTEND 61 +run_test 08 I-O INPUT 00 +run_test 09 I-O OUTPUT 61 +run_test 10 I-O I-O 00 +run_test 11 I-O EXTEND 00 +run_test 12 EXTEND INPUT 00 +run_test 13 EXTEND OUTPUT 61 +run_test 14 EXTEND I-O 00 +run_test 15 EXTEND EXTEND 00 +]) + +AT_CHECK([bash run.sh]) +AT_CLEANUP \ No newline at end of file