diff --git a/src/main/java/ecdar/Ecdar.java b/src/main/java/ecdar/Ecdar.java index 3b2d941a..4b965d63 100644 --- a/src/main/java/ecdar/Ecdar.java +++ b/src/main/java/ecdar/Ecdar.java @@ -1,11 +1,11 @@ package ecdar; import ecdar.abstractions.*; -import ecdar.backend.BackendDriver; +import ecdar.backend.BackendException; import ecdar.backend.BackendHelper; -import ecdar.backend.QueryHandler; import ecdar.code_analysis.CodeAnalysis; import ecdar.controllers.EcdarController; +import ecdar.issues.ExitStatusCodes; import ecdar.presentations.*; import ecdar.utility.keyboard.Keybind; import ecdar.utility.keyboard.KeyboardTracker; @@ -50,8 +50,6 @@ public class Ecdar extends Application { private static BooleanProperty isUICached = new SimpleBooleanProperty(); public static BooleanProperty shouldRunBackgroundQueries = new SimpleBooleanProperty(true); private static final BooleanProperty isSplit = new SimpleBooleanProperty(false); - private static BackendDriver backendDriver = new BackendDriver(); - private static QueryHandler queryHandler = new QueryHandler(backendDriver); private Stage debugStage; /** @@ -179,20 +177,6 @@ public static void toggleCanvasSplit() { isSplit.set(!isSplit.get()); } - /** - * Returns the backend driver used to execute queries and handle simulation - * - * @return BackendDriver - */ - public static BackendDriver getBackendDriver() { - return backendDriver; - } - - public static QueryHandler getQueryExecutor() { - return queryHandler; - - } - public static double getDpiScale() { if (!autoScalingEnabled.getValue()) return 1; @@ -256,6 +240,21 @@ public void start(final Stage stage) { Ecdar.showToast("The application icon could not be loaded"); } + BackendHelper.addEngineInstanceListener(() -> { + // When the engines change, clear the backendDriver + // to prevent dangling connections and queries + try { + presentation.getController().queryPane.getController().stopAllQueries(); + BackendHelper.clearEngineConnections(); + } catch (BackendException e) { + showToast("An exception was encountered during shutdown of engine connections"); + } + }); + + // Whenever the Runtime is requested to exit, first stop all queries and exit the Platform + Runtime.getRuntime().addShutdownHook(new Thread(presentation.getController().queryPane.getController()::stopAllQueries)); + Runtime.getRuntime().addShutdownHook(new Thread(Platform::exit)); + // We're now ready! Let the curtains fall! stage.show(); @@ -298,28 +297,19 @@ public void start(final Stage stage) { })); stage.setOnCloseRequest(event -> { - BackendHelper.stopQueries(); - + int statusCode = ExitStatusCodes.SHUTDOWN_SUCCESSFUL.getStatusCode(); try { - backendDriver.closeAllEngineConnections(); - } catch (IOException e) { - e.printStackTrace(); + BackendHelper.clearEngineConnections(); + } catch (BackendException e) { + statusCode = ExitStatusCodes.CLOSE_ENGINE_CONNECTIONS_FAILED.getStatusCode(); } - Platform.exit(); - System.exit(0); - }); - - BackendHelper.addEngineInstanceListener(() -> { - // When the engines change, re-instantiate the backendDriver - // to prevent dangling connections and queries try { - backendDriver.closeAllEngineConnections(); - } catch (IOException e) { - throw new RuntimeException(e); + System.exit(statusCode); + } catch (SecurityException e) { + // Forcefully shutdown the Java Runtime + Runtime.getRuntime().halt(ExitStatusCodes.GRACEFUL_SHUTDOWN_FAILED.getStatusCode()); } - - backendDriver = new BackendDriver(); }); project = presentation.getController().projectPane.getController().project; diff --git a/src/main/java/ecdar/abstractions/Engine.java b/src/main/java/ecdar/abstractions/Engine.java deleted file mode 100644 index 3dc6f4e7..00000000 --- a/src/main/java/ecdar/abstractions/Engine.java +++ /dev/null @@ -1,119 +0,0 @@ -package ecdar.abstractions; - -import com.google.gson.JsonObject; -import ecdar.utility.serialize.Serializable; -import javafx.beans.property.SimpleBooleanProperty; - -public class Engine implements Serializable { - private static final String NAME = "name"; - private static final String IS_LOCAL = "isLocal"; - private static final String IS_DEFAULT = "isDefault"; - private static final String LOCATION = "location"; - private static final String PORT_RANGE_START = "portRangeStart"; - private static final String PORT_RANGE_END = "portRangeEnd"; - private static final String LOCKED = "locked"; - - private String name; - private boolean isLocal; - private boolean isDefault; - private String engineLocation; - private int portStart; - private int portEnd; - private SimpleBooleanProperty locked = new SimpleBooleanProperty(false); - - public Engine() {}; - - public Engine(final JsonObject jsonObject) { - deserialize(jsonObject); - }; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public boolean isLocal() { - return isLocal; - } - - public void setLocal(boolean local) { - isLocal = local; - } - - public boolean isDefault() { - return isDefault; - } - - public void setDefault(boolean aDefault) { - isDefault = aDefault; - } - - public String getEngineLocation() { - return engineLocation; - } - - public void setEngineLocation(String engineLocation) { - this.engineLocation = engineLocation; - } - - public int getPortStart() { - return portStart; - } - - public void setPortStart(int portStart) { - this.portStart = portStart; - } - - public int getPortEnd() { - return portEnd; - } - - public void setPortEnd(int portEnd) { - this.portEnd = portEnd; - } - - public int getNumberOfInstances() { - return this.portEnd - this.portStart + 1; - } - - public void lockInstance() { - locked.set(true); - } - - public SimpleBooleanProperty getLockedProperty() { - return locked; - } - - @Override - public JsonObject serialize() { - final JsonObject result = new JsonObject(); - result.addProperty(NAME, getName()); - result.addProperty(IS_LOCAL, isLocal()); - result.addProperty(IS_DEFAULT, isDefault()); - result.addProperty(LOCATION, getEngineLocation()); - result.addProperty(PORT_RANGE_START, getPortStart()); - result.addProperty(PORT_RANGE_END, getPortEnd()); - result.addProperty(LOCKED, getLockedProperty().get()); - - return result; - } - - @Override - public void deserialize(final JsonObject json) { - setName(json.getAsJsonPrimitive(NAME).getAsString()); - setLocal(json.getAsJsonPrimitive(IS_LOCAL).getAsBoolean()); - setDefault(json.getAsJsonPrimitive(IS_DEFAULT).getAsBoolean()); - setEngineLocation(json.getAsJsonPrimitive(LOCATION).getAsString()); - setPortStart(json.getAsJsonPrimitive(PORT_RANGE_START).getAsInt()); - setPortEnd(json.getAsJsonPrimitive(PORT_RANGE_END).getAsInt()); - if (json.getAsJsonPrimitive(LOCKED).getAsBoolean()) lockInstance(); - } - - @Override - public String toString() { - return name; - } -} diff --git a/src/main/java/ecdar/abstractions/Query.java b/src/main/java/ecdar/abstractions/Query.java index 92f0f99c..52b0643b 100644 --- a/src/main/java/ecdar/abstractions/Query.java +++ b/src/main/java/ecdar/abstractions/Query.java @@ -1,15 +1,10 @@ package ecdar.abstractions; -import ecdar.Ecdar; import ecdar.backend.*; -import ecdar.controllers.EcdarController; import ecdar.utility.serialize.Serializable; import com.google.gson.JsonObject; -import javafx.application.Platform; import javafx.beans.property.*; -import java.util.function.Consumer; - public class Query implements Serializable { private static final String QUERY = "query"; private static final String COMMENT = "comment"; @@ -18,49 +13,20 @@ public class Query implements Serializable { private final StringProperty query = new SimpleStringProperty(""); private final StringProperty comment = new SimpleStringProperty(""); - private final StringProperty errors = new SimpleStringProperty(""); private final SimpleBooleanProperty isPeriodic = new SimpleBooleanProperty(false); private final ObjectProperty queryState = new SimpleObjectProperty<>(QueryState.UNKNOWN); private final ObjectProperty type = new SimpleObjectProperty<>(); private Engine engine; - - private final Consumer successConsumer = (aBoolean) -> { - if (aBoolean) { - setQueryState(QueryState.SUCCESSFUL); - } else { - setQueryState(QueryState.ERROR); - } - }; - - private Boolean forcedCancel = false; - private final Consumer failureConsumer = (e) -> { - if (forcedCancel) { - setQueryState(QueryState.UNKNOWN); - } else { - setQueryState(QueryState.SYNTAX_ERROR); - if (e instanceof BackendException.MissingFileQueryException) { - Ecdar.showToast("Please save the project before trying to run queries"); - } - - this.addError(e.getMessage()); - final Throwable cause = e.getCause(); - if (cause != null) { - // We had trouble generating the model if we get a NullPointerException - if (cause instanceof NullPointerException) { - setQueryState(QueryState.UNKNOWN); - } else { - Platform.runLater(() -> EcdarController.openQueryDialog(this, cause.toString())); - } - } - } - }; - - public Query(final String query, final String comment, final QueryState queryState) { + public Query(final String query, final String comment, final QueryState queryState, final Engine engine) { this.query.set(query); this.comment.set(comment); this.queryState.set(queryState); - setEngine(BackendHelper.getDefaultEngine()); + setEngine(engine); + } + + public Query(final String query, final String comment, final QueryState queryState) { + this(query, comment, queryState, BackendHelper.getDefaultEngine()); } public Query(final JsonObject jsonElement) { @@ -103,8 +69,6 @@ public StringProperty commentProperty() { return comment; } - public StringProperty errors() { return errors; } - public boolean isPeriodic() { return isPeriodic.get(); } @@ -137,14 +101,6 @@ public ObjectProperty getTypeProperty() { return this.type; } - public Consumer getSuccessConsumer() { - return successConsumer; - } - - public Consumer getFailureConsumer() { - return failureConsumer; - } - @Override public JsonObject serialize() { final JsonObject result = new JsonObject(); @@ -181,19 +137,4 @@ public void deserialize(final JsonObject json) { setEngine(BackendHelper.getDefaultEngine()); } } - - public void cancel() { - if (getQueryState().equals(QueryState.RUNNING)) { - forcedCancel = true; - setQueryState(QueryState.UNKNOWN); - } - } - - public void addError(String e) { - errors.set(errors.getValue() + e + "\n"); - } - - public String getCurrentErrors() { - return errors.getValue(); - } } diff --git a/src/main/java/ecdar/backend/BackendDriver.java b/src/main/java/ecdar/backend/BackendDriver.java deleted file mode 100644 index 606fc91f..00000000 --- a/src/main/java/ecdar/backend/BackendDriver.java +++ /dev/null @@ -1,211 +0,0 @@ -package ecdar.backend; - -import EcdarProtoBuf.ComponentProtos; -import EcdarProtoBuf.EcdarBackendGrpc; -import EcdarProtoBuf.QueryProtos; -import com.google.protobuf.Empty; -import ecdar.Ecdar; -import ecdar.abstractions.Engine; -import ecdar.abstractions.Component; -import io.grpc.*; -import io.grpc.stub.StreamObserver; -import org.springframework.util.SocketUtils; - -import java.io.*; -import java.util.*; -import java.util.concurrent.ArrayBlockingQueue; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.TimeUnit; -import java.util.function.Supplier; -import java.util.stream.Stream; - -public class BackendDriver { - private final int responseDeadline = 20000; - private final int rerunRequestDelay = 200; - private final int numberOfRetriesPerQuery = 5; - - private final List startedEngineConnections = new ArrayList<>(); - private final Map> availableEngineConnections = new HashMap<>(); - private final BlockingQueue requestQueue = new ArrayBlockingQueue<>(200); - - public BackendDriver() { - // ToDo NIELS: Consider multiple consumer threads using 'for(int i = 0; i < x; i++) {}' - GrpcRequestConsumer consumer = new GrpcRequestConsumer(); - Thread consumerThread = new Thread(consumer); - consumerThread.start(); - } - - public int getResponseDeadline() { - return responseDeadline; - } - - /** - * Add a GrpcRequest to the request queue to be executed when an engine is available - * - * @param request The GrpcRequest to be executed later - */ - public void addRequestToExecutionQueue(GrpcRequest request) { - requestQueue.add(request); - } - - public void setConnectionAsAvailable(EngineConnection engineConnection) { - var relatedQueue = this.availableEngineConnections.get(engineConnection.getEngine()); - if (!relatedQueue.contains(engineConnection)) relatedQueue.add(engineConnection); - } - - /** - * Close all open engine connection and kill all locally running processes - * - * @throws IOException if any of the sockets do not respond - */ - public void closeAllEngineConnections() throws IOException { - for (EngineConnection ec : startedEngineConnections) ec.close(); - } - - /** - * Filters the list of open {@link EngineConnection}s to the specified {@link Engine} and returns the - * first match or attempts to start a new connection if none is found. - * - * @param engine engine to get a connection to (e.g. Reveaal, j-Ecdar, custom_engine) - * @return a EngineConnection object linked to the engine, either from the open engine connection list - * or a newly started connection. - * @throws BackendException.NoAvailableEngineConnectionException if unable to retrieve a connection to the engine - * and unable to start a new one - */ - private EngineConnection getEngineConnection(Engine engine) throws BackendException.NoAvailableEngineConnectionException { - EngineConnection connection; - try { - if (!availableEngineConnections.containsKey(engine)) - availableEngineConnections.put(engine, new ArrayBlockingQueue<>(engine.getNumberOfInstances() + 1)); - - // If no open connection is free, attempt to start a new one - if (availableEngineConnections.get(engine).size() < 1) { - tryStartNewEngineConnection(engine); - } - - // Block until a connection becomes available - connection = availableEngineConnections.get(engine).take(); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - - return connection; - } - - /** - * Attempts to start a new connection to the specified engine. On success, the engine is added to the associated - * queue, otherwise, nothing happens. - * - * @param engine the target engine for the connection - */ - private void tryStartNewEngineConnection(Engine engine) { - Process p = null; - String hostAddress = (engine.isLocal() ? "127.0.0.1" : engine.getEngineLocation()); - long portNumber = 0; - - if (engine.isLocal()) { - try { - portNumber = SocketUtils.findAvailableTcpPort(engine.getPortStart(), engine.getPortEnd()); - } catch (IllegalStateException e) { - // No port was available in range, we assume that connections are running on all ports - return; - } - - do { - ProcessBuilder pb = new ProcessBuilder(engine.getEngineLocation(), "-p", hostAddress + ":" + portNumber); - - try { - p = pb.start(); - } catch (IOException ioException) { - Ecdar.showToast("Unable to start local engine instance"); - ioException.printStackTrace(); - return; - } - // If the process is not alive, it failed while starting up, try again - } while (!p.isAlive()); - } else { - // Filter open connections to this engine and map their used ports to an int stream - // and use supplier to reuse the stream for each check - Supplier> activeEnginePortsStream = () -> startedEngineConnections.stream() - .filter(ec -> ec.getEngine().equals(engine)) - .mapToInt(EngineConnection::getPort).boxed(); - - int currentPort = engine.getPortStart(); - for (int port = engine.getPortStart(); port <= engine.getPortEnd(); port++) { - int tempPort = port; - if (activeEnginePortsStream.get().noneMatch((i) -> i == tempPort)) { - currentPort = port; - break; - } - } - - if (currentPort > engine.getPortEnd()) { - Ecdar.showToast("Could not create a new connection to '" + engine.getName() + ". All ports in range " + engine.getPortStart() + " - " + engine.getPortEnd() + " are already in use."); - return; - } - } - - ManagedChannel channel = ManagedChannelBuilder.forTarget(hostAddress + ":" + portNumber) - .usePlaintext() - .keepAliveTime(1000, TimeUnit.MILLISECONDS) - .build(); - - EcdarBackendGrpc.EcdarBackendStub stub = EcdarBackendGrpc.newStub(channel); - EngineConnection newConnection = new EngineConnection(engine, p, stub, channel); - startedEngineConnections.add(newConnection); - - QueryProtos.ComponentsUpdateRequest.Builder componentsBuilder = QueryProtos.ComponentsUpdateRequest.newBuilder(); - for (Component c : Ecdar.getProject().getComponents()) { - componentsBuilder.addComponents(ComponentProtos.Component.newBuilder().setJson(c.serialize().toString()).build()); - } - - StreamObserver observer = new StreamObserver<>() { - @Override - public void onNext(Empty value) { - } - - @Override - public void onError(Throwable t) { - } - - @Override - public void onCompleted() { - setConnectionAsAvailable(newConnection); - } - }; - - newConnection.getStub().withDeadlineAfter(responseDeadline, TimeUnit.MILLISECONDS) - .updateComponents(componentsBuilder.build(), observer); - } - - private class GrpcRequestConsumer implements Runnable { - @Override - public void run() { - while (true) { - try { - GrpcRequest request = requestQueue.take(); - - try { - request.tries++; - request.execute(getEngineConnection(request.getEngine())); - } catch (BackendException.NoAvailableEngineConnectionException e) { - e.printStackTrace(); - if (request.tries < numberOfRetriesPerQuery) { - new Timer().schedule(new TimerTask() { - @Override - public void run() { - requestQueue.add(request); - } - }, rerunRequestDelay); - } else { - Ecdar.showToast("Unable to find a connection to the requested engine"); - } - return; - } - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - } - } -} diff --git a/src/main/java/ecdar/backend/BackendException.java b/src/main/java/ecdar/backend/BackendException.java index 14b27baf..db66a21c 100644 --- a/src/main/java/ecdar/backend/BackendException.java +++ b/src/main/java/ecdar/backend/BackendException.java @@ -19,6 +19,26 @@ public NoAvailableEngineConnectionException(final String message, final Throwabl } } + public static class gRpcChannelShutdownException extends BackendException { + public gRpcChannelShutdownException(final String message) { + super(message); + } + + public gRpcChannelShutdownException(final String message, final Throwable cause) { + super(message, cause); + } + } + + public static class EngineProcessDestructionException extends BackendException { + public EngineProcessDestructionException(final String message) { + super(message); + } + + public EngineProcessDestructionException(final String message, final Throwable cause) { + super(message, cause); + } + } + public static class BadBackendQueryException extends BackendException { public BadBackendQueryException(final String message) { super(message); diff --git a/src/main/java/ecdar/backend/BackendHelper.java b/src/main/java/ecdar/backend/BackendHelper.java index 3cfd4bde..29df5cb7 100644 --- a/src/main/java/ecdar/backend/BackendHelper.java +++ b/src/main/java/ecdar/backend/BackendHelper.java @@ -21,7 +21,7 @@ public final class BackendHelper { final static String TEMP_DIRECTORY = "temporary"; private static Engine defaultEngine = null; - private static ObservableList engines = new SimpleListProperty<>(); + private static ObservableList engines = FXCollections.observableArrayList(); private static final List enginesUpdatedListeners = new ArrayList<>(); /** @@ -57,10 +57,21 @@ public static String getTempDirectoryAbsolutePath() throws URISyntaxException { } /** - * Stop all running queries. + * Clears all queued queries, stops all active engines, and closes all open engine connections */ - public static void stopQueries() { - Ecdar.getProject().getQueries().forEach(Query::cancel); + public static void clearEngineConnections() throws BackendException { + BackendException exception = new BackendException("Exceptions were thrown while attempting to close engine connections"); + for (Engine engine : engines) { + try { + engine.closeConnections(); + } catch (BackendException e) { + exception.addSuppressed(e); + } + } + + if (exception.getSuppressed().length > 0) { + throw exception; + } } /** @@ -75,25 +86,7 @@ public static String getLocationReachableQuery(final Location location, final Co } /** - * Generates a string for a deadlock query based on the component - * - * @param component The component which should be checked for deadlocks - * @return A deadlock query string - */ - public static String getExistDeadlockQuery(final Component component) { - // Get the names of the locations of this component. Used to produce the deadlock query - final String templateName = component.getName(); - final List locationNames = new ArrayList<>(); - - for (final Location location : component.getLocations()) { - locationNames.add(templateName + "." + location.getId()); - } - - return "(" + String.join(" || ", locationNames) + ") && deadlock"; - } - - /** - * Returns the Engine with the specified name, or null, if no such Engine exists + * Returns the BackendInstance with the specified name, or null, if no such BackendInstance exists * * @param engineName Name of the Engine to return * @return The Engine with matching name diff --git a/src/main/java/ecdar/backend/Engine.java b/src/main/java/ecdar/backend/Engine.java new file mode 100644 index 00000000..2f4a0995 --- /dev/null +++ b/src/main/java/ecdar/backend/Engine.java @@ -0,0 +1,354 @@ +package ecdar.backend; + +import EcdarProtoBuf.ComponentProtos; +import EcdarProtoBuf.QueryProtos; +import com.google.gson.JsonObject; +import com.google.protobuf.Empty; +import ecdar.Ecdar; +import ecdar.abstractions.Component; +import ecdar.abstractions.Query; +import ecdar.utility.serialize.Serializable; +import io.grpc.stub.StreamObserver; +import javafx.beans.property.SimpleBooleanProperty; + +import java.util.*; +import java.util.concurrent.*; +import java.util.function.Consumer; + +public class Engine implements Serializable { + private static final String NAME = "name"; + private static final String IS_LOCAL = "isLocal"; + private static final String IS_DEFAULT = "isDefault"; + private static final String LOCATION = "location"; + private static final String PORT_RANGE_START = "portRangeStart"; + private static final String PORT_RANGE_END = "portRangeEnd"; + private static final String LOCKED = "locked"; + private final int responseDeadline = 20000; + private final int rerunRequestDelay = 200; + private final int numberOfRetriesPerQuery = 5; + + private String name; + private boolean isLocal; + private boolean isDefault; + private int portStart; + private int portEnd; + private SimpleBooleanProperty locked = new SimpleBooleanProperty(false); + /** + * This is either a path to the engines executable or an IP address at which the engine is running + */ + private String engineLocation; + + private final ArrayList startedConnections = new ArrayList<>(); + private final BlockingQueue requestQueue = new ArrayBlockingQueue<>(200); // Magic number + // ToDo NIELS: Refactor to resize queue on port range change + private final BlockingQueue availableConnections = new ArrayBlockingQueue<>(200); // Magic number + private final EngineConnectionStarter connectionStarter = new EngineConnectionStarter(this); + + public Engine() { + GrpcRequestConsumer consumer = new GrpcRequestConsumer(); + Thread consumerThread = new Thread(consumer); + consumerThread.start(); + } + + public Engine(final JsonObject jsonObject) { + this(); + deserialize(jsonObject); + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public boolean isLocal() { + return isLocal; + } + + public void setLocal(boolean local) { + isLocal = local; + } + + public boolean isDefault() { + return isDefault; + } + + public void setDefault(boolean aDefault) { + isDefault = aDefault; + } + + public String getEngineLocation() { + return engineLocation; + } + + public void setEngineLocation(String engineLocation) { + this.engineLocation = engineLocation; + } + + public String getIpAddress() { + if (isLocal()) { + return "127.0.0.1"; + } else { + return getEngineLocation(); + } + } + + public int getPortStart() { + return portStart; + } + + public void setPortStart(int portStart) { + this.portStart = portStart; + } + + public int getPortEnd() { + return portEnd; + } + + public void setPortEnd(int portEnd) { + this.portEnd = portEnd; + } + + public int getNumberOfInstances() { + return this.portEnd - this.portStart + 1; + } + + public void lockInstance() { + locked.set(true); + } + + public SimpleBooleanProperty getLockedProperty() { + return locked; + } + + public ArrayList getStartedConnections() { + return startedConnections; + } + + /** + * Enqueue query for execution with consumers for success and error + * + * @param query the query to enqueue for execution + * @param successConsumer consumer for returned QueryResponse + * @param errorConsumer consumer for any throwable that might result from the execution + */ + public void enqueueQuery(Query query, Consumer successConsumer, Consumer errorConsumer) { + GrpcRequest request = new GrpcRequest(engineConnection -> { + StreamObserver responseObserver = new StreamObserver<>() { + @Override + public void onNext(QueryProtos.QueryResponse value) { + successConsumer.accept(value); + } + + @Override + public void onError(Throwable t) { + errorConsumer.accept(t); + setConnectionAsAvailable(engineConnection); + } + + @Override + public void onCompleted() { + // Release engine connection + setConnectionAsAvailable(engineConnection); + } + }; + + var queryBuilder = QueryProtos.Query.newBuilder() + .setId(0) + .setQuery(query.getType().getQueryName() + ": " + query.getQuery()); + + engineConnection.getStub().withDeadlineAfter(responseDeadline, TimeUnit.MILLISECONDS) + .sendQuery(queryBuilder.build(), responseObserver); + }); + + requestQueue.add(request); + } + + /** + * Signal that the EngineConnection can be used not in use and available for queries + * + * @param connection to make available + */ + public void setConnectionAsAvailable(EngineConnection connection) { + if (!availableConnections.contains(connection)) availableConnections.add(connection); + } + + /** + * Clears all queued queries, stops all active engines, and closes all open engine connections + */ + public void clear() throws BackendException { + requestQueue.clear(); + closeConnections(); + } + + /** + * Filters the list of open {@link EngineConnection}s to the specified {@link Engine} and returns the + * first match or attempts to start a new connection if none is found. + * + * @return a EngineConnection object linked to the engine, either from the open engine connection list + * or a newly started connection. + * @throws BackendException.NoAvailableEngineConnectionException if unable to retrieve a connection to the engine + * and unable to start a new one + */ + private EngineConnection getConnection() throws BackendException.NoAvailableEngineConnectionException { + EngineConnection connection; + try { + // If no open connection is free, attempt to start a new one + if (availableConnections.size() < 1) { + EngineConnection newConnection = this.connectionStarter.tryStartNewConnection(); + + if (newConnection != null) { + startedConnections.add(newConnection); + initializeConnection(newConnection); + } + } + + // Blocks until a connection becomes available + connection = availableConnections.take(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + return connection; + } + + /** + * Executes the gRPC requests required to initialize the connection for query execution + * NOTE: This will be unnecessary with the SW5 changes for the ProtoBuf + */ + private void initializeConnection(EngineConnection connection) { + QueryProtos.ComponentsUpdateRequest.Builder componentsBuilder = QueryProtos.ComponentsUpdateRequest.newBuilder(); + for (Component c : Ecdar.getProject().getComponents()) { + componentsBuilder.addComponents(ComponentProtos.Component.newBuilder().setJson(c.serialize().toString()).build()); + } + + StreamObserver observer = new StreamObserver<>() { + @Override + public void onNext(Empty value) { + } + + @Override + public void onError(Throwable t) { + try { + connection.close(); + } catch (BackendException.gRpcChannelShutdownException | + BackendException.EngineProcessDestructionException e) { + Ecdar.showToast("An error occurred while trying to start new connection to: \"" + getName() + "\" and an exception was thrown while trying to remove gRPC channel and potential process"); + } + startedConnections.remove(connection); + } + + @Override + public void onCompleted() { + if (startedConnections.contains(connection)) setConnectionAsAvailable(connection); + } + }; + + connection.getStub().withDeadlineAfter(responseDeadline, TimeUnit.MILLISECONDS) + .updateComponents(componentsBuilder.build(), observer); + } + + /** + * Close all open engine connections and kill all locally running processes + * + * @throws BackendException if one or more connections throw an exception on {@link EngineConnection#close()} + * (use getSuppressed() to see all thrown exceptions) + */ + public void closeConnections() throws BackendException { + // Create a list for storing all terminated connection + List> closeFutures = new ArrayList<>(); + BackendException exceptions = new BackendException("Exceptions were thrown while attempting to close engine connections on " + getName()); + + // Attempt to close all connections + for (EngineConnection ec : startedConnections) { + CompletableFuture closeFuture = CompletableFuture.supplyAsync(() -> { + try { + ec.close(); + } catch (BackendException.gRpcChannelShutdownException | + BackendException.EngineProcessDestructionException e) { + throw new RuntimeException(e); + } + + return ec; + }); + + closeFutures.add(closeFuture); + } + + for (CompletableFuture closeFuture : closeFutures) { + try { + EngineConnection ec = closeFuture.get(); + + availableConnections.remove(ec); + startedConnections.remove(ec); + } catch (InterruptedException | ExecutionException e) { + exceptions.addSuppressed(e.getCause()); + } + } + + if (!startedConnections.isEmpty()) throw exceptions; + } + + private class GrpcRequestConsumer implements Runnable { + @Override + public void run() { + while (true) { + try { + GrpcRequest request = requestQueue.take(); + + try { + request.tries++; + request.execute(getConnection()); + } catch (BackendException.NoAvailableEngineConnectionException e) { + e.printStackTrace(); + if (request.tries < numberOfRetriesPerQuery) { + new Timer().schedule(new TimerTask() { + @Override + public void run() { + requestQueue.add(request); + } + }, rerunRequestDelay); + } else { + Ecdar.showToast("Unable to find a connection to the requested engine"); + } + return; + } + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + } + + @Override + public JsonObject serialize() { + final JsonObject result = new JsonObject(); + result.addProperty(NAME, getName()); + result.addProperty(IS_LOCAL, isLocal()); + result.addProperty(IS_DEFAULT, isDefault()); + result.addProperty(LOCATION, getEngineLocation()); + result.addProperty(PORT_RANGE_START, getPortStart()); + result.addProperty(PORT_RANGE_END, getPortEnd()); + result.addProperty(LOCKED, getLockedProperty().get()); + + return result; + } + + @Override + public void deserialize(final JsonObject json) { + setName(json.getAsJsonPrimitive(NAME).getAsString()); + setLocal(json.getAsJsonPrimitive(IS_LOCAL).getAsBoolean()); + setDefault(json.getAsJsonPrimitive(IS_DEFAULT).getAsBoolean()); + setEngineLocation(json.getAsJsonPrimitive(LOCATION).getAsString()); + setPortStart(json.getAsJsonPrimitive(PORT_RANGE_START).getAsInt()); + setPortEnd(json.getAsJsonPrimitive(PORT_RANGE_END).getAsInt()); + if (json.getAsJsonPrimitive(LOCKED).getAsBoolean()) lockInstance(); + } + + @Override + public String toString() { + return name; + } +} diff --git a/src/main/java/ecdar/backend/EngineConnection.java b/src/main/java/ecdar/backend/EngineConnection.java index 355e78f8..3cd9a5ff 100644 --- a/src/main/java/ecdar/backend/EngineConnection.java +++ b/src/main/java/ecdar/backend/EngineConnection.java @@ -1,26 +1,31 @@ package ecdar.backend; import EcdarProtoBuf.EcdarBackendGrpc; -import ecdar.abstractions.Engine; import io.grpc.ManagedChannel; +import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; public class EngineConnection { - private final Process process; + private final Engine engine; private final EcdarBackendGrpc.EcdarBackendStub stub; private final ManagedChannel channel; - private final Engine engine; + private final Process process; private final int port; - EngineConnection(Engine engine, Process process, EcdarBackendGrpc.EcdarBackendStub stub, ManagedChannel channel) { - this.process = process; + EngineConnection(Engine engine, ManagedChannel channel, EcdarBackendGrpc.EcdarBackendStub stub, Process process) { this.engine = engine; this.stub = stub; this.channel = channel; + this.process = process; this.port = Integer.parseInt(getStub().getChannel().authority().split(":", 2)[1]); } + EngineConnection(Engine engine, ManagedChannel channel, EcdarBackendGrpc.EcdarBackendStub stub) { + this(engine, channel, stub, null); + } + /** * Get the gRPC stub of the connection to use for query execution * @@ -47,22 +52,33 @@ public int getPort() { /** * Close the gRPC connection and end the process + * + * @throws BackendException.gRpcChannelShutdownException if an InterruptedException is encountered while trying to shut down the gRPC channel. + * @throws BackendException.EngineProcessDestructionException if the connected engine process throws an ExecutionException, an InterruptedException, or a TimeoutException. */ - public void close() { + public void close() throws BackendException.gRpcChannelShutdownException, BackendException.EngineProcessDestructionException { if (!channel.isShutdown()) { try { channel.shutdown(); if (!channel.awaitTermination(45, TimeUnit.SECONDS)) { channel.shutdownNow(); // Forcefully close the connection } - } catch (Exception e) { - e.printStackTrace(); + } catch (InterruptedException e) { + // Engine location is either the file path or the IP, here we want the channel address + throw new BackendException.gRpcChannelShutdownException("The gRPC channel to \"" + this.engine.getName() + "\" instance running at: " + engine.getIpAddress() + ":" + this.port + "was interrupted during termination", e.getCause()); } } // If the engine is remote, there will not be a process if (process != null) { - process.destroy(); + try { + java.util.concurrent.CompletableFuture terminated = process.onExit(); + process.destroy(); + terminated.get(45, TimeUnit.SECONDS); + } catch (ExecutionException | InterruptedException | TimeoutException e) { + // Add the engine location to the exception, as it contains the path to the executable + throw new BackendException.EngineProcessDestructionException("A process running: " + this.engine.getEngineLocation() + " on port " + this.port + " threw an exception during shutdown", e.getCause()); + } } } } diff --git a/src/main/java/ecdar/backend/EngineConnectionStarter.java b/src/main/java/ecdar/backend/EngineConnectionStarter.java new file mode 100644 index 00000000..be8c3a2f --- /dev/null +++ b/src/main/java/ecdar/backend/EngineConnectionStarter.java @@ -0,0 +1,119 @@ +package ecdar.backend; + +import EcdarProtoBuf.EcdarBackendGrpc; +import ecdar.Ecdar; +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import org.springframework.util.SocketUtils; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; +import java.util.stream.Stream; + +public class EngineConnectionStarter { + private final Engine engine; + private final int maxRetriesForStartingEngineProcess = 3; + + EngineConnectionStarter(Engine engine) { + this.engine = engine; + } + + /** + * Attempts to start a new connection to the specified engine. + * + * @return the started EngineConnection if successful, + * otherwise, null. + */ + protected EngineConnection tryStartNewConnection() { + EngineConnection newConnection; + + if (engine.isLocal()) { + newConnection = startLocalConnection(); + } else { + newConnection = startRemoteConnection(); + } + + // If the connection is null, no new connection was started + return newConnection; + } + + /** + * Starts a process, creates an EngineConnection to it, and returns that connection + * + * @return an EngineConnection to a local engine running in a Process or null if all ports are already in use + */ + private EngineConnection startLocalConnection() { + long port; + try { + port = SocketUtils.findAvailableTcpPort(engine.getPortStart(), engine.getPortEnd()); + } catch (IllegalStateException e) { + // All ports specified for engine are already used for running engines + return null; + } + + // Start local process of engine + Process p; + int attempts = 0; + + do { + attempts++; + ProcessBuilder pb = new ProcessBuilder(engine.getEngineLocation(), "-p", engine.getIpAddress() + ":" + port); + + try { + p = pb.start(); + } catch (IOException ioException) { + Ecdar.showToast("Unable to start local engine instance"); + ioException.printStackTrace(); + return null; + } + } while (!p.isAlive() && attempts < maxRetriesForStartingEngineProcess); + + ManagedChannel channel = startGrpcChannel(engine.getIpAddress(), port); + EcdarBackendGrpc.EcdarBackendStub stub = EcdarBackendGrpc.newStub(channel); + return new EngineConnection(engine, channel, stub, p); + } + + /** + * Creates and returns an EngineConnection to the remote engine + * + * @return an EngineConnection to a remote engine or null if all ports are already connected to + */ + private EngineConnection startRemoteConnection() { + // Get a stream of ports already used for connections + Supplier> activeEnginePortsStream = () -> engine.getStartedConnections().stream() + .mapToInt(EngineConnection::getPort).boxed(); + + long port = engine.getPortStart(); + for (int currentPort = engine.getPortStart(); currentPort <= engine.getPortEnd(); currentPort++) { + final int tempPort = currentPort; + if (activeEnginePortsStream.get().anyMatch((i) -> i == tempPort)) { + port = currentPort; + break; + } + } + + if (port > engine.getPortEnd()) { + // All ports specified for engine are already used for connections + return null; + } + + ManagedChannel channel = startGrpcChannel(engine.getIpAddress(), port); + EcdarBackendGrpc.EcdarBackendStub stub = EcdarBackendGrpc.newStub(channel); + return new EngineConnection(engine, channel, stub); + } + + /** + * Connects a gRPC channel to the address at the specified port, expecting that an engine is running there + * + * @param address of the target engine + * @param port of the target engine at the address + * @return the created gRPC channel + */ + private ManagedChannel startGrpcChannel(final String address, final long port) { + return ManagedChannelBuilder.forTarget(address + ":" + port) + .usePlaintext() + .keepAliveTime(1000, TimeUnit.MILLISECONDS) + .build(); + } +} diff --git a/src/main/java/ecdar/backend/GrpcRequest.java b/src/main/java/ecdar/backend/GrpcRequest.java index 311d2a2b..642ebfd0 100644 --- a/src/main/java/ecdar/backend/GrpcRequest.java +++ b/src/main/java/ecdar/backend/GrpcRequest.java @@ -1,24 +1,16 @@ package ecdar.backend; -import ecdar.abstractions.Engine; - import java.util.function.Consumer; public class GrpcRequest { private final Consumer request; - private final Engine engine; public int tries = 0; - public GrpcRequest(Consumer request, Engine engine) { + public GrpcRequest(Consumer request) { this.request = request; - this.engine = engine; } public void execute(EngineConnection engineConnection) { this.request.accept(engineConnection); } - - public Engine getEngine() { - return engine; - } } \ No newline at end of file diff --git a/src/main/java/ecdar/backend/QueryHandler.java b/src/main/java/ecdar/backend/QueryHandler.java deleted file mode 100644 index 98b7ffea..00000000 --- a/src/main/java/ecdar/backend/QueryHandler.java +++ /dev/null @@ -1,156 +0,0 @@ -package ecdar.backend; - -import EcdarProtoBuf.QueryProtos; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; -import ecdar.Ecdar; -import ecdar.abstractions.Component; -import ecdar.abstractions.Query; -import ecdar.abstractions.QueryState; -import ecdar.utility.UndoRedoStack; -import ecdar.utility.helpers.StringValidator; -import io.grpc.stub.StreamObserver; -import javafx.application.Platform; -import javafx.collections.ObservableList; - -import java.util.NoSuchElementException; -import java.util.concurrent.TimeUnit; - -public class QueryHandler { - private final BackendDriver backendDriver; - - public QueryHandler(BackendDriver backendDriver) { - this.backendDriver = backendDriver; - } - - /** - * Executes the specified query - * @param query query to be executed - */ - public void executeQuery(Query query) throws NoSuchElementException { - if (query.getQueryState().equals(QueryState.RUNNING) || !StringValidator.validateQuery(query.getQuery())) return; - - if (query.getQuery().isEmpty()) { - query.setQueryState(QueryState.SYNTAX_ERROR); - query.addError("Query is empty"); - return; - } - - query.setQueryState(QueryState.RUNNING); - query.errors().set(""); - - GrpcRequest request = new GrpcRequest(engineConnection -> { - StreamObserver responseObserver = new StreamObserver<>() { - @Override - public void onNext(QueryProtos.QueryResponse value) { - handleQueryResponse(value, query); - } - - @Override - public void onError(Throwable t) { - handleQueryBackendError(t, query); - backendDriver.setConnectionAsAvailable(engineConnection); - } - - @Override - public void onCompleted() { - // Release engine connection - backendDriver.setConnectionAsAvailable(engineConnection); - } - }; - - var queryBuilder = QueryProtos.Query.newBuilder() - .setId(0) - .setQuery(query.getType().getQueryName() + ": " + query.getQuery()); - - engineConnection.getStub().withDeadlineAfter(backendDriver.getResponseDeadline(), TimeUnit.MILLISECONDS) - .sendQuery(queryBuilder.build(), responseObserver); - }, query.getEngine()); - - backendDriver.addRequestToExecutionQueue(request); - } - - private void handleQueryResponse(QueryProtos.QueryResponse value, Query query) { - // If the query has been cancelled, ignore the result - if (query.getQueryState() == QueryState.UNKNOWN) return; - - if (value.hasRefinement() && value.getRefinement().getSuccess()) { - query.setQueryState(QueryState.SUCCESSFUL); - query.getSuccessConsumer().accept(true); - } else if (value.hasConsistency() && value.getConsistency().getSuccess()) { - query.setQueryState(QueryState.SUCCESSFUL); - query.getSuccessConsumer().accept(true); - } else if (value.hasDeterminism() && value.getDeterminism().getSuccess()) { - query.setQueryState(QueryState.SUCCESSFUL); - query.getSuccessConsumer().accept(true); - } else if (value.hasComponent()) { - query.setQueryState(QueryState.SUCCESSFUL); - query.getSuccessConsumer().accept(true); - JsonObject returnedComponent = (JsonObject) JsonParser.parseString(value.getComponent().getComponent().getJson()); - addGeneratedComponent(new Component(returnedComponent)); - } else { - query.setQueryState(QueryState.ERROR); - query.getSuccessConsumer().accept(false); - } - } - - private void handleQueryBackendError(Throwable t, Query query) { - // If the query has been cancelled, ignore the error - if (query.getQueryState() == QueryState.UNKNOWN) return; - - // Each error starts with a capitalized description of the error equal to the gRPC error type encountered - String errorType = t.getMessage().split(":\\s+", 2)[0]; - - if ("DEADLINE_EXCEEDED".equals(errorType)) { - query.setQueryState(QueryState.ERROR); - query.getFailureConsumer().accept(new BackendException.QueryErrorException("The engine did not answer the request in time")); - } else { - try { - query.setQueryState(QueryState.ERROR); - query.getFailureConsumer().accept(new BackendException.QueryErrorException("The execution of this query failed with message:" + System.lineSeparator() + t.getLocalizedMessage())); - } catch (Exception e) { - e.printStackTrace(); - } - } - } - - private void addGeneratedComponent(Component newComponent) { - Platform.runLater(() -> { - newComponent.setTemporary(true); - - ObservableList listOfGeneratedComponents = Ecdar.getProject().getTempComponents(); // ToDo NIELS: Refactor - Component matchedComponent = null; - - for (Component currentGeneratedComponent : listOfGeneratedComponents) { - int comparisonOfNames = currentGeneratedComponent.getName().compareTo(newComponent.getName()); - - if (comparisonOfNames == 0) { - matchedComponent = currentGeneratedComponent; - break; - } else if (comparisonOfNames < 0) { - break; - } - } - - if (matchedComponent == null) { - UndoRedoStack.pushAndPerform(() -> { // Perform - Ecdar.getProject().getTempComponents().add(newComponent); - }, () -> { // Undo - Ecdar.getProject().getTempComponents().remove(newComponent); - }, "Created new component: " + newComponent.getName(), "add-circle"); - } else { - // Remove current component with name and add the newly generated one - Component finalMatchedComponent = matchedComponent; - UndoRedoStack.pushAndPerform(() -> { // Perform - Ecdar.getProject().getTempComponents().remove(finalMatchedComponent); - Ecdar.getProject().getTempComponents().add(newComponent); - }, () -> { // Undo - Ecdar.getProject().getTempComponents().remove(newComponent); - Ecdar.getProject().getTempComponents().add(finalMatchedComponent); - }, "Created new component: " + newComponent.getName(), "add-circle"); - } - - Ecdar.getProject().addComponent(newComponent); - }); - } -} diff --git a/src/main/java/ecdar/controllers/CanvasController.java b/src/main/java/ecdar/controllers/CanvasController.java index e72b705e..ddf32b02 100644 --- a/src/main/java/ecdar/controllers/CanvasController.java +++ b/src/main/java/ecdar/controllers/CanvasController.java @@ -150,13 +150,7 @@ private void onActiveModelChanged(final HighLevelModelPresentation oldObject, fi } else if (newObject instanceof DeclarationsPresentation) { activeComponentPresentation = null; modelPane.getChildren().add(newObject); - - // Bind size of Declaration to size of the model pane to ensure alignment and avoid drag - DeclarationsController declarationsController = (DeclarationsController) newObject.getController(); - declarationsController.root.minWidthProperty().bind(modelPane.minWidthProperty()); - declarationsController.root.maxWidthProperty().bind(modelPane.maxWidthProperty()); - declarationsController.root.minHeightProperty().bind(modelPane.minHeightProperty()); - declarationsController.root.maxHeightProperty().bind(modelPane.maxHeightProperty()); + ((DeclarationsController) newObject.getController()).bindWidthAndHeightToPane(modelPane); } else if (newObject instanceof SystemPresentation) { activeComponentPresentation = null; modelPane.getChildren().add(newObject); diff --git a/src/main/java/ecdar/controllers/DeclarationsController.java b/src/main/java/ecdar/controllers/DeclarationsController.java index ee474f86..65f0c931 100644 --- a/src/main/java/ecdar/controllers/DeclarationsController.java +++ b/src/main/java/ecdar/controllers/DeclarationsController.java @@ -55,6 +55,18 @@ private void initializeText() { declarations.get().setDeclarationsText(newDeclaration)); } + /** + * Bind width and height of the text editor field, such that it fills up the provided canvas + */ + public void bindWidthAndHeightToPane(StackPane pane) { + // Fetch width and height of canvas and update + root.minWidthProperty().bind(pane.minWidthProperty()); + root.maxWidthProperty().bind(pane.maxWidthProperty()); + root.minHeightProperty().bind(pane.minHeightProperty()); + root.maxHeightProperty().bind(pane.maxHeightProperty()); + textArea.setTranslateY(20); + } + /** * Updates highlighting of the text in the text area. */ diff --git a/src/main/java/ecdar/controllers/EcdarController.java b/src/main/java/ecdar/controllers/EcdarController.java index da8c543d..dc25531e 100644 --- a/src/main/java/ecdar/controllers/EcdarController.java +++ b/src/main/java/ecdar/controllers/EcdarController.java @@ -6,6 +6,7 @@ import ecdar.Ecdar; import ecdar.abstractions.*; import ecdar.backend.BackendHelper; +import ecdar.backend.Engine; import ecdar.code_analysis.CodeAnalysis; import ecdar.mutation.MutationTestPlanPresentation; import ecdar.mutation.models.MutationTestPlan; @@ -75,8 +76,6 @@ public class EcdarController implements Initializable { public StackPane dialogContainer; public JFXDialog dialog; public StackPane modalBar; - public JFXTextField queryTextField; - public JFXTextField commentTextField; public JFXRippler colorSelected; public JFXRippler deleteSelected; public JFXRippler undo; @@ -155,7 +154,7 @@ public class EcdarController implements Initializable { private static Text _queryTextResult; private static Text _queryTextQuery; private static final Text temporaryComponentWatermark = new Text("Temporary component"); - + public static void runReachabilityAnalysis() { if (!reachabilityServiceEnabled) return; @@ -178,9 +177,7 @@ public static void setTemporaryComponentWatermarkVisibility(boolean visibility) * @param node The "root" to start the search from */ public void scaleIcons(Node node) { - Platform.runLater(() -> { scaleIcons(node, getNewCalculatedScale()); - }); } private void scaleIcons(Node node, double size) { @@ -199,6 +196,11 @@ private void scaleIcons(Node node, double size) { } private double getNewCalculatedScale() { + // If the UI is not fully loaded, no toggle will be selected + if (scaling.getSelectedToggle() == null) { + return Ecdar.getDpiScale() * 13.0; + } + return (Double.parseDouble(scaling.getSelectedToggle().getProperties().get("scale").toString()) * Ecdar.getDpiScale()) * 13.0; } @@ -209,20 +211,29 @@ private void scaleEdgeStatusToggle(double size) { @Override public void initialize(final URL location, final ResourceBundle resources) { - initilizeDialogs(); - initializeCanvasPane(); - initializeEdgeStatusHandling(); - initializeKeybindings(); - initializeStatusBar(); - initializeMenuBar(); - intitializeTemporaryComponentWatermark(); - startBackgroundQueriesThread(); // Will terminate immediately if background queries are turned off + initializeQueryPane(); - bottomFillerElement.heightProperty().bind(messageTabPane.maxHeightProperty()); - messageTabPane.getController().setRunnableForOpeningAndClosingMessageTabPane(this::changeInsetsOfFileAndQueryPanes); + Platform.runLater(() -> { + initializeDialogs(); + initializeCanvasPane(); + initializeEdgeStatusHandling(); + initializeKeybindings(); + initializeStatusBar(); + initializeMenuBar(); + initializeTemporaryComponentWatermark(); + startBackgroundQueriesThread(); // Will terminate immediately if background queries are turned off + + bottomFillerElement.heightProperty().bind(messageTabPane.maxHeightProperty()); + messageTabPane.getController().setRunnableForOpeningAndClosingMessageTabPane(this::changeInsetsOfFileAndQueryPanes); + }); } - private void initilizeDialogs() { + private void initializeQueryPane() { + queryPane = new QueryPanePresentation(); + rightPane.getChildren().add(queryPane); + } + + private void initializeDialogs() { dialog.setDialogContainer(dialogContainer); dialogContainer.opacityProperty().bind(dialog.getChildren().get(0).scaleXProperty()); dialog.setOnDialogClosed(event -> dialogContainer.setVisible(false)); @@ -272,8 +283,10 @@ private void initializeDialog(JFXDialog dialog, StackPane dialogContainer) { dialogContainer.setMouseTransparent(false); }); - projectPane.getStyleClass().add("responsive-pane-sizing"); - queryPane.getStyleClass().add("responsive-pane-sizing"); + Platform.runLater(() -> { + projectPane.getStyleClass().add("responsive-pane-sizing"); + queryPane.getStyleClass().add("responsive-pane-sizing"); + }); initializeEdgeStatusHandling(); initializeKeybindings(); @@ -283,7 +296,7 @@ private void initializeDialog(JFXDialog dialog, StackPane dialogContainer) { /** * Initializes the watermark for temporary/generated components */ - private void intitializeTemporaryComponentWatermark() { + private void initializeTemporaryComponentWatermark() { temporaryComponentWatermark.getStyleClass().add("display4"); temporaryComponentWatermark.setOpacity(0.1); temporaryComponentWatermark.setRotate(-45); @@ -424,8 +437,9 @@ private void startBackgroundQueriesThread() { // Stop thread if background queries have been toggled off if (!Ecdar.shouldRunBackgroundQueries.get()) return; - Ecdar.getProject().getQueries().forEach(query -> { - if (query.isPeriodic()) Ecdar.getQueryExecutor().executeQuery(query); + queryPane.getController().queriesList.getChildren().forEach(queryPresentation -> { + QueryController queryController = ((QueryPresentation) queryPresentation).getController(); + if (queryController.getQuery().isPeriodic()) queryController.runQuery(); }); // List of threads to start @@ -443,9 +457,11 @@ private void startBackgroundQueriesThread() { Query reachabilityQuery = new Query(locationReachableQuery, "", QueryState.UNKNOWN); reachabilityQuery.setType(QueryType.REACHABILITY); - Ecdar.getQueryExecutor().executeQuery(reachabilityQuery); + QueryController controller = new QueryController(); + controller.setQuery(reachabilityQuery); + controller.runQuery(); - final Thread verifyThread = new Thread(() -> Ecdar.getQueryExecutor().executeQuery(reachabilityQuery)); + final Thread verifyThread = new Thread(controller::runQuery); verifyThread.setName(locationReachableQuery + " (" + verifyThread.getName() + ")"); Debug.addThread(verifyThread); @@ -1299,11 +1315,11 @@ private static int getAutoCropRightX(final BufferedImage image) { private void changeInsetsOfFileAndQueryPanes() { if (messageTabPane.getController().isOpen()) { projectPane.showBottomInset(false); - queryPane.showBottomInset(false); + queryPane.getController().showBottomInset(false); getActiveCanvasPresentation().getController().updateOffset(false); } else { projectPane.showBottomInset(true); - queryPane.showBottomInset(true); + queryPane.getController().showBottomInset(true); getActiveCanvasPresentation().getController().updateOffset(true); } } diff --git a/src/main/java/ecdar/controllers/EngineInstanceController.java b/src/main/java/ecdar/controllers/EngineInstanceController.java index 3d1b6d4c..ccf26c51 100644 --- a/src/main/java/ecdar/controllers/EngineInstanceController.java +++ b/src/main/java/ecdar/controllers/EngineInstanceController.java @@ -3,7 +3,7 @@ import com.jfoenix.controls.JFXCheckBox; import com.jfoenix.controls.JFXRippler; import com.jfoenix.controls.JFXTextField; -import ecdar.abstractions.Engine; +import ecdar.backend.Engine; import javafx.application.Platform; import javafx.beans.property.BooleanProperty; import javafx.fxml.FXML; @@ -98,7 +98,7 @@ public void setEngine(Engine instance) { if (isLocal.isSelected()) { this.pathToEngine.setText(instance.getEngineLocation()); } else { - this.address.setText(instance.getEngineLocation()); + this.address.setText(instance.getIpAddress()); } this.portRangeStart.setText(String.valueOf(instance.getPortStart())); diff --git a/src/main/java/ecdar/controllers/EngineOptionsDialogController.java b/src/main/java/ecdar/controllers/EngineOptionsDialogController.java index 33862cc9..fafd6592 100644 --- a/src/main/java/ecdar/controllers/EngineOptionsDialogController.java +++ b/src/main/java/ecdar/controllers/EngineOptionsDialogController.java @@ -5,7 +5,8 @@ import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXRippler; import ecdar.Ecdar; -import ecdar.abstractions.Engine; +import ecdar.backend.Engine; +import ecdar.backend.BackendException; import ecdar.backend.BackendHelper; import ecdar.presentations.EnginePresentation; import javafx.fxml.Initializable; @@ -72,8 +73,8 @@ public boolean saveChangesToEngineOptions() { // Close all engine connections to avoid dangling engine connections when port range is changed try { - Ecdar.getBackendDriver().closeAllEngineConnections(); - } catch (IOException e) { + BackendHelper.clearEngineConnections(); + } catch (BackendException e) { e.printStackTrace(); } diff --git a/src/main/java/ecdar/controllers/LocationController.java b/src/main/java/ecdar/controllers/LocationController.java index 98001b64..b72137a5 100644 --- a/src/main/java/ecdar/controllers/LocationController.java +++ b/src/main/java/ecdar/controllers/LocationController.java @@ -16,6 +16,7 @@ import ecdar.utility.keyboard.NudgeDirection; import ecdar.utility.keyboard.Nudgeable; import com.jfoenix.controls.JFXPopup; +import javafx.application.Platform; import javafx.beans.property.*; import javafx.beans.value.ObservableDoubleValue; import javafx.collections.ListChangeListener; @@ -201,7 +202,6 @@ public void initializeDropDownMenu() { final Query query = new Query(reachabilityQuery, reachabilityComment, QueryState.UNKNOWN); query.setType(QueryType.REACHABILITY); Ecdar.getProject().getQueries().add(query); - Ecdar.getQueryExecutor().executeQuery(query); dropDownMenu.hide(); }); diff --git a/src/main/java/ecdar/controllers/QueryController.java b/src/main/java/ecdar/controllers/QueryController.java index 680e384e..7f9a3e3e 100644 --- a/src/main/java/ecdar/controllers/QueryController.java +++ b/src/main/java/ecdar/controllers/QueryController.java @@ -1,50 +1,348 @@ package ecdar.controllers; +import EcdarProtoBuf.QueryProtos; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.jfoenix.controls.*; +import ecdar.Ecdar; import com.jfoenix.controls.JFXComboBox; import com.jfoenix.controls.JFXRippler; -import ecdar.abstractions.Engine; +import ecdar.abstractions.Component; +import ecdar.backend.BackendException; +import ecdar.backend.Engine; import ecdar.abstractions.Query; +import ecdar.abstractions.QueryState; import ecdar.abstractions.QueryType; import ecdar.backend.BackendHelper; +import ecdar.presentations.DropDownMenu; +import ecdar.presentations.InformationDialogPresentation; +import ecdar.presentations.MenuElement; +import ecdar.utility.UndoRedoStack; import ecdar.utility.colors.Color; +import ecdar.utility.helpers.StringValidator; import javafx.application.Platform; +import javafx.beans.binding.When; import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.collections.ObservableList; import javafx.fxml.Initializable; +import javafx.geometry.Insets; +import javafx.scene.Cursor; +import javafx.scene.control.Label; import javafx.scene.control.Tooltip; +import javafx.scene.input.KeyCode; +import javafx.scene.layout.Background; +import javafx.scene.layout.BackgroundFill; +import javafx.scene.layout.CornerRadii; +import javafx.scene.layout.VBox; import javafx.scene.text.Text; import org.kordamp.ikonli.javafx.FontIcon; +import org.kordamp.ikonli.material.Material; import java.net.URL; -import java.util.HashMap; -import java.util.Map; -import java.util.ResourceBundle; +import java.util.*; +import java.util.function.Consumer; + +import static javafx.scene.paint.Color.TRANSPARENT; public class QueryController implements Initializable { - public JFXRippler actionButton; + public VBox stateIndicator; + public FontIcon statusIcon; public JFXRippler queryTypeExpand; public Text queryTypeSymbol; + public FontIcon queryTypeExpandIcon; + public JFXTextField queryTextField; + public JFXTextField commentTextField; + public JFXSpinner progressIndicator; + public JFXRippler actionButton; + public FontIcon actionButtonIcon; + public JFXRippler detailsButton; + public FontIcon detailsButtonIcon; public JFXComboBox enginesDropdown; + + private final Tooltip tooltip = new Tooltip(); + private final Tooltip noQueryTypeSetTooltip = new Tooltip("Please select a query type beneath the status icon"); private Query query; private final Map queryTypeListElementsSelectedState = new HashMap<>(); - private final Tooltip noQueryTypeSetTooltip = new Tooltip("Please select a query type beneath the status icon"); + private final StringProperty queryErrors = new SimpleStringProperty(""); + + private final Consumer successConsumer = (aBoolean) -> { + if (aBoolean) { + getQuery().setQueryState(QueryState.SUCCESSFUL); + } else { + getQuery().setQueryState(QueryState.ERROR); + } + }; + private Boolean forcedCancel = false; + private final Consumer failureConsumer = (e) -> { + if (forcedCancel) { + getQuery().setQueryState(QueryState.UNKNOWN); + } else { + getQuery().setQueryState(QueryState.SYNTAX_ERROR); + if (e instanceof BackendException.MissingFileQueryException) { + Ecdar.showToast("Please save the project before trying to run queries"); + } + + addError(e.getMessage()); + final Throwable cause = e.getCause(); + if (cause != null) { + // We had trouble generating the model if we get a NullPointerException + if (cause instanceof NullPointerException) { + getQuery().setQueryState(QueryState.UNKNOWN); + } else { + Platform.runLater(() -> EcdarController.openQueryDialog(getQuery(), cause.toString())); + } + } + } + }; @Override public void initialize(URL location, ResourceBundle resources) { + initializeStateIndicator(); + initializeProgressIndicator(); initializeActionButton(); + initializeDetailsButton(); + initializeTextFields(); + initializeMoreInformationButtonAndQueryTypeSymbol(); + initializeBackendsDropdown(); + } + + private void initializeBackendsDropdown() { + enginesDropdown.setItems(BackendHelper.getEngines()); + Tooltip backendDropdownTooltip = new Tooltip(); + backendDropdownTooltip.setText("Current backend used for the query"); + JFXTooltip.install(enginesDropdown, backendDropdownTooltip); + enginesDropdown.setValue(BackendHelper.getDefaultEngine()); + } + + private void initializeTextFields() { + Platform.runLater(() -> { + queryTextField.setText(getQuery().getQuery()); + commentTextField.setText(getQuery().getComment()); + + getQuery().queryProperty().bind(queryTextField.textProperty()); + getQuery().commentProperty().bind(commentTextField.textProperty()); + + + queryTextField.setOnKeyPressed(EcdarController.getActiveCanvasPresentation().getController().getLeaveTextAreaKeyHandler(keyEvent -> { + Platform.runLater(() -> { + if (keyEvent.getCode().equals(KeyCode.ENTER)) { + runQuery(); + } + }); + })); + + queryTextField.focusedProperty().addListener((observable, oldValue, newValue) -> { + if (!newValue && !StringValidator.validateQuery(queryTextField.getText())) { + queryTextField.getStyleClass().add("input-violation"); + } else { + queryTextField.getStyleClass().remove("input-violation"); + } + }); + + commentTextField.setOnKeyPressed(EcdarController.getActiveCanvasPresentation().getController().getLeaveTextAreaKeyHandler()); + }); + } + + private void initializeDetailsButton() { + Platform.runLater(() -> { + detailsButtonIcon.setIconColor(Color.GREY.getColor(Color.Intensity.I900)); + + detailsButton.setCursor(Cursor.HAND); + detailsButton.setRipplerFill(Color.GREY.getColor(Color.Intensity.I500)); + detailsButton.setMaskType(JFXRippler.RipplerMask.CIRCLE); + + final DropDownMenu dropDownMenu = new DropDownMenu(detailsButton); + + dropDownMenu.addToggleableListElement("Run Periodically", getQuery().isPeriodicProperty(), event -> { + // Toggle the property + getQuery().setIsPeriodic(!getQuery().isPeriodic()); + dropDownMenu.hide(); + }); + dropDownMenu.addSpacerElement(); + dropDownMenu.addClickableListElement("Clear Status", event -> { + // Clear the state + getQuery().setQueryState(QueryState.UNKNOWN); + dropDownMenu.hide(); + }); + dropDownMenu.addSpacerElement(); + dropDownMenu.addClickableListElement("Delete", event -> { + // Remove the query + Ecdar.getProject().getQueries().remove(getQuery()); + dropDownMenu.hide(); + }); + detailsButton.getChildren().get(0).setOnMousePressed(event -> { + // Show the popup + dropDownMenu.show(JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.RIGHT, -24, 20); + }); + }); + } + + private void initializeActionButton() { + Platform.runLater(() -> { + // Find the action icon + if (getQuery() == null) { + actionButtonIcon.setIconColor(Color.GREY.getColor(Color.Intensity.I500)); + } + + actionButton.setCursor(Cursor.HAND); + actionButton.setRipplerFill(Color.GREY.getColor(Color.Intensity.I500)); + + // Delegate that based on the query state updated the action icon + final Consumer updateIcon = (queryState) -> { + Platform.runLater(() -> { + if (queryState.equals(QueryState.RUNNING)) { + actionButtonIcon.setIconLiteral("gmi-stop"); + } else { + actionButtonIcon.setIconLiteral("gmi-play-arrow"); + } + }); + }; + + // Update the icon initially + updateIcon.accept(getQuery().getQueryState()); + + // Update the icon when ever the query state is updated + getQuery().queryStateProperty().addListener((observable, oldValue, newValue) -> updateIcon.accept(newValue)); + + actionButton.setMaskType(JFXRippler.RipplerMask.CIRCLE); + + actionButton.getChildren().get(0).setOnMousePressed(event -> { + Platform.runLater(() -> { + if (getQuery().getQueryState().equals(QueryState.RUNNING)) { + cancelQuery(); + } else { + runQuery(); + } + }); + }); + + Platform.runLater(() -> { + if (query.getType() == null) { + actionButton.setDisable(true); + actionButtonIcon.setIconColor(Color.GREY.getColor(Color.Intensity.I500)); + Tooltip.install(actionButton.getParent(), noQueryTypeSetTooltip); + } + }); + }); + } + + private void initializeProgressIndicator() { + Platform.runLater(() -> { + // If the query is running show the indicator, otherwise hide it + progressIndicator.visibleProperty().bind(new When(getQuery().queryStateProperty().isEqualTo(QueryState.RUNNING)).then(true).otherwise(false)); + }); + } + + private void initializeStateIndicator() { + Platform.runLater(() -> { + // Delegate that based on a query state updates tooltip of the query + final Consumer updateToolTip = (queryState) -> { + if (queryState.getStatusCode() == 1) { + if(queryState.getIconCode().equals(Material.DONE)) { + this.tooltip.setText("This query was a success!"); + } else { + this.tooltip.setText("The component has been created (can be accessed in the project pane)"); + } + } else if (queryState.getStatusCode() == 3) { + this.tooltip.setText("The query has not been executed yet"); + } else { + this.tooltip.setText(getCurrentErrors()); + } + }; + + // Delegate that based on a query state updates the color of the state indicator + final Consumer updateStateIndicator = (queryState) -> Platform.runLater(() -> { + this.tooltip.setText(""); + + final Color color = queryState.getColor(); + final Color.Intensity colorIntensity = queryState.getColorIntensity(); + + if (queryState.equals(QueryState.UNKNOWN) || queryState.equals(QueryState.RUNNING)) { + stateIndicator.setBackground(new Background(new BackgroundFill(TRANSPARENT, + CornerRadii.EMPTY, + Insets.EMPTY) + )); + } else { + stateIndicator.setBackground(new Background(new BackgroundFill(color.getColor(colorIntensity), + CornerRadii.EMPTY, + Insets.EMPTY) + )); + } + + setStatusIndicatorContentColor(new javafx.scene.paint.Color(1, 1, 1, 1), statusIcon, queryTypeExpandIcon, queryState); + + if (queryState.equals(QueryState.RUNNING) || queryState.equals(QueryState.UNKNOWN)) { + setStatusIndicatorContentColor(Color.GREY.getColor(Color.Intensity.I700), statusIcon, queryTypeExpandIcon, null); + } + + // The tooltip is updated here to handle all cases that are not syntax error + updateToolTip.accept(queryState); + }); + + // Update the initial color + updateStateIndicator.accept(getQuery().getQueryState()); + + // Ensure that the color is updated when ever the query state is updated + getQuery().queryStateProperty().addListener((observable, oldValue, newValue) -> updateStateIndicator.accept(newValue)); + + // Ensure that the tooltip is updated when new errors are added + queryErrors.addListener((observable, oldValue, newValue) -> updateToolTip.accept(getQuery().getQueryState())); + this.tooltip.setMaxWidth(300); + this.tooltip.setWrapText(true); + + // Installing the tooltip on the statusIcon itself scales the tooltip unexpectedly, hence its parent StackPane is used + Tooltip.install(statusIcon.getParent(), this.tooltip); + + queryTypeSymbol.setText(getQuery() != null && getQuery().getType() != null ? getQuery().getType().getSymbol() : "---"); + + statusIcon.setOnMouseClicked(event -> { + if (getQuery().getQuery().isEmpty()) return; + + Label label = new Label(tooltip.getText()); + JFXDialog dialog = new InformationDialogPresentation("Result from query: " + getQuery().getQuery(), label); + dialog.show(Ecdar.getPresentation()); + }); + }); } - public void setQuery(Query query) { + private void initializeMoreInformationButtonAndQueryTypeSymbol() { + Platform.runLater(() -> { + queryTypeExpand.setVisible(true); + queryTypeExpand.setMaskType(JFXRippler.RipplerMask.RECT); + queryTypeExpand.setPosition(JFXRippler.RipplerPos.BACK); + queryTypeExpand.setRipplerFill(Color.GREY_BLUE.getColor(Color.Intensity.I500)); + + final DropDownMenu queryTypeDropDown = new DropDownMenu(queryTypeExpand); + + queryTypeDropDown.addListElement("Query Type"); + QueryType[] queryTypes = QueryType.values(); + for (QueryType type : queryTypes) { + addQueryTypeListElement(type, queryTypeDropDown); + } + + queryTypeExpand.setOnMousePressed((e) -> { + e.consume(); + queryTypeDropDown.show(JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.LEFT, 16, 16); + }); + + queryTypeSymbol.setText(getQuery() != null && getQuery().getType() != null ? getQuery().getType().getSymbol() : "---"); + }); + } + + public void setQuery(final Query query) { this.query = query; this.query.getTypeProperty().addListener(((observable, oldValue, newValue) -> { if(newValue != null) { actionButton.setDisable(false); - ((FontIcon) actionButton.lookup("#actionButtonIcon")).setIconColor(Color.GREY.getColor(Color.Intensity.I900)); + actionButtonIcon.setIconColor(Color.GREY.getColor(Color.Intensity.I900)); Platform.runLater(() -> { Tooltip.uninstall(actionButton.getParent(), noQueryTypeSetTooltip); }); } else { actionButton.setDisable(true); - ((FontIcon) actionButton.lookup("#actionButtonIcon")).setIconColor(Color.GREY.getColor(Color.Intensity.I500)); + actionButtonIcon.setIconColor(Color.GREY.getColor(Color.Intensity.I500)); Platform.runLater(() -> { Tooltip.install(actionButton.getParent(), noQueryTypeSetTooltip); }); @@ -82,13 +380,161 @@ public Query getQuery() { return query; } - private void initializeActionButton() { + private void setStatusIndicatorContentColor(javafx.scene.paint.Color color, FontIcon statusIcon, FontIcon queryTypeExpandIcon, QueryState queryState) { + statusIcon.setIconColor(color); + queryTypeSymbol.setFill(color); + queryTypeExpandIcon.setIconColor(color); + + if (queryState != null) { + statusIcon.setIconLiteral("gmi-" + queryState.getIconCode().toString().toLowerCase().replace('_', '-')); + } + } + + private void addQueryTypeListElement(final QueryType type, final DropDownMenu dropDownMenu) { + MenuElement listElement = new MenuElement(type.getQueryName() + " [" + type.getSymbol() + "]", "gmi-done", mouseEvent -> { + getQuery().setType(type); + queryTypeSymbol.setText(type.getSymbol()); + dropDownMenu.hide(); + + Set> queryTypesSelected = getQueryTypeListElementsSelectedState().entrySet(); + + // Reflect the selection on the dropdown menu + for (Map.Entry pair : queryTypesSelected) { + pair.getValue().set(pair.getKey().equals(type)); + } + }); + + // Add boolean to the element to handle selection + SimpleBooleanProperty selected = new SimpleBooleanProperty(getQuery().getType() != null && getQuery().getType().getSymbol().equals(type.getSymbol())); + getQueryTypeListElementsSelectedState().put(type, selected); + listElement.setToggleable(selected); + + dropDownMenu.addMenuElement(listElement); + } + + /** + * Executes the query + */ + public void runQuery() throws NoSuchElementException { + if (getQuery().getQueryState().equals(QueryState.RUNNING) || !StringValidator.validateQuery(getQuery().getQuery())) + return; + + if (getQuery().getQuery().isEmpty()) { + getQuery().setQueryState(QueryState.SYNTAX_ERROR); + addError("Query is empty"); + return; + } + + getQuery().setQueryState(QueryState.RUNNING); + clearQueryErrors(); + + getQuery().getEngine().enqueueQuery(getQuery(), this::handleQueryResponse, this::handleQueryBackendError); + } + + public void cancelQuery() { + if (query.getQueryState().equals(QueryState.RUNNING)) { + setForcedCancel(true); + query.setQueryState(QueryState.UNKNOWN); + } + } + + public void setForcedCancel(Boolean forcedCancel) { + this.forcedCancel = forcedCancel; + } + + public void addError(String e) { + queryErrors.set(queryErrors.getValue() + e + "\n"); + } + + private void clearQueryErrors() { + queryErrors.set(""); + } + + public String getCurrentErrors() { + return queryErrors.getValue(); + } + + private void handleQueryResponse(QueryProtos.QueryResponse value) { + // If the query has been cancelled, ignore the result + if (getQuery().getQueryState() == QueryState.UNKNOWN) return; + + if (value.hasRefinement() && value.getRefinement().getSuccess()) { + getQuery().setQueryState(QueryState.SUCCESSFUL); + successConsumer.accept(true); + } else if (value.hasConsistency() && value.getConsistency().getSuccess()) { + getQuery().setQueryState(QueryState.SUCCESSFUL); + successConsumer.accept(true); + } else if (value.hasDeterminism() && value.getDeterminism().getSuccess()) { + getQuery().setQueryState(QueryState.SUCCESSFUL); + successConsumer.accept(true); + } else if (value.hasComponent()) { + getQuery().setQueryState(QueryState.SUCCESSFUL); + successConsumer.accept(true); + JsonObject returnedComponent = (JsonObject) JsonParser.parseString(value.getComponent().getComponent().getJson()); + addGeneratedComponent(new Component(returnedComponent)); + } else { + getQuery().setQueryState(QueryState.ERROR); + successConsumer.accept(false); + } + } + + private void handleQueryBackendError(Throwable t) { + // If the query has been cancelled, ignore the error + if (getQuery().getQueryState() == QueryState.UNKNOWN) return; + + // Each error starts with a capitalized description of the error equal to the gRPC error type encountered + String errorType = t.getMessage().split(":\\s+", 2)[0]; + + if ("DEADLINE_EXCEEDED".equals(errorType)) { + getQuery().setQueryState(QueryState.ERROR); + failureConsumer.accept(new BackendException.QueryErrorException("The engine did not answer the request in time")); + } else { + try { + getQuery().setQueryState(QueryState.ERROR); + failureConsumer.accept(new BackendException.QueryErrorException("The execution of this query failed with message:" + System.lineSeparator() + t.getLocalizedMessage())); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + private void addGeneratedComponent(Component newComponent) { Platform.runLater(() -> { - if (query.getType() == null) { - actionButton.setDisable(true); - ((FontIcon) actionButton.lookup("#actionButtonIcon")).setIconColor(Color.GREY.getColor(Color.Intensity.I500)); - Tooltip.install(actionButton.getParent(), noQueryTypeSetTooltip); + newComponent.setTemporary(true); + + ObservableList listOfGeneratedComponents = Ecdar.getProject().getTempComponents(); // ToDo NIELS: Refactor + Component matchedComponent = null; + + for (Component currentGeneratedComponent : listOfGeneratedComponents) { + int comparisonOfNames = currentGeneratedComponent.getName().compareTo(newComponent.getName()); + + if (comparisonOfNames == 0) { + matchedComponent = currentGeneratedComponent; + break; + } else if (comparisonOfNames < 0) { + break; + } } + + if (matchedComponent == null) { + UndoRedoStack.pushAndPerform(() -> { // Perform + Ecdar.getProject().getTempComponents().add(newComponent); + }, () -> { // Undo + Ecdar.getProject().getTempComponents().remove(newComponent); + }, "Created new component: " + newComponent.getName(), "add-circle"); + } else { + // Remove current component with name and add the newly generated one + Component finalMatchedComponent = matchedComponent; + UndoRedoStack.pushAndPerform(() -> { // Perform + Ecdar.getProject().getTempComponents().remove(finalMatchedComponent); + Ecdar.getProject().getTempComponents().add(newComponent); + }, () -> { // Undo + Ecdar.getProject().getTempComponents().remove(newComponent); + Ecdar.getProject().getTempComponents().add(finalMatchedComponent); + }, "Created new component: " + newComponent.getName(), "add-circle"); + } + + Ecdar.getProject().addComponent(newComponent); }); } diff --git a/src/main/java/ecdar/controllers/QueryPaneController.java b/src/main/java/ecdar/controllers/QueryPaneController.java index d5aa904a..0165f9bd 100644 --- a/src/main/java/ecdar/controllers/QueryPaneController.java +++ b/src/main/java/ecdar/controllers/QueryPaneController.java @@ -3,17 +3,21 @@ import ecdar.Ecdar; import ecdar.abstractions.Query; import ecdar.abstractions.QueryState; +import ecdar.backend.BackendHelper; import ecdar.presentations.QueryPresentation; import com.jfoenix.controls.JFXRippler; import ecdar.utility.colors.Color; +import ecdar.utility.helpers.DropShadowHelper; import javafx.application.Platform; import javafx.collections.ListChangeListener; import javafx.fxml.FXML; import javafx.fxml.Initializable; import javafx.geometry.Insets; import javafx.scene.Cursor; +import javafx.scene.Node; import javafx.scene.control.Label; import javafx.scene.control.ScrollPane; +import javafx.scene.control.Tooltip; import javafx.scene.layout.*; import java.net.URL; @@ -57,7 +61,14 @@ public void initialize(final URL location, final ResourceBundle resources) { } }); + initializeLeftBorder(); + initializeToolbar(); + initializeBackground(); initializeResizeAnchor(); + + // When the backend instances change, reset the backendDriver and + // cancel all queries to prevent dangling connections and queries + BackendHelper.addEngineInstanceListener(this::stopAllQueries); } private void initializeResizeAnchor() { @@ -70,6 +81,85 @@ private void initializeResizeAnchor() { resizeAnchor.setBackground(new Background(new BackgroundFill(color.getColor(colorIntensity), CornerRadii.EMPTY, Insets.EMPTY))); } + private void initializeLeftBorder() { + toolbar.setBorder(new Border(new BorderStroke( + Color.GREY_BLUE.getColor(Color.Intensity.I900), + BorderStrokeStyle.SOLID, + CornerRadii.EMPTY, + new BorderWidths(0, 0, 0, 1) + ))); + + showBottomInset(true); + } + + private void initializeBackground() { + queriesList.setBackground(new Background(new BackgroundFill( + Color.GREY.getColor(Color.Intensity.I200), + CornerRadii.EMPTY, + Insets.EMPTY + ))); + } + + private void initializeToolbar() { + final Color color = Color.GREY_BLUE; + final Color.Intensity colorIntensity = Color.Intensity.I800; + + // Set the background of the toolbar + toolbar.setBackground(new Background(new BackgroundFill( + color.getColor(colorIntensity), + CornerRadii.EMPTY, + Insets.EMPTY))); + + // Set the font color of elements in the toolbar + toolbarTitle.setTextFill(color.getTextColor(colorIntensity)); + + runAllQueriesButton.setBackground(new Background(new BackgroundFill( + javafx.scene.paint.Color.TRANSPARENT, + new CornerRadii(100), + Insets.EMPTY))); + + addButton.setMaskType(JFXRippler.RipplerMask.CIRCLE); + addButton.setRipplerFill(color.getTextColor(colorIntensity)); + Tooltip.install(addButton, new Tooltip("Add query")); + + runAllQueriesButton.setMaskType(JFXRippler.RipplerMask.CIRCLE); + runAllQueriesButton.setRipplerFill(color.getTextColor(colorIntensity)); + Tooltip.install(runAllQueriesButton, new Tooltip("Run all queries")); + + clearAllQueriesButton.setMaskType(JFXRippler.RipplerMask.CIRCLE); + clearAllQueriesButton.setRipplerFill(color.getTextColor(colorIntensity)); + Tooltip.install(clearAllQueriesButton, new Tooltip("Clear all queries")); + + // Set the elevation of the toolbar + toolbar.setEffect(DropShadowHelper.generateElevationShadow(8)); + } + + + /** + * Inserts an edge/inset at the bottom of the scrollView + * which is used to push up the elements of the scrollview + * @param shouldShow boolean indicating whether to push up the items + */ + public void showBottomInset(final Boolean shouldShow) { + double bottomInsetWidth = 0; + if(shouldShow) { + bottomInsetWidth = 20; + } + + scrollPane.setBorder(new Border(new BorderStroke( + Color.GREY.getColor(Color.Intensity.I400), + BorderStrokeStyle.NONE, + CornerRadii.EMPTY, + new BorderWidths(0, 1, bottomInsetWidth, 0) + ))); + } + + public void stopAllQueries() { + for (Node qp : queriesList.getChildren()) { + ((QueryPresentation) qp).getController().cancelQuery(); + } + } + @FXML private void addButtonClicked() { Ecdar.getProject().getQueries().add(new Query("", "", QueryState.UNKNOWN)); @@ -77,11 +167,14 @@ private void addButtonClicked() { @FXML private void runAllQueriesButtonClicked() { - Ecdar.getProject().getQueries().forEach(query -> { - if (query.getType() == null) return; - query.cancel(); - Ecdar.getQueryExecutor().executeQuery(query); - }); + for (Node qp : queriesList.getChildren()) { + if (!(qp instanceof QueryPresentation)) continue; + QueryController controller = ((QueryPresentation) qp).getController(); + + if (controller.getQuery().getType() == null) return; + controller.cancelQuery(); + controller.runQuery(); + } } @FXML diff --git a/src/main/java/ecdar/issues/ExitStatusCodes.java b/src/main/java/ecdar/issues/ExitStatusCodes.java new file mode 100644 index 00000000..5fc46972 --- /dev/null +++ b/src/main/java/ecdar/issues/ExitStatusCodes.java @@ -0,0 +1,17 @@ +package ecdar.issues; + +/** + * Enum for representing the status of a requested exit + */ +public enum ExitStatusCodes { + SHUTDOWN_SUCCESSFUL(0), + GRACEFUL_SHUTDOWN_FAILED(-1), + CLOSE_ENGINE_CONNECTIONS_FAILED(-2); + + private final int statusCode; + ExitStatusCodes(int statusCode) { this.statusCode = statusCode; } + + public int getStatusCode() { + return statusCode; + } +} \ No newline at end of file diff --git a/src/main/java/ecdar/presentations/EcdarPresentation.java b/src/main/java/ecdar/presentations/EcdarPresentation.java index 0e45b3c8..4f1b15d8 100644 --- a/src/main/java/ecdar/presentations/EcdarPresentation.java +++ b/src/main/java/ecdar/presentations/EcdarPresentation.java @@ -53,24 +53,28 @@ public class EcdarPresentation extends StackPane { public EcdarPresentation() { controller = new EcdarFXMLLoader().loadAndGetController("EcdarPresentation.fxml", this); - initializeTopBar(); - initializeToolbar(); - initializeQueryDetailsDialog(); - initializeColorSelector(); + initializeResizeQueryPane(); + + Platform.runLater(() -> { + initializeTopBar(); + initializeToolbar(); + initializeQueryDetailsDialog(); + initializeColorSelector(); - initializeToggleQueryPaneFunctionality(); - initializeToggleFilePaneFunctionality(); + initializeToggleQueryPaneFunctionality(); + initializeToggleFilePaneFunctionality(); - initializeSelectDependentToolbarButton(controller.colorSelected); - Tooltip.install(controller.colorSelected, new Tooltip("Colour")); + initializeSelectDependentToolbarButton(controller.colorSelected); + Tooltip.install(controller.colorSelected, new Tooltip("Colour")); - initializeSelectDependentToolbarButton(controller.deleteSelected); - Tooltip.install(controller.deleteSelected, new Tooltip("Delete")); + initializeSelectDependentToolbarButton(controller.deleteSelected); + Tooltip.install(controller.deleteSelected, new Tooltip("Delete")); - initializeToolbarButton(controller.undo); - initializeToolbarButton(controller.redo); - initializeUndoRedoButtons(); - initializeSnackbar(); + initializeToolbarButton(controller.undo); + initializeToolbarButton(controller.redo); + initializeUndoRedoButtons(); + initializeSnackbar(); + }); // Open the file and query panel initially Platform.runLater(() -> { @@ -113,8 +117,6 @@ public EcdarPresentation() { initializeHelpImages(); KeyboardTracker.registerKeybind(KeyboardTracker.UNDO, new Keybind(new KeyCodeCombination(KeyCode.Z, KeyCombination.SHORTCUT_DOWN), UndoRedoStack::undo)); KeyboardTracker.registerKeybind(KeyboardTracker.REDO, new Keybind(new KeyCodeCombination(KeyCode.Z, KeyCombination.SHORTCUT_DOWN, KeyCombination.SHIFT_DOWN), UndoRedoStack::redo)); - - initializeResizeQueryPane(); } private void initializeSnackbar() { diff --git a/src/main/java/ecdar/presentations/EnginePresentation.java b/src/main/java/ecdar/presentations/EnginePresentation.java index 36cd75ad..4923221c 100644 --- a/src/main/java/ecdar/presentations/EnginePresentation.java +++ b/src/main/java/ecdar/presentations/EnginePresentation.java @@ -2,7 +2,7 @@ import com.jfoenix.controls.JFXRippler; import ecdar.Ecdar; -import ecdar.abstractions.Engine; +import ecdar.backend.Engine; import ecdar.controllers.EngineInstanceController; import ecdar.utility.colors.Color; import javafx.application.Platform; diff --git a/src/main/java/ecdar/presentations/QueryPanePresentation.java b/src/main/java/ecdar/presentations/QueryPanePresentation.java index 585e6178..a5c8952a 100644 --- a/src/main/java/ecdar/presentations/QueryPanePresentation.java +++ b/src/main/java/ecdar/presentations/QueryPanePresentation.java @@ -1,11 +1,6 @@ package ecdar.presentations; import ecdar.controllers.QueryPaneController; -import ecdar.utility.colors.Color; -import ecdar.utility.helpers.DropShadowHelper; -import com.jfoenix.controls.JFXRippler; -import javafx.geometry.Insets; -import javafx.scene.control.Tooltip; import javafx.scene.layout.*; public class QueryPanePresentation extends StackPane { @@ -13,83 +8,6 @@ public class QueryPanePresentation extends StackPane { public QueryPanePresentation() { controller = new EcdarFXMLLoader().loadAndGetController("QueryPanePresentation.fxml", this); - - initializeLeftBorder(); - initializeToolbar(); - initializeBackground(); - } - - private void initializeLeftBorder() { - controller.toolbar.setBorder(new Border(new BorderStroke( - Color.GREY_BLUE.getColor(Color.Intensity.I900), - BorderStrokeStyle.SOLID, - CornerRadii.EMPTY, - new BorderWidths(0, 0, 0, 1) - ))); - - showBottomInset(true); - } - - private void initializeBackground() { - controller.queriesList.setBackground(new Background(new BackgroundFill( - Color.GREY.getColor(Color.Intensity.I200), - CornerRadii.EMPTY, - Insets.EMPTY - ))); - } - - private void initializeToolbar() { - final Color color = Color.GREY_BLUE; - final Color.Intensity colorIntensity = Color.Intensity.I800; - - // Set the background of the toolbar - controller.toolbar.setBackground(new Background(new BackgroundFill( - color.getColor(colorIntensity), - CornerRadii.EMPTY, - Insets.EMPTY))); - - // Set the font color of elements in the toolbar - controller.toolbarTitle.setTextFill(color.getTextColor(colorIntensity)); - - controller.runAllQueriesButton.setBackground(new Background(new BackgroundFill( - javafx.scene.paint.Color.TRANSPARENT, - new CornerRadii(100), - Insets.EMPTY))); - - controller.addButton.setMaskType(JFXRippler.RipplerMask.CIRCLE); - controller.addButton.setRipplerFill(color.getTextColor(colorIntensity)); - Tooltip.install(controller.addButton, new Tooltip("Add query")); - - controller.runAllQueriesButton.setMaskType(JFXRippler.RipplerMask.CIRCLE); - controller.runAllQueriesButton.setRipplerFill(color.getTextColor(colorIntensity)); - Tooltip.install(controller.runAllQueriesButton, new Tooltip("Run all queries")); - - controller.clearAllQueriesButton.setMaskType(JFXRippler.RipplerMask.CIRCLE); - controller.clearAllQueriesButton.setRipplerFill(color.getTextColor(colorIntensity)); - Tooltip.install(controller.clearAllQueriesButton, new Tooltip("Clear all queries")); - - // Set the elevation of the toolbar - controller.toolbar.setEffect(DropShadowHelper.generateElevationShadow(8)); - } - - - /** - * Inserts an edge/inset at the bottom of the scrollView - * which is used to push up the elements of the scrollview - * @param shouldShow boolean indicating whether to push up the items - */ - public void showBottomInset(final Boolean shouldShow) { - double bottomInsetWidth = 0; - if(shouldShow) { - bottomInsetWidth = 20; - } - - controller.scrollPane.setBorder(new Border(new BorderStroke( - Color.GREY.getColor(Color.Intensity.I400), - BorderStrokeStyle.NONE, - CornerRadii.EMPTY, - new BorderWidths(0, 1, bottomInsetWidth, 0) - ))); } public QueryPaneController getController() { diff --git a/src/main/java/ecdar/presentations/QueryPresentation.java b/src/main/java/ecdar/presentations/QueryPresentation.java index 7d7fca41..18d4eba5 100644 --- a/src/main/java/ecdar/presentations/QueryPresentation.java +++ b/src/main/java/ecdar/presentations/QueryPresentation.java @@ -1,318 +1,23 @@ package ecdar.presentations; -import com.jfoenix.controls.*; import ecdar.Ecdar; import ecdar.abstractions.*; -import ecdar.backend.*; import ecdar.controllers.QueryController; -import ecdar.controllers.EcdarController; -import ecdar.utility.colors.Color; -import ecdar.utility.helpers.StringValidator; import javafx.application.Platform; -import javafx.beans.binding.When; -import javafx.beans.property.SimpleBooleanProperty; -import javafx.geometry.Insets; -import javafx.scene.Cursor; -import javafx.scene.control.Label; -import javafx.scene.control.Tooltip; -import javafx.scene.input.KeyCode; import javafx.scene.layout.*; -import org.kordamp.ikonli.javafx.FontIcon; -import org.kordamp.ikonli.material.Material; - -import java.util.Map; -import java.util.Set; -import java.util.function.Consumer; - -import static javafx.scene.paint.Color.*; public class QueryPresentation extends HBox { - private final Tooltip tooltip = new Tooltip(); private final QueryController controller; public QueryPresentation(final Query query) { controller = new EcdarFXMLLoader().loadAndGetController("QueryPresentation.fxml", this); controller.setQuery(query); - initializeStateIndicator(); - initializeProgressIndicator(); - initializeActionButton(); - initializeDetailsButton(); - initializeTextFields(); - initializeMoreInformationButtonAndQueryTypeSymbol(); - initializeEnginesDropdown(); - // Ensure that the icons are scaled to current font scale Platform.runLater(() -> Ecdar.getPresentation().getController().scaleIcons(this)); } - private void initializeEnginesDropdown() { - controller.enginesDropdown.setItems(BackendHelper.getEngines()); - - Tooltip enginesDropdownTooltip = new Tooltip(); - enginesDropdownTooltip.setText("Current engine used for the query"); - JFXTooltip.install(controller.enginesDropdown, enginesDropdownTooltip); - controller.enginesDropdown.setValue(BackendHelper.getDefaultEngine()); - } - - private void initializeTextFields() { - Platform.runLater(() -> { - final JFXTextField queryTextField = (JFXTextField) lookup("#query"); - final JFXTextField commentTextField = (JFXTextField) lookup("#comment"); - - queryTextField.setText(controller.getQuery().getQuery()); - commentTextField.setText(controller.getQuery().getComment()); - - controller.getQuery().queryProperty().bind(queryTextField.textProperty()); - controller.getQuery().commentProperty().bind(commentTextField.textProperty()); - - - queryTextField.setOnKeyPressed(EcdarController.getActiveCanvasPresentation().getController().getLeaveTextAreaKeyHandler(keyEvent -> { - Platform.runLater(() -> { - if (keyEvent.getCode().equals(KeyCode.ENTER)) { - runQuery(); - } - }); - })); - - queryTextField.focusedProperty().addListener((observable, oldValue, newValue) -> { - if (!newValue && !StringValidator.validateQuery(queryTextField.getText())) { - queryTextField.getStyleClass().add("input-violation"); - } else { - queryTextField.getStyleClass().remove("input-violation"); - } - }); - - commentTextField.setOnKeyPressed(EcdarController.getActiveCanvasPresentation().getController().getLeaveTextAreaKeyHandler()); - }); - } - - private void initializeDetailsButton() { - Platform.runLater(() -> { - final JFXRippler detailsButton = (JFXRippler) lookup("#detailsButton"); - final FontIcon detailsButtonIcon = (FontIcon) lookup("#detailsButtonIcon"); - - detailsButtonIcon.setIconColor(Color.GREY.getColor(Color.Intensity.I900)); - - detailsButton.setCursor(Cursor.HAND); - detailsButton.setRipplerFill(Color.GREY.getColor(Color.Intensity.I500)); - detailsButton.setMaskType(JFXRippler.RipplerMask.CIRCLE); - - final DropDownMenu dropDownMenu = new DropDownMenu(detailsButton); - - dropDownMenu.addToggleableListElement("Run Periodically", controller.getQuery().isPeriodicProperty(), event -> { - // Toggle the property - controller.getQuery().setIsPeriodic(!controller.getQuery().isPeriodic()); - dropDownMenu.hide(); - }); - dropDownMenu.addSpacerElement(); - dropDownMenu.addClickableListElement("Clear Status", event -> { - // Clear the state - controller.getQuery().setQueryState(QueryState.UNKNOWN); - dropDownMenu.hide(); - }); - dropDownMenu.addSpacerElement(); - dropDownMenu.addClickableListElement("Delete", event -> { - // Remove the query - Ecdar.getProject().getQueries().remove(controller.getQuery()); - dropDownMenu.hide(); - }); - detailsButton.getChildren().get(0).setOnMousePressed(event -> { - // Show the popup - dropDownMenu.show(JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.RIGHT, -24, 20); - }); - }); - } - - private void initializeActionButton() { - Platform.runLater(() -> { - // Find the action icon - final FontIcon actionButtonIcon = (FontIcon) lookup("#actionButtonIcon"); - - if (controller.getQuery() == null) { - actionButtonIcon.setIconColor(Color.GREY.getColor(Color.Intensity.I500)); - } - - controller.actionButton.setCursor(Cursor.HAND); - controller.actionButton.setRipplerFill(Color.GREY.getColor(Color.Intensity.I500)); - - // Delegate that based on the query state updated the action icon - final Consumer updateIcon = (queryState) -> { - Platform.runLater(() -> { - if (queryState.equals(QueryState.RUNNING)) { - actionButtonIcon.setIconLiteral("gmi-stop"); - } else { - actionButtonIcon.setIconLiteral("gmi-play-arrow"); - } - }); - }; - - // Update the icon initially - updateIcon.accept(controller.getQuery().getQueryState()); - - // Update the icon when ever the query state is updated - controller.getQuery().queryStateProperty().addListener((observable, oldValue, newValue) -> updateIcon.accept(newValue)); - - controller.actionButton.setMaskType(JFXRippler.RipplerMask.CIRCLE); - - controller.actionButton.getChildren().get(0).setOnMousePressed(event -> { - Platform.runLater(() -> { - if (controller.getQuery().getQueryState().equals(QueryState.RUNNING)) { - controller.getQuery().cancel(); - } else { - runQuery(); - } - }); - }); - }); - } - - private void initializeProgressIndicator() { - Platform.runLater(() -> { - // Find the progress indicator - final JFXSpinner progressIndicator = (JFXSpinner) lookup("#progressIndicator"); - - // If the query is running show the indicator, otherwise hide it - progressIndicator.visibleProperty().bind(new When(controller.getQuery().queryStateProperty().isEqualTo(QueryState.RUNNING)).then(true).otherwise(false)); - }); - } - - private void initializeStateIndicator() { - Platform.runLater(() -> { - // Find the state indicator from the inflated xml - final VBox stateIndicator = (VBox) lookup("#stateIndicator"); - final FontIcon statusIcon = (FontIcon) stateIndicator.lookup("#statusIcon"); - final FontIcon queryTypeExpandIcon = (FontIcon) stateIndicator.lookup("#queryTypeExpandIcon"); - - // Delegate that based on a query state updates tooltip of the query - final Consumer updateToolTip = (queryState) -> { - if (queryState.getStatusCode() == 1) { - if(queryState.getIconCode().equals(Material.DONE)) { - this.tooltip.setText("This query was a success!"); - } else { - this.tooltip.setText("The component has been created (can be accessed in the project pane)"); - } - } else if (queryState.getStatusCode() == 3) { - this.tooltip.setText("The query has not been executed yet"); - } else { - this.tooltip.setText(controller.getQuery().getCurrentErrors()); - } - }; - - // Delegate that based on a query state updates the color of the state indicator - final Consumer updateStateIndicator = (queryState) -> { - Platform.runLater(() -> { - this.tooltip.setText(""); - - final Color color = queryState.getColor(); - final Color.Intensity colorIntensity = queryState.getColorIntensity(); - - if (queryState.equals(QueryState.UNKNOWN) || queryState.equals(QueryState.RUNNING)) { - stateIndicator.setBackground(new Background(new BackgroundFill(TRANSPARENT, - CornerRadii.EMPTY, - Insets.EMPTY) - )); - } else { - stateIndicator.setBackground(new Background(new BackgroundFill(color.getColor(colorIntensity), - CornerRadii.EMPTY, - Insets.EMPTY) - )); - } - - setStatusIndicatorContentColor(new javafx.scene.paint.Color(1, 1, 1, 1), statusIcon, queryTypeExpandIcon, queryState); - - if (queryState.equals(QueryState.RUNNING) || queryState.equals(QueryState.UNKNOWN)) { - setStatusIndicatorContentColor(Color.GREY.getColor(Color.Intensity.I700), statusIcon, queryTypeExpandIcon, null); - } - - // The tooltip is updated here to handle all cases that are not syntax error - updateToolTip.accept(queryState); - }); - }; - - // Update the initial color - updateStateIndicator.accept(controller.getQuery().getQueryState()); - - // Ensure that the color is updated when ever the query state is updated - controller.getQuery().queryStateProperty().addListener((observable, oldValue, newValue) -> updateStateIndicator.accept(newValue)); - - // Ensure that the tooltip is updated when new errors are added - controller.getQuery().errors().addListener((observable, oldValue, newValue) -> updateToolTip.accept(controller.getQuery().getQueryState())); - this.tooltip.setMaxWidth(300); - this.tooltip.setWrapText(true); - - // Installing the tooltip on the statusIcon itself scales the tooltip unexpectedly, hence its parent StackPane is used - Tooltip.install(statusIcon.getParent(), this.tooltip); - - controller.queryTypeSymbol.setText(controller.getQuery() != null && controller.getQuery().getType() != null ? controller.getQuery().getType().getSymbol() : "---"); - - statusIcon.setOnMouseClicked(event -> { - if (controller.getQuery().getQuery().isEmpty()) return; - - Label label = new Label(tooltip.getText()); - JFXDialog dialog = new InformationDialogPresentation("Result from query: " + controller.getQuery().getQuery(), label); - dialog.show(Ecdar.getPresentation()); - }); - }); - } - - private void setStatusIndicatorContentColor(javafx.scene.paint.Color color, FontIcon statusIcon, FontIcon queryTypeExpandIcon, QueryState queryState) { - statusIcon.setIconColor(color); - controller.queryTypeSymbol.setFill(color); - queryTypeExpandIcon.setIconColor(color); - - if (queryState != null) { - statusIcon.setIconLiteral("gmi-" + queryState.getIconCode().toString().toLowerCase().replace('_', '-')); - } - } - - private void initializeMoreInformationButtonAndQueryTypeSymbol() { - Platform.runLater(() -> { - controller.queryTypeExpand.setVisible(true); - controller.queryTypeExpand.setMaskType(JFXRippler.RipplerMask.RECT); - controller.queryTypeExpand.setPosition(JFXRippler.RipplerPos.BACK); - controller.queryTypeExpand.setRipplerFill(Color.GREY_BLUE.getColor(Color.Intensity.I500)); - - final DropDownMenu queryTypeDropDown = new DropDownMenu(controller.queryTypeExpand); - - queryTypeDropDown.addListElement("Query Type"); - QueryType[] queryTypes = QueryType.values(); - for (QueryType type : queryTypes) { - addQueryTypeListElement(type, queryTypeDropDown); - } - - controller.queryTypeExpand.setOnMousePressed((e) -> { - e.consume(); - queryTypeDropDown.show(JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.LEFT, 16, 16); - }); - - controller.queryTypeSymbol.setText(controller.getQuery() != null && controller.getQuery().getType() != null ? controller.getQuery().getType().getSymbol() : "---"); - }); - } - - private void addQueryTypeListElement(final QueryType type, final DropDownMenu dropDownMenu) { - MenuElement listElement = new MenuElement(type.getQueryName() + " [" + type.getSymbol() + "]", "gmi-done", mouseEvent -> { - controller.getQuery().setType(type); - controller.queryTypeSymbol.setText(type.getSymbol()); - dropDownMenu.hide(); - - Set> queryTypesSelected = controller.getQueryTypeListElementsSelectedState().entrySet(); - - // Reflect the selection on the dropdown menu - for (Map.Entry pair : queryTypesSelected) { - pair.getValue().set(pair.getKey().equals(type)); - } - }); - - // Add boolean to the element to handle selection - SimpleBooleanProperty selected = new SimpleBooleanProperty(controller.getQuery().getType() != null && controller.getQuery().getType().getSymbol().equals(type.getSymbol())); - controller.getQueryTypeListElementsSelectedState().put(type, selected); - listElement.setToggleable(selected); - - dropDownMenu.addMenuElement(listElement); - } - - private void runQuery() { - Ecdar.getQueryExecutor().executeQuery(this.controller.getQuery()); + public QueryController getController() { + return controller; } } diff --git a/src/main/resources/ecdar/presentations/EcdarPresentation.fxml b/src/main/resources/ecdar/presentations/EcdarPresentation.fxml index ad248d97..2c61da47 100644 --- a/src/main/resources/ecdar/presentations/EcdarPresentation.fxml +++ b/src/main/resources/ecdar/presentations/EcdarPresentation.fxml @@ -97,11 +97,7 @@ - - - + diff --git a/src/main/resources/ecdar/presentations/QueryPresentation.fxml b/src/main/resources/ecdar/presentations/QueryPresentation.fxml index 8f0cb331..46f45024 100644 --- a/src/main/resources/ecdar/presentations/QueryPresentation.fxml +++ b/src/main/resources/ecdar/presentations/QueryPresentation.fxml @@ -2,8 +2,6 @@ - - - + - + - @@ -33,10 +31,10 @@ - - @@ -46,10 +44,10 @@ - + - @@ -57,9 +55,9 @@ - + - diff --git a/src/test/java/ecdar/backend/QueryHandlerTest.java b/src/test/java/ecdar/backend/QueryHandlerTest.java deleted file mode 100644 index be0fe103..00000000 --- a/src/test/java/ecdar/backend/QueryHandlerTest.java +++ /dev/null @@ -1,20 +0,0 @@ -package ecdar.backend; - -import ecdar.abstractions.Query; -import ecdar.abstractions.QueryState; -import org.junit.jupiter.api.Test; - -import static org.mockito.Mockito.*; - -public class QueryHandlerTest { - @Test - public void testExecuteQuery() { - BackendDriver bd = mock(BackendDriver.class); - QueryHandler qh = new QueryHandler(bd); - - Query q = new Query("refinement: (Administration || Machine || Researcher) \u003c\u003d Spec", "", QueryState.UNKNOWN); - qh.executeQuery(q); - - verify(bd, times(1)).addRequestToExecutionQueue(any()); - } -}