diff --git a/docs/language-server/protocol-language-server.md b/docs/language-server/protocol-language-server.md index 7006d840adf4..e47567953a2a 100644 --- a/docs/language-server/protocol-language-server.md +++ b/docs/language-server/protocol-language-server.md @@ -392,7 +392,7 @@ interface ExpressionUpdate { An information about the computed value. ```typescript -type ExpressionUpdatePayload = Value | DatafalowError | Panic | Pending; +type ExpressionUpdatePayload = Value | DataflowError | Panic | Pending; /** Indicates that the expression was computed to a value. */ interface Value { @@ -424,6 +424,8 @@ interface Pending { /** Optional amount of already done work as a number between `0.0` to `1.0`. */ progress?: number; + /** Indicates whether the computation of the expression has been interrupted and will be retried. */ + wasInterrupted: boolean; } /** Information about warnings associated with the value. */ diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextEventsListener.scala b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextEventsListener.scala index 154a4328a8e5..e6596b450334 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextEventsListener.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextEventsListener.scala @@ -230,8 +230,8 @@ final class ContextEventsListener( functionSchema.map(toProtocolFunctionSchema) ) - case Api.ExpressionUpdate.Payload.Pending(m, p) => - ContextRegistryProtocol.ExpressionUpdate.Payload.Pending(m, p) + case Api.ExpressionUpdate.Payload.Pending(m, p, i) => + ContextRegistryProtocol.ExpressionUpdate.Payload.Pending(m, p, i) case Api.ExpressionUpdate.Payload.DataflowError(trace) => ContextRegistryProtocol.ExpressionUpdate.Payload.DataflowError(trace) diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextRegistryProtocol.scala b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextRegistryProtocol.scala index 29e083f50699..aef9df5d3167 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextRegistryProtocol.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextRegistryProtocol.scala @@ -231,8 +231,17 @@ object ContextRegistryProtocol { ) } - case class Pending(message: Option[String], progress: Option[Double]) - extends Payload + /** Indicates that an expression is pending a computation + */ + case class Pending( + message: Option[String], + progress: Option[Double], + wasInterrupted: Boolean + ) extends Payload + + /** Indicates that an expression's computation has been interrupted and shall be retried. + */ + case object PendingInterrupted extends Payload /** Indicates that the expression was computed to an error. * @@ -258,6 +267,8 @@ object ContextRegistryProtocol { val Pending = "Pending" + val PendingInterrupted = "PendingInterrupted" + val DataflowError = "DataflowError" val Panic = "Panic" @@ -291,6 +302,14 @@ object ContextRegistryProtocol { .deepMerge( Json.obj(CodecField.Type -> PayloadType.Pending.asJson) ) + case m: Payload.PendingInterrupted.type => + Encoder[Payload.PendingInterrupted.type] + .apply(m) + .deepMerge( + Json.obj( + CodecField.Type -> PayloadType.PendingInterrupted.asJson + ) + ) } implicit val decoder: Decoder[Payload] = @@ -307,6 +326,9 @@ object ContextRegistryProtocol { case PayloadType.Pending => Decoder[Payload.Pending].tryDecode(cursor) + + case PayloadType.PendingInterrupted => + Decoder[Payload.PendingInterrupted.type].tryDecode(cursor) } } } diff --git a/engine/polyglot-api/src/main/scala/org/enso/polyglot/runtime/Runtime.scala b/engine/polyglot-api/src/main/scala/org/enso/polyglot/runtime/Runtime.scala index 9d9ab4ccab90..cec87900947b 100644 --- a/engine/polyglot-api/src/main/scala/org/enso/polyglot/runtime/Runtime.scala +++ b/engine/polyglot-api/src/main/scala/org/enso/polyglot/runtime/Runtime.scala @@ -158,11 +158,14 @@ object Runtime { ) } - /** TBD + /** Indicates that an expression is pending a computation */ @named("expressionUpdatePayloadPending") - case class Pending(message: Option[String], progress: Option[Double]) - extends Payload; + case class Pending( + message: Option[String], + progress: Option[Double], + wasInterrupted: Boolean = false + ) extends Payload /** Indicates that the expression was computed to an error. * diff --git a/engine/runtime-instrument-common/src/main/scala/org/enso/interpreter/instrument/job/ProgramExecutionSupport.scala b/engine/runtime-instrument-common/src/main/scala/org/enso/interpreter/instrument/job/ProgramExecutionSupport.scala index 283ed9c2893d..53d89c8eaa7d 100644 --- a/engine/runtime-instrument-common/src/main/scala/org/enso/interpreter/instrument/job/ProgramExecutionSupport.scala +++ b/engine/runtime-instrument-common/src/main/scala/org/enso/interpreter/instrument/job/ProgramExecutionSupport.scala @@ -95,6 +95,21 @@ object ProgramExecutionSupport { val onComputedValueCallback: Consumer[ExpressionValue] = { value => if (callStack.isEmpty) { logger.log(Level.FINEST, s"ON_COMPUTED ${value.getExpressionId}") + + if (VisualizationResult.isInterruptedException(value.getValue)) { + value.getValue match { + case e: AbstractTruffleException => + sendInterruptedExpressionUpdate( + contextId, + executionFrame.syncState, + value + ) + // Bail out early. Any references to this value that do not expect + // Interrupted error will likely return `No_Such_Method` otherwise. + throw new ThreadInterruptedException(e); + case _ => + } + } sendExpressionUpdate(contextId, executionFrame.syncState, value) sendVisualizationUpdates( contextId, @@ -377,6 +392,50 @@ object ProgramExecutionSupport { Api.ExecutionResult.Failure(ex.getMessage, None) } + private def sendInterruptedExpressionUpdate( + contextId: ContextId, + syncState: UpdatesSynchronizationState, + value: ExpressionValue + )(implicit ctx: RuntimeContext): Unit = { + val expressionId = value.getExpressionId + val methodCall = toMethodCall(value) + if ( + !syncState.isExpressionSync(expressionId) || + (methodCall.isDefined && !syncState.isMethodPointerSync( + expressionId + )) + ) { + val payload = + Api.ExpressionUpdate.Payload.Pending(None, None, wasInterrupted = true) + ctx.endpoint.sendToClient( + Api.Response( + Api.ExpressionUpdates( + contextId, + Set( + Api.ExpressionUpdate( + value.getExpressionId, + Option(value.getTypes).map(_.toVector), + methodCall, + value.getProfilingInfo.map { case e: ExecutionTime => + Api.ProfilingInfo.ExecutionTime(e.getNanoTimeElapsed) + }.toVector, + value.wasCached(), + value.isTypeChanged || value.isFunctionCallChanged, + payload + ) + ) + ) + ) + ) + + syncState.setExpressionSync(expressionId) + ctx.state.expressionExecutionState.setExpressionExecuted(expressionId) + if (methodCall.isDefined) { + syncState.setMethodPointerSync(expressionId) + } + } + } + private def sendExpressionUpdate( contextId: ContextId, syncState: UpdatesSynchronizationState, diff --git a/engine/runtime-integration-tests/src/test/scala/org/enso/interpreter/test/instrument/RuntimeAsyncCommandsTest.scala b/engine/runtime-integration-tests/src/test/scala/org/enso/interpreter/test/instrument/RuntimeAsyncCommandsTest.scala index 59d323a64a31..a86699d67d83 100644 --- a/engine/runtime-integration-tests/src/test/scala/org/enso/interpreter/test/instrument/RuntimeAsyncCommandsTest.scala +++ b/engine/runtime-integration-tests/src/test/scala/org/enso/interpreter/test/instrument/RuntimeAsyncCommandsTest.scala @@ -500,16 +500,15 @@ class RuntimeAsyncCommandsTest responses should contain theSameElementsAs Seq( Api.Response(requestId, Api.RecomputeContextResponse(contextId)), - TestMessages.update( + TestMessages.pendingInterrupted( contextId, - vId, - ConstantsGen.INTEGER, methodCall = Some( MethodCall( MethodPointer("Enso_Test.Test.Main", "Enso_Test.Test.Main", "loop"), Vector(1) ) - ) + ), + vId ), context.executionComplete(contextId) ) diff --git a/engine/runtime-integration-tests/src/test/scala/org/enso/interpreter/test/instrument/TestMessages.scala b/engine/runtime-integration-tests/src/test/scala/org/enso/interpreter/test/instrument/TestMessages.scala index e0dd02287428..b997f500651c 100644 --- a/engine/runtime-integration-tests/src/test/scala/org/enso/interpreter/test/instrument/TestMessages.scala +++ b/engine/runtime-integration-tests/src/test/scala/org/enso/interpreter/test/instrument/TestMessages.scala @@ -479,4 +479,33 @@ object TestMessages { ) ) + /** Create an pending interrupted response. + * + * @param contextId an identifier of the context + * @param expressionIds a list of pending expressions + * @return the expression update response + */ + def pendingInterrupted( + contextId: UUID, + methodCall: Option[Api.MethodCall], + expressionIds: UUID* + ): Api.Response = + Api.Response( + Api.ExpressionUpdates( + contextId, + expressionIds.toSet.map { expressionId => + Api.ExpressionUpdate( + expressionId, + None, + methodCall, + Vector(Api.ProfilingInfo.ExecutionTime(0)), + false, + true, + Api.ExpressionUpdate.Payload + .Pending(None, None, wasInterrupted = true) + ) + } + ) + ) + } diff --git a/engine/runtime/src/main/java/org/enso/interpreter/runtime/control/ThreadInterruptedException.java b/engine/runtime/src/main/java/org/enso/interpreter/runtime/control/ThreadInterruptedException.java index 2693b6ee1411..cd004b800607 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/runtime/control/ThreadInterruptedException.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/runtime/control/ThreadInterruptedException.java @@ -1,4 +1,10 @@ package org.enso.interpreter.runtime.control; /** Thrown when guest code discovers a thread interrupt. */ -public class ThreadInterruptedException extends RuntimeException {} +public class ThreadInterruptedException extends RuntimeException { + public ThreadInterruptedException() {} + + public ThreadInterruptedException(Throwable e) { + super(e); + } +} diff --git a/std-bits/table/src/main/java/org/enso/table/excel/ExcelConnectionPool.java b/std-bits/table/src/main/java/org/enso/table/excel/ExcelConnectionPool.java index af8a43263ac3..5221576f113a 100644 --- a/std-bits/table/src/main/java/org/enso/table/excel/ExcelConnectionPool.java +++ b/std-bits/table/src/main/java/org/enso/table/excel/ExcelConnectionPool.java @@ -22,6 +22,7 @@ import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.enso.base.cache.ReloadDetector; import org.enso.table.excel.xssfreader.XSSFReaderWorkbook; +import org.enso.table.util.FunctionWithException; public class ExcelConnectionPool { public static final ExcelConnectionPool INSTANCE = new ExcelConnectionPool(); @@ -29,7 +30,7 @@ public class ExcelConnectionPool { private ExcelConnectionPool() {} public ReadOnlyExcelConnection openReadOnlyConnection(File file, ExcelFileFormat format) - throws IOException { + throws IOException, InterruptedException { synchronized (this) { if (isCurrentlyWriting) { throw new IllegalStateException( @@ -134,7 +135,7 @@ public R writeWorkbook(File file, Function writeAction) throws */ public R lockForWriting( File file, ExcelFileFormat format, File[] accompanyingFiles, Function action) - throws IOException { + throws IOException, InterruptedException { synchronized (this) { if (isCurrentlyWriting) { throw new IllegalStateException( @@ -242,7 +243,8 @@ static class ConnectionRecord { private ExcelWorkbook workbook; private IOException initializationException = null; - T withWorkbook(Function action) throws IOException { + T withWorkbook(FunctionWithException action) + throws IOException, InterruptedException { synchronized (this) { return action.apply(accessCurrentWorkbook()); } @@ -258,7 +260,7 @@ public void close() throws IOException { } } - void reopen(boolean throwOnFailure) throws IOException { + void reopen(boolean throwOnFailure) throws IOException, InterruptedException { synchronized (this) { if (workbook != null) { throw new IllegalStateException("The workbook is already open."); diff --git a/std-bits/table/src/main/java/org/enso/table/excel/ExcelRange.java b/std-bits/table/src/main/java/org/enso/table/excel/ExcelRange.java index 552f3385fd27..4d6526829d1d 100644 --- a/std-bits/table/src/main/java/org/enso/table/excel/ExcelRange.java +++ b/std-bits/table/src/main/java/org/enso/table/excel/ExcelRange.java @@ -181,7 +181,8 @@ public static ExcelRange forRows(String sheetName, int topRow, int bottomRow) { * @param sheet ExcelSheet containing the range refers to. * @return Expanded range covering the connected table of cells. */ - public static ExcelRange expandSingleCell(ExcelRange excelRange, ExcelSheet sheet) { + public static ExcelRange expandSingleCell(ExcelRange excelRange, ExcelSheet sheet) + throws InterruptedException { ExcelRow currentRow = sheet.get(excelRange.getTopRow()); if (currentRow == null || currentRow.isEmpty(excelRange.getLeftColumn())) { return new ExcelRange( @@ -337,7 +338,7 @@ public int getRowCount() { return isWholeColumn() ? Integer.MAX_VALUE : bottomRow - topRow + 1; } - public int getLastNonEmptyRow(ExcelSheet sheet) { + public int getLastNonEmptyRow(ExcelSheet sheet) throws InterruptedException { int lastRow = Math.min(sheet.getLastRow(), isWholeColumn() ? sheet.getLastRow() : bottomRow) + 1; diff --git a/std-bits/table/src/main/java/org/enso/table/excel/ExcelSheet.java b/std-bits/table/src/main/java/org/enso/table/excel/ExcelSheet.java index 4d2dd42a2a32..4dbe433d5d62 100644 --- a/std-bits/table/src/main/java/org/enso/table/excel/ExcelSheet.java +++ b/std-bits/table/src/main/java/org/enso/table/excel/ExcelSheet.java @@ -12,10 +12,10 @@ public interface ExcelSheet { String getName(); /** Gets the initial row index within the sheet (1-based). */ - int getFirstRow(); + int getFirstRow() throws InterruptedException; /** Gets the final row index within the sheet (1-based). */ - int getLastRow(); + int getLastRow() throws InterruptedException; /** * Gets the row at the given index within the sheet (1-based) @@ -23,7 +23,7 @@ public interface ExcelSheet { * @param row the row index (1-based)/ * @return the row object or null if the row index is out of range or doesn't exist. */ - ExcelRow get(int row); + ExcelRow get(int row) throws InterruptedException; /** Gets the underlying Apache POI Sheet object - may be null. Provided for Writer use only. */ Sheet getSheet(); diff --git a/std-bits/table/src/main/java/org/enso/table/excel/ReadOnlyExcelConnection.java b/std-bits/table/src/main/java/org/enso/table/excel/ReadOnlyExcelConnection.java index 3cbac859648a..894771c96670 100644 --- a/std-bits/table/src/main/java/org/enso/table/excel/ReadOnlyExcelConnection.java +++ b/std-bits/table/src/main/java/org/enso/table/excel/ReadOnlyExcelConnection.java @@ -1,7 +1,7 @@ package org.enso.table.excel; import java.io.IOException; -import java.util.function.Function; +import org.enso.table.util.FunctionWithException; public class ReadOnlyExcelConnection implements AutoCloseable { @@ -27,7 +27,9 @@ public synchronized void close() throws IOException { record = null; } - public synchronized T withWorkbook(Function f) throws IOException { + public synchronized T withWorkbook( + FunctionWithException f) + throws IOException, InterruptedException { if (record == null) { throw new IllegalStateException("ReadOnlyExcelConnection is being used after it was closed."); } diff --git a/std-bits/table/src/main/java/org/enso/table/excel/xssfreader/XSSFReaderSheet.java b/std-bits/table/src/main/java/org/enso/table/excel/xssfreader/XSSFReaderSheet.java index cdb79cbdbd5b..2fc288ab0716 100644 --- a/std-bits/table/src/main/java/org/enso/table/excel/xssfreader/XSSFReaderSheet.java +++ b/std-bits/table/src/main/java/org/enso/table/excel/xssfreader/XSSFReaderSheet.java @@ -1,6 +1,7 @@ package org.enso.table.excel.xssfreader; import java.io.IOException; +import java.nio.channels.ClosedByInterruptException; import java.util.HashMap; import java.util.Map; import java.util.SortedMap; @@ -33,7 +34,7 @@ public XSSFReaderSheet(int sheetIdx, String sheetName, String relId, XSSFReaderW this.parent = parent; } - private synchronized void ensureReadSheetData() { + private synchronized void ensureReadSheetData() throws InterruptedException { if (hasReadSheetData) { return; } @@ -70,6 +71,8 @@ protected void onCell(int rowNumber, short columnNumber, String ref, CellValue v try { var sheet = reader.getSheet(relId); xmlReader.parse(new InputSource(sheet)); + } catch (ClosedByInterruptException e) { + throw new InterruptedException(e.getMessage()); } catch (SAXException | InvalidFormatException | IOException e) { throw new RuntimeException(e); } @@ -94,25 +97,25 @@ public String getName() { return sheetName; } - public String getDimensions() { + public String getDimensions() throws InterruptedException { ensureReadSheetData(); return dimensions; } @Override - public int getFirstRow() { + public int getFirstRow() throws InterruptedException { ensureReadSheetData(); return firstRow; } @Override - public int getLastRow() { + public int getLastRow() throws InterruptedException { ensureReadSheetData(); return lastRow; } @Override - public ExcelRow get(int row) { + public ExcelRow get(int row) throws InterruptedException { ensureReadSheetData(); if (!rowData.containsKey(row)) { diff --git a/std-bits/table/src/main/java/org/enso/table/excel/xssfreader/XSSFReaderWorkbook.java b/std-bits/table/src/main/java/org/enso/table/excel/xssfreader/XSSFReaderWorkbook.java index 6502057ff416..46f1d441914f 100644 --- a/std-bits/table/src/main/java/org/enso/table/excel/xssfreader/XSSFReaderWorkbook.java +++ b/std-bits/table/src/main/java/org/enso/table/excel/xssfreader/XSSFReaderWorkbook.java @@ -1,6 +1,7 @@ package org.enso.table.excel.xssfreader; import java.io.IOException; +import java.nio.channels.ClosedByInterruptException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -8,7 +9,6 @@ import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.function.Consumer; import javax.xml.XMLConstants; import javax.xml.namespace.NamespaceContext; import javax.xml.xpath.XPathConstants; @@ -26,6 +26,7 @@ import org.apache.poi.xssf.usermodel.XSSFRelation; import org.enso.table.excel.ExcelSheet; import org.enso.table.excel.ExcelWorkbook; +import org.enso.table.util.ConsumerWithException; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.w3c.dom.NodeList; @@ -90,7 +91,7 @@ public Iterator getPrefixes(String namespaceURI) { private SharedStrings sharedStrings; private XSSFReaderFormats styles; - public XSSFReaderWorkbook(String path) throws IOException { + public XSSFReaderWorkbook(String path) throws IOException, InterruptedException { this.path = path; // Read the workbook data @@ -101,7 +102,8 @@ public String getPath() { return path; } - void withReader(Consumer action) throws IOException { + void withReader(ConsumerWithException action) + throws IOException, InterruptedException { try (var pkg = OPCPackage.open(path, PackageAccess.READ)) { var reader = new XSSFReader(pkg); action.accept(reader); @@ -115,7 +117,7 @@ private record SheetInfo(int index, int sheetId, String name, String relID, bool private record NamedRange(String name, String formula) {} - private void readWorkbookData() throws IOException { + private void readWorkbookData() throws IOException, InterruptedException { withReader( reader -> { try { @@ -124,6 +126,8 @@ private void readWorkbookData() throws IOException { read1904DateSetting(workbookDoc); readSheetInfo(workbookDoc); readNamedRanges(workbookDoc); + } catch (ClosedByInterruptException e) { + throw new InterruptedException(e.getMessage()); } catch (SAXException | IOException | InvalidFormatException @@ -171,7 +175,7 @@ private void read1904DateSetting(Document workbookDoc) throws XPathExpressionExc } } - private synchronized void ensureReadShared() { + private synchronized void ensureReadShared() throws InterruptedException { if (hasReadShared) { return; } @@ -207,6 +211,8 @@ public int getUniqueCount() { styles = new XSSFReaderFormats(stylesTable); hasReadShared = true; + } catch (ClosedByInterruptException e) { + throw new InterruptedException(e.getMessage()); } catch (InvalidFormatException | IOException e) { throw new RuntimeException(e); } @@ -258,12 +264,12 @@ public String getNameFormula(String name) { return namedRange == null ? null : namedRange.formula; } - public SharedStrings getSharedStrings() { + public SharedStrings getSharedStrings() throws InterruptedException { ensureReadShared(); return sharedStrings; } - public XSSFReaderFormats getStyles() { + public XSSFReaderFormats getStyles() throws InterruptedException { ensureReadShared(); return styles; } diff --git a/std-bits/table/src/main/java/org/enso/table/read/ExcelReader.java b/std-bits/table/src/main/java/org/enso/table/read/ExcelReader.java index 6f6b289e8998..f4587fc6cb1e 100644 --- a/std-bits/table/src/main/java/org/enso/table/read/ExcelReader.java +++ b/std-bits/table/src/main/java/org/enso/table/read/ExcelReader.java @@ -4,7 +4,6 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; -import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.IntStream; import org.apache.poi.ss.util.CellReference; @@ -24,6 +23,7 @@ import org.enso.table.excel.ExcelWorkbook; import org.enso.table.excel.ReadOnlyExcelConnection; import org.enso.table.problems.ProblemAggregator; +import org.enso.table.util.FunctionWithException; import org.graalvm.polyglot.Context; /** A table reader for MS Excel files. */ @@ -36,7 +36,8 @@ public class ExcelReader { * @return a String[] containing the sheet names. * @throws IOException when the action fails */ - public static String[] readSheetNames(File file, ExcelFileFormat format) throws IOException { + public static String[] readSheetNames(File file, ExcelFileFormat format) + throws IOException, InterruptedException { return withWorkbook(file, format, ExcelReader::readSheetNames); } @@ -65,7 +66,8 @@ public static String[] readSheetNames(ExcelWorkbook workbook) { * @return a String[] containing the range names. * @throws IOException when the action fails */ - public static String[] readRangeNames(File file, ExcelFileFormat format) throws IOException { + public static String[] readRangeNames(File file, ExcelFileFormat format) + throws IOException, InterruptedException { return withWorkbook(file, format, ExcelWorkbook::getRangeNames); } @@ -89,7 +91,7 @@ public static Table readSheetByName( Integer row_limit, ExcelFileFormat format, ProblemAggregator problemAggregator) - throws IOException, InvalidLocationException { + throws IOException, InvalidLocationException, InterruptedException { return withWorkbook( file, format, @@ -130,7 +132,7 @@ public static Table readSheetByIndex( Integer row_limit, ExcelFileFormat format, ProblemAggregator problemAggregator) - throws IOException, InvalidLocationException { + throws IOException, InvalidLocationException, InterruptedException { return withWorkbook( file, format, @@ -175,7 +177,7 @@ public static Table readRangeByName( Integer row_limit, ExcelFileFormat format, ProblemAggregator problemAggregator) - throws IOException, InvalidLocationException { + throws IOException, InvalidLocationException, InterruptedException { return withWorkbook( file, format, @@ -202,7 +204,7 @@ public static Table readRangeByName( int skip_rows, Integer row_limit, ProblemAggregator problemAggregator) - throws InvalidLocationException { + throws InvalidLocationException, InterruptedException { int sheetIndex = workbook.getSheetIndex(rangeNameOrAddress); if (sheetIndex != -1) { return readTable( @@ -247,7 +249,7 @@ public static Table readRange( Integer row_limit, ExcelFileFormat format, ProblemAggregator problemAggregator) - throws IOException, InvalidLocationException { + throws IOException, InvalidLocationException, InterruptedException { return withWorkbook( file, format, @@ -256,7 +258,10 @@ public static Table readRange( } private static T withWorkbook( - File file, ExcelFileFormat format, Function action) throws IOException { + File file, + ExcelFileFormat format, + FunctionWithException action) + throws IOException, InterruptedException { try (ReadOnlyExcelConnection connection = ExcelConnectionPool.INSTANCE.openReadOnlyConnection(file, format)) { return connection.withWorkbook(action); @@ -270,7 +275,7 @@ public static Table readRange( int skip_rows, Integer row_limit, ProblemAggregator problemAggregator) - throws InvalidLocationException { + throws InvalidLocationException, InterruptedException { int sheetIndex = workbook.getSheetIndex(excelRange.getSheetName()); if (sheetIndex == -1) { throw new InvalidLocationException( @@ -294,7 +299,8 @@ private static Table readTable( ExcelHeaders.HeaderBehavior headers, int skipRows, int rowCount, - ProblemAggregator problemAggregator) { + ProblemAggregator problemAggregator) + throws InterruptedException { ExcelSheet sheet = workbook.getSheetAt(sheetIndex); diff --git a/std-bits/table/src/main/java/org/enso/table/util/ConsumerWithException.java b/std-bits/table/src/main/java/org/enso/table/util/ConsumerWithException.java new file mode 100644 index 000000000000..9f9343854bbd --- /dev/null +++ b/std-bits/table/src/main/java/org/enso/table/util/ConsumerWithException.java @@ -0,0 +1,45 @@ +package org.enso.table.util; + +import java.util.Objects; + +/** + * Same as {@link java.util.function.Consumer} except that a one can declare a checked exception, E. + * Represents an operation that accepts a single input argument and returns no result. Unlike most + * other functional interfaces, {@code Consumer} is expected to operate via side-effects. + * + *

This is a functional interface whose functional method is + * {@link #accept(Object)}. + * + * @param the type of the input to the operation + * @param the type of the checked exception + */ +@FunctionalInterface +public interface ConsumerWithException { + + /** + * Performs this operation on the given argument. + * + * @param t the input argument + */ + void accept(T t) throws E; + + /** + * Returns a composed {@code Consumer} that performs, in sequence, this operation followed by the + * {@code after} operation. If performing either operation throws an exception, it is relayed to + * the caller of the composed operation. If performing this operation throws an exception, the + * {@code after} operation will not be performed. + * + * @param after the operation to perform after this operation + * @return a composed {@code Consumer} that performs in sequence this operation followed by the + * {@code after} operation + * @throws NullPointerException if {@code after} is null + */ + default ConsumerWithException andThen(java.util.function.Consumer after) + throws E { + Objects.requireNonNull(after); + return (T t) -> { + accept(t); + after.accept(t); + }; + } +} diff --git a/std-bits/table/src/main/java/org/enso/table/util/FunctionWithException.java b/std-bits/table/src/main/java/org/enso/table/util/FunctionWithException.java new file mode 100644 index 000000000000..5d41f4686cb2 --- /dev/null +++ b/std-bits/table/src/main/java/org/enso/table/util/FunctionWithException.java @@ -0,0 +1,51 @@ +package org.enso.table.util; + +import java.util.Objects; +import java.util.function.Function; + +/** + * Same as {@link Function} except that a one can declare a checked exception, E. Represents a + * function that accepts one argument and produces a result. + * + *

This is a functional interface whose functional method is + * {@link #apply(Object)}. + * + * @param the type of the input to the function + * @param the type of the result of the function + * @param the type of the checked exception + */ +@FunctionalInterface +public interface FunctionWithException { + + /** + * Applies this function to the given argument. + * + * @param t the function argument + * @return the function result + */ + R apply(T t) throws E; + + default FunctionWithException compose( + FunctionWithException before) { + Objects.requireNonNull(before); + return (V v) -> apply(before.apply(v)); + } + + /** + * Returns a composed function that first applies this function to its input, and then applies the + * {@code after} function to the result. If evaluation of either function throws an exception, it + * is relayed to the caller of the composed function. + * + * @param the type of output of the {@code after} function, and of the composed function + * @param after the function to apply after this function is applied + * @return a composed function that first applies this function and then applies the {@code after} + * function + * @throws NullPointerException if after is null + * @see #compose(Function) + */ + default FunctionWithException andThen( + FunctionWithException after) { + Objects.requireNonNull(after); + return (T t) -> after.apply(apply(t)); + } +} diff --git a/std-bits/table/src/main/java/org/enso/table/write/ExcelWriter.java b/std-bits/table/src/main/java/org/enso/table/write/ExcelWriter.java index 2ced1f7a65bd..48dbc780c444 100644 --- a/std-bits/table/src/main/java/org/enso/table/write/ExcelWriter.java +++ b/std-bits/table/src/main/java/org/enso/table/write/ExcelWriter.java @@ -53,7 +53,8 @@ public static void writeTableToSheet( ExistingDataException, IllegalStateException, ColumnNameMismatchException, - ColumnCountMismatchException { + ColumnCountMismatchException, + InterruptedException { if (sheetIndex == 0 || sheetIndex > workbook.getNumberOfSheets()) { int i = 1; while (workbook.getSheet("Sheet" + i) != null) { @@ -116,7 +117,8 @@ public static void writeTableToSheet( ExistingDataException, IllegalStateException, ColumnNameMismatchException, - ColumnCountMismatchException { + ColumnCountMismatchException, + InterruptedException { int sheetIndex = workbook.getNumberOfSheets() == 0 ? -1 : workbook.getSheetIndex(sheetName); if (sheetIndex == -1) { writeTableToSheet( @@ -169,7 +171,8 @@ public static void writeTableToRange( RangeExceededException, ExistingDataException, ColumnNameMismatchException, - ColumnCountMismatchException { + ColumnCountMismatchException, + InterruptedException { Name name = workbook.getName(rangeNameOrAddress); ExcelRange excelRange; try { @@ -194,7 +197,8 @@ public static void writeTableToRange( RangeExceededException, ExistingDataException, ColumnNameMismatchException, - ColumnCountMismatchException { + ColumnCountMismatchException, + InterruptedException { int sheetIndex = workbook.getSheetIndex(range.getSheetName()); if (sheetIndex == -1) { throw new InvalidLocationException( @@ -263,7 +267,8 @@ private static void appendRangeWithTable( throws RangeExceededException, ExistingDataException, ColumnNameMismatchException, - ColumnCountMismatchException { + ColumnCountMismatchException, + InterruptedException { Table mappedTable = switch (existingDataMode) { case APPEND_BY_INDEX -> ColumnMapper.mapColumnsByPosition( @@ -333,7 +338,7 @@ private static void updateRangeWithTable( Long rowLimit, ExcelHeaders.HeaderBehavior headers, ExcelSheet sheet) - throws RangeExceededException, ExistingDataException { + throws RangeExceededException, ExistingDataException, InterruptedException { boolean writeHeaders = headers == ExcelHeaders.HeaderBehavior.USE_FIRST_ROW_AS_HEADERS; int requiredRows = Math.min(table.rowCount(), rowLimit == null ? Integer.MAX_VALUE : rowLimit.intValue()) @@ -383,7 +388,8 @@ private static void updateRangeWithTable( * @param sheet Sheet containing the range. * @return True if range is empty and clear is False, otherwise returns False. */ - private static boolean rangeIsNotEmpty(Workbook workbook, ExcelRange range, ExcelSheet sheet) { + private static boolean rangeIsNotEmpty(Workbook workbook, ExcelRange range, ExcelSheet sheet) + throws InterruptedException { ExcelRange fullRange = range.getAbsoluteRange(workbook); for (int row = fullRange.getTopRow(); row <= fullRange.getBottomRow(); row++) { ExcelRow excelRow = sheet.get(row); @@ -401,7 +407,8 @@ private static boolean rangeIsNotEmpty(Workbook workbook, ExcelRange range, Exce * @param range The range to clear. * @param sheet Sheet containing the range. */ - private static void clearRange(Workbook workbook, ExcelRange range, ExcelSheet sheet) { + private static void clearRange(Workbook workbook, ExcelRange range, ExcelSheet sheet) + throws InterruptedException { ExcelRange fullRange = range.getAbsoluteRange(workbook); for (int row = fullRange.getTopRow(); row <= fullRange.getBottomRow(); row++) { ExcelRow excelRow = sheet.get(row); @@ -547,7 +554,7 @@ private static void writeValueToCell( * @return EXCEL_COLUMN_NAMES if the range has headers, otherwise USE_FIRST_ROW_AS_HEADERS. */ private static ExcelHeaders.HeaderBehavior shouldWriteHeaders( - ExcelSheet excelSheet, int topRow, int startCol, int endCol) { + ExcelSheet excelSheet, int topRow, int startCol, int endCol) throws InterruptedException { ExcelRow row = excelSheet.get(topRow); // If the first row is missing or empty, should write headers.